iOS中如何自定义手机通讯录

在工作中,很多时候会有读取系统通讯录的需求.尤其是一些IM相关的项目.

我们公司的几个项目都是IM相关的,所以自然免不了这块儿的内容.用到的地方多了,所以就自己简单地封装了一个工具类来读取.

首先,要实现的需求如下


屏幕快照 2017-02-16 下午8.25.56.png

这个界面布局是很简单的.实现这个功能无非三步:

1.获取通讯录的联系人数据

2.将获取的联系人数据上传服务器(服务器要对每个联系人进行判断处理,以便实现 加好友 或 邀请下载APP的功能)

3.从服务器拉去联系人数据,根据拉去到的数据做显示操作

可以看出这三步中,2.3是相对简单的.比较复杂的就是第一步了,获取系统的通讯录联系人数据.那么,如何获取系统通讯录数据呢,接下来,就让我们来仔细梳理一下iOS中如何获取系统通讯录吧.

首先,读取系统通讯录要导入与通讯录相关的库.鉴于ios9之前和之后与通讯录相关的库发生了改变,同时如果要兼容ios8的话,就得同时导入这两个头文件:

//ios9之前
#import <AddressBook/AddressBook.h>
//ios9之后
#import <ContactsUI/ContactsUI.h>

接下来呢,我们知道苹果是非常注重用户隐私的,所以程序在试图访问通讯录时都得先经过用户的授权,而系统的授权只有一次.那么如何获取用户授权呢,直接上代码

#pragma mark---获取授权---
- (void)getAuthoriseWithSuccess:(void (^)(void))success failed:(void (^)(void))failed {
    
    if (TMiOS8) {
        
        [self getAuthoriseBeforeIos_9WithSuccess:success failed:failed];
        
    }else if(TMiOS9){
        
        [self getAuthoriseAfterIos_9WithSuccess:success failed:failed];
    }

}
#pragma mark---ios9之前获取授权---
- (void)getAuthoriseBeforeIos_9WithSuccess:(void (^)(void))success failed:(void (^)(void))failed {
    
    //获取授权状态
    ABAuthorizationStatus status = ABAddressBookGetAuthorizationStatus();
    
    if (status ==  kABAuthorizationStatusNotDetermined) {
        
        //The user has not yet made a choice regarding whether this app can access the data class.(用户尚未作出选择,关于此应用程序是否可以访问数据类。)
        
        ABAddressBookRef book =   ABAddressBookCreateWithOptions(NULL, NULL);
        ABAddressBookRequestAccessWithCompletion(book, ^(bool granted, CFErrorRef error) {
            
            if (granted) {
                
                TMLog(@"授权成功!");
                GCDMainBlock(success);
                
            }else{
                
                TMLog(@"授权失败!");
                CGDMainBack;
            }
            
        });
    }
    else if (status == kABAuthorizationStatusAuthorized) {
        
        //This application is authorized to access the data class. (此应用程序被授权访问数据类。)
        GCDMainBlock(success);
        
    }
    else if (status == kABAuthorizationStatusDenied) {
        
        //The user explicitly denied access to the data class for this application.(用户明确拒绝此应用程序访问数据类。)
        GCDMainBlock(failed);
    }
    else if (status == kABAuthorizationStatusRestricted) {
        
        //This application is not authorized to access the data class. The user cannot change this application’s status, possibly due to active restrictions such as parental controls being in place.(此应用程序未授权访问数据类。用户不能更改此应用程序的状态,可能是由于活动限制)
        GCDMainBlock(failed);
    }
}
#pragma mark---ios9之后获取通讯录授权---
- (void)getAuthoriseAfterIos_9WithSuccess:(void (^)(void))success failed:(void (^)(void))failed {
    
    //获取授权状态
    CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
    
    if (status == CNAuthorizationStatusNotDetermined) {
        
        //The user has not yet made a choice regarding whether the application may access contact data.(还未对应用程序进行过获取联系人授权的选择) 请求系统授权(系统授权弹框仅有一次)
        CNContactStore *store = [[CNContactStore alloc] init];
        
        [store requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
            if (granted) {
                
                TMLog(@"授权成功!");
                GCDMainBlock(success);
                
            }else{
                
                TMLog(@"授权失败!");
                CGDMainBack;
            }
        }];
        
    }else if (status == CNAuthorizationStatusAuthorized) {
        
        //The application is authorized to access contact data.(用户授权应用程序访问联系人数据)
        GCDMainBlock(success);
        
    }else if (status == CNAuthorizationStatusDenied) {
        
        //The user explicitly denied access to contact data for the application.(用户明确拒绝应用程序访问联系人数据。)
        GCDMainBlock(failed);
        
    }else if (status == CNAuthorizationStatusRestricted) {
        
        //The application is not authorized to access contact data.The user cannot change this application’s status, possibly due to active restrictions such as parental controls being in place.(应用程序没有权限访问联系人数据.并且用户无法更改应用程序的权限状态,可能是由于某种活动限制)
        GCDMainBlock(failed);
    }
}

两个block参数分别是判断系统授权成功与否的回调.当判断授权成功时调用success,授权失败时调用faild.
这里有个问题,可以发现我在调用block的时候,套用了事先用宏定义的方法,为什么呢?是因为在程序调试过程中我发现明明已经授权成功了,但是却迟迟不能完成界面跳转,后来找来公司同事帮忙找bug,发现获取授权是新开了异步线程,而授权成功之后迟迟没有响应则是因为新开的异步线程还未销毁,所以这里就手动调用方法让它回到主线程(我也不知道这样说对不对,有哪里不对的地方还请指正~手动比心)

#pragma mark - GCD Block
#define GCDBlock(block) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block)
#define GCDMainBlock(block) dispatch_async(dispatch_get_main_queue(),block)
#define CGDMainBack GCDMainBlock(^(){})

完成这一步之后,关于系统通讯录的授权部分我们就已经可以实现了.只要用该封装好的单例类调用获取授权的方法就好了.


IMG_0038.PNG

在以上添加好友界面点击"手机通讯录中的朋友"时写如下代码即可

    [[TMAddressBookTool shareTool] getAuthoriseWithSuccess:^{
        
        //授权成功
        //跳转界面
        [self openViewControllerByPush:[[SFAddressBookTableVC alloc] init]];
        
    } failed:^{
        
        //授权失败 提示授权失败 同时跳转到设置界面更改权限
        [self showAlertWithTitle:@"提示" message:@"请您先去\n手机设置权限管理里面\n修改权限" confirmTitle:@"确定" isCancel:NO queding:^{
            [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
        } cancel:nil];
        
    }];

如果是第一次授权,那么会弹出如下弹框


IMG_0039.JPG

点击"不允许"时弹框消失,不再有后续反应.此时如若再次点击"手机通讯录中的朋友",就会走失败回调,给用户比较友好的提示,如下图所示,我们公司的需求是点击弹框中的"确定"按钮后跳转到设置界面以便让用户更改授权状态.


IMG_0037.PNG

点击"好"之后会回调授权成功的block,进行页面跳转.

IMG_0040.PNG

哈哈哈哈,我刚换了手机,通讯录什么的没导进来,也就是说我的手机通讯录里面什么都没有哈.接下来我们谈谈在这个界面要做哪些操作呢.其实这个界面要做的在文章一开始就已经说过了.

1.获取通讯录的联系人数据

2.将获取的联系人数据上传服务器(服务器要对每个联系人进行判断处理,以便实现 加好友 或 邀请下载APP的功能)

3.从服务器拉去联系人数据,根据拉去到的数据做显示操作

我们重点来说第一点,之前写的那一堆内容都是第一点的铺垫.下面开始介绍如何获取通讯录的联系人数据.

#pragma mark---读取通讯录---
//返回联系人数组 并将联系人数组转化为json字符串
- (NSString *)readAddressBook {
    
    NSArray *addressBook = [NSArray array];
    if (TMiOS8) {
    
        addressBook = [self readAddressBookBeforeIos_9];
        
    }else if(TMiOS9) {
        
        addressBook = [self readAddressBookAfterIos_9];

    }
    //将通讯录跟本地记录做比较并且保存
    return [self getJsonStr:addressBook.copy];

}
#pragma mark---读取通讯录 ios9之前---
- (NSArray *)readAddressBookBeforeIos_9 {
    
    NSMutableArray *addressBook = [[NSMutableArray alloc] init];
    
    //创建一个通讯录实例
    ABAddressBookRef book =   ABAddressBookCreateWithOptions(NULL, NULL);
    
    //获取联系人总数
    CFArrayRef  allpeople =  ABAddressBookCopyArrayOfAllPeople(book);
    CFIndex count =  CFArrayGetCount(allpeople);
    
    //遍历
    for (CFIndex i = 0; i <count ; i++) {
        
        //获取联系人对象的引用
        ABRecordRef record =   CFArrayGetValueAtIndex(allpeople, i);
        //获取当前联系人姓氏
        CFStringRef strLast =   ABRecordCopyValue(record, kABPersonLastNameProperty);
        NSString *lastName =  (__bridge_transfer NSString *)strLast;
        NSString *name = lastName;
        
        //创建一个可变数组用来存放该联系人的电话号码(同一个联系人的电话号码不止一个)
        NSMutableArray *phoneArr = [NSMutableArray array];
        
        //获取当前联系人的电话(数组)
        ABMultiValueRef multivalue =  ABRecordCopyValue(record, kABPersonPhoneProperty);
        //遍历电话数组
        for (CFIndex i = 0; i < ABMultiValueGetCount(multivalue); i++) {
            
            //获取电话号码 并转换成NSString类型
            CFStringRef phoneStr =   ABMultiValueCopyValueAtIndex(multivalue, i);
            NSString *phone = (__bridge_transfer  NSString *)(phoneStr);
            
            //判断手机号码同时将其加入到事先创建好的手机号码数组
            [self judgePhoneNumberWithPhoneArray:phoneArr phoneNum:phone name:name];

        }
        
        //当电话数组不为空时,以字典形式包装联系人名称和电话数组(我们跟后台定好的字段是"memo"/"mobile",这里具体按你们自己的字段为准),同时将字典添加到预先创建的可变数组中
        [self judgePhoneArray:phoneArr contactsArray:addressBook name:name];

    }
    return addressBook.copy;

}
#pragma mark---读取通讯录 ios9之后---
- (NSArray *)readAddressBookAfterIos_9 {
    
    //初始化一个可变数组,用来存放遍历到的所有联系人
    NSMutableArray *addressBook = [[NSMutableArray alloc] init];

    //创建通讯录对象
    CNContactStore *store = [[CNContactStore alloc] init];
    //定义所有打算获取的属性对应的key值,这里我们要获取姓名/姓氏/以及手机号码
    NSArray *keys = @[CNContactGivenNameKey,CNContactFamilyNameKey,CNContactPhoneNumbersKey];
    //创建CNContactFetchRequest对象
    CNContactFetchRequest *request = [[CNContactFetchRequest alloc] initWithKeysToFetch:keys];
    
    //遍历所有的联系人
    [store enumerateContactsWithFetchRequest:request error:nil usingBlock:^(CNContact * _Nonnull contact, BOOL * _Nonnull stop) {
        
        //拿到当前联系人的姓名
        NSString *name = [NSString stringWithFormat:@"%@%@",contact.familyName,contact.givenName];
        
        //初始化一个可变数组,用来存放手机号码(同一个联系人可能对用多个手机号)
        NSMutableArray *phoneArr = [NSMutableArray array];
        
        //遍历当前联系人的号码数组
        for (CNLabeledValue * objc in contact.phoneNumbers
             ) {
            
            //获取当前手机号码
            CNPhoneNumber *num = objc.value;
            NSString *phoneStr = num.stringValue;
            
            //处理手机号码字符串  过滤掉"+","(",")","-"等多余字符(便于后台处理数据)
            phoneStr = [self getStandardPhoneNum:phoneStr];
            
            //判断手机号码同时将其加入到事先创建好的手机号码数组
            [self judgePhoneNumberWithPhoneArray:phoneArr phoneNum:phoneStr name:name];

        }
        //当电话数组不为空时,以字典形式包装联系人名称和电话数组(我们跟后台定好的字段是"memo"/"mobile",这里具体按你们自己的字段为准),同时将字典添加到预先创建的可变数组中
        [self judgePhoneArray:phoneArr contactsArray:addressBook name:name];

    }];
    
    return addressBook.copy;
}
#pragma mark---判断手机号码同时将其加入到事先创建好的手机号码数组---
- (void)judgePhoneNumberWithPhoneArray:(NSMutableArray *)phoneArray phoneNum:(NSString *)phone name:(NSString *)name {
    
    //当前联系人的名字为空但是电话不为空时  默认将名字设为电话号码(我们公司是这样做的)
    if (phone.length > 0 && name.length == 0) {
        
        name = phone;
    }
    //当电话不为空时 将当前电话号码添加到电话数组中
    if (phone.length > 0) {
        
        [phoneArray addObject:phone];
    }
}
#pragma mark---当电话数组不为空时,以字典形式包装联系人名称和电话数组,同时将字典添加到预先创建的可变数组中---
- (void)judgePhoneArray:(NSMutableArray *)phoneArray contactsArray:(NSMutableArray *)contactsArray name:(NSString *)name {
    
    if (phoneArray.count > 0) {
        
        //(我们跟后台定好的字段是"memo"/"mobile",这里具体按你们自己的字段为准)
        NSDictionary *contactInfo =  @{@"mobile": phoneArray,@"memo": name};
        [contactsArray addObject:contactInfo];
    }
}
#pragma mark---将联系人数组转换成json字符串---
- (NSString *)getJsonStr:(NSArray *)contacts {
    
    NSData *data = [NSJSONSerialization dataWithJSONObject:contacts options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    jsonStr = [jsonStr stringByReplacingOccurrencesOfString:@"\n" withString:@""];
    jsonStr = [jsonStr stringByReplacingOccurrencesOfString:@" " withString:@""];
    
    return jsonStr;
}

#pragma mark---处理电话号码---
- (NSString *)getStandardPhoneNum: (NSString *)phoneStr {
    
    phoneStr = [phoneStr stringByReplacingOccurrencesOfString:@"-" withString:@""];
    phoneStr = [phoneStr stringByReplacingOccurrencesOfString:@"(" withString:@""];
    phoneStr = [phoneStr stringByReplacingOccurrencesOfString:@")" withString:@""];
    phoneStr = [phoneStr stringByReplacingOccurrencesOfString:@" " withString:@""];
    phoneStr = [phoneStr stringByReplacingOccurrencesOfString:@"+" withString:@""];
    
    return phoneStr;
}

也就是说在"手机通讯录"界面写如下代码就可以拿到你的通讯录的所有联系人了

//这里你拿到的addressBookStr就是通讯录的所有联系人的json字符串
NSString *addressBookStr = [[SFAddressBookTool shareTool] readAddressBook];

下一步就可以上传通讯录了,上传成功之后再调用获取通讯录的接口,这样就能从后台拿到关于你的手机通讯录中的某个人是否注册过该应用以便于做对应的显示.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容