美文网首页程序员
第四篇:XMPP实现IM--拉取好友列表、添加删除好友

第四篇:XMPP实现IM--拉取好友列表、添加删除好友

作者: 意一ineyee | 来源:发表于2018-07-10 11:09 被阅读194次

    目录

    一、拉取好友列表
    二、添加好友、删除好友
     问题1、添加好友时,如果A向B发起好友申请时,B不在线会出现怎样的情况?
     问题2、添加好友时,如果A向B发起好友申请时,B待审核状态却没去审核,而是向A也发起了好友申请,会出现什么样的状况?

    一、拉取好友列表


    XMPP好友列表是做了本地存储的,那么拉取好友列表时如果检测到有本地存储信息,则拉取本地存储的信息,否则从服务端拉取并存储在本地(比方说你卸载掉App了,重新下载依旧可以从服务端拉取到好友列表并存在本地。)

    同样的,在开始之前,我们先把拉取好友列表功能会用到的一些类和方法列在这里,方便下面对比查看,其实很少的。

    XMPPRoster:好友列表类,关于好友管理的一些操作基本上都归它管。
    XMPPRosterCoreDataStorage:好友列表本地存储的一个类。
    
    // 拉取好友列表
    - (void)fetchRoster;
    
    
    #pragma mark - XMPPRosterDelegate
    
    // 开始获取好友列表
    - (void)xmppRosterDidBeginPopulating:(XMPPRoster *)sender;
    
    // 获取到一个好友
    - (void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(DDXMLElement *)item;
    
    // 获取好友列表结束
    - (void)xmppRosterDidEndPopulating:(XMPPRoster *)sender;
    
    1、XMPP的五种订阅状态

    在XMPP体制中,好友关系是通过订阅对方的在线状态来实现的。如果双方互相订阅了在线状态,彼此就是好友;如果双方没有互相没订阅在线状态,彼此就不是好友。共有五种订阅状态,如下:

    订阅状态(subscription) 什么情况下会出现
    none 双方互相没订阅在线状态。
    both 双方互相订阅了在线状态,可认为是好友关系。
    from/to 对方申请订阅自己的在线状态,自己也申请订阅对方的在线状态,但是双方都还没同意时,双方的订阅状态就互相为from和to,但因为未同意,所以不能认为双方是好友关系;添加和删除好友的时候,也会出现这样的过渡状态。
    remove 删除好友的时候会出现这种状态。
    2、拉取好友列表功能的实现

    同样的,我们在初始化ProjectXMPP单例的时候,就需要把好友列表相关的类初始化,其中我们关闭了自动拉取好友列表的功能,为的是我们可以根据业务在适当的时机自己拉取好友列表(比如登录成功后,我们要拉取一下好友列表,刷新界面的时候要拉取一下好友列表等。)

    -----------ProjectXMPP.m-----------
    
    // 好友相关
    self.rosterCoreDataStorage = [XMPPRosterCoreDataStorage sharedInstance];
    self.roster = [[XMPPRoster alloc] initWithRosterStorage:self.rosterCoreDataStorage dispatchQueue:dispatch_get_global_queue(0, 0)];
    // 将roster在stream中激活
    [self.roster activate:self.stream];
    // 关闭自动拉取好友列表功能,我们可以根据业务在适当的时机自己拉取好友列表
    self.roster.autoFetchRoster = NO;
    

    拉取好友列表的方法也很简单,就是上面的- (void)fetchRoster;,我们这里定义一个方法如下。

    -----------ProjectXMPP.m-----------
    
    - (void)fetchFriendsList {
        
        [self.roster fetchRoster];
    }
    

    此外,因为好友列表的数据很多个地方有用到,因此我们写了一个专门的单例ProjectSingleton来存储好友列表。

    -----------ProjectSingleton.h-----------
    
    /// 好友列表数组
    @property (strong, nonatomic) NSMutableArray *friendsListArray;
    

    好,接下来的工作,我们将转移到好友列表控制器FriendsListViewController中。

    首先我们把FriendsListViewController作为roster的代理。

    -----------FriendsListViewController .m-----------
    
    // 设置代理
    [[ProjectXMPP sharedXMPP].roster addDelegate:self delegateQueue:dispatch_get_main_queue()];
    

    然后处理拉取好友列表相关的三个代理方法,在有好友的前提下,这三个方法经常会被触发,比如我们获取好友列表时会触发它们,我们添加好友、删除好友完成后,这三个方法同样会被自动触发。开始拉取和拉取完成比较简单,我们会着重解释一下获取到一个好友这个代理方法内部做了什么。

    -----------FriendsListViewController .m-----------
    
    #pragma mark - XMPPRosterDelegate
    
    // 开始获取好友列表
    - (void)xmppRosterDidBeginPopulating:(XMPPRoster *)sender {
        
        NSLog(@"===========>开始获取好友列表");
    }
    
    // 获取到一个好友
    - (void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(DDXMLElement *)item {
        
        NSLog(@"===========>获取到一个好友:%@", item);
        
        // 我们这里只获取双向好友
        NSString *subscription = [[item attributeForName:@"subscription"] stringValue];
        if ([subscription isEqualToString:@"both"]) {
            
            // 获取好友的jid
            NSString *friendJidString = [[item attributeForName:@"jid"] stringValue];
            XMPPJID *friendJid = [XMPPJID jidWithUser:friendJidString domain:kDomainName resource:kResource];
            
            // 构建userModel
            UserModel *tempUser = [[UserModel alloc] init];
            tempUser.jid = friendJid;
    
            // 因为这个代理方法经常会被触发,比如添加、删除好友都会触发这个代理,因此这里就可能对同一个好友拉取多次,所以为了避免好友重复,要判断一下
            for (UserModel *tempUser1 in [ProjectSingleton sharedSingleton].friendsListArray) {
                
                if ([tempUser1.jid.user isEqualToString:tempUser.jid.user]) {
                    
                    return;
                }
            }
            
            [[ProjectSingleton sharedSingleton].friendsListArray addObject:tempUser];
        }
    }
    
    // 获取好友列表结束
    - (void)xmppRosterDidEndPopulating:(XMPPRoster *)sender {
        
        NSLog(@"===========>获取好友列表结束");
        
        [self.tableView reloadData];
    }
    

    获取到一个好友的回调中,我们可以看到首先是对拉取到的好友根据订阅状态subscription进行了一下过滤,上面第1小节我们阐述了subscription五种订阅状态的含义,所以这里我们仅仅显示的是好友,即subscription为both

    -----------FriendsListViewController .m-----------
    
    NSString *subscription = [[item attributeForName:@"subscription"] stringValue];
    if ([subscription isEqualToString:@"both"]) {
        
    
    }
    

    然后我们就可以获取好友的唯一标识jid,来构建一个UserModel的对象(因为后面要做用户的电子名片,因此不直接存储用户的jid,而是包了一层,存储我们自定义的userModel),存储在好友列表中,进而显示在好友列表界面上。同时因为这个代理方法经常会被触发,比如添加、删除好友都会触发这个代理,因此这里就可能对同一个好友拉取多次,所以为了避免好友重复,要判断一下。

    -----------FriendsListViewController .m-----------
    
    // 获取好友的jid
    NSString *friendJidString = [[item attributeForName:@"jid"] stringValue];
    XMPPJID *friendJid = [XMPPJID jidWithUser:friendJidString domain:kDomainName resource:kResource];
    
    // 构建userModel
    UserModel *tempUser = [[UserModel alloc] init];
    tempUser.jid = friendJid;
    
    // 因为这个代理方法经常会被触发,比如添加、删除好友都会触发这个代理,因此这里就可能对同一个好友拉取多次,所以为了避免好友重复,要判断一下
    for (UserModel *tempUser1 in [ProjectSingleton sharedSingleton].friendsListArray) {
        
        if ([tempUser1.jid.user isEqualToString:tempUser.jid.user]) {
            
            return;
        }
    }
    
    [[ProjectSingleton sharedSingleton].friendsListArray addObject:tempUser];
    

    好了,拉取好友列表其实就算完成了,很简单的。此时,我们在登录的回调里,包括第一次登录(LoginViewController中)和自动登录(AppDelegate中),调用一下拉取好友列表的方法[[ProjectXMPP sharedXMPP] fetchFriendsList]就可以了。如果你发现并没有触发上面三个拉取好友列表的回调方法,不要慌,那是因为我们此时没有好友,我们可以在openfire服务器的后台先手动的为用户添加好友关系,先看看效果。接下来,我们就做添加和删除好友的功能,这样好友列表就可以有其存在感了。

    二、添加好友、删除好友


    同样,在实现添加好友、删除好友功能之前,我们先看下需要新认识哪些方法。其实也不多了,总共才6个而已。

    // 添加好友:根据给定的账户名,把对方用户添加到自己的好友列表中,并且申请订阅对方用户的在线状态
    - (void)addUser:(XMPPJID *)jid withNickname:(NSString *)optionalName;
    
    // 删除好友:从好友列表中删除对方用户,并且取消订阅对方用户的在线状态,同时取消对方用户对我们自己在线状态的订阅(如果对方设置允许这样的话)
    - (void)removeUser:(XMPPJID *)jid;
    
    // 同意好友请求
    - (void)acceptPresenceSubscriptionRequestFrom:(XMPPJID *)jid andAddToRoster:(BOOL)flag;
    
    // 拒绝好友请求
    - (void)rejectPresenceSubscriptionRequestFrom:(XMPPJID *)jid;
    
    
    #pragma mark - XMPPRosterDelegate
    
    // 收到一个好友申请的回调
    - (void)xmppRoster:(XMPPRoster *)sender didReceivePresenceSubscriptionRequest:(XMPPPresence *)presence;
    
    
    #pragma mark - XMPPStreamDelegate
    
    // 监测好友在线状态的回调(拒绝好友请求之后会触发)
    - (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence;
    
    1、添加好友的实现

    因为添加好友的逻辑比较多,所以我们在这里先从整体上梳理下添加好友的流程,至于细节,我们后面会结合代码逐条分析。

    接下来我们会根据着这个的流程图,并结合代码来看看添加好友的具体实现以及其中需要注意的一些小细节。

    首先,A向B发起好友申请,这个时候,我们需要做一些过滤,比如不能添加自己为好友,如果对方已经是自己的好友也不要发起申请。

    -----------FriendsListViewController.m-----------
    
    - (void)addFriendAction:(UIButton *)button {
        
        UIAlertView *alret = [[UIAlertView alloc] initWithTitle:@"添加好友"  message:@"请输入好友名称" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"确定", nil];
        alret.alertViewStyle = UIAlertViewStylePlainTextInput;
        [alret show];
    }
    
    - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
        
        if (buttonIndex == 1) {// 添加
            
            UITextField *textField = [alertView textFieldAtIndex:0];
            
            if ([textField.text isEqualToString:[UserModel currentUser].jid.user]) {// 不能添加自己为好友
                
                [ProjectHUD showMBProgressHUDToView:kWindow withText:@"不能添加自己为好友!" atPosition:(MBProgressHUDTextPositionMiddle) autohideAfter:2 completionHandlerAfterAutohide:nil];
                return;
            }
            
            for (UserModel *model in [ProjectSingleton sharedSingleton].friendsListArray) {// 如果对方已经是自己的好友,不能再次添加
                
                if ([model.jid.user isEqualToString:textField.text] && [model.jid.domain isEqualToString:kDomainName]) {
                    
                    [ProjectHUD showMBProgressHUDToView:kWindow withText:@"对方已经是您的好友!" atPosition:(MBProgressHUDTextPositionMiddle) autohideAfter:2 completionHandlerAfterAutohide:nil];
                    return;
                }
            }
            
            // 添加好友
            [[ProjectXMPP sharedXMPP] addFriendWithAccount:textField.text];
        }
    }
    

    发起申请后,因为会立马触发A获取到一个好友的回调,因此我们在那个方法里要过滤掉订阅状态为none的好友,刚好我们在做拉取好友列表的时候,已经做了这个过滤操作。

    -----------FriendsListViewController .m-----------
    
    NSString *subscription = [[item attributeForName:@"subscription"] stringValue];
    if ([subscription isEqualToString:@"both"]) {
        
    
    }
    

    接下来,我们会把AppDelegate作为了streamroster的代理,并实现了收到一个好友申请监测好友在线状态两个代理方法。此时,B收到A的好友申请后,就会触发收到一个好友申请的回调,我们的处理如下。

    -----------AppDelegate.m-----------
    
    // 设置代理
    [[ProjectXMPP sharedXMPP].stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
    [[ProjectXMPP sharedXMPP].roster addDelegate:self delegateQueue:dispatch_get_main_queue()];
    
    -----------AppDelegate.m-----------
    
    #pragma mark - XMPPRosterDelegate
    
    // 收到一个好友申请
    - (void)xmppRoster:(XMPPRoster *)sender didReceivePresenceSubscriptionRequest:(XMPPPresence *)presence {
    
        // 如果这个好友请求已经发起了,是待确认的状态,那么再次收到同样的好友申请的时候就忽略掉,避免数据重复
        for (UserModel *tempUser in [ProjectSingleton sharedSingleton].pre_newFriendArray) {
    
            if ([presence.from.user isEqualToString:tempUser.jid.user] && [presence.from.domain isEqualToString:tempUser.jid.domain]) {
    
                return;
            }
        }
    
        // 新朋友界面的数组新增一条数据
        UserModel *tempUser = [[UserModel alloc] init];
        tempUser.jid = presence.from;
        [[ProjectSingleton sharedSingleton].pre_newFriendArray addObject:tempUser];
        // 发出通知,好友列表界面和新朋友界面会接收通知,做相应的处理
        [[NSNotificationCenter defaultCenter] postNotificationName:@"DidReceiveFriendRequest" object:nil];
    }
    

    这里我们是通过通知的办法告诉好友列表和新朋友界面有新好友的,然后,好友列表和新朋友界面收到通知后,做相应的数据和UI的处理,这个就不多说了,如有需要可下载代码看。

    继续看添加好友的主流程。

    如果B同意了A的好友申请,A和B获取到一个好友- (void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(DDXMLElement *)item;的代理方法都会被触发,我们在这里照常构建好UserModel,并添加到friendsListArray里,如果A和B此时在好友列表界面,刷新一下界面就可以了,没什么大问题;要是A和B当前不再好友列表界面,则需要在B点击同意好友申请的地方,发一个“同意了好友申请”的通知,好友列表界面接收通知来刷新界面。

    -----------NewFriendViewController.m-----------
    
    cell.acceptBlock = ^{
        
        // 同意好友申请
        [[ProjectXMPP sharedXMPP] acceptFriendRequestWithAccount:((UserModel *)[ProjectSingleton sharedSingleton].pre_newFriendArray[indexPath.row]).jid.user];
        
        [ProjectHUD showMBProgressHUDToView:kWindow withText:[NSString stringWithFormat:@"%@已成为您的好友", ((XMPPJID *)[ProjectSingleton sharedSingleton].pre_newFriendArray[indexPath.row]).user] atPosition:(MBProgressHUDTextPositionMiddle) autohideAfter:2 completionHandlerAfterAutohide:^{
            
            // 新朋友数组删除这条数据
            [[ProjectSingleton sharedSingleton].pre_newFriendArray removeObjectAtIndex:indexPath.row];
            // 刷新界面
            [self.tableView reloadData];
            
            // 发出同意了好友申请的通知
            [[NSNotificationCenter defaultCenter] postNotificationName:@"DidAcceptFriendRequest" object:nil];
        }];
    };
    
    -----------FriendsListViewController.m-----------
    
    // 同意好友申请的通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didAcceptFriendRequest:) name:@"DidAcceptFriendRequest" object:nil];
    
    
    - (void)didAcceptFriendRequest:(NSNotification *)notification {
        
        if ([ProjectSingleton sharedSingleton].pre_newFriendArray.count == 0) {
            
            [self.pre_newFriendButton setTitle:@"新朋友" forState:(UIControlStateNormal)];
        }else {
            
            [self.pre_newFriendButton setTitle:[NSString stringWithFormat:@"新朋友(%ld)", [ProjectSingleton sharedSingleton].pre_newFriendArray.count] forState:(UIControlStateNormal)];
        }
    
        // 拉取好友列表会走在刷新之前的
        [self.tableView reloadData];
    }
    

    如果B拒绝了A的好友申请,B并不会触发任何方法,但是B需要删除roster里的A,同样在此时发出一个“拒绝了好友申请”的通知,以便A和B刷新好友列表界面的某些状态。

    -----------NewFriendViewController.m-----------
    
    cell.rejectBlock = ^{
        
        // 拒绝好友申请
        [[ProjectXMPP sharedXMPP] rejectFriendRequestWithAccount:((XMPPJID *)[ProjectSingleton sharedSingleton].pre_newFriendArray[indexPath.row]).user];
    
        [ProjectHUD showMBProgressHUDToView:kWindow withText:[NSString stringWithFormat:@"您拒绝了%@的好友请求", ((UserModel *)[ProjectSingleton sharedSingleton].pre_newFriendArray[indexPath.row]).jid.user] atPosition:(MBProgressHUDTextPositionMiddle) autohideAfter:2 completionHandlerAfterAutohide:^{
            
            // roster删除这条数据
            [[ProjectXMPP sharedXMPP] removeFriendWithAccount:((UserModel *)[ProjectSingleton sharedSingleton].pre_newFriendArray[indexPath.row]).jid.user];
            
            // 新朋友数组删除这条数据
            [[ProjectSingleton sharedSingleton].pre_newFriendArray removeObjectAtIndex:indexPath.row];
            // 刷新界面
            [self.tableView reloadData];
            
            // 发出同意了好友的通知
            [[NSNotificationCenter defaultCenter] postNotificationName:@"DidRejectFriendRequest" object:nil];
        }];
    };
    
    -----------FriendsListViewController.m-----------
    
    // 拒绝好友申请的通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didRejectFriendRequest:) name:@"DidRejectFriendRequest" object:nil];
    
    
    - (void)didRejectFriendRequest:(NSNotification *)notification {
        
        if ([ProjectSingleton sharedSingleton].pre_newFriendArray.count == 0) {
            
            [self.pre_newFriendButton setTitle:@"新朋友" forState:(UIControlStateNormal)];
        }else {
            
            [self.pre_newFriendButton setTitle:[NSString stringWithFormat:@"新朋友(%ld)", [ProjectSingleton sharedSingleton].pre_newFriendArray.count] forState:(UIControlStateNormal)];
        }
    }
    

    但是对A来说,如果B拒绝了A,会触发A“监测好友在线状态”- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence;的回调,我们需要在这里把B从A的roster里删掉。

    -----------AppDelegate.m-----------
    
    #pragma mark - XMPPStreamDelegate
    
    // 监测好友在线状态
    - (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence {
        
        NSLog(@"===========>好友:%@,状态:%@", presence.from, presence.type);
        
        // 对方拒绝了我的好友申请时
        if ([presence.type isEqualToString:@"unsubscribe"]) {
            
            [[ProjectXMPP sharedXMPP] removeFriendWithAccount:presence.from.user];
        }
    }
    

    好的,这样我们就完成了好友的添加操作。接下来我们需要考虑两个问题:

    问题1:如果A向B发起好友申请时,B不在线会出现怎样的情况?
    这种情况没关系,等B上线的时候,还是会触发B收到一个好友申请的回调。这是没有关系的,因为XMPP是C/S架构,也就是说服务端值跟单端的客户端有关系,客户端1把消息发给服务端,其实并不关系客户端2在不在线,等客户端2在线的时候,服务端会自动把消息推给它的。

    问题2:如果A向B发起好友申请时,B待审核状态却没去审核,而是向A也发起了好友申请,会出现什么样的状况?
    如果发生了这种状况,对于A来说是不会触发收到一个好友申请的回调的,因为B已经在它的roster里了,而是A和B都会触发获取到一个好友的回调,但此时A对B的订阅状态为from,B对A的订阅状态为to,俩人并不能称之为好友关系。
    那么这里也有很多处理方式,微信是可以双方互发,这种方法的话就需要A在向B发起好友申请的时候,要及时的把A从B的roster里移除,当然后续也需要别的处理,这里只是提供思路,为简单起见,此处仅做判断,如果A向B发了请求,那么B就不能向A发起请求,得先去同意或拒绝。

    2、删除好友的实现

    删除好友的逻辑比较简单,我们这里设置左滑删除好友。

    那么当A删除B的时候,我们只需要调用一下

    [[ProjectXMPP sharedXMPP] removeFriendWithAccount:friend];
    

    这样A就算把B删掉了,此时会触发A的获取到一个好友- (void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(DDXMLElement *)item;的回调,此时A对B的订阅状态为
    remove,我们需要在这里把B从A的friendsListArray里移除掉。

    // 删除好友
    if ([subscription isEqualToString:@"remove"]) {
        
        // 获取好友的jid
        NSString *friendJidString = [[item attributeForName:@"jid"] stringValue];
        XMPPJID *friendJid = [XMPPJID jidWithString:friendJidString];
        
        // 构建userModel
        UserModel *tempUser = [[UserModel alloc] init];
        tempUser.jid = friendJid;
        
        [[ProjectSingleton sharedSingleton].friendsListArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            
            if ([((UserModel *)obj).jid.user isEqualToString:tempUser.jid.user]) {
                
                [[ProjectSingleton sharedSingleton].friendsListArray removeObjectAtIndex:idx];
                [self.tableView reloadData];
            }
        }];
    }
    

    而对于B来说,则会触发B的监测好友在线状态- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence;的回调,B在这里会收到被A删掉的消息,也是[presence.type isEqualToString:@"unsubscribe"],和拒绝好友申请其实是一个逻辑。

    // 对方拒绝了我的好友申请时,或者对方删除了我时,我会触发这个回调
    if ([presence.type isEqualToString:@"unsubscribe"]) {
        
        [[ProjectXMPP sharedXMPP] removeFriendWithAccount:presence.from.user];
    }
    

    这里B删完后,会再次触发B的获取到一个好友- (void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(DDXMLElement *)item;的回调,也会把A从friendsListArray里移除掉并刷新界面。


    • 综上:
      1、拉取好友列表的一堆类和方法其实相当于XMPP为我们提供了的一些接口,而不用我们服务端自己去写接口拉数据,其实我们也可以通过接口去服务端拉数据,因为这些数据其实都是存储在我们自己服务器上的一个数据库的。但是,现在更多的社交App的模式都不是纯的好友列表这种存在形式(比如附近的人、今日最热什么的,可以随便发起聊天),都不用XMPP的这种纯好友列表,而是自己维护一些好友关系,仅仅使用XMPP的聊天能力。所以如果要做纯好友列表这样的东西,就可以直接使用XMPP的好友列表,这样省了写各种接口的力,如果App更灵活也可以自己写接口来维护好友关系。
      2、当然如果要用XMPP这种纯好友列表,就用它的添加和删除好友,同样也是省去了很多写接口、对接口的力。如果App更灵活也可以通过接口作出类似关注和取消关注这样的效果。

    也就是说XMPP的好友列表和添加删除好友这些功能不是说就肯定会用到,可以根据自己的业务看要不要用它们。

    • 效果:
    上一篇:XMPP实现IM--登录注册及断线重连、重新连接 下一篇:XMPP实现IM--电子名片、用户在线状态监测

    相关文章

      网友评论

        本文标题:第四篇:XMPP实现IM--拉取好友列表、添加删除好友

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