通过Native.js访问iOS原生通讯录界面(ContactsUI/AddressBookUI)iOS14系统不可用

网上有关于Native.js访问Android原生通讯录的案例(可以参考:Android调用系统通讯录控件,native.js实现监听startActivityForResult后返回结果),但是关于iOS的一直没找到,所以决定自己写,写的过程中发现果然有坑。下面将一一道来,最后把Android和iOS的统一封装到了一个js文件中,可以很方便的使用(急需使用的小伙伴可以直接点击Demo链接下载:5+App demo链接 , uni-app demo链接)。
原创文章,欢迎转载.转载请注明出处: https://www.jianshu.com/p/b78b02d64472

iOS访问通讯录

在写NJS(Native.js)的代码之前,首先写了一份iOS访问通讯录的代码,以便根据这份代码翻译成对应的NJS代码。由于AddressBookUI在iOS9之后已经被废弃,当前已经是iOS12了,所以决定用iOS9之后的新框架ContactsUI。实现的代码如下:

//
//  ViewController.m
//  ContactsDemo
//
//  Created by xian on 2018/11/5.
//  Copyright © 2018年 xian. All rights reserved.
//

#import "ViewController.h"
#import 

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

#pragma mark - 访问通讯录按钮点击事件
- (IBAction)vistContacts:(UIButton *)sender {
    //通讯录控制器
    CNContactPickerViewController *contactPickerVC = [CNContactPickerViewController new];
    //设置代理
    //contactPickerVC.delegate = self;
    [contactPickerVC setDelegate:self];
    //从当前控制器present到通讯录控制器
    //[self presentViewController:contactPickerVC animated:YES completion:nil];
    //  先获取当前控制器(这样写为了和NJS代码对应)
    UIViewController *currentVC = [self viewControllerByView:self.view];
    [currentVC presentViewController:contactPickerVC animated:YES completion:nil];
}

#pragma - 根据view获取当前控制器
/*写这段代码的目的是为了在MUI项目中使用
 *因为在MUI项目中,要想获取当前页面的控制器,要通过webView来获取
 */
- (UIViewController *)viewControllerByView:(UIView *)view{
    while (view) {
        UIResponder *responder = [view nextResponder];
        if ([responder isKindOfClass:[UIViewController class]]) {
            return (UIViewController *)responder;
        }
        view = [view superview];
    }
    return nil;
}

#pragma mark - CNContactPickerDelegate 代理方法
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContact:(CNContact *)contact{
    //姓名/公司
    NSMutableString *name = [NSMutableString string];
    //姓
    NSString *familyName = contact.familyName;
    //名
    NSString *givenName = contact.givenName;
    //公司
    NSString *organizationName = contact.organizationName;
    [name appendString:familyName];
    [name appendString:givenName];
    if (name.length <= 0) {
        [name appendString:organizationName];
    }
    //手机号码
    NSString *phoneNo = @"";
    NSArray *phoneNumbers = contact.phoneNumbers;
    if (phoneNumbers.count > 0) {
        //这里只取第一个手机号
        CNLabeledValue *phone = phoneNumbers.firstObject;
        CNPhoneNumber *phoneNumber = phone.value;
        phoneNo = phoneNumber.stringValue;
    }
    NSLog(@"姓名/公司:%@,手机号码:%@", name, phoneNo);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

NJS调用iOS原生通讯录(ContactsUI,仅支持模拟器)

有了上面iOS访问通讯录的OC代码,可以很轻松的翻译成对应的NJS代码,如下:




    
    
    
    
    
    


    

调用通讯录

好不容易将OC代码翻译成了NJS代码,真机运行之后成功进入了iOS通讯录界面,选中一个联系人后可以正常返回到MUI页面,但是代理方法没有返回任何信息(确定已经成功实现了代理方法,因为假如没有成功实现代理方法的话,选中一个联系人是会跳到详情界面的,感觉这应该是NJS的Bug)。
最后,我在模拟器上试了一下,成功拿到了联系人信息!更加确信是NJS的Bug,于是到官网报了如下Bug:
【报Bug】Native.js调用iOS原生通讯录界面(ContactsUI)时,选中联系人,真机无法获取到回调值。

NJS调用iOS原生通讯录(AddressBookUI,仅支持真机)

报了Bug后,想了一下何不用AdressBookUI试试呢,虽然苹果官方已经废弃,但是iOS上还是可以正常使用滴,于是决定碰碰运气。还是老套路,先写OC代码,在上面OC代码基础上加上通过AddressBookUI访问通讯录的代码:

/**
 * 第二种方法(使用AddressBookUI)
 */
#pragma - 访问通讯录按钮点击事件
- (IBAction)visitAddressBook:(UIButton *)sender {
    //通讯录控制器
    ABPeoplePickerNavigationController *peoplePickerNavController = [[ABPeoplePickerNavigationController alloc]init];
    //设置代理
    //peoplePickerNavController.peoplePickerDelegate = self;
    [peoplePickerNavController setPeoplePickerDelegate:self];
    //从当前控制器present到通讯录控制器
    //[self presentViewController:peoplePickerNavController animated:YES completion:nil];
    //  先获取当前控制器(这样写为了和NJS代码对应)
    UIViewController *currentVC = [self viewControllerByView:self.view];
    [currentVC presentViewController:peoplePickerNavController animated:YES completion:nil];
}

#pragma mark - ABPeoplePickerNavigationControllerDelegate 代理方法
- (void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person{
    //姓名/公司
    NSMutableString *name = [NSMutableString string];
    //姓
    NSString *lastName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonLastNameProperty));
    //名
    NSString *firstName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonFirstNameProperty));
    //公司
    NSString *organizationName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonOrganizationProperty));
    [name appendString:lastName];
    [name appendString:firstName];
    if (name.length <= 0) {
        [name appendString:organizationName];
    }
    
    //电话号码
    NSString *phoneNo = @"";
    ABMultiValueRef phoneNumbers = ABRecordCopyValue(person, kABPersonPhoneProperty);
    CFIndex count = ABMultiValueGetCount(phoneNumbers);
    if (count > 0) {
        //这里只取第一个手机号
        phoneNo = CFBridgingRelease(ABMultiValueCopyValueAtIndex(phoneNumbers, 0));
    }
    CFRelease(phoneNumbers);
    NSLog(@"姓名/公司:%@,手机号码:%@", name, phoneNo);
}

注意:上面的代码需要引入AddressBookUI.h文件,为了规范最好让当前控制器遵守ABPeoplePickerNavigationControllerDelegate代理

将上述OC代码翻译成对应NJS代码时发现了戏剧性的Bug,代理方法【- (void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person;】返回的数据类型和iOS原生API完全对不上:

  • 本该返回peoplePicker(ABPeoplePickerNavigationController类型)的地方返回的是CNContact类型的对象;
  • 本该返回person(ABRecordRef类型)的地方什么都没有返回;
  • 非常巧的是CNContact刚好是ContactsUI里存储联系人信息的类型;
  • 不从CNContact实例对象里取联系人信息的时候,模拟器和真机都可以正常执行代理方法,一旦从CNContact实例对象中取值,模拟器便不会再执行这个方法,但是真机可以正常执行而且还可以获取选中的联系人信息。

最后翻译的NJS代码如下:




    
    
    
    
    
    


    

调用通讯录

通过上面的NJS代码,总算可以真机访问通讯录了。但是仍然需要注意这是NSJ的Bug,哪天官方修复了这个Bug,这段代码就会失效。

封装

下面将iOS模拟器和真机的代码以及Android的代码封装到一个js文件中。使用的时候只需调用下面这个方法(通过回调函数callBack拿到选中的联系人姓名和手机号码):

  • nativeCommon.contacts.getContact(function callBack(name, phoneNumber));

封装后的代码如下:

/**
 * nativeCommon,通过Native.js调用原生API
 */
var nativeCommon = {
    /**
     * 通讯录模块
     */
    contacts:{
        getContact:function(callBack){
            switch (plus.os.name){
                case "iOS":
                    if (plus.device.model === "iPhoneSimulator") {
                        //模拟器
                        nativeCommon.contacts.ios.visitContacts(function(name, phoneNumber){
                            callBack(name, phoneNumber);
                        });
                    } else {
                        //真机
                        nativeCommon.contacts.ios.visitAddressBook(function(name, phoneNumber){
                            callBack(name, phoneNumber);
                        });
                    }
                    break;
                case "Android":
                    // Android通过plus.contacts.getAddressBook可弹出通讯录授权提示框
                    plus.contacts.getAddressBook(plus.contacts.ADDRESSBOOK_PHONE, function (addressbook) {
                        nativeCommon.contacts.android.visitContacts(function(name, phoneNumber){
                            callBack(name, phoneNumber);
                        });
                    }, function (e) {
                        plus.nativeUI.alert("Get address book failed: " + e.message);
                    });
                    break;
                default:
                    break;
            }
        },
        ios:{//供iOS系统调用
            /**
             * 访问通讯录,将获取的联系人信息通过callBack返回
             * 仅限模拟器使用(Native.js 的bug)
             * @param {Object} callBack回调
             */
            visitContacts: function(callBack){
                var contactPickerVC = plus.ios.newObject("CNContactPickerViewController");
                //实现代理方法【- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContact:(CNContact *)contact;】
                //同时生成遵守CNContactPickerDelegate协议的代理对象delegate
                var delegate = plus.ios.implements("CNContactPickerDelegate", {
                    "contactPicker:didSelectContact:":function(picker, contact){
                        console.log(JSON.stringify(picker));
                        console.log(JSON.stringify(contact));
                        //姓名/公司
                        var name = "";
                        //姓氏
                        var familyName = contact.plusGetAttribute("familyName");
                        //名字
                        var givenName = contact.plusGetAttribute("givenName");
                        //公司
                        var organizationName = contact.plusGetAttribute("organizationName");
                        name = familyName+givenName;
                        if (name.length <= 0) {
                            name = organizationName;
                        }
                        //电话号码
                        var phoneNo = "";
                        var phoneNumbers = contact.plusGetAttribute("phoneNumbers");
                        if (phoneNumbers.plusGetAttribute("count") > 0) {
                            var phone = phoneNumbers.plusGetAttribute("firstObject");
                            var phoneNumber = phone.plusGetAttribute("value");
                            phoneNo = phoneNumber.plusGetAttribute("stringValue");
                        }
                        if(callBack){
                            callBack(name, phoneNo);
                        }
                    }
                });
                //给通讯录控制器contactPickerVC设置代理
                plus.ios.invoke(contactPickerVC, "setDelegate:", delegate);
                //获取当前UIWebView视图
                var currentWebview = plus.ios.currentWebview();
                //根据当前UIWebView视图获取当前控制器
                var currentVC = nativeCommon.contacts.ios.getViewControllerByView(currentWebview);
                //由当前控制器present到通讯录控制器
                plus.ios.invoke(currentVC, "presentViewController:animated:completion:", contactPickerVC, true, null);
            },
            /**
             * 访问通讯录,将获取的联系人信息通过callBack返回
             * 仅限真机使用(Native.js 的bug)
             * @param {Object} callBack
             */
            visitAddressBook:function(callBack){
                var peoplePickerNavController = plus.ios.newObject("ABPeoplePickerNavigationController");
                //实现代理方法【- (void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person;】
                //同时生成遵守ABPeoplePickerNavigationControllerDelegate协议的代理对象peoplePickerDelegate
                var peoplePickerDelegate = plus.ios.implements("ABPeoplePickerNavigationControllerDelegate", {
                    "peoplePickerNavigationController:didSelectPerson:":function(peoplePicker, person){
                        //这里的peoplePicker竟然是CNContact实例对象,person是undefined
                        console.log(JSON.stringify(peoplePicker));
                        console.log(JSON.stringify(person));
                        console.log(typeof person);
                        
                        //所以之前的代码不用改
                        var contact = peoplePicker;
                        //姓名
                        var name = "";
                        //姓氏
                        var familyName = contact.plusGetAttribute("familyName");
                        //名字
                        var givenName = contact.plusGetAttribute("givenName");
                        //公司
                        var organizationName = contact.plusGetAttribute("organizationName");
                        name = familyName+givenName;
                        if (name.length <= 0) {
                            name = organizationName;
                        }
                        //电话号码
                        var phoneNo = "";
                        var phoneNumbers = contact.plusGetAttribute("phoneNumbers");
                        if (phoneNumbers.plusGetAttribute("count") > 0) {
                            var phone = phoneNumbers.plusGetAttribute("firstObject");
                            var phoneNumber = phone.plusGetAttribute("value");
                            phoneNo = phoneNumber.plusGetAttribute("stringValue");
                        }
                        if (callBack) {
                            callBack(name, phoneNo);
                        }
                    }
                });
                //给通讯录控制器peoplePickerNavController设置代理
                plus.ios.invoke(peoplePickerNavController, "setPeoplePickerDelegate:", peoplePickerDelegate);
                //获取当前UIWebView视图
                var currentWebview = plus.ios.currentWebview();
                //根据当前UIWebView视图获取当前控制器
                var currentVC = nativeCommon.contacts.ios.getViewControllerByView(currentWebview);
                //由当前控制器present到通讯录控制器
                plus.ios.invoke(currentVC, "presentViewController:animated:completion:", peoplePickerNavController, true, null);
            },
            /**
             * 根据view获取到当前控制器
             * @param {Object} view
             */
            getViewControllerByView: function(view){
                //UIViewController类对象
                var UIViewController = plus.ios.invoke("UIViewController", "class");
                while(view){
                    var responder = plus.ios.invoke(view, "nextResponder");
                    if (plus.ios.invoke(responder, "isKindOfClass:", UIViewController)) {
                        return responder;
                    }
                    view = plus.ios.invoke(view, "superview");
                }
                return null;
            }
        },
        android:{//供android系统调用
            visitContacts:function(callBack){
                var REQUESTCODE = 1000;
                main = plus.android.runtimeMainActivity();
                var Intent = plus.android.importClass('android.content.Intent');
                var ContactsContract = plus.android.importClass('android.provider.ContactsContract');
                var intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
                main.onActivityResult = function(requestCode, resultCode, data) { 
                    if (REQUESTCODE == requestCode) {
                        var phoneNumber = "";
                        var resultString = "";
                        var context = main;
                        plus.android.importClass(data);
                        var contactData = data.getData();
                        var resolver = context.getContentResolver();
                        plus.android.importClass(resolver);
                        var cursor = resolver.query(contactData, null, null, null, null);
                        plus.android.importClass(cursor);
                        cursor.moveToFirst();
                        //姓名
                        var givenName = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)) || "";
                        var contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID));
                        var pCursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " + contactId, null, null);
                        if (pCursor.moveToNext()) {
                                phoneNumber =   pCursor.getString( pCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                        }
                        if (callBack) {
                                callBack(givenName, phoneNumber);
                        }
                        cursor.close();
                        pCursor.close();
                    }
                };
                main.startActivityForResult(intent, REQUESTCODE);
            }
        }
    }
}

注意:这里只返回选中联系人的姓名和手机号码,如果有多个号码只返回第一个;假如项目中需要返回所有的手机号码,可以修改对应的代码将手机号码组装成数组返回。

使用案例:

使用前要先引入封装的js文件,本文是将其封装在了native.common.js文件中,使用案例的代码如下:




    
    
    
    
    
    
    


    

调用通讯录

适配uni-app:

最近uni-app项目中用到了这一块功能,发现会闪退。经排查发现是“根据view获取到当前控制器”这段代码中通过viewnextResponder方法获取到的responder为空:

  /**
   * 根据view获取到当前控制器
   * @param {Object} view
   */
  getViewControllerByView: function(view){
      if (plus.os.name != "iOS") {
          return null;
      }
      //UIViewController类对象
      var UIViewController = plus.ios.invoke("UIViewController", "class");
      while(view){
          //uni-app中responder为undefined
          var responder = plus.ios.invoke(view, "nextResponder");
          //uni-app中由于responder为undefined,所以调用"isKindOfClass:"方法闪退
          if (plus.ios.invoke(responder, "isKindOfClass:", UIViewController)) {
              return responder;
          }
          view = plus.ios.invoke(view, "superview");
      }
      return null;
  }

解决方案:

既然在uni-app中通过当前view获取控制器的方式行不通,那就直接拿到跟控制器,由跟控制器去present到通讯录控制器:

  • 获取跟控制的iOS代码:

    #pragma mark - 获取跟控制
    - (UIViewController *)getRootViewController {
        UIApplication *sharedApplication = [UIApplication sharedApplication];
        id appDelegate = [sharedApplication delegate];
        UIWindow *appWindow = [appDelegate window];
        return [appWindow rootViewController];
    }
    
  • 转换成NJS代码如下:

    /**
     * 获取跟控制器
     */
    getRootViewController: function(){
        //UIApplication类对象
        var UIApplication = plus.ios.invoke("UIApplication", "class");
        var sharedApplication = plus.ios.invoke(UIApplication, "sharedApplication");
        var appDelegate = plus.ios.invoke(sharedApplication, "delegate");
        var appWindow = plus.ios.invoke(appDelegate, "window");
        return plus.ios.invoke(appWindow, "rootViewController");
    },
    
  • 跳转通讯录控制器的代码如下:

    • visitContacts中跳转通讯录的代码:
    /*注释掉由当前控制器跳转通讯录的代码
    //获取当前UIWebView视图
    var currentWebview = plus.ios.currentWebview();
    //根据当前UIWebView视图获取当前控制器
    var currentVC = nativeCommon.contacts.ios.getViewControllerByView(currentWebview);
    //由当前控制器present到通讯录控制器
    plus.ios.invoke(currentVC, "presentViewController:animated:completion:", contactPickerVC, true, null);
    */
    //获取跟控制器
    var rootVc = nativeCommon.contacts.ios.getRootViewController();
    //由跟控制器present到通讯录控制器
    plus.ios.invoke(rootVc, "presentViewController:animated:completion:", contactPickerVC, true, null);
    
    • visitAddressBook中跳转通讯录的代码:
    /*注释掉由当前控制器跳转通讯录的代码
    //获取当前UIWebView视图
    var currentWebview = plus.ios.currentWebview();
    //根据当前UIWebView视图获取当前控制器
    var currentVC = nativeCommon.contacts.ios.getViewControllerByView(currentWebview);
    //由当前控制器present到通讯录控制器
    plus.ios.invoke(currentVC, "presentViewController:animated:completion:", peoplePickerNavController, true, null);
    */
    //获取跟控制器
    var rootVc = nativeCommon.contacts.ios.getRootViewController();
    //由跟控制器present到通讯录控制器
    plus.ios.invoke(rootVc, "presentViewController:animated:completion:", peoplePickerNavController, true, null);
    

这种由跟控制器present到通讯录控制器的方式不仅适用于uni-app也适用于的5+App。uni-app的demo链接已经放到文章中,有需要的小伙伴可以点击下载。

5+App demo链接:https://github.com/w-wh/VisitContactsDemo
uni-app demo链接:https://github.com/w-wh/VisitContactsDemo_uni-app

你可能感兴趣的:(通过Native.js访问iOS原生通讯录界面(ContactsUI/AddressBookUI)iOS14系统不可用)