效果图:
![](https://img.haomeiwen.com/i875869/b19e912e766931f1.gif)
iPod.gif
![](https://img.haomeiwen.com/i875869/68932f08df5e5a76.gif)
iPhone.gif
-
上一篇已实现了Socket的连接及文字/表情符的相互发送
- 接下来实现的是图片的传输
1. 图片的来源相册/相机
2. 该项目没有服务端也没有云存储所以图片需要保存至本地 (ps:聊天记录的保存准备在最后在建立数据库)
3. 图片的发送策略
4. 图片的接收策略
![](https://img.haomeiwen.com/i875869/0823c51c519f9b0f.GIF)
IMG_2437.GIF
一.键盘右边的+按钮的实现(ESAddOpationView)
// Created by 张海军 on 2017/11/27.
// Copyright © 2017年 baoqianli. All rights reserved.
// 加号按钮的view
#import <UIKit/UIKit.h>
@class OpationItem;
typedef NS_ENUM(NSInteger, OpationItem_type)
{
OpationItem_image = 0 // 照片选择
};
@interface ESAddOpationView : UIView
/// 功能选项数组
@property (nonatomic, strong) NSArray<OpationItem*>* opationItem;
/// 选中回调
@property (nonatomic, copy) void(^selectedOpationHandle)(OpationItem_type type);
+ (instancetype)addOpationView;
@end
// 选项模型
@interface OpationItem : NSObject
/// 选项名称
@property (nonatomic, copy) NSString *itemName;
/// 选项图片名
@property (nonatomic, copy) NSString *itemIconName;
/// 类型
@property (nonatomic, assign) OpationItem_type type;
+ (instancetype)opationItemWithName:(NSString *)itemName iconName:(NSString *)iconName type:(OpationItem_type)type;
@end
// 自定义选项按钮
@interface OpationButton : UIButton
@end
与表情键盘的相互切换
// 1.表情键盘(ESEmoticonView) 功能选项(ESAddOpationView) 加到通过一个父view(inputView) 设置键盘的inputView 通过显示隐藏来实现
self.inputTextView.inputView = inputView;
// 表情按钮的点击
- (void)emoticonButtonDidClick:(UIButton *)button
{
if (self.addButton.selected) {
self.addButton.selected = NO;
}
self.addOpationView.hidden = YES;
self.emoticonView.hidden = NO;
if (!self.emoticonView.superview) {
[self.inputView addSubview:self.emoticonView];
}
[self changeInputView:self.inputView selected:button.selected];
button.selected = !button.selected;
}
// 功能选项按钮点击
- (void)addButtonDidClick:(UIButton *)button{
if (self.emoticonButton.selected) {
self.emoticonButton.selected = NO;
}
self.emoticonView.hidden = YES;
self.addOpationView.hidden = NO;
if (!self.addOpationView.superview) {
[self.inputView addSubview:self.addOpationView];
}
[self changeInputView:self.inputView selected:button.selected];
button.selected = !button.selected;
}
二.图片选择及回调
#import <Foundation/Foundation.h>
#import <TZImagePickerController.h>
typedef void(^pickerFinishBlock)(NSArray<UIImage *> *photos,NSArray *assets );
@interface PickerImageVideoTool : NSObject
+ (instancetype)sharePickerImageVideoTool;
- (void)showImagePickerWithMaxCount:(NSInteger)maxCount completion:(pickerFinishBlock)finishBlock;
@end
三.触发图片选择的回调仍有 ESKeyBoardToolView 来触发
#import <UIKit/UIKit.h>
#import "ESAddOpationView.h"
/// 输入框最多显示多少行
static NSInteger maxLines = 4;
/// 输入框的高度
static CGFloat const TitleViewHeight = 44.0;
typedef NS_ENUM(NSInteger, ESKeyBoardToolView_type)
{
ESKeyBoardToolView_typeEmoticon = 0, // 表情按钮的点击
ESKeyBoardToolView_typeAdd // 加号按钮的点击
};
@class ESKeyBoardToolView;
@protocol ESKeyBoardToolViewDelegate <NSObject>
@optional
/// 加号选项view的点击
- (void)ESKeyBoardToolViewAddOpationDidSelected:(ESKeyBoardToolView *)view withType:(OpationItem_type)type;
/// 点击发送按钮
- (void)ESKeyBoardToolViewSendButtonDidClick:(ESKeyBoardToolView *)view message:(NSString *)message;
/// 当正在编辑文字时view的Y值变化
- (void)ESKeyBoardToolViewDidEditing:(ESKeyBoardToolView *)view changeY:(CGFloat)yValue;
/// 结束编辑回调
- (void)ESKeyBoardToolViewDidEndEdit:(ESKeyBoardToolView *)view;
@end
四. ESKeyBoardToolViewDelegate 的代理实现
#pragma mark - ESKeyBoardToolViewDelegate
- (void)ESKeyBoardToolViewAddOpationDidSelected:(ESKeyBoardToolView *)view withType:(OpationItem_type)type{
switch (type) {
case OpationItem_image:{
[self sendImageOrVideo];
}
break;
default:
break;
}
}
}
图片选择完成后的回调
- (void)sendImageOrVideo{
WS(weakSelf);
[[PickerImageVideoTool sharePickerImageVideoTool] showImagePickerWithMaxCount:1 completion:^(NSArray<UIImage *> *photos, NSArray *assets) {
NSInteger count = assets.count;
id objc = nil;
for (NSInteger i = 0; i < count; i++) {
objc = assets[i];
if (![objc isKindOfClass:[PHAsset class]]) {
continue;
}
PHAsset *asset = (PHAsset *)objc;
ChatMessageModel *messageM = [ChatMessageModel new];
messageM.isFormMe = YES;
messageM.userName = [UIDevice currentDevice].name;
messageM.asset = asset;
messageM.fileName = [ZPPublicMethod getAssetsName:asset only:YES];
messageM.temImage = photos[i];
if (asset.mediaType == PHAssetMediaTypeImage){
messageM.chatMessageType = ChatMessageImage;
messageM.fileSize = UIImagePNGRepresentation(messageM.temImage).length;
// 本地存储
SDImageCache *cache = [SDImageCache sharedImageCache];
[cache storeImage:messageM.temImage forKey:messageM.fileName toDisk:YES completion:^{
messageM.mediaMessageUrl = [NSURL fileURLWithPath:[cache defaultCachePathForKey:messageM.fileName]];
}];
// 发送图片
[weakSelf sendMessageWithItem:messageM];
}else if (asset.mediaType == PHAssetMediaTypeAudio){
// 预留处理
messageM.chatMessageType = ChatMessageAudio;
}else if (asset.mediaType == PHAssetMediaTypeVideo) {
// 预留处理
messageM.chatMessageType = ChatMessageVideo;
}
}
}];
五.图片的发送
- 由于是点对点的 所以复杂程度没那么高
- 发送图片前会先把图片的头部信息发送过去 接收端通过 isWaitAcceptFile 字段判断是否需要等待接受文件
- 发送端在接收到头部发送完成的回调后 进行图片的发送 变改变 isWaitAcceptFile = NO 状态
- 接受端通过 NSOutputStream 来进行图片的接收本地的存储并通过 ChatMessageModel 记录文件的URL地址
- 发送/接受完成后回调刷新
消息体的发送
/// 发送数据
- (void)sendMessageWithItem:(ChatMessageModel *)item{
self.currentSendItem = item;
NSData *textData = [self creationMessageDataWithItem:item];
[self writeMediaMessageWithData:textData];
}
// 创建消息体
- (NSData *)creationMessageDataWithItem:(ChatMessageModel *)item{
NSMutableDictionary *messageData = [NSMutableDictionary dictionary];
messageData[@"fileName"] = item.fileName;
messageData[@"userName"] = item.userName;
messageData[@"chatMessageType"] = [NSNumber numberWithInt:item.chatMessageType];
messageData[@"fileSize"] = [NSNumber numberWithInteger:item.fileSize];
if (item.chatMessageType == ChatMessageText) {
messageData[@"messageContent"] = item.messageContent;
}else if (item.chatMessageType == ChatMessageImage){
// 等待接受字段
item.isWaitAcceptFile = YES;
messageData[@"isWaitAcceptFile"] = [NSNumber numberWithBool:YES];
}
NSString *bodStr = [NSString hj_dicToJsonStr:messageData];
return [bodStr dataUsingEncoding:NSUTF8StringEncoding];
}
// 图片或者视频文件传输
- (void)imageOrVideoFileSend:(ChatMessageModel *)sendItem{
if (sendItem.chatMessageType == ChatMessageImage) {
NSData *sendData = UIImagePNGRepresentation(sendItem.temImage);
[self writeMediaMessageWithData:sendData];
}else if (sendItem.chatMessageType == ChatMessageVideo){
// 视频发送的处理
}
}
文件发送中及发送完成的回调
// 分段传输完成后的 回调
- (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag {
self.currentSendItem.upSize += partialLength;
if ([self.delegate respondsToSelector:@selector(socketManager:itemUpingrefresh:)] && (tag==self.currentSendItem.sendTag)) {
self.currentSendItem.isSending = YES;
[self.delegate socketManager:self itemUpingrefresh:self.currentSendItem];
}
}
// 文件传输完毕后的回调
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
if (self.currentSendItem.sendTag == tag) {
if (!self.currentSendItem.isWaitAcceptFile) {
self.currentSendItem.temImage = nil;
if ([self.delegate respondsToSelector:@selector(socketManager:itemUpFinishrefresh:)]) {
[self.delegate socketManager:self itemUpFinishrefresh:self.currentSendItem];
}
}else{
// 接下来需要传输文件
self.currentSendItem.isWaitAcceptFile = NO; // 改变状态
[self imageOrVideoFileSend:self.currentSendItem];
}
}
[self.tcpSocketManager setAutoDisconnectOnClosedReadStream:YES];
}
文件的接收处理
/// 接收到消息
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
NSString *readStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSDictionary *readDic = [readStr hj_jsonStringToDic];
if ([readDic isKindOfClass:[NSDictionary class]]) {
self.acceptItem = [ChatMessageModel mj_objectWithKeyValues:readDic];
self.acceptItem.isFormMe = NO;
self.acceptItem.finishAccept = self.acceptItem.chatMessageType != ChatMessageText ? NO : YES;
}else if (self.acceptItem.isWaitAcceptFile) {
self.acceptItem.finishAccept = NO;
self.acceptItem.acceptSize += data.length;
self.acceptItem.beginAccept = YES;
if (!self.outputStream) {
self.acceptItem.acceptFilePath = [self.dataSavePath stringByAppendingPathComponent:[self.acceptItem.fileName lastPathComponent]];
self.acceptItem.mediaMessageUrl = [NSURL fileURLWithPath:self.acceptItem.acceptFilePath];
self.outputStream = [[NSOutputStream alloc] initToFileAtPath:self.acceptItem.acceptFilePath append:YES];
[self.outputStream open];
}
// 输出流 写数据
NSInteger byt = [self.outputStream write:data.bytes maxLength:data.length];
if (self.acceptItem.acceptSize >= self.acceptItem.fileSize) {
self.acceptItem.finishAccept = YES;
[self.outputStream close];
self.outputStream = nil;
}
}
if ([self.delegate respondsToSelector:@selector(socketManager:itemAcceptingrefresh:)]) {
[self.delegate socketManager:self itemAcceptingrefresh:self.acceptItem];
}
[sock readDataWithTimeout:- 1 tag:0];
}
文件接收中及接收完成 table的展示
#pragma mark - SocketManagerDelegate
// 正在接受的文件回调
- (void)socketManager:(SocketManager *)manager itemAcceptingrefresh:(ChatMessageModel *)acceptingItem{
if (acceptingItem.finishAccept) {
[self.messageItems addObject:acceptingItem];
[self.tableView reloadData];
[self scrollToLastCell];
}else{
// 刷新当前进度
}
}
网友评论