Post

iOS Caller ID with Swift|Build Your Own Number Recognition & Tagging

iOS developers struggling with call identification can implement a Swift-based solution to recognize and tag phone numbers efficiently, enhancing user experience with accurate incoming call info and streamlined number management.

iOS Caller ID with Swift|Build Your Own Number Recognition & Tagging

点击这里查看本文章简体中文版本。

點擊這裡查看本文章正體中文版本。

This post was translated with AI assistance — let me know if anything sounds off!


Recognize Your Own Phone (Swift)

iOS Self-made Whoscall Incoming Call Identification and Phone Number Tagging Features

Origin

I have always been a loyal Whoscall user, starting from when I used an Android phone. It can instantly display information about unknown callers, allowing me to decide whether to answer immediately. Later, when I switched to the Apple ecosystem, my first Apple phone was the iPhone 6 (iOS 9). Using Whoscall then was very inconvenient because it couldn’t identify calls in real-time; I had to copy the phone number and search in the app. Later, Whoscall offered a service to install the unknown caller database locally on the phone. Although this solved the real-time identification issue, it often messed up your phone contacts!

Until iOS 10+, Apple opened the Call Directory Extension permission to developers, allowing whoscall to offer an experience comparable to the Android version, if not better. (The Android version has many ads, but this is understandable from a developer’s perspective.)

Purpose?

What can the Call Directory Extension do?

  1. Phone Call Identification Tag

  2. Phone Incoming Call Identification Tag

  3. Call Log Recognition Tags

  4. Call Reject Blacklist Settings

Limitations?

  1. Users must manually go to “Settings” > “Phone” > “Call Blocking & Identification” to enable your app before use.

  2. Can only recognize phone numbers using an offline database (cannot obtain caller information in real-time to call APIs for lookup, only pre-stored number-to-name mappings in the phone database).
    Therefore, Whoscall regularly pushes notifications asking users to open the app and update the caller ID database.

  3. Quantity limit? Currently, no information found. It likely depends on the user’s phone storage with no specific limit; however, if the number is large, the recognition list and block list should be processed and written in batches!

  4. Software requirements: iOS version must be ≥ 10

「Settings」->「Phone」->「Call Blocking & Identification」

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

Application Scenarios?

  1. Messaging apps and office communication apps may have the other person’s contact within the app, but their phone number is not actually saved in your phone’s address book. This feature helps prevent missed calls from colleagues or even your boss by avoiding them being marked as unknown callers.

  2. Our site ( 結婚吧 ) or my personal ( 591房屋交易 ) uses forwarding numbers for calls made by users to contact stores or landlords. The calls are routed through a transfer center to the target phone. The general process is as follows:

The phone numbers dialed by users are all forwarded to the call center’s main number (#extension), so the real phone numbers are not revealed. This protects personal privacy and allows tracking how many people contact the business (to evaluate effectiveness). It also identifies where the call originated (e.g., webpage shows #1234, app shows #5678). Additionally, free service can be promoted, with our side covering the phone communication costs.

However, this approach brings an unavoidable issue: phone numbers become messy. Users cannot identify who they are calling or, when the store calls back, they don’t know who the caller is. Using phone identification features can greatly solve this problem and improve the user experience!

Here is the final product image directly:

[Wedding 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"}

Wedding App

You can see that when entering a phone number or receiving a call, the recognition result is displayed directly. The call history list is also organized, with the recognition result shown at the bottom.

Call Directory Extension Call Identification Process:

Start Work:

Let’s get started!

1. Add Call Directory Extension to iOS Project

Xcode -> File -> New -> Target

Xcode -> File -> New -> Target

Select Call Directory Extension

Select Call Directory Extension

Enter Extension Name

Enter Extension Name

You can also add Scheme for easier debugging

You can also add Scheme for easier debugging.

Under the directory, the Call Directory Extension folder and program will appear

Under the directory, the Call Directory Extension folder and program will appear.

First, return to the main iOS project.

The first question is how to determine if the user’s device supports Call Directory Extension or if the “Call Blocking & Identification” setting is enabled:

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 call directory extension's bundle identifier here", completionHandler: { (status, error) in
        if status == .enabled {
          // Enabled
        } else if status == .disabled {
          // Disabled
        } else {
          // Unknown, not supported
        }
    })
}

As mentioned earlier, the caller ID function operates by maintaining a recognition database locally; next is the key question of how to achieve this feature.

Unfortunately, you cannot directly write data to the Call Directory Extension. Therefore, you need to maintain a corresponding structure separately, and the Call Directory Extension will read your structure to write the identification data into the database. The process is as follows:

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

It means we need to maintain our own database file, then let the Extension read and write it on the phone.

What should the so-called identification data or files look like?

It’s actually just a dictionary structure, like: [“phone”:”Wang Daming”]

Local files can use some Local DB (but the Extension must also support installation and usage). Here, a .json file is directly stored on the phone; storing directly in UserDefaults is not recommended—it’s okay for testing or very small data, but strongly discouraged for real applications!

Alright, let’s begin:

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. Lee","886912002456":"Big Guy"]
        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) {
                    // JSON file write completed
                }
            }
        }
    }
}

It’s just regular local file maintenance. The key point is that the directory must be accessible from the Extension as well.

Supplement — Phone Number Format:

  1. For landline and mobile numbers in Taiwan, remove the leading 0 and replace it with 886: for example, 0255667788 -> 886255667788

  2. Phone numbers should be strings composed purely of digits, without any symbols like “-“, “,”, “#” or others.

  3. For landline numbers that include an extension, simply append it directly without any symbols: for example, 0255667788,0718 -> 8862556677880718

  4. To convert a standard iOS phone number format into a format accepted by the recognition 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 data recognition is maintained, you need to notify the Call Directory Extension to refresh the data on the phone side:

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 get the execution result. (At this point, it will call the beginRequest in the Call Directory Extension. Please continue reading below.)

The main iOS project’s code ends here!

3. Start Modifying the Call Directory Extension Code

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

The only method to implement is beginRequest, which handles actions when processing phone call data. The default examples have already set this up for us, so there’s usually no need to modify it:

  1. addAllBlockingPhoneNumbers: Handles adding phone numbers to the blacklist (full addition)

  2. addOrRemoveIncrementalBlockingPhoneNumbers: Handles adding phone numbers to the blacklist (incremental method)

  3. addAllIdentificationPhoneNumbers: Handles adding caller identification numbers (full addition)

  4. addOrRemoveIncrementalIdentificationPhoneNumbers: Handles adding caller identification numbers (incremental method)

We only need to complete the above function implementation. The blacklist feature and call identification method work on the same principle, so they will not be explained 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 cross-Extension Group Identifier name") {
        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 our site’s data is not very large and my local data structure is quite simple, incremental updates are not possible; therefore, we uniformly use a full addition method. If using incremental updates, the old data must be deleted first (this step is very important, otherwise the reload extension will fail!).

Completed!

That’s it! The implementation is very simple!

Tips:

  1. If the app keeps loading or cannot identify numbers after opening “Settings” > “Phone” > “Call Blocking & Identification,” first check if the numbers are correct, if the locally maintained .json data is accurate, and if reloading the extension was successful. You can also try restarting the device. If the issue persists, select the call directory extension’s Scheme Build to check for error messages.

  2. The most challenging part of this feature is not the coding but guiding users to manually enable the setting. For specific methods and guidance, refer to whoscall:

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

Whoscall

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


Buy me a beer

This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.

Improve this page on Github.

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