Integrating iOS Contact Picker into Flutter Applications using MethodChannel
Flutter Tanzania
Posted on February 2, 2023
During the development of a project, I was tasked with accessing the user's contact information. In an effort to find a suitable solution, I conducted a search on the pub.dev repository for any available packages that could help me resolve this issue. While I came across some great packages that provided a solution, most of them did not utilize the native contact picker. Although these packages offered a viable solution, I found it more professional to implement the functionality using the native contact picker. This not only provides a more seamless experience for the user but also demonstrates a higher level of technical proficiency.
However, after conducting thorough research, I was unable to find a resource that provided a clear and straightforward explanation on how to achieve this functionality. As a result, I decided to take it upon myself to create a demonstration on the implementation of accessing user contacts using the native contact picker. This not only serves as a solution to my own challenge but also serves as a valuable resource for others who may encounter the same issue in the future.
By the end of this article we will be able to have the following function.
Writing Custom Platform-Specific Code to access contact picker
In order to accomplish this function we will need to write native code for iOS.
Flutter uses a flexible system that allows you to call platform-specific APIs in a language that works directly with those APIs:
- Kotlin or Java on Android
- Swift or Objective-C on iOS
- C++ on Windows
- Objective-C on macOS
- C on Linux
To achieve the goal of accessing user contacts, we will utilize a channel named MethodChannel
as a means of communication between the Dart portion of the application and its native counterpart. The Dart part will send messages through the channel and the native part will listen for these messages and take appropriate actions in response. This approach allows for seamless and efficient communication between the two parts of the application, resulting in the successful implementation of the desired functionality.
MethodChannel facilitates the invocation of named methods, which can include or exclude arguments, between the Dart and native code. It provides a means for sending a one-time send-and-reply message. It is important to note that this channel does not support the continuous transmission of data streams.
The following explanation is from flutter official docs to explain how method channel works
Let's start by creating a flutter app
Clear Flutter Starter app and create a new dart file home.dart
import 'package:contact_specific/home.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
);
}
}
Setting up Method Channel on iOS With Swift
To receive method calls on iOS, it is necessary to have the channel's name and a closure. The channel's name serves as an identifier for the channel and must be consistent across iOS, Flutter, and Android. When a method is invoked on the MethodChannel from the Dart side, the Flutter Platform Engine will call the assigned closure. Within this closure, the name and parameters of the invoked method can be retrieved and consumed. This mechanism enables seamless communication between the Dart and native portions of the application.
To implement communication between the Dart and native code in iOS, it is necessary to open the AppDelegate.swift
file located in the iOS
folder. If you are not familiar with Swift or Kotlin, there is no need to worry as you can easily find the necessary information on Stack Overflow. The key requirement is to establish a channel of communication between the two codebases. With this in mind, you can proceed to implement the required functionality.
Add the following imports in AppDelegate.swift
file.
import ContactsUI
import Foundation
These are two import statements in Swift, which bring in the functionality of two framework libraries.
import ContactsUI
: This statement imports the Contacts User Interface framework, which provides UI components for displaying contact information and allows users to pick and select contacts.import Foundation
: This statement imports the Foundation framework, which provides a base layer of functionality for iOS and macOS apps, including data types, collections, and utility classes for networking, file systems, and more.
Before AppDelegate
class add the following class
class ContactPickerDelegate: NSObject, CNContactPickerDelegate {
public var onSelectContact: (CNContact) -> Void
public var onCancel: () -> Void
init(onSelectContact: @escaping (CNContact) -> Void,
onCancel: @escaping () -> Void) {
self.onSelectContact = onSelectContact
self.onCancel = onCancel
super.init()
}
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
picker.presentingViewController?.dismiss(animated: true, completion: nil)
onSelectContact(contact)
}
func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
picker.presentingViewController?.dismiss(animated: true, completion: nil)
onCancel()
}
}
This is a Swift class ContactPickerDelegate
, which conforms to the CNContactPickerDelegate
protocol. The purpose of this class is to handle the selection and cancellation events when a user interacts with the native contact picker view.
onSelectContact
: This is a closure that takes aCNContact
object as a parameter and is executed when a user selects a contact from the picker view.onCancel
: This is a closure that is executed when a user cancels the contact picker view.
The class has two initializers which takes two closures as parameters, onSelectContact
and onCancel
, and stores them as class properties.
The class implements two delegate methods of the CNContactPickerDelegate
protocol:
contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact)
: This method is called when a user selects a contact from the picker view. It dismisses the picker view and calls theonSelectContact
closure with the selectedCNContact
object.contactPickerDidCancel(_ picker: CNContactPickerViewController)
: This method is called when a user cancels the picker view. It dismisses the picker view and calls theonCancel
closure.
Next, add the following code above GeneratedPluginRegistrant.register(with: self)
inside application()
:
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let contactChannel = FlutterMethodChannel(name: "com.prosper.specific", binaryMessenger: controller.binaryMessenger)
contactChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
guard call.method == "getAContact" else {
result(FlutterMethodNotImplemented)
return
}
self.getAContact(withResult: result)
})
Here’s what the code above does:
This code creates an instance of FlutterMethodChannel
class and sets its name to "com.prosper.specific". FlutterMethodChannel
class is used to communicate between the Flutter framework (in Dart) and native platform code. The binaryMessenger
property of the FlutterMethodChannel
is set to the binaryMessenger
property of a FlutterViewController
object, which allows the channel to send messages between the Flutter and native parts of the application.
The code then sets the setMethodCallHandler
method of the contactChannel
object. This method takes a closure as a parameter which is called whenever a method call is received from the Flutter side. In the closure, it checks if the method received is equal to "getAContact". If the method received is not "getAContact", the closure returns a FlutterMethodNotImplemented
result. If the method received is "getAContact", the closure calls a function getAContact
with the result
parameter.
What we need to do now is to display the contacts picker when the getAContact
message is received, so let's add this to our class:
Add the following code below application()
var contactPickerDelegate: ContactPickerDelegate?
private func getAContact(withResult result: @escaping FlutterResult) {
let contactPicker = CNContactPickerViewController()
contactPickerDelegate = ContactPickerDelegate(onSelectContact: { contact in
result(contact.phoneNumbers[0].value.stringValue)
self.contactPickerDelegate = nil
},
onCancel: {
result(nil)
self.contactPickerDelegate = nil
})
contactPicker.delegate = contactPickerDelegate
let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow })
let rootViewController = keyWindow?.rootViewController
DispatchQueue.main.async {
rootViewController?.present(contactPicker, animated: true)
}
}
The code sets up the contact picker view controller and its delegate, ContactPickerDelegate
. The method getAContact
is called when the Flutter side sends a message through the MethodChannel with the method name "getAContact". When this method is called, the contact picker view controller is presented to the user.
The delegate, ContactPickerDelegate
, handles the user's interaction with the contact picker. When a user selects a contact, the onSelectContact
closure is executed, and the phone number of the selected contact is returned as the result. If the user cancels the selection, the onCancel
closure is executed, and nil
is returned as the result.
The iOS part is done!
Setting up the Method Channel on Flutter
Back to home.dart file, add the following import.
import 'package:flutter/services.dart';
below HomaPage
class add the following class,
class ContactPicker {
static const MethodChannel _channel = MethodChannel('com.prosper.specific');
static Future<String> getAContact() async {
final String contact = await _channel.invokeMethod('getAContact');
return contact;
}
}
This code defines a Dart class named ContactPicker
. The class contains a static MethodChannel instance named _channel
with a hardcoded identifier of 'com.prosper.specific'.
The class also contains a static method named getAContact
that retrieves a contact information from the native platform (iOS) using the invokeMethod function on the _channel instance.
The invokeMethod
function is used to call a native platform code from Dart and the string argument "getAContact" represents the name of the method that needs to be executed in the native platform. The function returns the contact information as a string, which is the result of the method call.
Add the following function inside the HomePage
class just after build
_getAContact() async {
String contact;
try {
contact = await ContactPicker.getAContact();
} on PlatformException {
contact = 'Failed to get contact.';
}
if (!mounted) return;
setState(() {
_contact = contact;
});
}
}
This code defines an asynchronous method named _getAContact. The method retrieves a contact information using the getAContact method from the ContactPicker class.
The method uses a try-catch block to handle exceptions that may occur during the retrieval of the contact information. If the getAContact method throws a PlatformException, the catch block sets the contact variable to a string "Failed to get contact.".
After retrieving the contact information, the method checks if the widget is still mounted using the mounted property. If the widget is not mounted, the method returns immediately.
Finally, the method uses the setState method to update the _contact variable with the retrieved contact information. The setState method is used to trigger a rebuild of the widget's UI.
In the HomePage
class define the following variable for storing contact data.
String _contact = 'Unknown';
Finally in your build
function return the following code.
Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MaterialButton(
color: Colors.blue,
textColor: Colors.white,
child: const Text('Pick a contact'),
onPressed: () => _getAContact(),
),
const SizedBox(
height: 10,
),
Text(_contact)
],
),
),
);
This code calls _getContact
function when the user presses the button, and displays the value of _contact
variable, which have the contact information.
You may need to stop your app and run again for the function to run properly.
You may find the full code in our GitHub repo here
In conclusion, this article has shown how to access the user's contacts using the native contact picker in an iOS app developed using Flutter. This approach uses MethodChannel to communicate between the Dart part and the native part of the app. The channel's name and a closure are used to receive method calls on iOS. The Flutter Platform Engine calls the closure when a method on the Method Channel is invoked from the Dart side. The code example demonstrated how to display the contacts picker when the getAContact
message is received, retrieve the selected contact, and pass it back to the Dart side of the app. This approach provides a professional and native experience for accessing user contacts in Flutter-based iOS apps.
Posted on February 2, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024