程序员

XMPPFramework开发(三):好友列表

搞事前言


前一篇博客,我们对XMPPFramework的登录注册功能以及逻辑做了详细的说明,用户登录完成之后,我们需要做的就是获取到当前账号的好友列表和个人信息,今天这一篇博客就是对好友列表的相关逻辑以及代理方法来做一下讲解说明.我们先看看SDChat中的好友列表示意图.

XMPPFramework中好友关系说明解释


在XMPPFramework中呢,好友关系是可以通过订阅来实现的,也就是说A与B相互订阅,那么A与B就是好友了,如果A只是订阅了B,B没有订阅A,那么我们就说A与B两者不是好友,当然了,我在实际过程中搞好友添加的逻辑还是比较多的,这里需要了解A与B相互订阅(openfire服务器中订阅状态为both,当然了,订阅状态也有from和to,这样的也算是好友.具体情况后面会详细说明),那么A与B就是好友这一个逻辑即可.

好友列表获取流程.


当用户登录成功之后,我们做的最主要的一个模块就是加载好友列表模块.那么好友加载模块的整体流程是怎样的呢?我们先看一个SDChat好友列表的流程图,帮助我们熟悉好友列表在实际过程中如何展现的.(图片可能看不清楚,请自行下载查看,谢谢.)

好友服务器数据获取代码部分

XMPPFramework中好友列表的管理核心类是XMPPRoster,这个类可以用来对好友的信息获取,添加,删除等操作.在SDChat中,我们把XMPPRoster声明为SDXmppManager的一个属性对象,并且在初始化过程中激活好友模块.代码如下所示.(说明:XMPPRosterCoreDataStorage对象使用存储好友数据的.)

self.rosterCoreDataStorage= [XMPPRosterCoreDataStorage sharedInstance];

self.roster = [[XMPPRoster alloc]initWithRosterStorage:self.rosterCoreDataStorage dispatchQueue:dispatch_get_global_queue(0, 0)];

//激活roster
[self.roster activate:self.stream];

其实好友获取是有两种方式的,骚栋使用的是代理方法获取好友节点的.由于代理方法默认的是在登录成功之后默认就会调用获取好友节点,但是我做页面的时候需要调节一下好友节点获取的时机,所以,我就把XMPPRoster的自动获取好友节点功能关掉了.当然了,你可以使用自动获取.这个需要根据实际情况而定,实现代码如下所示.

self.roster.autoFetchRoster = NO;

这样在我们登录完成之后,我们需要在- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender这个方法中手动调起获取好友的方法.调起方法也很简单,只需要一行代码就可以.

[[SDXmppManager defaulManager].roster fetchRoster];

当我们调起了获取好友的方法之后,我们需要在在联系人列表(SDContactsVC)这个控制器中先设置XMPPRoster对象的代理.我是在初始化就设置了代理对象.

[[SDXmppManager defaulManager].roster addDelegate:self delegateQueue:dispatch_get_main_queue()];

设置完成之后代理方法其实总共是有三个的,三个代理调取的实际分别是所有好友节点获取开始,每一个好友节点获取到的时候,所有好友节点获取完成之后,我们可以根据实际情况来进行不同的操作.比如我们在获取开始之前初始化好友节点数组,获取结束刷新页面等等,具体的三个代理方法如下所示.

//开始获取好友节点列表的时候
-(void)xmppRosterDidBeginPopulating:(XMPPRoster *)sender;

//获取每一个好友节点的时候
-(void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(DDXMLElement *)item;

//结束获取好友节点列表的时候
-(void)xmppRosterDidEndPopulating:(XMPPRoster *)sender;

这是我只使用了后面的两个代理方法,这是有原因的,因为我的好友节点数组([SDUser defaulUser].contactsArray)不需要每一次获取都进行更新,而且这三个代理方法在实际使用过程中,自动调取的次数很多,比如添加完好友或者删除完好友都能自动调取这个三个代理方法,为了不必要的麻烦,所以我只是用了后面的两个代理方法.还是那句话,大家可以根据自己的实际情况自行调用不同的代理方法.

我们先看一下SDChat中在-(void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(DDXMLElement *)item;骚栋都做了什么样的操作.首先,我们获取到每一个好友节点item,然后我们先判断item节点的订阅信息subscription的值,我们需要的好友是双方相互订阅,也就是”subscription”属性为”both”、”from”、”to”才是我们需要的好友节点.所以符合这三种情况的都是我们需要的好友节点,所以if的筛选条件就出来了,如下代码所示.其他订阅类型不同的节点我们后面会说到具体的情况.

if ([[[item attributeForName:@"subscription"] stringValue] isEqualToString:@"both"]||[[[item attributeForName:@"subscription"] stringValue] isEqualToString:@"from"]||[[[item attributeForName:@"subscription"] stringValue] isEqualToString:@"to"]) {

}

在筛选完成之后,我们需要做的事情就是获取到item节点的JID信息了,这里我们只需要两行代码就可以完成了.

NSString *SJid = [[item attributeForName:@"jid"] stringValue];

XMPPJID *jid = [XMPPJID jidWithString:SJid];

不管是前期界面上显示好友的JID信息,还是后期显示电子名片信息,我们都需要先遍历好友节点数组([SDUser defaulUser].contactsArray)判断数组中是否已经存在该好友信息了.这样做的原因是因为-(void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(DDXMLElement *)item;这个代理方法可能对一个好友节点获取多次,如果我们不进行选择性的添加的话,数组可能会出现好友重复的现象的,所以我们需要先判断是否存在该好友信息,具体代码如下所示.(关于isDeleteFriend布尔值的存在的意义,当我们删除好友的时候,订阅信息subscription的值可能并不是”remove”,有可能是”both”,所有我们这里需要加一个布尔值,后面的删除好友,我们会详细说明的.)

BOOL isExist = NO;

for (SDContactModel *contact in self.user.contactsArray) {

       if ([contact.jid.user isEqualToString:jid.user]) {

            isExist = YES;

          }
}

判断完是否存在好友信息之后,我们就可以根据isExist这个布尔值来判断了是否要添加数据了,添加数据过程如下代码所示,这里我是获取了好友的名片信息进行的添加,前期的话可以直接添加JID.

if (!isExist) {
    //添加数据
    XMPPvCardTemp *vCard =  [[SDXmppManager defaulManager].vCardTempModule vCardTempForJID:jid shouldFetch:YES];

    SDContactModel *contact =[[SDContactModel alloc]init];
    contact.jid = jid;
    contact.vCard =vCard;
    contact.isAvailable = NO;

    [self.user.contactsArray addObject:contact];

}

上面就是从服务器获取到好友数据的基本流程了.

好友数据本地整理代码部分

当我们获取好友数据完成之后,我们并不是直接展现到页面上,我们需要对好友数据进行整理然后再展现到界面之上.我们通过-(void)xmppRosterDidEndPopulating:(XMPPRoster *)sender;这个代理方法来调取我们的好友数据整理方法-(void)networkingWithContactsArray;.

-(void)xmppRosterDidEndPopulating:(XMPPRoster *)sender{

    [self networkingWithContactsArray];

}

SDChat的好友界面是类似于微信的好友界面的,是分组展示的.所以,数据存储的整体思路是,列表的数据源是存储于一个字典当中,我们把首字母相同的JID或者是用户名存储于一个数组当中.每个key是每一个JID的首字母大写(或者是用户名称的首字母大写).因为字典是无序的,那么如何做到有序的排列呢?我们需要建立另外一个数组作为排序数组,同时也起着索引数组的作用.我们把字典中所有的Key放入数组中,然后排序,数据提取过程中我们只需要根据数组的排列顺序拿取即可.示意图如下所示.

那么我们看一下实际代码过程中,对于字典的数据添加整理部分,首先我们要初始化字典对象,然后我们遍历好友节点数组([SDUser defaulUser].contactsArray),取出我们需要排序的每一个关键字符串(不管是JID还是用户名).我们调用-(NSString *)transform:(NSString *)chinese这个方法回去首字母并且大写.在这个方法中我们有几种情况需要处理,一种是获取字符串失败,也就是说传入的是一个nil值,我们直接返回 “#” ,另外一种是如果首字母是数字,那么我们也是需要返回”#”的.所以这样返回首字母所使用到的方法总共就有了三个,两个用来判断是否是数组,一个则是截取并且进行字母大写的操作.三个方法如下所示.

//截取首字母并且大写
-(NSString *)transform:(NSString *)chinese{

    if (chinese == nil ||[chinese isEqualToString:@""]) {

        return @"#";

    }

    NSMutableString *pinyin = [chinese mutableCopy];
    CFStringTransform((__bridge CFMutableStringRef)pinyin, NULL, kCFStringTransformMandarinLatin, NO);
    CFStringTransform((__bridge CFMutableStringRef)pinyin, NULL, kCFStringTransformStripCombiningMarks, NO);

    NSString *subString = [[pinyin uppercaseString] substringWithRange:NSMakeRange(0, 1)];

    if ([self isPureInt:subString] || [self isPureFloat:subString]) {

        return  @"#";
    }

    return subString;
}

//判断是否为整型:
- (BOOL)isPureInt:(NSString*)string{
    NSScanner* scan = [NSScanner scannerWithString:string];
    int val;
    return [scan scanInt:&val] && [scan isAtEnd];
}
//判断是否为浮点型:
- (BOOL)isPureFloat:(NSString*)string{
    NSScanner* scan = [NSScanner scannerWithString:string];
    float val;
    return[scan scanFloat:&val] && [scan isAtEnd];
}

那么通过返回首字母,我们需要判断一下当前的字典对象中是否已经存在了改分组的数组,如果存在,那么直接存储,如果不存在,那么初始化一个数组之后,以首字母为Key,空数组为Value保存到字典中,然后再把数据存储到数组中去.具体代码如下所示.

if (self.contactsPinyinDic[firstWord] ==nil) {

    //如果联系人字典数组中没有该分组,那么就初始化一个分组数组,然后存储.
    NSMutableArray *sectionArray =[NSMutableArray  arrayWithCapacity:16];

    [sectionArray addObject:contact];

    [self.contactsPinyinDic setValue:sectionArray forKey:firstWord];

}else{

    NSMutableArray *sectionArray =self.contactsPinyinDic[firstWord];

    [sectionArray addObject:contact];

}

添加完成之后,我们就需要对索引数组进行操作了,首先我们还是先初始化我们的索引数组,初始化的过程中,我们就把所有的key值添加到我们的数组当中去.然后我们需要添加一个空字符串@"",这是为了给”新的朋友”那个分组做准备的.代码如下所示.

self.indexArray = [NSMutableArray arrayWithArray:self.contactsPinyinDic.allKeys];
[self.indexArray addObject:@""];//添加一个空的字符串,用于菜单分组

然后,我们对索引数组进行遍历排序操作.

    for (int i = 0; iself.indexArray[i]) {

                NSString *objString = self.indexArray[j];
                self.indexArray[j] =  self.indexArray[i];
                self.indexArray[i] = objString;

            }
        }
    }

如果存在”#”,为了界面的美观,我们把”#”放在索引数组的最后一位,然后,我们就刷新我们的页面即可.

//索引数组移动#号到最后.
if ([self isIncludeWithJing]) {

    [self.indexArray removeObject:@"#"];

    [self.indexArray addObject:@"#"];
}

[self.contactsList reloadData];

这样在tableView的数据源方法中,分组个数为索引数组的元素个数self.indexArray.count;每一个section中元素的个数(除了一个分组)都是为字典中对应的每一个value数组的个数.

然后,我们就可以做出开始的界面的样子来了.当然了,这样的画面需要我们做很多工作的,也是我们下一篇博客所要说到的,电子名片的实现.

结束


SDChat中的好友获取的逻辑和代理方法就说到这里了,这里我要先声明一下,SDChat中可能还存在着Bug,如果有任何问题,欢迎联系骚栋,谢谢.接下来的一篇我觉得应该先把XMPPFramework电子名片的实现说一下,XMPPFramework我觉得最坑的就是添加好友这一块了,逻辑比较多,准备在第五篇中进行讲解说明.希望大家持续关注~最后把SDChat的传送门送给大家.大家可以对照着Demo来看本篇博客.

–>SDChat传送门🚪