本文同步更新地址:
- https://dotnet9.com/11520.html 
- https://terminalmacs.com/861.html 
阅读导航:
- 一、功能说明 
- 二、代码实现 
- 三、源码获取 
- 四、参考资料 
- 五、后面计划 
一、功能说明

完整思维导图:https://github.com/dotnet9/TerminalMACS/blob/master/docs/TerminalMACS.xmind
本文介绍图中右侧画红圈处的功能,即使用Xamarin.Forms获取和展示Android和iOS的通讯录信息,下面是最终效果,由于使用的是真实手机,所以联系人姓名及电话号码打码显示。

并简单的进行了搜索功能处理,之所以说简单,是因为通讯录列表是全部读取出来了,搜索是直接从此列表进行过滤的。
下图来自:https://www.xamboy.com/2019/10/10/getting-phone-contacts-in-xamarin-forms/,本功能是参考此文所写,所以直接引用文中的图片。


二、代码实现
1、共享库工程创建联系人实体类:Contacts.cs
- namespace TerminalMACS.Clients.App.Models 
- { 
- /// 
- /// 通讯录 
- /// 
- public class Contact 
- { 
- /// 
- /// 获取或者设置名称 
- /// 
- public string Name { get; set; } 
- /// 
- /// 获取或者设置 头像 
- /// 
- public string Image { get; set; } 
- /// 
- /// 获取或者设置 邮箱地址 
- /// 
- public string[] Emails { get; set; } 
- /// 
- /// 获取或者设置 手机号码 
- /// 
- public string[] PhoneNumbers { get; set; } 
- } 
- } 
2、共享库创建通讯录服务接口:IContactsService.cs
包括:
- 一个通讯录获取请求接口:RetrieveContactsAsync 
- 一个读取一条通讯结果通知事件:OnContactLoaded 
- using System; 
- using System.Collections.Generic; 
- using System.Threading; 
- using System.Threading.Tasks; 
- using TerminalMACS.Clients.App.Models; 
- namespace TerminalMACS.Clients.App.Services 
- { 
- /// 
- /// 通讯录事件参数 
- /// 
- public class ContactEventArgs:EventArgs 
- { 
- public Contact Contact { get; } 
- public ContactEventArgs(Contact contact) 
- { 
- Contact = contact; 
- } 
- } 
- /// 
- /// 通讯录服务接口,android和iOS终端具体的通讯录获取服务需要继承此接口 
- /// 
- public interface IContactsService 
- { 
- /// 
- /// 读取一条数据通知 
- /// 
- event EventHandler OnContactLoaded; 
- /// 
- /// 是否正在加载 
- /// 
- bool IsLoading { get; } 
- /// 
- /// 尝试获取所有通讯录 
- /// 
- /// 
- /// 
- Task> RetrieveContactsAsync(CancellationToken? token = null); 
- } 
- } 
3、iOS工程中添加通讯录服务,实现IContactsService接口:
- using Contacts; 
- using Foundation; 
- using System; 
- using System.Collections.Generic; 
- using System.IO; 
- using System.Linq; 
- using System.Threading; 
- using System.Threading.Tasks; 
- using TerminalMACS.Clients.App.Models; 
- using TerminalMACS.Clients.App.Services; 
- namespace TerminalMACS.Clients.App.iOS.Services 
- { 
- /// 
- /// 通讯录获取服务 
- /// 
- public class ContactsService : NSObject, IContactsService 
- { 
- const string ThumbnailPrefix = "thumb"; 
- bool requestStop = false; 
- public event EventHandler OnContactLoaded; 
- bool _isLoading = false; 
- public bool IsLoading => _isLoading; 
- /// 
- /// 异步请求权限 
- /// 
- /// 
- public async Task<bool> RequestPermissionAsync() 
- { 
- var status = CNContactStore.GetAuthorizationStatus(CNEntityType.Contacts); 
- Tuple<bool, NSError> authotization = new Tuple<bool, NSError>(status == CNAuthorizationStatus.Authorized, null); 
- if (status == CNAuthorizationStatus.NotDetermined) 
- { 
- using (var store = new CNContactStore()) 
- { 
- authotization = await store.RequestAccessAsync(CNEntityType.Contacts); 
- } 
- } 
- return authotization.Item1; 
- } 
- /// 
- /// 异步请求通讯录,此方法由界面真正调用 
- /// 
- /// 
- /// 
- public async Task> RetrieveContactsAsync(CancellationToken? cancelToken = null) 
- { 
- requestStop = false; 
- if (!cancelToken.HasValue) 
- cancelToken = CancellationToken.None; 
- // 我们创建了一个十进制的TaskCompletionSource 
- var taskCompletionSource = new TaskCompletionSource>(); 
- // 在cancellationToken中注册lambda 
- cancelToken.Value.Register(() => 
- { 
- // 我们收到一条取消消息,取消TaskCompletionSource.Task 
- requestStop = true; 
- taskCompletionSource.TrySetCanceled(); 
- }); 
- _isLoading = true; 
- var task = LoadContactsAsync(); 
- // 等待两个任务中的第一个任务完成 
- var completedTask = await Task.WhenAny(task, taskCompletionSource.Task); 
- _isLoading = false; 
- return await completedTask; 
- } 
- /// 
- /// 异步加载通讯录,具体的通讯录读取方法 
- /// 
- /// 
- async Task> LoadContactsAsync() 
- { 
- IList contacts = new List(); 
- var hasPermission = await RequestPermissionAsync(); 
- if (hasPermission) 
- { 
- NSError error = null; 
- var keysToFetch = new[] { CNContactKey.PhoneNumbers, CNContactKey.GivenName, CNContactKey.FamilyName, CNContactKey.EmailAddresses, CNContactKey.ImageDataAvailable, CNContactKey.ThumbnailImageData }; 
- var request = new CNContactFetchRequest(keysToFetch: keysToFetch); 
- request.SortOrder = CNContactSortOrder.GivenName; 
- using (var store = new CNContactStore()) 
- { 
- var result = store.EnumerateContacts(request, out error, new CNContactStoreListContactsHandler((CNContact c, ref bool stop) => 
- { 
- string path = null; 
- if (c.ImageDataAvailable) 
- { 
- path = path = Path.Combine(Path.GetTempPath(), $"{ThumbnailPrefix}-{Guid.NewGuid()}"); 
- if (!File.Exists(path)) 
- { 
- var imageData = c.ThumbnailImageData; 
- imageData?.Save(path, true); 
- } 
- } 
- var contact = new Contact() 
- { 
- Name = string.IsNullOrEmpty(c.FamilyName) ? c.GivenName : $"{c.GivenName} {c.FamilyName}", 
- Image = path, 
- PhoneNumbers = c.PhoneNumbers?.Select(p => p?.Value?.StringValue).ToArray(), 
- Emails = c.EmailAddresses?.Select(p => p?.Value?.ToString()).ToArray(), 
- }; 
- if (!string.IsNullOrWhiteSpace(contact.Name)) 
- { 
- OnContactLoaded?.Invoke(this, new ContactEventArgs(contact)); 
- contacts.Add(contact); 
- } 
- stop = requestStop; 
- })); 
- } 
- } 
- return contacts; 
- } 
- } 
- } 
4、在iOS工程中的Info.plist文件添加通讯录权限使用说明

5、在Android工程中添加读取通讯录权限配置:AndroidManifest.xml
- android:name="android.permission.READ_CONTACTS"/> 
完整权限配置如下
- xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.terminalmacs.clients.app"> 
- android:minSdkVersion="21" android:targetSdkVersion="28" /> 
- android:label="TerminalMACS.Clients.App.Android"> 
- android:name="android.permission.ACCESS_NETWORK_STATE" /> 
- android:name="android.permission.READ_CONTACTS"/> 
- android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> 
6、在Android工程中添加通讯录服务,实现IContactServer接口:ContactsService.cs
- using Acr.UserDialogs; 
- using Android; 
- using Android.App; 
- using Android.Content; 
- using Android.Content.PM; 
- using Android.Database; 
- using Android.Provider; 
- using Android.Runtime; 
- using Android.Support.V4.App; 
- using Plugin.CurrentActivity; 
- using System; 
- using System.Collections.Generic; 
- using System.IO; 
- using System.Linq; 
- using System.Threading; 
- using System.Threading.Tasks; 
- using TerminalMACS.Clients.App.Models; 
- using TerminalMACS.Clients.App.Services; 
- namespace TerminalMACS.Clients.App.Droid.Services 
- { 
- /// 
- /// 通讯录获取服务 
- /// 
- public class ContactsService : IContactsService 
- { 
- const string ThumbnailPrefix = "thumb"; 
- bool stopLoad = false; 
- static TaskCompletionSource<bool> contactPermissionTcs; 
- public string TAG 
- { 
- get 
- { 
- return "MainActivity"; 
- } 
- } 
- bool _isLoading = false; 
- public bool IsLoading => _isLoading; 
- //权限请求状态码 
- public const int RequestContacts = 1239; 
- /// 
- /// 获取通讯录需要的请求权限 
- /// 
- static string[] PermissionsContact = { 
- Manifest.Permission.ReadContacts 
- }; 
- public event EventHandler OnContactLoaded; 
- /// 
- /// 异步请求通讯录权限 
- /// 
- async void RequestContactsPermissions() 
- { 
- //检查是否可以弹出申请读、写通讯录权限 
- if (ActivityCompat.ShouldShowRequestPermissionRationale(CrossCurrentActivity.Current.Activity, Manifest.Permission.ReadContacts) 
- || ActivityCompat.ShouldShowRequestPermissionRationale(CrossCurrentActivity.Current.Activity, Manifest.Permission.WriteContacts)) 
- { 
- // 如果未授予许可,请向用户提供其他理由用户将从使用权限的附加上下文中受益。 
- // 例如,如果请求先前被拒绝。 
- await UserDialogs.Instance.AlertAsync("通讯录权限", "此操作需要“通讯录”权限", "确定"); 
- } 
- else 
- { 
- // 尚未授予通讯录权限。直接请求这些权限。 
- ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, PermissionsContact, RequestContacts); 
- } 
- } 
- /// 
- /// 收到用户响应请求权限操作后的结果 
- /// 
- /// 
- /// 
- /// 
- public static void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) 
- { 
- if (requestCode == RequestContacts) 
- { 
- // 我们请求了多个通讯录权限,因此需要检查相关的所有权限 
- if (PermissionUtil.VerifyPermissions(grantResults)) 
- { 
- // 已授予所有必需的权限,显示联系人片段。 
- contactPermissionTcs.TrySetResult(true); 
- } 
- else 
- { 
- contactPermissionTcs.TrySetResult(false); 
- } 
- } 
- } 
- /// 
- /// 异步请求权限 
- /// 
- /// 
- public async Task<bool> RequestPermissionAsync() 
- { 
- contactPermissionTcs = new TaskCompletionSource<bool>(); 
- // 验证是否已授予所有必需的通讯录权限。 
- if (Android.Support.V4.Content.ContextCompat.CheckSelfPermission(CrossCurrentActivity.Current.Activity, Manifest.Permission.ReadContacts) != (int)Permission.Granted 
- || Android.Support.V4.Content.ContextCompat.CheckSelfPermission(CrossCurrentActivity.Current.Activity, Manifest.Permission.WriteContacts) != (int)Permission.Granted) 
- { 
- // 尚未授予通讯录权限。 
- RequestContactsPermissions(); 
- } 
- else 
- { 
- // 已授予通讯录权限。 
- contactPermissionTcs.TrySetResult(true); 
- } 
- return await contactPermissionTcs.Task; 
- } 
- /// 
- /// 异步请求通讯录,此方法由界面真正调用 
- /// 
- /// 
- /// 
- public async Task> RetrieveContactsAsync(CancellationToken? cancelToken = null) 
- { 
- stopLoad = false; 
- if (!cancelToken.HasValue) 
- cancelToken = CancellationToken.None; 
- // 我们创建了一个十进制的TaskCompletionSource 
- var taskCompletionSource = new TaskCompletionSource>(); 
- // 在cancellationToken中注册lambda 
- cancelToken.Value.Register(() => 
- { 
- // 我们收到一条取消消息,取消TaskCompletionSource.Task 
- stopLoad = true; 
- taskCompletionSource.TrySetCanceled(); 
- }); 
- _isLoading = true; 
- var task = LoadContactsAsync(); 
- // 等待两个任务中的第一个任务完成 
- var completedTask = await Task.WhenAny(task, taskCompletionSource.Task); 
- _isLoading = false; 
- return await completedTask; 
- } 
- /// 
- /// 异步加载通讯录,具体的通讯录读取方法 
- /// 
- /// 
- async Task> LoadContactsAsync() 
- { 
- IList contacts = new List(); 
- var hasPermission = await RequestPermissionAsync(); 
- if (!hasPermission) 
- { 
- return contacts; 
- } 
- var uri = ContactsContract.Contacts.ContentUri; 
- var ctx = Application.Context; 
- await Task.Run(() => 
- { 
- // 暂时只请求通讯录Id、DisplayName、PhotoThumbnailUri,可以扩展 
- var cursor = ctx.ApplicationContext.ContentResolver.Query(uri, new string[] 
- { 
- ContactsContract.Contacts.InterfaceConsts.Id, 
- ContactsContract.Contacts.InterfaceConsts.DisplayName, 
- ContactsContract.Contacts.InterfaceConsts.PhotoThumbnailUri 
- }, null, null, $"{ContactsContract.Contacts.InterfaceConsts.DisplayName} ASC"); 
- if (cursor.Count > 0) 
- { 
- while (cursor.MoveToNext()) 
- { 
- var contact = CreateContact(cursor, ctx); 
- if (!string.IsNullOrWhiteSpace(contact.Name)) 
- { 
- // 读取出一条,即通知界面展示 
- OnContactLoaded?.Invoke(this, new ContactEventArgs(contact)); 
- contacts.Add(contact); 
- } 
- if (stopLoad) 
- break; 
- } 
- } 
- }); 
- return contacts; 
- } 
- /// 
- /// 读取一条通讯录数据 
- /// 
- /// 
- /// 
- /// 
- Contact CreateContact(ICursor cursor, Context ctx) 
- { 
- var contactId = GetString(cursor, ContactsContract.Contacts.InterfaceConsts.Id); 
- var numbers = GetNumbers(ctx, contactId); 
- var emails = GetEmails(ctx, contactId); 
- var uri = GetString(cursor, ContactsContract.Contacts.InterfaceConsts.PhotoThumbnailUri); 
- string path = null; 
- if (!string.IsNullOrEmpty(uri)) 
- { 
- try 
- { 
- using (var stream = Android.App.Application.Context.ContentResolver.OpenInputStream(Android.Net.Uri.Parse(uri))) 
- { 
- path = Path.Combine(Path.GetTempPath(), $"{ThumbnailPrefix}-{Guid.NewGuid()}"); 
- using (var fstream = new FileStream(path, FileMode.Create)) 
- { 
- stream.CopyTo(fstream); 
- fstream.Close(); 
- } 
- stream.Close(); 
- } 
- } 
- catch (Exception ex) 
- { 
- System.Diagnostics.Debug.WriteLine(ex); 
- } 
- } 
- var contact = new Contact 
- { 
- Name = GetString(cursor, ContactsContract.Contacts.InterfaceConsts.DisplayName), 
- Emails = emails, 
- Image = path, 
- PhoneNumbers = numbers, 
- }; 
- return contact; 
- } 
- /// 
- /// 读取联系人电话号码 
- /// 
- /// 
- /// 
- /// 
- string[] GetNumbers(Context ctx, string contactId) 
- { 
- var key = ContactsContract.CommonDataKinds.Phone.Number; 
- var cursor = ctx.ApplicationContext.ContentResolver.Query( 
- ContactsContract.CommonDataKinds.Phone.ContentUri, 
- null, 
- ContactsContract.CommonDataKinds.Phone.InterfaceConsts.ContactId + " = ?", 
- new[] { contactId }, 
- null 
- ); 
- return ReadCursorItems(cursor, key)?.ToArray(); 
- } 
- /// 
- /// 读取联系人邮箱地址 
- /// 
- /// 
- /// 
- /// 
- string[] GetEmails(Context ctx, string contactId) 
- { 
- var key = ContactsContract.CommonDataKinds.Email.InterfaceConsts.Data; 
- var cursor = ctx.ApplicationContext.ContentResolver.Query( 
- ContactsContract.CommonDataKinds.Email.ContentUri, 
- null, 
- ContactsContract.CommonDataKinds.Email.InterfaceConsts.ContactId + " = ?", 
- new[] { contactId }, 
- null); 
- return ReadCursorItems(cursor, key)?.ToArray(); 
- } 
- IEnumerable<string> ReadCursorItems(ICursor cursor, string key) 
- { 
- while (cursor.MoveToNext()) 
- { 
- var value = GetString(cursor, key); 
- yield return value; 
- } 
- cursor.Close(); 
- } 
- string GetString(ICursor cursor, string key) 
- { 
- return cursor.GetString(cursor.GetColumnIndex(key)); 
- } 
- } 
- } 
需要添加 Plugin.CurrentActivity 和 Acr.UserDialogs 包。
7、Android工程添加权限处理判断类
Permission.Util.cs
- using Android.Content.PM; 
- namespace TerminalMACS.Clients.App.Droid 
- { 
- public static class PermissionUtil 
- { 
- /** 
- * 通过验证给定数组中的每个条目的值是否为Permission.Granted,检查是否已授予所有给定权限。 
- * 
- * See Activity#onRequestPermissionsResult (int, String[], int[]) 
- */ 
- public static bool VerifyPermissions(Permission[] grantResults) 
- { 
- // 必须至少检查一个结果. 
- if (grantResults.Length < 1) 
- return false; 
- // 验证是否已授予每个必需的权限,否则返回false. 
- foreach (Permission result in grantResults) 
- { 
- if (result != Permission.Granted) 
- { 
- return false; 
- } 
- } 
- return true; 
- } 
- } 
- } 
MainActivity.OnRequestPermissionResult是权限申请结果处理函数,在此函数中调用ContactsService.OnRequestPermissionsResult通知通讯录服务权限处理结果。
MainActivity.cs
- using Acr.UserDialogs; 
- using Android.App; 
- using Android.Content.PM; 
- using Android.OS; 
- using Android.Runtime; 
- using TerminalMACS.Clients.App.Droid.Services; 
- using TerminalMACS.Clients.App.Services; 
- namespace TerminalMACS.Clients.App.Droid 
- { 
- [Activity(Label = "TerminalMACS.Clients.App", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] 
- public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity 
- { 
- IContactsService contactsService = new ContactsService(); 
- protected override void OnCreate(Bundle savedInstanceState) 
- { 
- TabLayoutResource = Resource.Layout.Tabbar; 
- ToolbarResource = Resource.Layout.Toolbar; 
- base.OnCreate(savedInstanceState); 
- Xamarin.Essentials.Platform.Init(this, savedInstanceState); 
- global::Xamarin.Forms.Forms.Init(this, savedInstanceState); 
- UserDialogs.Init(() => this); 
- // 将通讯录服务实例传递给共享库,由共享库使用读取通讯录接口 
- LoadApplication(new App(contactsService)); 
- } 
- public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) 
- { 
- Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); 
- // 通讯录服务处理权限请求结果 
- ContactsService.OnRequestPermissionsResult(requestCode, permissions, grantResults); 
- base.OnRequestPermissionsResult(requestCode, permissions, grantResults); 
- } 
- } 
- } 
8、创建通讯录ViewModel,并使用通讯录服务
- using System; 
- using System.Collections; 
- using System.Collections.Generic; 
- using System.Collections.ObjectModel; 
- using System.Linq; 
- using System.Threading.Tasks; 
- using System.Windows.Input; 
- using TerminalMACS.Clients.App.Models; 
- using TerminalMACS.Clients.App.Services; 
- using Xamarin.Forms; 
- namespace TerminalMACS.Clients.App.ViewModels 
- { 
- /// 
- /// 通讯录ViewModel 
- /// 
- public class ContactViewModel : BaseViewModel 
- { 
- /// 
- /// 通讯录服务接口 
- /// 
- IContactsService _contactService; 
- /// 
- /// 标题 
- /// 
- public new string Title => "通讯录"; 
- private string _SearchText; 
- /// 
- /// 搜索关键字 
- /// 
- public string SearchText 
- { 
- get { return _SearchText; } 
- set 
- { 
- SetProperty(ref _SearchText, value); 
- } 
- } 
- /// 
- /// 通讯录搜索命令 
- /// 
- public ICommand RaiseSearchCommand { get; } 
- /// 
- /// 通讯录列表 
- /// 
- public ObservableCollection Contacts { get; set; } 
- private List _FilteredContacts; 
- /// 
- /// 通讯录过滤列表 
- /// 
- public List FilteredContacts 
- { 
- get { return _FilteredContacts; } 
- set 
- { 
- SetProperty(ref _FilteredContacts, value); 
- } 
- } 
- public ContactViewModel(IContactsService contactService) 
- { 
- _contactService = contactService; 
- Contacts = new ObservableCollection(); 
- Xamarin.Forms.BindingBase.EnableCollectionSynchronization(Contacts, null, ObservableCollectionCallback); 
- _contactService.OnContactLoaded += OnContactLoaded; 
- LoadContacts(); 
- RaiseSearchCommand = new Command(RaiseSearchHandle); 
- } 
- /// 
- /// 过滤通讯录 
- /// 
- void RaiseSearchHandle() 
- { 
- if (string.IsNullOrEmpty(SearchText)) 
- { 
- FilteredContacts = Contacts.ToList(); 
- return; 
- } 
- Funcbool> checkContact = (s) => 
- { 
- if (!string.IsNullOrWhiteSpace(s.Name) && s.Name.ToLower().Contains(SearchText.ToLower())) 
- { 
- return true; 
- } 
- else if (s.PhoneNumbers.Length > 0 && s.PhoneNumbers.ToList().Exists(cu => cu.ToString().Contains(SearchText))) 
- { 
- return true; 
- } 
- return false; 
- }; 
- FilteredContacts = Contacts.ToList().Where(checkContact).ToList(); 
- } 
- /// 
- /// BindingBase.EnableCollectionSynchronization 为集合启用跨线程更新 
- /// 
- /// 
- /// 
- /// 
- /// 
- void ObservableCollectionCallback(IEnumerable collection, object context, Action accessMethod, bool writeAccess) 
- { 
- // `lock` ensures that only one thread access the collection at a time 
- lock (collection) 
- { 
- accessMethod?.Invoke(); 
- } 
- } 
- /// 
- /// 收到事件通知,读取一条通讯录信息 
- /// 
- /// 
- /// 
- private void OnContactLoaded(object sender, ContactEventArgs e) 
- { 
- Contacts.Add(e.Contact); 
- RaiseSearchHandle(); 
- } 
- /// 
- /// 异步读取终端通讯录 
- /// 
- /// 
- async Task LoadContacts() 
- { 
- try 
- { 
- await _contactService.RetrieveContactsAsync(); 
- } 
- catch (TaskCanceledException) 
- { 
- Console.WriteLine("任务已经取消"); 
- } 
- } 
- } 
- } 
9、添加通讯录页面展示通讯录数据
- <?xml version="1.0" encoding="utf-8" ?> 
- "http://xamarin.com/schemas/2014/forms" 
- xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
- xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
- xmlns:d="http://xamarin.com/schemas/2014/forms/design" 
- xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core" 
- mc:Ignorable="d" 
- Title="{Binding Title}" 
- x:Class="TerminalMACS.Clients.App.Views.ContactPage" 
- ios:Page.UseSafeArea="true"> 
- "filterText" 
- HeightRequest="40" 
- Text="{Binding SearchText}" 
- SearchCommand="{Binding RaiseSearchCommand}"/> 
- "{Binding FilteredContacts}" 
- HasUnevenRows="True"> 
- "10" 
- Orientation="Horizontal"> 
- "{Binding Image}" 
- VerticalOptions="Center" 
- x:Name="image" 
- Aspect="AspectFit" 
- HeightRequest="60"/> 
- "Center"> 
- "{Binding Name}" 
- FontAttributes="Bold"/> 
- "{Binding PhoneNumbers[0]}"/> 
- "{Binding Emails[0]}"/> 
三、源码获取
- 1.完整源码:https://github.com/dotnet9/TerminalMACS 
- 2.Android客户端可成功取得通讯录数据,并可查询; 
已编译的Android客户端:https://terminalmacs.com/terminalmacs-clients-app-android
- 3.iOS读取通讯录功能代码也已添加,但由于本人没有iOS测试环境,所以未验证,有条件的朋友可以测试下iOS的通讯录读取功能,如果代码不起作用,可参考本文参考的文章检查iOS代码。 
四、参考资料
Getting phone contacts in Xamarin Forms:https://www.xamboy.com/2019/10/10/getting-phone-contacts-in-xamarin-forms/
参考文章末尾有源代码链接。
五、后面计划
Xamarin.Forms客户端基本信息获取,比如IMEI、IMSI、本机号码、Mac地址等。