局域网内端到端的聊天项目(四)

作者: _令_狐_冲_ | 来源:发表于2017-11-29 16:36 被阅读147次
    效果图: iPod.gif iPhone.gif
    • 上一篇已实现了Socket的连接及文字/表情符的相互发送
    • 接下来实现的是图片的传输
     1. 图片的来源相册/相机
     2. 该项目没有服务端也没有云存储所以图片需要保存至本地 (ps:聊天记录的保存准备在最后在建立数据库)
     3. 图片的发送策略
     4. 图片的接收策略
    
    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;
        }
      }
    }
    
    
    图片选择完成后的回调
    • 通过 SDImageCache 进行本地的缓存
    - (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{
            // 刷新当前进度
        }
    }
    

    相关文章

      网友评论

      本文标题:局域网内端到端的聊天项目(四)

      本文链接:https://www.haomeiwen.com/subject/vsnubxtx.html