DEV Community

Flutter Tanzania
Flutter Tanzania

Posted on

Integrating iOS Contact Picker into Flutter Applications using MethodChannel

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.

Image description

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

Image description

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(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

These are two import statements in Swift, which bring in the functionality of two framework libraries.

  1. 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.

  2. 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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

  1. onSelectContact: This is a closure that takes a CNContact object as a parameter and is executed when a user selects a contact from the picker view.

  2. 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:

  1. 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 the onSelectContact closure with the selected CNContact object.

  2. contactPickerDidCancel(_ picker: CNContactPickerViewController): This method is called when a user cancels the picker view. It dismisses the picker view and calls the onCancel 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)
})
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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)
      ],
    ),
  ),
);
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
Sloan, the sloth mascot
Comment deleted