⬆️ ⬇️

Identify numbers with CallKit





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.

Source: https://habr.com/ru/post/333364/



All Articles