(字节跳动公司&中山大学合作)IOS科研实训个人报告

IOS科研实训个人报告

(2019年春季学期)

一、实验题目

IM聊天工具

二、实现内容

  • 个人详情页面
  • 个人信息修改页面
  • 修改头像
  • 好友聊天——图片发送与显示

三、实验结果

1.个人详情页面

效果截图

个人详情页面是使用UITableView来模仿微信聊天工具的详情所制作的

InfoViewController.h

#import <UIKit/UIKit.h>
#import "UserModel.h"

NS_ASSUME_NONNULL_BEGIN

@interface InfoViewController : UIViewController
@property (weak, nonatomic) IBOutlet UILabel *Birthplace;
@property (weak, nonatomic) IBOutlet UILabel *NickName;
@property (weak, nonatomic) IBOutlet UILabel *ID;
@property (weak, nonatomic) IBOutlet UILabel *Gender;
@property (strong, nonatomic) UserModel* User;
@property (weak, nonatomic) IBOutlet UIImageView *ProfilePicture;

@end

首先在.h文件定义所用到的一些属性,包括用户名,地区,性别,id号,头像等,还有一个登陆后的User的信息。

InfoViewController.m

这里首先定义所用到TableView,以及左侧的标题列表,右侧的内容列表来存储数据

#import "InfoViewController.h"

@interface InfoViewController ()<UITableViewDelegate, UITableViewDataSource>

@property(nonatomic, strong) UITableView *tableView;
@property(nonatomic, strong) NSMutableArray<NSString*> *titleList;
@property(nonatomic, strong) NSMutableArray<NSString*> *contentList;

@end

viewDidLoad函数中加载数据

  • 绑定User中的信息到之前定义的属性当中。
  • 定义navigationItem的标题名。
  • 获取屏幕的宽与高来定义tableview视图的大小与位置
  • 取消tableview默认的多余的横线
- (void)viewDidLoad {
    [super viewDidLoad];
    // [self.navigationController setNavigationBarHidden:NO animated:NO];
    self.navigationController.navigationBarHidden = NO;
    // Do any additional setup after loading the view.
    if (self.User == nil)
    {
        self.User = [[UserModel alloc] initWithProperties:@"peppa ID" NickName:@"Peppa" RemarkName:@"peppy" Gender:@"female" Birthplace:@"UK" ProfilePicture:@"peppa.jpg"];
    }
    self.ProfilePicture.image = [UIImage imageNamed:self.User.ProfilePicture];
    self.NickName.text = self.User.NickName;
    self.ID.text = self.User.UserID;
    self.Gender.text = self.User.Gender;
    self.Birthplace.text = self.User.Birthplace;
    
    self.navigationItem.title = @"个人信息";
    // 获取屏幕的宽高
    CGRect rect = [[UIScreen mainScreen] bounds];
    CGSize size = rect.size;
    CGFloat width = size.width;
    CGFloat height = size.height;
    
    self.tableView = ({
        UITableView *tableView = [[UITableView alloc]
                                  initWithFrame:CGRectMake(0, 50, width, height/2+70) style:UITableViewStylePlain];
        tableView.delegate = self;
        tableView.dataSource = self;
        tableView;
    });
    // 取消多余的横线
    self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
    
    [self.view addSubview:self.tableView];
    
    [self loadData];
}

加载数据,定义titleList,contentList

- (void)loadData {
    self.titleList = [NSMutableArray array];
    self.contentList = [NSMutableArray array];
    [self.titleList addObjectsFromArray:[[NSArray alloc] initWithObjects:@"头像", @"昵称", @"账号", @"性别", @"地区",nil]];
    [self.contentList addObjectsFromArray:[[NSArray alloc] initWithObjects:@"小猪佩奇", @"Peppa", @"peppy", @"female", @"UK",nil]];
}
  • 用numberOfSectionsInTableView定义tableview的section数目
  • 用heightForRowAtIndexPath定义tableview每一个cell的高度
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.titleList.count;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 80;
}

根据不同的行来给每个cell自定义accessoryView, 并绑定不同的数据。这里我们第一行是图片,故需要特殊处理来显示图片,而其他行则是显示内容,我这里修改一下它的字体与大小使得更加美观,对比度更加高。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *cellID = [NSString stringWithFormat:@"cellID:%zd", indexPath.section];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
    if (nil == cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
    }
    
    // cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    cell.textLabel.text = self.titleList[indexPath.row];
    cell.textLabel.font = [UIFont fontWithName:@"PingFangSC-Regular" size:18.f];
    if (indexPath.row != 0){
        UILabel *rightLabel = [[UILabel alloc]initWithFrame:CGRectMake(0,0,70,55)];
        rightLabel.text = self.contentList[indexPath.row];
        rightLabel.font = [UIFont fontWithName:@"PingFangSC-Regular" size:14.f];
        rightLabel.textColor = [UIColor grayColor];
        cell.accessoryView = rightLabel;
        //cell.accessoryView.backgroundColor = [UIColor redColor];   //加上红色容易看清楚
    }
    else{
        cell.accessoryView = ({
            UIImageView *imgV = [[UIImageView alloc] initWithImage:[UIImage imageNamed:self.User.ProfilePicture]];
            CGRect frame = imgV.frame;
            frame = CGRectMake(0, 0, 100, 55);
            imgV.frame = frame;
            [imgV setContentMode:UIViewContentModeScaleAspectFit];
            imgV;
        });
    }
    return cell;
}

点击列表的item时跳转至修改页面

#pragma mark ------------ UITableViewDelegate ------------------

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    InfoViewController *controller = [[InfoViewController alloc] init];
    controller.hidesBottomBarWhenPushed = YES;
    [self.navigationController pushViewController:controller animated:YES];
}

用户退出按钮绑定事件,这里要与服务器进行交互,删除之前登陆的session

- (IBAction)logout:(id)sender {
    NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate:nil delegateQueue:[NSOperationQueue mainQueue]];
    NSURL *url = [NSURL URLWithString:@"http://118.89.65.154:8000/account/logout/"];
    NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if(error == nil) {
            if(NSClassFromString(@"NSJSONSerialization")) {
                NSError *e = nil;
                id object = [NSJSONSerialization JSONObjectWithData:data options:0 error:&e];
                if(e) {
                    NSLog(@"error");
                }
                if([object isKindOfClass:[NSDictionary class]]) {
                    NSDictionary *result = object;
                    if([result[@"state"] isEqualToString:@"ok"]) {
                        NSLog(@"logout success");
                        UIStoryboard *indexStoryboard = [UIStoryboard storyboardWithName:@"Index" bundle:nil];
                        [UIApplication sharedApplication].keyWindow.rootViewController = indexStoryboard.instantiateInitialViewController;
                    }
                    else {
                        NSLog(@"logout fail");
                    }
                }
                else {
                    NSLog(@"Not dictionary");
                }
            }
        }
        else {
            NSLog(@"网络异常");
        }
    }];
    [task resume];
}
2. 个人信息修改页面

效果截图

a.处理查看与修改个人详情的区别

由于个人详情页面是用于两种情况,一是点击tab进入个人信息,二是在好友列表中点击好友在发起聊天前的个人信息。

用户个人详情页面

这两个页面复用同一个UI,只是最下面的button的字进行判断改变。

我在点击事件跳转前添加判断,你所点击的用户是否是当前登陆的用户。若是则可以修改跳转,若不是则不允许触发点击事件。

// 判断当前用户
self.User == [[UserManager getInstance] getLoginModel]
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    // 处理跳转情况
    if (self.User == [[UserManager getInstance] getLoginModel]){
        NSString* str = self.titleList[indexPath.row];
        // 处理头像的情况
        if([str isEqualToString:@"头像"]){
            // 修改本地显示
            [self alterHeadPortrait];
            
            // 上传到云端
            
        }
        InfoModifiedViewController *controller = [[InfoModifiedViewController alloc] initWithString:str];
        controller.hidesBottomBarWhenPushed = YES;
        [self.navigationController pushViewController:controller animated:YES];
    }
    else{
        NSLog(@"不能修改别的用户信息");
    }
}
2. 个人详情页面跳转修改页面

在完成个人详情页面的前提下,为每个item添加相应的跳转页面。例如点击昵称,则跳转到昵称的修改页面。而点击性别,则跳转到性别的修改页面,这里是一组选择按钮。

如何处理不同的item呢,这里是通过上个页面所传递的参数所决定的。由于点击事件可以获取改item的行号,自然就能获得该行的元素信息。我通过这个信息传递到下一个修改页面,就可以复用其中的代码,唯一的修改也只是hint部分。

// 修改别的文字信息
else{
    // 提示信息
    NSMutableString* commonHint = @"请输入你要修改的";

    // 获取屏幕的宽高
    CGRect rect = [[UIScreen mainScreen] bounds];
    CGSize size = rect.size;
    CGFloat width = size.width;
    CGFloat height = size.height;
    // 设置背景颜色
    self.view.backgroundColor = [UIColor whiteColor];

    self.editText = [[UITextField alloc]initWithFrame:CGRectMake(10, 30, width-30, 30)];

    self.editText.placeholder = [commonHint stringByAppendingString:self.titleText];
    [self.editText setValue:[UIColor grayColor] forKeyPath:@"_placeholderLabel.textColor"];
    [self.editText setValue:[UIFont boldSystemFontOfSize:12] forKeyPath:@"_placeholderLabel.font"];

    // 下划线
    UIView * onLine = [[UIView alloc]initWithFrame:CGRectMake(0,self.editText.frame.size.height-2,self.editText.frame.size.width,2)];
    onLine.backgroundColor = [UIColor blackColor];

    // 添加导航栏右侧按钮
    [self addRightBtn];
    [self.editText addSubview:onLine];
    [self.view addSubview:self.editText];
}

这里为了确定修改,在导航栏的右侧添加了确定按钮。并对这个确定按钮绑定事件,当点击的时候就会返回修改了的信息到之前的个人详情页面,从而达到修改用户个人详情的功能。

- (void)addRightBtn {
    UIBarButtonItem *rightBarItem = [[UIBarButtonItem alloc] initWithTitle:@"确认" style:UIBarButtonItemStylePlain target:self action:@selector(onClickedOKbtn)];
    self.navigationItem.rightBarButtonItem = rightBarItem;
}

- (void)onClickedOKbtn {
    NSLog(@"onClickedOKbtn");
    [self goBackToPersonInfoVCWithNickName:@"test"];
}

- (void) goBackToPersonInfoVCWithNickName:(NSString *) nickName{
    InfoViewController *infoVC = [self.navigationController.viewControllers objectAtIndex:self.navigationController.viewControllers.count-2];
    //初始化其属性
    //传递参数过去
//    UserModel user;
//    infoVC.User = user;
    //使用popToViewController返回并传值到上一页面
    [self.navigationController popToViewController:infoVC animated:true];
    
}

对于性别的修改,由于只有两种情况,所以我们不想让用户来进行输入,避免非法输入,所以我这里利用button group来进行处理,顺便对于这一组件来进行学习。

// 修改性别需要单独处理,使用button来选择而不是输入
if ([self.titleText isEqualToString:@"性别"]) {
    [self markArray];
    [self btnArray];
    [self setupRadioBtnView];
    // 添加导航栏右侧按钮
    [self addRightBtn];
}

首先是初始化数据

- (NSArray *)markArray {
    if (!_markArray) {
        NSArray *array = [NSArray array];
        array = @[@"男", @"女",@"unknown"];
        _markArray = array;
    }
    return _markArray;
}

- (NSMutableArray *)btnArray {
    if (!_btnArray) {
        NSMutableArray *array = [NSMutableArray array];
        _btnArray = array;
        
    }
    return _btnArray;
}

接着定义UI显示

- (void)setupRadioBtnView {
    CGFloat UI_View_Width = [UIScreen mainScreen].bounds.size.width;
    CGFloat marginX = 15;
    CGFloat top = 100;
    CGFloat btnH = 30;
    CGFloat width = (250 - marginX * 4) / 3;
    // 按钮背景
//    UIView *btnsBgView = [[UIView alloc] initWithFrame:CGRectMake((UI_View_Width - 250) * 0.5, 50, 250, 300)];
    self.view.backgroundColor = [UIColor whiteColor];
//    [self.view addSubview:btnsBgView];
    // 循环创建按钮
    NSInteger maxCol = 2;
    for (NSInteger i = 0; i < 2; i++) {
        
        UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
        btn.backgroundColor = [UIColor grayColor];
        btn.layer.cornerRadius = 3.0; // 按钮的边框弧度
        btn.clipsToBounds = YES;
        btn.titleLabel.font = [UIFont boldSystemFontOfSize:12];
        btn.titleLabel.textColor = [UIColor blackColor];
        [btn addTarget:self action:@selector(chooseMark:) forControlEvents:UIControlEventTouchUpInside];
        NSInteger col = i % maxCol; //列
        CGFloat x = marginX + col * (width + marginX);
        NSInteger row = i / maxCol; //行
        CGFloat y = top + row * (btnH + marginX);
        btn.frame=CGRectMake(x+UI_View_Width/2-width-marginX, y, width, btnH);
        [btn setTitle:self.markArray[i] forState:UIControlStateNormal];
        [self.view addSubview:btn];
        btn.tag = i;
        [self.btnArray addObject:btn];
    }
    
    // 创建完btn后再判断是否能选择(之前是已经选取过的)
    for (UIButton *btn in self.view.subviews) {
        if ([@"男" isEqualToString:btn.titleLabel.text]) {
            btn.selected = YES;
            btn.backgroundColor = [UIColor blueColor];
            break;
        }
    }
}

最后,处理点击事件,判断点击的是什么按钮

- (void)chooseMark:(UIButton *)sender {
    NSLog(@"点击了%@", sender.titleLabel.text);
    
    self.selectedBtn = sender;
    
    sender.selected = !sender.selected;
    
    for (NSInteger j = 0; j < [self.btnArray count]; j++) {
        UIButton *btn = self.btnArray[j] ;
        if (sender.tag == j) {
            btn.selected = sender.selected;
        } else {
            btn.selected = NO;
        }
        btn.backgroundColor = [UIColor grayColor];
    }
    
    UIButton *btn = self.btnArray[sender.tag];
    if (btn.selected) {
        btn.backgroundColor = [UIColor blueColor];
    } else {
        btn.backgroundColor = [UIColor grayColor];
    }
}

完成这三步,一个可选择并记录历史选择的按钮组就实现完成了。它不仅仅适用于这里,还可以适用于普适性的场景,仅仅需要修改数据的大小即可。

修改用户个人详情,与服务器交互

修改用户个人详情,包括修改用户的昵称,性别,地区的信息。这些都是可以通过键值对来进行修改,比修改图片要容易,这里利用tableview的选择函数进行判断,修改的是哪一个内容,然后再根据这个内容选择不同的api进行修改。

// 上传到云端
if ([str isEqualToString:@"昵称"])
    [[UserManager getInstance] modifyInfo:@"Nickname" withValue:self.User.NickName];
else if ([str isEqualToString:@"性别"])
    [[UserManager getInstance] modifyInfo:@"Gender" withValue:self.User.Gender];
else if ([str isEqualToString:@"地区"])
    [[UserManager getInstance] modifyInfo:@"Region" withValue:self.User.Birthplace];

modifyInfo函数

  • 定义参数
  • 定义handler
  • 定义api
  • 使用之前实现的SessionHelper工具类进行put请求,修改用户对应属性的值
// 根据要修改的属性attr,与修改后的值value来调用网络api
-(void) modifyInfo:(NSString *)attr withValue:(NSString *)value
{
    void (^modifyInfoEvent)(id) = ^void (id object)
    {
        NSDictionary *result = object;
        if([result[@"state"] isEqualToString:@"ok"])
        {
            NSLog(@"modifyInfo success");
        }
        else
        {
            NSLog(result[@"msg"]);
            NSLog(@"modifyInfo fail");
        }
    };
    NSString *params = [[NSString alloc] initWithFormat:@"value=%@", value];
    NSString *api = [[NSString alloc] initWithFormat:@"/account/info/%@", attr];
    NSLog(api);
    NSLog(params);
    [SessionHelper sendRequest:api method:@"put" parameters:params handler:modifyInfoEvent];
}
3. 修改头像

效果截图

修改头像的功能需要访问我们手机的图库,所以首先要判断权限,接着利用UIAlertController弹出提示框,通过点击提示框的相册选择来选择一张图片,最后通过回调函数来获取该图片的信息来对个人详情页面进行改变。

- (void)alterHeadPortrait{
    PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
    // 判断授权情况
    if (status == PHAuthorizationStatusRestricted ||
        status == PHAuthorizationStatusDenied) {
        //无权限  这个时候最好给个提示,用户点击是就跳转到应用的权限设置内 用户动动小手即可允许权限
        NSLog(@"no auth");
    }
    else{
        NSLog(@"has auth!!!!!");
    }
    //初始化提示框
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
    //按钮:从相册选择,类型:UIAlertActionStyleDefault
    [alert addAction:[UIAlertAction actionWithTitle:@"从相册选择" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        //初始化UIImagePickerController
        UIImagePickerController *PickerImage = [[UIImagePickerController alloc]init];
        //获取方式1:通过相册(呈现全部相册),UIImagePickerControllerSourceTypePhotoLibrary
        //获取方法2,通过相册(呈现全部图片),UIImagePickerControllerSourceTypeSavedPhotosAlbum
        PickerImage.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
        //允许编辑,即放大裁剪
        PickerImage.allowsEditing = YES;
        //自代理
        PickerImage.delegate = self;
        //页面跳转
        [self presentViewController:PickerImage animated:YES completion:nil];
    }]];
    //按钮:取消,类型:UIAlertActionStyleCancel
    [alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]];
    [self presentViewController:alert animated:YES completion:nil];
    
}

选择完成后的回调函数处理

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *) info{
    //定义一个newPhoto,用来存放我们选择的图片。
    // UIImagePickerControllerMediaURL 获取媒体的url
    UIImage *newPhoto = [info objectForKey:@"UIImagePickerControllerEditedImage"];
    
    NSData *data = UIImageJPEGRepresentation(newPhoto,0.1);
    UIImage *newPhoto2 = [UIImage imageWithData: data];
    
    // resize photo
    CGSize size={100, 100};
    UIGraphicsBeginImageContext(CGSizeMake(size.width, size.height));
    [newPhoto2 drawInRect:CGRectMake(0, 0, size.width, size.height)];
    UIImage *reSizeImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    [self dismissViewControllerAnimated:YES completion:nil];
    self.User.ProfilePicture = @"image";
    UIImageView *imageView = [[UIImageView alloc] initWithImage:reSizeImage];
    self.head = imageView;
    [self.tableView reloadData];
    
    // 上传到云端
    [[UserManager getInstance] uploadImage:@"/account/info/avatar" withImage:reSizeImage];
}

回调函数主要进行三样操作,首先是对获取到的图片进行剪切成正方形,显示会更加美观。其次,修改后返回到个人信息页面,在个人信息页面显示更新后到图像,这里先使用的是本地的图片,而不是重新从服务器上拉取,这样子显示效果和效率都更加的高。最后一步就是上传图片到云端。

使用并封装后台上传图片的接口

完成显示后,下一步要进行的就是上传服务器的工作。这里服务器提供的接口是需要上传form-data/multipart的文件,这与其他个人信息改变有些区别,不是简单的键值对。这里我使用到了AFNetworking的第三方网络开源工具库。

这里有几个关键的步骤:

  • 定义session 设置为multipart/form-data
  • 处理url,定义好api
  • 将UIImage图片转成NSData
  • 将NSData加入到formData后就可以执行post请求
  • 处理上传结果回调
// 上传图片到服务器
-(void) uploadImage:(NSString* )path withImage:(UIImage* )image
{
    AFHTTPSessionManager *session = [AFHTTPSessionManager manager];
    [session.requestSerializer setValue:@"multipart/form-data" forHTTPHeaderField:@"Content-Type"];
    
    // 处理url
    NSString* serverDomain = @"http://172.18.32.97:8000";
    NSString* urlString = [serverDomain stringByAppendingString:path];
    NSLog(urlString);
    [session POST:urlString parameters:nil constructingBodyWithBlock:
     ^(id<AFMultipartFormData> _Nonnull formData){
        // 图片转data
        NSData *data = UIImagePNGRepresentation(image);
        [formData appendPartWithFileData :data name:@"file" fileName:@"iName.png"
                                 mimeType:@"multipart/form-data"];
    } progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nonnull responseObject){
        NSLog(@"uploadImage success");
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error){
        NSLog(@"uploadImage fail");
        NSLog(error.localizedDescription);
    }];
}
4. 好友聊天——图片发送与显示

效果截图

a. 使用SDWebImage框架

使用SDWebImage框架改写之前图片的显示,使得图片的显示不必要每次都从服务器拿取,而是先检查本地的缓存是否有保存过类似的图片

需求:IM聊天工具在用户登陆后会去服务器请求用户的信息,其中就包括用户的头像信息。我们的IM服务器会返回头像图片的url,这时需要客户端在加载的时候更新头像显示图片。

获取图片的url

// 获取用户的信息
-(void) getInfo{
  AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
  NSString *url = [URLHelper getURLwithPath:@"/account/info"];
  [manager GET:url parameters:nil progress:nil
   success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
     NSLog(@"getInfo success");
     self.loginUser = [[UserModel alloc] initWithProperties:responseObject[@"data"][@"Username"]
                       NickName:responseObject[@"data"][@"Nickname"]
                       RemarkName:responseObject[@"data"][@"Username"]
                       Gender:responseObject[@"data"][@"Gender"]
                       Birthplace:responseObject[@"data"][@"Region"]
                       // 这里就是头像的url
                       ProfilePicture:responseObject[@"data"][@"Avatar"]];
     NSLog(responseObject[@"data"][@"Avatar"]);
     [[DatabaseHelper getInstance] registerNewMessagesListener];
     [self.socket SRWebSocketOpen];
   }
   failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
     NSLog(@"getInfo fail");
     NSLog(@"%@", error.localizedDescription);
   }];
}

根据url来加载网络图片

// 使用SDWebImage第三方库加载网络图片,先设置默认头像等待网络请求

// step 1 : 定义UIImageView
UIImageView *imgV = [[UIImageView alloc]init];
// step 2 : 获取url的string后缀
NSString *imagePath = [SERVER_DOMAIN stringByAppendingString:self.User.ProfilePicture];
// step 3 :拼接字符串并转换为url类型
[imgV sd_setImageWithURL:[NSURL URLWithString:imagePath]
 // step 4 :设置默认图片,在本地的一张图片
 placeholderImage:[UIImage imageNamed:@"peppa"]
 // step 5 :加载完成后的函数,输出错误方便我们debug
 completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
   NSLog(@"error== %@",error);
 }];
b. 压缩图片后再上传

避免上传时间过长,这里只上传缩略图,所以必须对读取后的图片进行压缩操作。

// 压缩图片
NSData *data = UIImageJPEGRepresentation(image,0.1);

这里使用的是UIImageJPEGRepresentation函数,第一个参数为原图像,第二个参数为压缩率,经过测试手机原本1.7mb的图片经过该函数仅为56kb,使得无论是上传还是下载的速度更加快

c. 聊天界面添加发送图片的按钮
self.imageButton = [UIButton buttonWithType:UIButtonTypeContactAdd];
self.imageButton.frame = CGRectMake(SCREEN_WIDTH - 65, SCREEN_HEIGHT - 45, 40, 40);
[self addSubview:self.imageButton];

// 添加点击事件:从图库中选择一张图片
// 选择图片
- (void)chooseImage:(UIButton *)btn
{
    [self alterHeadPortrait];
}
d. 与服务器接口的交互

四个参数

  • 后台接口api字段
  • 图片 UImage类型
  • 用户名 NSString类型
  • 时间戳 NSDate类型

发送图片,包括要将图片传到服务器,还需要显示在chatview页面上,发送前判断该用户是否是你的好友,若不是则进行添加好友的通知显示。

与发送文字消息类似,发送图片需要将这条消息插入到数据库中,下次获取聊天记录则先从本地数据库查找出来显示。

// 发送图片
- (void)sendImage:(UIImage *)image {
    NSDate* timestamp = [NSDate date];
    // 网络部分
    NSString* path = @"/content/image";
    NSString* userName = self.chatUser.UserID;
    
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    [manager.requestSerializer setValue:@"multipart/form-data" forHTTPHeaderField:@"Content-Type"];
    
    // 处理url
    NSString* urlString = [URLHelper getURLwithPath:path];
    NSLog(@"%@", urlString);
    // 添加参数
    NSDictionary* params = @{@"to":userName, @"timestamp":[self.dateFormatter stringFromDate:timestamp]};
    // 发送图片
    [manager POST:urlString parameters:params constructingBodyWithBlock:
     ^(id<AFMultipartFormData> _Nonnull formData){
         // 图片转data
         // 压缩图片
         NSData *data = UIImageJPEGRepresentation(image,0.1);
         [formData appendPartWithFileData :data name:@"file" fileName:@"928-1.jpeg"
                                  mimeType:@"multipart/form-data"];
     } progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nonnull responseObject){
         NSLog(@"%@", responseObject[@"msg"]);
         NSLog(@"%@", responseObject[@"data"]);
         if([responseObject[@"state"] isEqualToString:@"ok"])
         {
             NSLog(@"send success");
             // 本地显示部分
             MessageModel* message = [[MessageModel alloc] init];
             message.Type = @"image";
             message.SenderID = self.loginUser.UserID;
             message.ReceiverID = self.chatUser.UserID;
             message.Content = responseObject[@"data"];
             //    message.ContentImage = image;
             message.TimeStamp = timestamp;
             [self addMessage:message];
             [self.databaseHelper insertMessageWithMessage:message];
         }
         else
         {
             NSLog(@"send fail");
             NSString* msg = @"你不是对方的好友";
             UIAlertController * alert = [UIAlertController
                                          alertControllerWithTitle:msg
                                          message:@""
                                          preferredStyle:UIAlertControllerStyleAlert];
             
             UIAlertAction* yesButton = [UIAlertAction
                                         actionWithTitle:@"确定"
                                         style:UIAlertActionStyleDefault
                                         handler:^(UIAlertAction * action) {
                                             //Handle your yes please button action here
                                         }];
             
             [alert addAction:yesButton];
             [self presentViewController:alert animated:YES completion:nil];
         }
         
     } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error){
         NSLog(@"sendImage fail");
         NSLog(@"%@", error.localizedDescription);
     }];
}

这个与头像图片上传,主要区别是多了一个body的参数,可以在post方法中添加参数即可,若仅需上传图片,则参数填为nil。该body参数保存的是当前消息的序号与时间戳,与发送方接收方的账号。

e.实现IM的聊天记录,基于UITableView图文混排

由于在实现了文字纯文本的好友聊天功能后,我们添加了发送图片这一功能,可以在好友记录中显示出来。这一来需要我改变之前显示聊天记录的TableView的显示,我使用的方案为UITextView结合NSAttributeString实现图文混排编辑

与文字消息不同

···
// 判断是图片类型的消息
else if ([model.Type isEqualToString:@"image"]){
        // 1.使用url来获取图片,而不是传参数
        self.contentImage = [[UIImageView alloc]init];
        NSString *imagePath = [URLHelper getURLwithPath:model.Content];
        
        //2.初始化富文本对象
        NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@""];
        // 3.初始化NSTextAttachment对象
        NSTextAttachment *attachment = [[NSTextAttachment alloc]init];
        attachment.bounds = CGRectMake(0, 0, 100, 100);//设置frame
        
  		  // 利用SDWebImageManager的loadImageWithURL下载图片,并保存在缓存中,避免多次访问服务器
        SDWebImageManager *manager = [SDWebImageManager sharedManager];
        [manager loadImageWithURL:[NSURL URLWithString:imagePath]
                          options:0
                         progress:nil
                        completed:^(UIImage *image, NSData *imageData, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                            if (image) {
                              //设置图片
                                attachment.image = image;
                            }
                        }];
        
        //4.创建带有图片的富文本
        NSAttributedString *string = [NSAttributedString attributedStringWithAttachment:(NSTextAttachment *)(attachment)];
  			//添加到尾部
        [attributedString appendAttributedString:string];   
        self.contentLabel.attributedText = attributedString;
        labelSize = [attributedString 
                     boundingRectWithSize: CGSizeMake(SCREEN_WIDTH-160, MAXFLOAT)     
                     options: NSStringDrawingUsesLineFragmentOrigin | 
                     					NSStringDrawingTruncatesLastVisibleLine
                     context: nil].size;
        self.contentLabel.frame = CGRectMake(isLoginUser ? 10 : 20 , 5, labelSize.width, labelSize.height + 10);
    }

如何显示根据图片计算气泡

  • 需要改变cell的高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    MessageModel *msgModel = self.chatMsg[indexPath.row];
    // 计算文字高度需和自定义cell内容尺寸同步
    if ([msgModel.Type isEqualToString:@"text"]){
    }
  	//计算图片高度需和自定义cell内容尺寸同步
    else {
      // 根据那个富文本的高度来决定
       NSAttributedString *string = [NSAttributedString attributedStringWithAttachment:(NSTextAttachment *)(attchment)];
        [attributedString appendAttributedString:string];
        CGSize labelSize = [attributedString 
                            boundingRectWithSize: CGSizeMake(SCREEN_WIDTH-160, MAXFLOAT)              
                            options: NSStringDrawingUsesLineFragmentOrigin | 
                            			NSStringDrawingTruncatesLastVisibleLine
                            context: nil].size;
        return labelSize.height + 40;
    }
  • 拉伸气泡
//计算气泡位置
CGFloat bubbleX = isLoginUser ? (SCREEN_WIDTH - ICON_WH - 25 - labelSize.width - 30) : (ICON_WH + 25);
self.bubbleIV.frame = CGRectMake(bubbleX, 20, self.contentLabel.frame.size.width + 30, self.contentLabel.frame.size.height+10);

//头像位置
CGFloat iconX = isLoginUser ? (SCREEN_WIDTH - ICON_WH - 15) : 15;
self.iconIV.frame = CGRectMake(iconX, 15, ICON_WH, ICON_WH);

NSString *imagePath = [URLHelper getURLwithPath:isLoginUser ? loginUser.ProfilePicture : [UserManager getInstance].chatUser.ProfilePicture];
[self.iconIV sd_setImageWithURL:[NSURL URLWithString:imagePath]
 placeholderImage:[UIImage imageNamed:@"peppa"]
 completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
   NSLog(@"error== %@",error);
 }];

//拉伸气泡
UIImage *backImage = [UIImage imageNamed: isLoginUser ?  @"bubble_right" : @"bubble_left"];
backImage = [backImage resizableImageWithCapInsets:UIEdgeInsetsMake(30, 30, 10, 30) resizingMode:UIImageResizingModeStretch];
self.bubbleIV.image = backImage;

四、实验思考及感想

​ 经过为期十周的开发,我们小组终于完成了相当不错的IM聊天工具,无论是UI还是功能上都比较理想。首先非常感谢小组另外三名同学在开发过程的帮助与讨论,大家都非常认真且负责地完成实验。一款好的IOS应用肯定也离不开功能强大且健壮性好的后台支持,我在开发前期也参与了部分后台的开发,写了几个api的接口,也基本了解了后台设计的过程。到基本完成后台内容后,我回到IOS客户端的开发,我负责的部分包括聊天工具的个人信息页面,这也是该应用三大主页面之一,其次还负责了该应用有关图片部分的处理,例如好友间的图片发送,修改个人的头像等需要操作图片,读取手机的图片库,上传图片,从服务器读取图片到本地等操作。

​ 在实验过程中,我也遇到了不少问题,例如如何上传图片,处理图片的显示等。这里用到了几个开源的库包括SDWebImage来显示网络图片并缓存到本地,减少多次进行网络访问,优化用户的体验,AFNetworking来处理网络请求,将图片转化为multipart再进行上传。最为困难的部分应该在于如何将图片显示在聊天记录中,由于聊天记录要文字与图片混排,且要根据内容的长度来自定义cell的长度,这里我是用富文本文字来解决这一困难,且用SDWebImage中的下载图片保存在缓存中,避免加载和reloaddata时候的重复网络请求。

​ 最后,在开发过程中也少不了字节跳动IOS工程师的帮助,其间的两次答疑,我们小组通过展示与提问也获得了不少解决问题的思路,例如通过SequenceNumber来解决好友添加,消息获取等问题。通过这次的IOS实训,我学习到了不仅仅是IOS客户端的知识,更从实践中感受到了开发的流程,对于日后职业的发展也有了更深的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值