When there are 57000 contacts in CRM, people don’t want to write them to iPhone at all. It is necessary to find a better solution, which will allow not only to search for contacts in a separate application, but also to display the name of the person during an incoming call. We were googling for a long time, and then we remembered the announcement of the CallKit framework from WWDC. Information on this topic was not so much: laconic
documentation ,
an article on Habré and not a single step-by-step guide. I want to fill this gap. Using the example of creating a simple application, I will show you how to teach CallKit to identify thousands of numbers.
Determine one number
To begin, let's try to identify one single number.
Let's start with an empty project. Create a Single View Application with the name TouchInApp.
')
Add an extension to define the numbers. In the Xcode menu, select File> New> Target ... In the Application Extension section, select the Call Directory Extension, click Next.
In the Product Name field, type TouchInCallExtension, click Finish. In the alert that appears, click Cancel.
I hope you have already prepared a test phone from which you will call. If not, now is the time.
In the Project navigator, expand the TouchInCallExtension and open CallDirectoryHandler.swift. Find the
addIdentificationPhoneNumbers
function. There you will see
phoneNumbers
and
labels
arrays. Delete the numbers from
phoneNumbers
, enter the test number there. Delete the contents of the
labels
array, enter “Test number” there.
You will have something like this:
private func addIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) throws { let phoneNumbers: [CXCallDirectoryPhoneNumber] = [ 79214203692 ] let labels = [ "Test number" ] for (phoneNumber, label) in zip(phoneNumbers, labels) { context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label) } }
CXCallDirectoryPhoneNumber is just a
typealias
for
Int64
. The number must be in the format 7XXXXXXXXXX, that is, first the country code (country calling code), then the number itself. The code of Russia is +7, so in this case we write 7.
Put the application on the device and immediately close it. There is nothing to do in it yet. Go to phone settings> Phone> Call Blocking & Identification. Find the TouchInApp application there and let it identify and block calls. It happens that the application does not immediately appear in the list. In this case, close the settings, open and close the application again and try again.
When you
addIdentificationPhoneNumbers
Switch to the On state,
addIdentificationPhoneNumbers
is
addIdentificationPhoneNumbers
from the previously added extension and reads the contacts from there.
Call from the test number to your device. Number must be determined.
We define thousands of numbers
All this, of course, great, but this is just one number. And at the beginning of the article it was about thousands of contacts. Obviously, we will not manually rewrite all of them into
phoneNumbers
and
labels
arrays.
So, we have to add contacts in the extension. From the application, we can not do this. We can only call the
reloadExtension
function, the call of which will lead to the call to
addIdentificationPhoneNumbers
. I will tell about it a bit later.
One way or another, the application will have access to contacts. Either they will immediately be delivered with it in a certain format, or we will receive them upon request to the API, or in some other way it does not matter. It is important that the extension should somehow get these contacts.
Let's digress for a second and draw a little analogy. Imagine you have a cat. If there is, you can not submit. You wake up in the morning and are going to feed him. How will you do it? In all likelihood, fill the food in a bowl. And already from it the cat will eat.
Now imagine that the Call Directory Extension is a cat, and you are an application. And you want to feed the contacts of the Call Directory Extension. What in our case will play the role of a bowl, which we must fill with contacts and from which the extension will subsequently consume them? Unfortunately, there are not so many options. We can not use Core Data or SQLite, as it is very limited in resources during the expansion.
When you edited the
addIdentificationPhoneNumbers
function, you probably noticed the comments. It says that "Numbers must be provided in numerically ascending order.". Sorting a sample from the database is too resource-intensive for expansion. Therefore, a solution using a database does not suit us.
All we have to do is use the file. For ease of implementation, we will use a text file of the following format:
Using this format does not lead to optimal performance. But this will make it possible to focus on the main points, instead of diving into working with binary files.
Alas, we cannot just take and get access to a single file both from the application and from the extension. However, if you use App Groups, it becomes possible.
Sharing contacts using App Groups
The App Group allows the application and extension to access shared data. For more information, see the
Apple documentation . If you have never worked with this - not scary, now I will tell you how to set it up.
In the Project navigator, click on your project. Select the target of the application, go to the Capabilities tab, enable App Groups. Add the group "group.ru.touchin.TouchInApp". The logic here is the same as with the bundle identifier. Just add the group prefix. I have a bundle identifier - “ru.touchin.TouchInApp”, respectively, the group is “group.ru.touchin.TouchInApp”.
Go to the expansion target, go to the Capabilities tab, turn on App Groups. There should appear a group that you entered earlier. Put a tick on it.
If we use the “Automatically manage signing” option, App Groups are configured quite easily. As you can see, I did fit in a couple of paragraphs. Thanks to this, I can not turn the CallKit article into an App Groups article. But if you use profiles from a developer account, then you need to add an App Group in your account and enable it in the App App ID and Extensions.
Write contacts to file
After enabling the App Group, we can access the container in which our file will be stored. This is done as follows:
let container = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: "group.ru.touchin.TouchInApp")
“Group.ru.touchin.TouchInApp” is our App Group, which we just added.
Let's name our file “contacts” and form the
URL
for it:
guard let fileUrl = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: "group.ru.touchin.TouchInApp")? .appendingPathComponent("contacts") else { return }
A little later you will see the full code, now I just want to clarify some points.
Now you need to write in his numbers and names. It is assumed that you have already prepared them in the following form:
let numbers = ["79214203692", "79640982354", "79982434663"] let labels = [" ", " ", " "]
Let me remind you that the numbers must be with the correct country code and sorted in ascending order.
Now let's form the future content of the file from the contacts:
var string = "" for (number, label) in zip(numbers, labels) { string += "\(number),\(label)\n" }
Each pair of number-name is written in one line, separated by a comma. End with a newline character.
Write the whole thing to the file:
try? string.write(to: fileUrl, atomically: true, encoding: .utf8)
And now the fun part. It is necessary to inform the extension that the bowl is full and it is time to eat. To do this, call the following function:
CXCallDirectoryManager.sharedInstance.reloadExtension( withIdentifier: "ru.touchin.TouchInApp.TouchInCallExtension")
The function parameter is the bundle identifier of the extension.
Full code:
@IBAction func addContacts(_ sender: Any) { let numbers = ["79214203692", "79640982354", "79982434663"] let labels = [" ", " ", " "] writeFileForCallDirectory(numbers: numbers, labels: labels) } private func writeFileForCallDirectory(numbers: [String], labels: [String]) { guard let fileUrl = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: "group.ru.touchin.TouchInApp")? .appendingPathComponent("contacts") else { return } var string = "" for (number, label) in zip(numbers, labels) { string += "\(number),\(label)\n" } try? string.write(to: fileUrl, atomically: true, encoding: .utf8) CXCallDirectoryManager.sharedInstance.reloadExtension( withIdentifier: "ru.touchin.TouchInApp.TouchInCallExtension") }
We read contacts from file
But that is not all. We did not prepare an extension to read this file. We ask him to read the file one line at a time, extract the number and name from the line. Then we do the same as with the test number.
Alas, iOS does not provide the ability to read text files line by line. We use the
approach proposed by the user StackOverflow. Copy to yourself the
LineReader
class along with the extension.
Let's go back to the CallDirectoryHandler.swift file and make changes. First we get the URL of our file. This is done in the same way as in the application. Then we initialize the
LineReader
file path. We read the file line by line and add contact after contact.
The code for the updated function
addIdentificationPhoneNumbers
:
private func addIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) throws { guard let fileUrl = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: "group.ru.touchin.TouchInApp")? .appendingPathComponent("contacts") else { return } guard let reader = LineReader(path: fileUrl.path) else { return } for line in reader { autoreleasepool { // let line = line.trimmingCharacters(in: .whitespacesAndNewlines) // var components = line.components(separatedBy: ",") // Int64 guard let phone = Int64(components[0]) else { return } let name = components[1] context.addIdentificationEntry(withNextSequentialPhoneNumber: phone, label: name) } } }
The function must use a minimum of resources, so wrap the loop iteration in the
autoreleasepool
. This will free up temporary objects and use less memory.
Everything. Now, after calling the
addContacts
function
addContacts
phone will be able to determine numbers from the
numbers
array.
You can download the final version of the project in the
repository on GitHub .
What's next?
This is just one of the solutions to the problem. It can be improved by using a binary file instead of a text file,
as 2GIS did . This will allow you to quickly write and read data. Accordingly, it is necessary to consider the structure of the file, as well as rewrite the functions for writing and reading.
When you have an idea of ​​how this works, everything is in your hands.