Home Identify Your Own Calls (Swift)
Post
Cancel

Identify Your Own Calls (Swift)

Identify Your Own Calls (Swift)

iOS DIY Whoscall Call Identification and Phone Number Tagging Features

Origin

I have always been a loyal user of Whoscall. I used it when I originally had an Android phone, and it could display unknown caller information very promptly, allowing me to decide whether to answer the call immediately. Later, I switched to the Apple camp, and my first Apple phone was the iPhone 6 (iOS 9). At that time, using Whoscall was very awkward; it couldn’t identify calls in real-time, and I had to copy the phone number to the app for inquiry. Later, Whoscall provided a service to install the unknown phone number database locally on the phone, which solved the real-time identification problem but easily messed up my phone contacts!

Until iOS 10+ when Apple opened the call identification feature (Call Directory Extension) permissions to developers, Whoscall’s experience at least matched the Android version, if not surpassed it (the Android version has a lot of ads, but from a developer’s standpoint, it’s understandable).

Purpose?

Call Directory Extension can do what?

  1. Phone outgoing call identification and tagging
  2. Phone incoming call identification and tagging
  3. Call history identification and tagging
  4. Phone blocking blacklist setup

Limitations?

  1. Users need to manually go to “Settings” -> “Phone” -> “Call Blocking & Identification” to enable your app.
  2. Can only identify calls using an offline database (cannot obtain incoming call information in real-time and then call an API for inquiry, can only pre-write number <-> name mappings in the phone database). *Therefore, Whoscall periodically pushes notifications asking users to open the app to update the call identification database.
  3. Quantity limit? No data found so far, it should depend on the user’s phone capacity with no special limit; however, a large number of identification lists and blocking lists need to be processed in batches!
  4. Software limitation: iOS version must be ≥ 10

“Settings” -> “Phone” -> “Call Blocking & Identification”

“Settings” -> “Phone” -> “Call Blocking & Identification”

Application Scenarios?

  1. Communication software, office communication software; in the app, you may have the contact of the other party but have not actually added the phone number to the phone contacts. This feature can avoid missing calls from colleagues or even the boss by treating them as unknown calls.
  2. Our site (Marry) or our private (591 Real Estate), when users contact stores or landlords, the calls are made through our transfer numbers, routed through the transfer center to the target phone. The general process is as follows:

The calls made by users are all representative numbers of the transfer center (#extension), and they will not know the real phone number; on one hand, it protects personal privacy, and on the other hand, it allows us to know how many people contacted the store (evaluate effectiveness) and even know where they saw it before calling (e.g., webpage shows #1234, app shows #5678). It also allows us to offer free services by absorbing the phone communication costs.

However, this approach brings an unavoidable problem: messy phone numbers. It is impossible to identify who the call is for or when the store calls back, the user does not know who the caller is. Using the call identification feature can greatly solve this problem and improve the user experience!

Here’s a finished product screenshot:

[Marry APP](https://itunes.apple.com/tw/app/%E7%B5%90%E5%A9%9A%E5%90%A7-%E6%9C%80%E5%A4%A7%E5%A9%9A%E7%A6%AE%E7%B1%8C%E5%82%99app/id1356057329?mt=8){:target="_blank"}

結婚吧 APP

You can see that when entering the phone number, the recognition result can be directly displayed during the call, and the call history list is no longer messy and can display the recognition result at the bottom.

Call Directory Extension Call Recognition Function Workflow:

Let’s Get Started:

Let’s start working!

1. Add Call Directory Extension to the iOS project

Xcode -> File -> New -> Target

Xcode -> File -> New -> Target

Select Call Directory Extension

Select Call Directory Extension

Enter Extension Name

Enter Extension Name

Optionally add Scheme for easier Debugging

Optionally add Scheme for easier Debugging

A folder and program for Call Directory Extension will appear under the directory

A folder and program for Call Directory Extension will appear under the directory

First, return to the main iOS project

The first question is how do we determine if the user’s device supports Call Directory Extension or if the “Call Blocking & Identification” in the settings is turned on:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import CallKit
//
//......
//
if #available(iOS 10.0, *) {
    CXCallDirectoryManager.sharedInstance.getEnabledStatusForExtension(withIdentifier: "Enter the bundle identifier of the call directory extension here", completionHandler: { (status, error) in
        if status == .enabled {
          //Enabled
        } else if status == .disabled {
          //Disabled
        } else {
          //Unknown, not supported
        }
    })
}

As mentioned earlier, the way call recognition works is to maintain a local recognition database; so how do we achieve this function?

Unfortunately, you cannot directly call and write data to the Call Directory Extension, so you need to maintain an additional corresponding structure, and then the Call Directory Extension will read your structure and write it into the recognition database. The process is as follows:

This means we need to maintain our own database file, and then let the Extension read and write it into the phone

This means we need to maintain our own database file, and then let the Extension read and write it into the phone

So what should the recognition data/file look like?

It is actually a Dictionary structure, such as: [“Phone”:”Wang Da Ming”]

The local file can use some Local DB (but the Extension must also be able to install and use it). Here, a .json file is directly stored on the phone; It is not recommended to store it directly in UserDefaults. If it is for testing or very little data, it is okay, but it is strongly not recommended for actual applications!

Okay, let’s start:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if #available(iOS 10.0, *) {
    if let dir = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "Your cross-Extension, Group Identifier name") {
        let fileURL = dir.appendingPathComponent("phoneIdentity.json")
        var datas:[String:String] = ["8869190001234":"Mr. Li","886912002456":"Handsome"]
        if let content = try? String(contentsOf: fileURL, encoding: .utf8),let text = content.data(using: .utf8),let json2 = try? JSONSerialization.jsonObject(with: text, options: .mutableContainers) as? Dictionary<String,String>,let json = json2 {
            datas = json
        }
        if let data = jsonToData(jsonDic: datas) {
            DispatchQueue(label: "phoneIdentity").async {
                if let _ = try? data.write(to: fileURL) {
                    //Writing json file completed
                }
            }
        }
    }
}

Just general local file maintenance, note that the directory needs to be readable by the Extension as well.

Supplement — Phone Number Format:

  1. For landline and mobile numbers in Taiwan, remove the 0 and replace it with 886: e.g., 0255667788 -> 886255667788
  2. The phone number format should be a string of pure numbers, do not mix in symbols like “-“, “,”, “#”, etc.
  3. If the landline phone number includes an extension, append it directly without any symbols: e.g., 0255667788,0718 -> 8862556677880718
  4. To convert the general iOS phone format into a format recognizable by the database, you can refer to the following two replacement methods:
1
2
3
4
5
6
7
var newNumber = "0255667788,0718"
if let regex = try? NSRegularExpression(pattern: "^0{1}") {
    newNumber = regex.stringByReplacingMatches(in: newNumber, options: [], range: NSRange(location: 0, length: newNumber.count), withTemplate: "886")
}
if let regex = try? NSRegularExpression(pattern: ",") {
    newNumber = regex.stringByReplacingMatches(in: newNumber, options: [], range: NSRange(location: 0, length: newNumber.count), withTemplate: "")
}

Next, as per the process, once the identification data is maintained, you need to notify the Call Directory Extension to refresh the data on the phone:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
if #available(iOS 10.0, *) {
    CXCallDirectoryManager.sharedInstance.reloadExtension(withIdentifier: "tw.com.marry.MarryiOS.CallDirectory") { errorOrNil in
        if let error = errorOrNil as? CXErrorCodeCallDirectoryManagerError {
            print("reload failed")
            
            switch error.code {
            case .unknown:
                print("error is unknown")
            case .noExtensionFound:
                print("error is noExtensionFound")
            case .loadingInterrupted:
                print("error is loadingInterrupted")
            case .entriesOutOfOrder:
                print("error is entriesOutOfOrder")
            case .duplicateEntries:
                print("error is duplicateEntries")
            case .maximumEntriesExceeded:
                print("maximumEntriesExceeded")
            case .extensionDisabled:
                print("extensionDisabled")
            case .currentlyLoading:
                print("currentlyLoading")
            case .unexpectedIncrementalRemoval:
                print("unexpectedIncrementalRemoval")
            }
        } else if let error = errorOrNil {
            print("reload error: \(error)")
        } else {
            print("reload succeeded")
        }
    }
}

Use the above method to notify the Extension to refresh and obtain the execution result. (At this time, the beginRequest in the Call Directory Extension will be called, please continue reading)

The main iOS project code ends here!

3. Start modifying the Call Directory Extension code

Open the Call Directory Extension directory and find the file CallDirectoryHandler.swift that has been created for you.

The only method that can be implemented is beginRequest for handling actions when processing phone data. The default examples are already set up for us, so there’s not much need to change them:

  1. addAllBlockingPhoneNumbers: Handles adding blacklist numbers (all at once)
  2. addOrRemoveIncrementalBlockingPhoneNumbers: Handles adding blacklist numbers (incrementally)
  3. addAllIdentificationPhoneNumbers: Handles adding caller identification numbers (all at once)
  4. addOrRemoveIncrementalIdentificationPhoneNumbers: Handles adding caller identification numbers (incrementally)

We just need to complete the implementation of the above functions. The principles for blacklist functionality and caller identification are the same, so they won’t be introduced in detail here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
private func fetchAll(context: CXCallDirectoryExtensionContext) {
    if let dir = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "Your App Group Identifier") {
        let fileURL = dir.appendingPathComponent("phoneIdentity.json")
        if let content = try? String(contentsOf: fileURL, encoding: .utf8), let text = content.data(using: .utf8), let numbers = try? JSONSerialization.jsonObject(with: text, options: .mutableContainers) as? Dictionary<String, String> {
            numbers?.sorted(by: { (Int($0.key) ?? 0) < Int($1.key) ?? 0 }).forEach({ (obj) in
                if let number = CXCallDirectoryPhoneNumber(obj.key) {
                    autoreleasepool {
                        if context.isIncremental {
                            context.removeIdentificationEntry(withPhoneNumber: number)
                        }
                        context.addIdentificationEntry(withNextSequentialPhoneNumber: number, label: obj.value)
                    }
                }
            })
        }
    }
}

private func addAllIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
    // Retrieve phone numbers to identify and their identification labels from data store. For optimal performance and memory usage when there are many phone numbers,
    // consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
    //
    // Numbers must be provided in numerically ascending order.
    //        let allPhoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1_877_555_5555, 1_888_555_5555 ]
    //        let labels = [ "Telemarketer", "Local business" ]
    //
    //        for (phoneNumber, label) in zip(allPhoneNumbers, labels) {
    //            context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
    //        }
    fetchAll(context: context)
}

private func addOrRemoveIncrementalIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
    // Retrieve any changes to the set of phone numbers to identify (and their identification labels) from data store. For optimal performance and memory usage when there are many phone numbers,
    // consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
    //        let phoneNumbersToAdd: [CXCallDirectoryPhoneNumber] = [ 1_408_555_5678 ]
    //        let labelsToAdd = [ "New local business" ]
    //
    //        for (phoneNumber, label) in zip(phoneNumbersToAdd, labelsToAdd) {
    //            context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
    //        }
    //
    //        let phoneNumbersToRemove: [CXCallDirectoryPhoneNumber] = [ 1_888_555_5555 ]
    //
    //        for phoneNumber in phoneNumbersToRemove {
    //            context.removeIdentificationEntry(withPhoneNumber: phoneNumber)
    //        }
    
    //context.removeIdentificationEntry(withPhoneNumber: CXCallDirectoryPhoneNumber("886277283610")!)
    //context.addIdentificationEntry(withNextSequentialPhoneNumber: CXCallDirectoryPhoneNumber("886277283610")!, label: "TEST")
    
    fetchAll(context: context)
    // Record the most-recently loaded set of identification entries in data store for the next incremental load...
}

Because the data on my site is not too much and my local data structure is quite simple, it is not possible to do incremental updates; therefore, we will use the method of completely adding new data. If using the incremental method, you must delete the old data first (this step is very important, otherwise reloading the extension will fail!).

Done!

That’s it! The implementation is very simple!

Tips:

  1. If the app keeps spinning when you open it in “Settings” > “Phone” > “Call Blocking & Identification” or if it cannot recognize numbers after opening, first check if the number is correct, if the local maintained .json data is correct, and if the extension reload was successful; or try rebooting. If you still can’t figure it out, you can select the Scheme Build of the call directory extension to see the error message.
  2. The most difficult part of this feature is not the programming aspect but guiding the user to manually set it up and turn it on. For specific methods and guidance, you can refer to Whoscall:

[Whoscall](https://whoscall.com/zh-TW/){:target="_blank"}

Whoscall

If you have any questions or comments, feel free to contact me.

===

本文中文版本

===

This article was first published in Traditional Chinese on Medium ➡️ View Here


This post is licensed under CC BY 4.0 by the author.

iOS tintAdjustmentMode Property

Perfect Implementation of One-Time Offers or Trials in iOS (Swift)