自己的電話自己辨識(Swift)
iOS自幹 Whoscall 來電辨識、電話號碼標記 功能
起源
一直以來都是Whoscall的忠實用戶,從原本用Android手機時就有使用,能夠非常即時的顯示陌生來電資訊,當下就能直接決定接通與否;後來轉跳蘋果陣營,第一隻蘋果手機是iPhone 6 (iOS 9),那時在使用Whoscall上非常彆扭,無法即時辨識電話,要複製電話號碼去APP查詢,後期Whoscall提供將陌生電話資料庫安裝在本地手機的服務,雖然能解決即時辨識的問題,但很容易就弄亂你的手機通訊錄!
直到 iOS 10+ 之後蘋果開放電話辨識功能(Call Directory Extension)權限給開發者,才使whoscall目前至少就體驗來說已和Android版無太大缺別,甚至超越Android版(Android版廣告超多,但以開發者的立場是可以理解的)
用途?
Call Directory Extension 能做到什麼呢?
- 電話 撥打 辨識標記
- 電話 來電 辨識標記
- 通話紀錄 辨識標記
- 電話 拒接 黑名單設置
限制?
- 使用者需手動進入「設定」「電話」「通話封鎖與識別」打開您的APP才能使用
- 僅能以離線資料庫方式辨識電話(無法即時取得來電資訊然後Call API查詢,僅能預先寫入號碼<->名稱對應在手機資料庫中) *也因此Whoscall會定期推播請使用者開APP更新來電辨識資料庫
- 數量上限?目前沒查到資料,應該是依照使用者手機容量無特別上限;但是數量多得辨識清單、封鎖清單要分批處理寫入!
- 軟體限制:iOS 版本需 ≥ 10
「設定」->「電話」->「通話封鎖與識別」
應用場景?
- 通訊軟體、辦公室通訊軟體;在APP內你可能有對方的聯絡人,但實際並未將手機號碼加入手機通訊錄中,這個功能就能避免同事甚至老闆來電時,被當陌生電話,結果漏接.
- 敝站( 結婚吧 )或敝私的( 591房屋交易 ),使用者與店家或房東聯繫時所撥打的電話都是我們的轉接號碼,經由轉接中心在轉撥到目標電話,大致流程如下:
使用者所撥打的電話都是轉接中心代表號( #分機),不會知道真實的電話號碼;一方面是保護個資隱私、另一方面也能知道有多少人聯絡商家(評估成效)甚至能知道是在哪看到然後撥打的(EX:網頁顯示#1234,APP顯示#5678)、還有也能推免費服務,由我方吸收電話通信費用.
但此做法會帶來ㄧ項不可避免的問題,就是電話號碼凌亂;無法辨識出是打給誰或是店家回撥時,使用者不知道來電者是誰,透過使用電話辨識功能就能大大解決這個問題,提升使用者體驗!
直接上一張成品圖:
可以看到在輸入電話、電話來電時能直接顯示辨識結果、通話記錄列表也不在亂糟糟ㄧ樣能在下方顯示辨識結果.
Call Directory Extension 電話辨識功能運作流程:
開工:
讓我們開始動手做吧!
1.為 iOS 專案加入 Call Directory Extension
Xcode -> File -> New -> Target
選擇 Call Directory Extension
輸入Extension名稱
可順帶加入 Scheme 方便 Debug
目錄底下就會出現Call Directory Extension的資料夾及程式
2.開始編寫 Call Directory Extension 相關程式
首先回到主 iOS 專案上
第一個問題是我們該如何判斷使用者的裝置支不支援Call Directory Extension或是設定中的「通話封鎖與識別」是否已經打開:
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: "這裡輸入call directory extension的bundle identifier", completionHandler: { (status, error) in
if status == .enabled {
//啟用中
} else if status == .disabled {
//未啟用
} else {
//未知,不支援
}
})
}
前面有提到,來電辨識的運作方式是要在本地維護一個辨識資料庫;再來就是重頭戲該如何達成這個功能?
很遺憾,您無法直接對Call Directory Extension進行呼叫寫入資料,所以你需要多維護一層對應結構,然後Call Directory Extension再去讀取你的結構再寫入辨識資料庫中,流程如下:
意旨我們需要多維護一個自己的資料庫文件,再讓Extenstion去讀取寫入到手機中
那所謂的辨識資料、檔案該長怎樣?
其實就是個Dictionary結構,如:[“電話”:”王大明”]
存在本地的檔案可用一些Local DB(但Extension那邊也要能裝能用),這邊是直接存一個.json檔在手機裡; 不建議直接存在UserDefaults,如果是測試或資料很少可以,實際應用強烈不建議!
好的,開始:
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: "你的跨Extesion,Group Identifier名稱") {
let fileURL = dir.appendingPathComponent("phoneIdentity.json")
var datas:[String:String] = ["8869190001234":"李先生","886912002456":"大帥"]
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檔完成
}
}
}
}
}
就只是一般的本地檔案維護,要注意的就是目錄需要在Extesion也能讀取的地方。
補充 — 電話號碼格式:
- 台灣地區市話、手機都需去掉0以886代替:如 0255667788 -> 886255667788
- 電話格式是純數字組合的字串,勿夾雜「-」、「,」、「#」…等符號
- 市話電話如有包含要辨識到 分機 ,直接接在後面即可不需帶任何符號:如 0255667788,0718 -> 8862556677880718
- 將一般iOS電話格式轉換成辨識資料庫可接受格式可參考以下兩個取代方法:
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: "")
}
再來就是如流程,辨識資料已維護好;需要通知Call Directory Extension去刷新手機那邊的資料:
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")
}
}
}
使用以上方法通知Extension刷新,並取得執行結果。(這時候會呼叫執行Call Directory Extension裡的beginRequest,請繼續往下看)
主 iOS 專案的程式就到這了!
3.開始修改 Call Directory Extension 的程式
打開Call Directory Extension 目錄,找到底下已經幫你建立好的檔案 CallDirectoryHandler.swift
能實作的方法只有 beginRequest 當要處理手機電話資料時的動作,預設範例都把我們建好了,不太需要去動:
- addAllBlockingPhoneNumbers :處理加入黑名單號碼(全新增)
- addOrRemoveIncrementalBlockingPhoneNumbers :處理加入黑名單號碼(遞增方式)
- addAllIdentificationPhoneNumbers :處理加入來電辨識號碼(全新增)
- addOrRemoveIncrementalIdentificationPhoneNumbers :處理加入來電辨識號碼(遞增方式)
我們只要完成以上的Function實作即可,黑名單功能跟來電辨識方式原理都ㄧ樣這邊就不多作介紹.
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: "你的跨Extesion,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...
}
因為敝站的資料不會到太多而且我的本地資料結構相當簡易,無法做到遞增;所以這邊 統一都用全新增的方式,如是遞增方式則要先刪除舊的(這步很重要不然會reload extensiton失敗!)
完工!
到此為止就完成囉!實作方面非常簡單!
Tips:
- 如果在「設定」「電話」「通話封鎖與識別」打開APP時一直轉或是打開後無法辨識號碼,可先確認號碼是否正確、本地維護的.json資料是否正確、reload extensiton是否成功;或重開機試試,都找不出來可以選call directory extension的Scheme Build 看看錯誤訊息.
- 這個功能 最困難的點不是程式方面而是要引導使用者手動去設定打開 ,具體方式及引導可參考whoscall:
有任何問題及指教歡迎 與我聯絡 。
===
View the English version of this article here.
本文首次發表於 Medium ➡️ 前往查看