iOS微信QQ聊天介面的UI框架以及Socket簡單實現群聊功能

Deft_MKJing宓珂璟發表於2016-12-27

7.1日更新 Python3 TCP Demo相關

https://blog.csdn.net/Deft_MKJing/article/details/80851879

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基本使用和麵試問題

相關文章