iOS微信QQ聊天界面的UI框架以及Socket简单实现群聊功能

2.2日更新,socket简易群聊通信,之前实现的是静态本地聊天模拟

Socket群聊
最新版本Demo传送门

1.需要的先下载下来,先开启SocketSeverce 2 这个服务器代码,已经封装好了Socket建立和连接

2.打开工程,自动会连上服务器,已经写好了socket的生成和连接

3.再打开一个终端,模拟第二个客户端telnet 192.168.31.150 3667 输入之后就能进行简单的群聊功能

// 客户端示例代码
// 连接到聊天服务器
    GCDAsyncSocket *socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];

    [socket connectToHost:@"127.0.0.1" onPort:3667 error:nil];
    self.clientSocket = socket;
// 服务端部分示例代码
- (instancetype)init
{
    if (self = [super init]) {

        /**
         注意:这里的服务端socket,只负责socket(),bind(),lisence(),accept(),他的任务到底结束,只负责监听是否有客户端socket来连接
         */
        self.serviceSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
    }
    return self;
}

- (void)connected
{
    NSError *error = nil;
    // 给一个需要连接的端口,0-1024是系统的
    [self.serviceSocket acceptOnPort:3667 error:&error];
    if (error) {
        NSLog(@"3666服务器开启失败。。。。。%@",error);
    }
    else
    {
        NSLog(@"开启成功,并开始监听");
    }
}




很早之前有写过一个很简单的文本聊天的思路微信QQ聊天简单Demo传送门,这东西显然不能在项目中拿来用,常年混在github上的少年,你会发现,很多模仿微信和QQ的小项目或者大项目,都会涉及到一个非常优雅的框架,在github上也有将近1W的star,没错,他就是JSQMessagesViewController

搞事情啊
这里写图片描述


这里写图片描述

个人非常喜欢老外写的框架,而且是还是不断更新的,这个简直是不断学习的好资料啊,人家那面向对象封装的,值得学习,那么花时间来简单介绍下该框架的应用场景

核心类名介绍

  • Mode数据类

    • JSQAudioMediaItem.h 语音
    • JSQLocationMediaItem.h 定位
    • JSQMediaItem.h 非文件的Media基类
    • JSQMessage.h 所有消息都由该类包装,因此,最外层 collectionView用到的就是数组包含该类
    • JSQMessageAvatarImageDataSource.h 头像数据代理
    • JSQMessageBubbleImageDataSource.h 气泡数据代理
    • JSQMessageData.h 发送消息ID date代理
    • JSQMessageMediaData.h 非文本消息数据代理
    • JSQMessagesAvatarImage.h 头像类
    • JSQMessagesBubbleImage.h 气泡类
    • JSQMessagesCollectionViewDataSource.h
    • JSQMessagesCollectionViewDelegateFlowLayout.h
    • JSQPhotoMediaItem.h 图片
    • JSQVideoMediaItem.h 视频
  • View类

    • JSQMessagesCellTextView.m 纯文本TextView
    • JSQMessagesCollectionView.m 核心collectionView继承原生的
    • JSQMessagesCollectionViewCell.m 核心cell
    • JSQMessagesCollectionViewCellIncoming.xib 收到消息cell
    • JSQMessagesCollectionViewCellOutgoing.xib 发送消息cell
    • JSQMessagesComposerTextView.m 粘贴文本
    • JSQMessagesInputToolbar.m 底部的toolBar
    • JSQMessagesLabel.m 头部时间或者底部文字Label
    • JSQMessagesLoadEarlierHeaderView.xib 更多加载View
    • JSQMessagesMediaPlaceholderView.m MediaPlaceHolderView
    • JSQMessagesTypingIndicatorFooterView.xib 预加载指示Bubble

类虽然很多,但是肯定越多越好,说明功能越强大啊
这里写图片描述

1.万事开头难,第一步

创建一个ViewController继承与JSQMessagesViewController,然后来一个数据model,来存放所有接受和发出去的消息,各种类型上面已经介绍了,直接放h文件的代码

// VC
@interface MKJChatViewcontroller : JSQMessagesViewController<UIActionSheetDelegate, JSQMessagesComposerTextViewPasteDelegate>
@property (strong, nonatomic) DemoModelData *demoData; //!< 消息模型

- (void)receiveMessagePressed:(UIBarButtonItem *)sender;

// Model
**
 *  This is for demo/testing purposes only. 
 *  This object sets up some fake model data.
 *  Do not actually do anything like this.
 *  假数据,用来展示玩玩的,别当真
 */

static NSString * const kJSQDemoAvatarDisplayNameSquires = @"Jesse Squires";
static NSString * const kJSQDemoAvatarDisplayNameCook = @"Tim Cook";
static NSString * const kJSQDemoAvatarDisplayNameJobs = @"Jobs";
static NSString * const kJSQDemoAvatarDisplayNameWoz = @"Steve Wozniak";

static NSString * const kJSQDemoAvatarIdSquires = @"053496-4509-289";
static NSString * const kJSQDemoAvatarIdCook = @"468-768355-23123";
static NSString * const kJSQDemoAvatarIdJobs = @"707-8956784-57";
static NSString * const kJSQDemoAvatarIdWoz = @"309-41802-93823";

@interface DemoModelData : NSObject

/*
 *  这里放的都是JSQMessage对象 该对象有两个初始化方式 1.media or noMedia
 */

@property (strong, nonatomic) NSMutableArray *messages; // message数组

@property (strong, nonatomic) NSDictionary *avatars; // 聊天人所有头像

@property (strong, nonatomic) JSQMessagesBubbleImage *outgoingBubbleImageData; // 发出去的气泡颜色

@property (strong, nonatomic) JSQMessagesBubbleImage *incomingBubbleImageData; // 收到的气泡颜色

@property (strong, nonatomic) NSDictionary *users; // 用户名字信息

- (void)addPhotoMediaMessage;//!< 图片消息

- (void)addLocationMediaMessageCompletion:(JSQLocationMediaItemCompletionBlock)completion; //!< 定位小心

- (void)addVideoMediaMessage; //!< 视频 无底图

- (void)addVideoMediaMessageWithThumbnail; //!< 视频带底图

- (void)addAudioMediaMessage; //!< 音频


首先注意的,这里的数据都是faker,没错,就是faker大魔王,拿来玩玩而已,具体需要根据业务逻辑来,那么再来看看伪造的实现数据

2.搞事情,搞数据啊

// 纯文本JSQMessage对象创建
self.messages = [[NSMutableArray alloc] initWithObjects:
                     [[JSQMessage alloc] initWithSenderId:kJSQDemoAvatarIdSquires
                                        senderDisplayName:kJSQDemoAvatarDisplayNameSquires
                                                     date:[NSDate distantPast]
                                                     text:NSLocalizedString(@"Welcome to JSQMessages: A messaging UI framework for iOS.", nil)]
// 非纯文本JSQMessage对象创建之图片
JSQPhotoMediaItem *photoItem = [[JSQPhotoMediaItem alloc] initWithImage:[UIImage imageNamed:@"goldengate"]];
    JSQMessage *photoMessage = [JSQMessage messageWithSenderId:kJSQDemoAvatarIdSquires
                                                   displayName:kJSQDemoAvatarDisplayNameSquires
                                                         media:photoItem];
                                                         // 非纯文本JSQMessage对象创建之Location定位
                                                         CLLocation *ferryBuildingInSF = [[CLLocation alloc] initWithLatitude:37.795313 longitude:-122.393757];

    JSQLocationMediaItem *locationItem = [[JSQLocationMediaItem alloc] init];
    [locationItem setLocation:ferryBuildingInSF withCompletionHandler:completion];

    JSQMessage *locationMessage = [JSQMessage messageWithSenderId:kJSQDemoAvatarIdSquires
                                                      displayName:kJSQDemoAvatarDisplayNameSquires
                                                            media:locationItem];
                                                            // 非纯文本JSQMessage对象创建之视频
                                                            NSURL *videoURL = [NSURL URLWithString:@"http://qingdan.img.iwala.net/v/twt/twt1612_720P.mp4"];

    JSQVideoMediaItem *videoItem = [[JSQVideoMediaItem alloc] initWithFileURL:videoURL isReadyToPlay:YES];
    JSQMessage *videoMessage = [JSQMessage messageWithSenderId:kJSQDemoAvatarIdSquires
                                                   displayName:kJSQDemoAvatarDisplayNameSquires
                                                         media:videoItem];       
// 非纯文本JSQMessage对象创建之语音
NSString * sample = [[NSBundle mainBundle] pathForResource:@"jsq_messages_sample" ofType:@"m4a"];
    NSData * audioData = [NSData dataWithContentsOfFile:sample];
    JSQAudioMediaItem *audioItem = [[JSQAudioMediaItem alloc] initWithData:audioData];
    JSQMessage *audioMessage = [JSQMessage messageWithSenderId:kJSQDemoAvatarIdSquires
                                                   displayName:kJSQDemoAvatarDisplayNameSquires
                                                         media:audioItem];
// 最后都加到Model的数据里面
[self.messages addObject:JSQMessage对象];                                                                                           


3.搞头像和气泡

// 头像图片制作工具类
        // 新方法
        // 通过文字和颜色创建头像
        JSQMessagesAvatarImage *jsqImage = [JSQMessagesAvatarImageFactory avatarImageWithUserInitials:@"MKJ"
                                                                                      backgroundColor:[UIColor colorWithWhite:0.85f alpha:1.0f]
                                                                                            textColor:[UIColor colorWithWhite:0.60f alpha:1.0f]
                                                                                                 font:[UIFont systemFontOfSize:14.0f]
                                                                                             diameter:kJSQMessagesCollectionViewAvatarSizeDefault+10];
        // 通过image创建头像
        JSQMessagesAvatarImage *cookImage = [JSQMessagesAvatarImageFactory avatarImageWithImage:[UIImage imageNamed:@"demo_avatar_cook"] diameter:kJSQMessagesCollectionViewAvatarSizeDefault];;

// 气泡图片制作工具类
        // [UIImage jsq_bubbleRegularImage]这个方法有很多种气泡模式,圆的,尖的以及边框形式的
        JSQMessagesBubbleImageFactory *bubbleFactory = [[JSQMessagesBubbleImageFactory alloc] initWithBubbleImage:[UIImage jsq_bubbleRegularImage] capInsets:UIEdgeInsetsZero];
        // 发出去的气泡颜色
        self.outgoingBubbleImageData = [bubbleFactory outgoingMessagesBubbleImageWithColor:[UIColor jsq_messageBubbleLightGrayColor]];
//        self.outgoingBubbleImageData = [bubbleFactory outgoingMessagesBubbleImageWithColor:[UIColor whiteColor]];
        // 收到的气泡颜色
        self.incomingBubbleImageData = [bubbleFactory incomingMessagesBubbleImageWithColor:[UIColor jsq_messageBubbleGreenColor]];

4.控制器写数据逻辑代理

先提一下那个滚动动画,慎用,有点不可控

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    /**
     *  Enable/disable springy bubbles, default is NO.
     *  You must set this from `viewDidAppear:`
     *  Note: this feature is mostly stable, but still experimental
     *  注意啊,这个有时候会蹦掉,玩玩就好了
     */
    // 一个bubbles的移动动画效果
    self.collectionView.collectionViewLayout.springinessEnabled = [[[NSUserDefaults standardUserDefaults] valueForKey:@"kDynamic"] boolValue];
}



模拟个右上角的按钮,来接受消息,最核心代码

// 收到别人发的消息了
- (void)receiveMessagePressed:(UIBarButtonItem *)sender
{
    // 这仅仅是模拟Demo
    /**
     *  Show the typing indicator to be shown
     *  是否需要一个加载指示
     */
    self.showTypingIndicator = YES;

    /**
     *  Scroll to actually view the indicator 滚动到最后
     */
    [self scrollToBottomAnimated:YES];

    /**
     *  Copy last sent message, this will be the new "received" message
     *  来一份上一次的数据
     */
    JSQMessage *copyMessage = [[self.demoData.messages lastObject] copy];

    if (!copyMessage) {
        copyMessage = [JSQMessage messageWithSenderId:kJSQDemoAvatarIdJobs
                                          displayName:kJSQDemoAvatarDisplayNameJobs
                                                 text:@"First received!"];
    }

    /**
     *  Allow typing indicator to show
     */
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        NSMutableArray *userIds = [[self.demoData.users allKeys] mutableCopy];
        [userIds removeObject:self.senderId];
        NSString *randomUserId = userIds[arc4random_uniform((int)[userIds count])];

        JSQMessage *newMessage = nil;
        id<JSQMessageMediaData> newMediaData = nil;
        id newMediaAttachmentCopy = nil;

        // JSQMessage对应的BOOL isMediaMessage = NO就是text,YES就是图片,音频,视频,定位
        if (copyMessage.isMediaMessage) {
            /**
             *  Last message was a media message
             */
            // 先把代理存储下
            id<JSQMessageMediaData> copyMediaData = copyMessage.media;

            // 如果是图片
            if ([copyMediaData isKindOfClass:[JSQPhotoMediaItem class]]) {
                JSQPhotoMediaItem *photoItemCopy = [((JSQPhotoMediaItem *)copyMediaData) copy];
                // 默认都是YES的,这句话的意思是气泡的小尖尖朝哪个方向,YES是发出去的,就朝右,反之
                photoItemCopy.appliesMediaViewMaskAsOutgoing = NO;
                newMediaAttachmentCopy = [UIImage imageWithCGImage:photoItemCopy.image.CGImage];

                /**
                 *  Set image to nil to simulate "downloading" the image
                 *  and show the placeholder view
                 *  代表发出去的消息会进行短暂的loading
                 */
                photoItemCopy.image = nil;

                newMediaData = photoItemCopy;
            }
            else if ([copyMediaData isKindOfClass:[JSQLocationMediaItem class]]) {
                // 坐标消息  同上
                JSQLocationMediaItem *locationItemCopy = [((JSQLocationMediaItem *)copyMediaData) copy];
                locationItemCopy.appliesMediaViewMaskAsOutgoing = NO;
                newMediaAttachmentCopy = [locationItemCopy.location copy];

                /**
                 *  Set location to nil to simulate "downloading" the location data
                 */
                locationItemCopy.location = nil;

                newMediaData = locationItemCopy;
            }
            else if ([copyMediaData isKindOfClass:[JSQVideoMediaItem class]]) {
                // 视频消息 同上
                JSQVideoMediaItem *videoItemCopy = [((JSQVideoMediaItem *)copyMediaData) copy];
                videoItemCopy.appliesMediaViewMaskAsOutgoing = NO;
                newMediaAttachmentCopy = [videoItemCopy.fileURL copy];

                /**
                 *  Reset video item to simulate "downloading" the video
                 */
                videoItemCopy.fileURL = nil;
                videoItemCopy.isReadyToPlay = NO;

                newMediaData = videoItemCopy;
            }
            else if ([copyMediaData isKindOfClass:[JSQAudioMediaItem class]]) {
                // 同上
                JSQAudioMediaItem *audioItemCopy = [((JSQAudioMediaItem *)copyMediaData) copy];
                audioItemCopy.appliesMediaViewMaskAsOutgoing = NO;
                newMediaAttachmentCopy = [audioItemCopy.audioData copy];

                /**
                 *  Reset audio item to simulate "downloading" the audio
                 */
                audioItemCopy.audioData = nil;

                newMediaData = audioItemCopy;
            }
            else {
                NSLog(@"%s error: unrecognized media item", __PRETTY_FUNCTION__);
            }

            // 除开Text外的消息类
            newMessage = [JSQMessage messageWithSenderId:randomUserId
                                             displayName:self.demoData.users[randomUserId]
                                                   media:newMediaData];
        }
        else {
            /**
             *  Last message was a text message  纯文本消息类
             */
            newMessage = [JSQMessage messageWithSenderId:randomUserId
                                             displayName:self.demoData.users[randomUserId]
                                                    text:copyMessage.text];
        }

        /**
         *  Upon receiving a message, you should:
         *
         *  1. Play sound (optional)
         *  2. Add new id<JSQMessageData> object to your data source
         *  3. Call `finishReceivingMessage`
         */

        // [JSQSystemSoundPlayer jsq_playMessageReceivedSound];

        // 播放声音
        [self.demoData.messages addObject:newMessage];
        [self finishReceivingMessageAnimated:YES];


        // 如果消息类型是Media  非文本形式
        if (newMessage.isMediaMessage) {
            /**
             *  Simulate "downloading" media  模拟下载
             */
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                /**
                 *  模拟下载,下载完之后重新刷
                 */

                if ([newMediaData isKindOfClass:[JSQPhotoMediaItem class]]) {
                    ((JSQPhotoMediaItem *)newMediaData).image = newMediaAttachmentCopy;
                    [self.collectionView reloadData];
                }
                else if ([newMediaData isKindOfClass:[JSQLocationMediaItem class]]) {
                    [((JSQLocationMediaItem *)newMediaData)setLocation:newMediaAttachmentCopy withCompletionHandler:^{
                        [self.collectionView reloadData];
                    }];
                }
                else if ([newMediaData isKindOfClass:[JSQVideoMediaItem class]]) {
                    ((JSQVideoMediaItem *)newMediaData).fileURL = newMediaAttachmentCopy;
                    ((JSQVideoMediaItem *)newMediaData).isReadyToPlay = YES;
                    [self.collectionView reloadData];
                }
                else if ([newMediaData isKindOfClass:[JSQAudioMediaItem class]]) {
                    ((JSQAudioMediaItem *)newMediaData).audioData = newMediaAttachmentCopy;
                    [self.collectionView reloadData];
                }
                else {
                    NSLog(@"%s error: unrecognized media item", __PRETTY_FUNCTION__);
                }

            });
        }

    });
}



注意:收到消息三步骤
1.playSound 可选
2.Add new id object to your data source 把数据源加入
3. Call finishReceivingMessage,告诉完成了,直接刷新数据


发送纯文本消息,步骤和上面一样

// 纯文本发送
- (void)didPressSendButton:(UIButton *)button
           withMessageText:(NSString *)text
                  senderId:(NSString *)senderId
         senderDisplayName:(NSString *)senderDisplayName
                      date:(NSDate *)date
{
    /**
     *  Sending a message. Your implementation of this method should do *at least* the following:
     *
     *  1. Play sound (optional)
     *  2. Add new id<JSQMessageData> object to your data source
     *  3. Call `finishSendingMessage`
     */
    // 套路三部曲 直接完成组装

    // [JSQSystemSoundPlayer jsq_playMessageSentSound];

    JSQMessage *message = [[JSQMessage alloc] initWithSenderId:senderId
                                             senderDisplayName:senderDisplayName
                                                          date:date
                                                          text:text];

    [self.demoData.messages addObject:message];

    [self finishSendingMessageAnimated:YES];
}



发送非文本消息,ActionSheet选择器

// 点击左侧accessory按钮启动actionSheet
- (void)didPressAccessoryButton:(UIButton *)sender
{
    [self.inputToolbar.contentView.textView resignFirstResponder];

    UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:NSLocalizedString(@"Media messages", nil)
                                                       delegate:self
                                              cancelButtonTitle:NSLocalizedString(@"Cancel", nil)
                                         destructiveButtonTitle:nil
                                              otherButtonTitles:NSLocalizedString(@"Send photo", nil), NSLocalizedString(@"Send location", nil), NSLocalizedString(@"Send video", nil), NSLocalizedString(@"Send video thumbnail", nil), NSLocalizedString(@"Send audio", nil), nil];

    [sheet showFromToolbar:self.inputToolbar];
}

// 点击左侧accessory按钮弹出sheet,选择需要发送的事件添加到数据源
- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == actionSheet.cancelButtonIndex) {
        [self.inputToolbar.contentView.textView becomeFirstResponder];
        return;
    }

    switch (buttonIndex) {
        case 0:
            [self.demoData addPhotoMediaMessage];
            break;

        case 1:
        {
            __weak UICollectionView *weakView = self.collectionView;

            [self.demoData addLocationMediaMessageCompletion:^{
                [weakView reloadData];
            }];
        }
            break;

        case 2:
            [self.demoData addVideoMediaMessage];
            break;

        case 3:
            [self.demoData addVideoMediaMessageWithThumbnail];
            break;

        case 4:
            [self.demoData addAudioMediaMessage];
            break;
    }

    // [JSQSystemSoundPlayer jsq_playMessageSentSound];

    [self finishSendingMessageAnimated:YES];
}

注意:发送消息三步骤和上面的收到的步骤一模一样的

剩下的都是一些修饰的代理数据UI以及一些附带事件的实现

// 发送的人ID
- (NSString *)senderId
// 发送人名字
- (NSString *)senderDisplayName
// 根据index返回需要加载的message对象
- (id<JSQMessageData>)collectionView:(JSQMessagesCollectionView *)collectionView messageDataForItemAtIndexPath:(NSIndexPath *)indexPath
// 删除消息
- (void)collectionView:(JSQMessagesCollectionView *)collectionView didDeleteMessageAtIndexPath:(NSIndexPath *)indexPath
// 聊天气泡,根据ID判断是发送的还是接受的
- (id<JSQMessageBubbleImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView messageBubbleImageDataForItemAtIndexPath:(NSIndexPath *)indexPath
// 头像
- (id<JSQMessageAvatarImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView avatarImageDataForItemAtIndexPath:(NSIndexPath *)indexPath
// 时间UI
- (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath
// 除本人以外显示bubble cell上面的名字
- (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath
// 气泡cell底部文字
- (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath



基本上的逻辑就已经完成了,只要根据业务加载实际的逻辑就能自己做一套聊天的了,但是细节还需要完善很多,毕竟一套成熟的聊天框架是需要不断完善的,这里抛砖引玉,觉得可以的同学可以顺手给个赞,有问题及时留言,多交流多学习总是不会错的



1.WechatQQ聊天初级Demo

2.Wechat朋友圈高度自适应

3.RunLoop理解和常见问题

4.RunTime基本使用和面试问题

猜你喜欢

转载自blog.csdn.net/deft_mkjing/article/details/53894216