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客户端的知识,更从实践中感受到了开发的流程,对于日后职业的发展也有了更深的理解。