iOS走近商城APP(四 runloop应用 获取通讯录并处理)

开篇

转眼又要过年了,我的程序员生涯默默的又过了一年,年终篇就先闲扯几句。从接触简书到写第一篇到现在差不多也将近一年了,简书的布局风格确实是赏心悦目,习惯了这种写法之后,再看以前的博客感觉好乱,哈哈。写了几篇文章,感觉有实用的也有的感觉回头看看比较水,希望来年再接再厉写一点干货来继续沉淀自己吧。

商城系列文章:

iOS走近商城APP(一)
iOS走近商城APP(二 购物车常用控件)
iOS走近商城APP(三 WKWebView 商品规格选择框架封装)

本篇文章主要内容
  • runloop的应用
    runloop的实际应用
    runloop的原理介绍
  • 获取手机通讯录
    iOS9 iOS8不同情况下的授权获取号码
    对号码的显示 输入的处理
runloop的应用

实战往往是解释问题的有效途径,先上图

订单倒计时.png

如上图所示,在tableview上或者滚动试图上用到定时器的情景还是比较常见的。在tableview加上定时器很简单

  timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(refreshLessTime) userInfo:@"" repeats:YES];

但是如果只是这么加上的话,会发现其实在滚动试图滚动,或者tableview滑动的时候,定时器是停止的,解决办法如下

    timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(refreshLessTime) userInfo:@"" repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

那么重点来了,为什么要这么做,runloop都包含什么呢?
Run loops是线程的基础架构部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop。
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
每个线程,包括程序的主线程(main thread)都有与之相应的run loop对象。主线程是默认启动的,因此第一次创建定时器的方法,虽然没有写,但是由于是在主线程注册的定时器,因此已经加入到runloop中。
其实相当于以下代码,默认加在了NSDefaultRunLoopMode中,

    timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(refreshLessTime) userInfo:@"" repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

那么NSDefaultRunLoopMode 与 NSRunLoopCommonModes有什么区别呢?
在runloop中系统默认注册了5个model

  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode)
    默认 Mode,主线程一般都是在这个 Mode 下。
  • UITrackingRunLoopMode
    界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  • UIInitializationRunLoopMode
    在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  • GSEventReceiveRunLoopMode
    接受系统事件的内部 Mode,通常用不到。
  • kCFRunLoopCommonModes
    不常用

我们根据不同的情况来选择吧定时器加入到model中,也明白了为什么滚动的时候定时器停止,因为默认的时候是加在NSDefaultRunLoopMode上的,但是当tableview或者滚动试图滚动的时候触发的是UITrackingRunLoopMode所以定时器停止了,只有滚动停止切换到NSDefaultRunLoopMode上时定时器继续。
但是为什么最后加到了NSRunLoopCommonModes一样解决了问题呢?而且这里的5中model并没有NSRunLoopCommonModes,NSRunLoopCommonModes是什么呢?
NSRunLoopCommonModes并不是一种Mode,而是一种特殊的标记,包含三种mode(kCFRunLoopDefaultMode、NSTaskDeathCheckMode、UITrackingRunLoopMode),添加到NSRunLoopCommonModes中的还没有执行的任务,会在mode切换时,再次添加到当前的mode中,这样就能保证不管当前runloop切换到哪一个mode,任务都能正常执行。并且被添加到NSRunLoopCommonModes中的任务会存储在runloop 的commonModeItems中。


表情.jpg

一句代码却包含这么多,至于更深入的底层的知识有空大家自己研究一下吧,进行下一个部分。

获取通讯录并处理

首先要设置获取通讯录的权限,如下图

权限设置.png

由于系统的不同获取权限以及协议方法的不同,要进行系统的判断,根据iOS9,以及iOS8做出不同的处理。
导入需要的头文件

#import <AddressBookUI/AddressBookUI.h> //iOS8
#import <ContactsUI/ContactsUI.h>  //iOS9

遵循的代理

<CNContactPickerDelegate,ABPeoplePickerNavigationControllerDelegate>

授权部分的代码

  if(UMSYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"9.0"))  {
      
        //让用户给权限,没有的话会被拒
        CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
        if (status == CNAuthorizationStatusNotDetermined) {
            CNContactStore *store = [[CNContactStore alloc] init];
            [store requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
                if (error) {

                }else
                {

                    CNContactPickerViewController * picker = [CNContactPickerViewController new];
                    picker.delegate = self;
                    picker.displayedPropertyKeys = @[CNContactPhoneNumbersKey];//只显示手机号
                    [self presentViewController: picker  animated:YES completion:nil];
                }
            }];
        }
        
        if (status == CNAuthorizationStatusAuthorized) {//有权限时
            CNContactPickerViewController * picker = [CNContactPickerViewController new];
            picker.delegate = self;
            picker.displayedPropertyKeys = @[CNContactPhoneNumbersKey];
            [self presentViewController: picker  animated:YES completion:nil];
        }
        else{
            
            [SVProgressHUD showInfoWithStatus:@"您未开启通讯录权限,请前往设置中心开启"];

        }

    }else{
        
        __weak typeof(self)weakSelf = self;
        ABAddressBookRef bookref = ABAddressBookCreateWithOptions(NULL, NULL);
        ABAuthorizationStatus status = ABAddressBookGetAuthorizationStatus();
        /*kABAuthorizationStatusNotDetermined = 0,    // 未进行授权选择
         kABAuthorizationStatusRestricted,           // 未授权,且用户无法更新,如家长控制情况下
         kABAuthorizationStatusDenied,               // 用户拒绝App使用
         kABAuthorizationStatusAuthorized            // 已授权,可使用*/
        if (status == kABAuthorizationStatusNotDetermined) {
            ABAddressBookRequestAccessWithCompletion(bookref, ^(bool granted, CFErrorRef error) {
                if (error) {

                }
                if (granted) {
                    NSLog(@"授权成功");
                    ABPeoplePickerNavigationController *peosonVC = [[ABPeoplePickerNavigationController alloc] init];
                    peosonVC.peoplePickerDelegate = weakSelf;
                    peosonVC.displayedProperties = @[[NSNumber numberWithInt:kABPersonPhoneProperty]];
                    [weakSelf presentViewController:peosonVC animated:YES completion:nil];
                }
            });
        }
        if (status == kABAuthorizationStatusAuthorized) {
            ABPeoplePickerNavigationController *peosonVC = [[ABPeoplePickerNavigationController alloc] init];
            peosonVC.peoplePickerDelegate = weakSelf;
            peosonVC.displayedProperties = @[[NSNumber numberWithInt:kABPersonPhoneProperty]];
            [weakSelf presentViewController:peosonVC animated:YES completion:nil];
        }else
        {
            [SVProgressHUD showInfoWithStatus:@"您未开启通讯录权限,请前往设置中心开启"];
        }
    }

如果走的iOS9.0及其以上走的是上面的代码,iOS8走的是else中的代码。获取通讯录后的处理的协议方法如下

#pragma mark - 点击某个联系人的某个属性(property)时触发并返回该联系人属性(contactProperty)。  iOS 9 以后写法
//只实现该方法时,可以进入到联系人详情页面(如果predicateForSelectionOfProperty属性没被设置或符合筛选条件,如不符合会触发默认操作,即打电话,发邮件等)。
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContactProperty:(CNContactProperty *)contactProperty {
    NSLog(@"%@",contactProperty);
    CNContact *contact = contactProperty.contact;
    NSLog(@"givenName: %@, familyName: %@", contact.givenName, contact.familyName);
    
    if (![contactProperty.value isKindOfClass:[CNPhoneNumber class]]) {
        [SVProgressHUD showInfoWithStatus:@"请选择11位手机号"];
        return;
    }
    CNPhoneNumber *phoneNumber = contactProperty.value;
    NSString * Str = phoneNumber.stringValue;
    NSCharacterSet *setToRemove = [[ NSCharacterSet characterSetWithCharactersInString:@"0123456789"]invertedSet];
    NSString *phoneStr = [[Str componentsSeparatedByCharactersInSet:setToRemove]componentsJoinedByString:@""];
    if (phoneStr.length != 11) {
        
        [SVProgressHUD showInfoWithStatus:@"请选择11位手机号"];
        
        return;
    }
    NSLog(@"-=-=%@",phoneStr);
    //号码赋值用于提交后台
    phoneNumberStr = phoneStr;
    
    //号码处理用于展示
        NSMutableString * str = [[NSMutableString alloc ] initWithString:phoneStr];
        [str insertString:@"-" atIndex:3];
        [str insertString:@"-" atIndex:8];
        phoneStr = str;
    
    self.phoneNumberFiled.text = phoneStr ;
}
#pragma mark - ios8 选中联系人的某个属性的时候调用  7.0以下 不做考虑
- (void)peoplePickerNavigationController:(ABPeoplePickerNavigationController*)peoplePicker didSelectPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier
{
    // 获取该联系人多重属性--电话号
    ABMutableMultiValueRef phoneMulti = ABRecordCopyValue(person, kABPersonPhoneProperty);
    
    // 获取该联系人的名字,简单属性,只需ABRecordCopyValue取一次值
    ABMutableMultiValueRef firstName = ABRecordCopyValue(person, kABPersonFirstNameProperty);
    NSString *name = (__bridge NSString *)(firstName);

    // 点击某个联系人电话后dismiss联系人控制器,并回调点击的数据
    [self dismissViewControllerAnimated:YES completion:^{
        // 从多重属性——电话号中取值,参数2是取点击的索引
        NSString *aPhone =  (__bridge_transfer NSString *)ABMultiValueCopyValueAtIndex(phoneMulti, ABMultiValueGetIndexForIdentifier(phoneMulti,identifier)) ;
        // 去掉电话号中的 "-"
        aPhone = [aPhone stringByReplacingOccurrencesOfString:@"-" withString:@"" ];
        if (aPhone.length != 11) {
            
            [SVProgressHUD showInfoWithStatus:@"请选择11位手机号"];
            
            return;
        }
        //号码赋值用于提交后台
        phoneNumberStr = aPhone;
        
        //号码处理用于展示
        NSMutableString * str = [[NSMutableString alloc ] initWithString:aPhone];
        [str insertString:@"-" atIndex:3];
        [str insertString:@"-" atIndex:8];
        aPhone = str;
        
        self.phoneNumberFiled.text = aPhone;
    }];
    [peoplePicker dismissViewControllerAnimated:YES completion:nil];

}

显示设置.png

其中对于获取通讯录后显示如图所示的设置具体代码为

  //号码处理用于展示
        NSMutableString * str = [[NSMutableString alloc ] initWithString:aPhone];
        [str insertString:@"-" atIndex:3];
        [str insertString:@"-" atIndex:8];
        aPhone = str;

但是选中设置之后同时要考虑输入框直接输入的样式设置以及删除时的问题设置,输入框的代码如下:
首先给输入框添加事件

        [_phoneNumberFiled addTarget:self action:@selector(textFieldDidEditing:) forControlEvents:UIControlEventEditingChanged];

具体方法的实现如下

-(void)textFieldDidEditing:(UITextField *)textField{
    
    if (textField == self.phoneNumberFiled) {
        if (textField.text.length > i) {
            if (textField.text.length == 4 || textField.text.length == 9 ) {//输入
                NSMutableString * str = [[NSMutableString alloc ] initWithString:textField.text];
                [str insertString:@"-" atIndex:(textField.text.length-1)];
                textField.text = str;
            }if (textField.text.length >= 13 ) {//输入完成
                textField.text = [textField.text substringToIndex:13];
                [textField resignFirstResponder];
            }
            i = textField.text.length;
            
        }else if (textField.text.length < i){//删除
            if (textField.text.length == 4 || textField.text.length == 9) {
                textField.text = [NSString stringWithFormat:@"%@",textField.text];
                textField.text = [textField.text substringToIndex:(textField.text.length-1)];
            }
            i = textField.text.length;
        }
    }
}

这样就实现了输入自动显示 为 012 - 3456 - 7890的样式了,自动添加"-',删除的时候同样做出自动处理。
这样就实现我们想要的效果了,哈哈。

表情2.gif
感谢

在写的时候也翻阅了许多文章学习,怕有表达不当误导大家,再次谢谢各位乐于分享的作者了。
参考文章举例
深入理解RunLoop
RunLoop 总结:RunLoop的应用场景(二)

后记

文章就到这里了,估计也是今年的最后一篇了,回家,买房,亲戚...各种琐事恐怕中间没时间再写了,哈哈。希望来年写更多有用的,同时希望自己和大家一样技术有很大进步,默默给自己和大家说一句春节快乐!

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

推荐阅读更多精彩内容