自己的電話自己辨識(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
2.開始編寫 Call Directory Extension 相關程式
首先回到主 iOS 專案上
第一個問題是我們該如何判斷使用者的裝置支不支援Call Directory Extension或是設定中的「通話封鎖與識別」是否已經打開:
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再去讀取你的結構再寫入辨識資料庫中,流程如下:
那所謂的辨識資料、檔案該長怎樣?
其實就是個Dictionary結構,如:[“電話”:”王大明”]
存在本地的檔案可用一些Local DB(但Extension那邊也要能裝能用),這邊是直接存一個.json檔在手機裡;不建議直接存在UserDefaults,如果是測試或資料很少可以,實際應用強烈不建議!
好的,開始:
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電話格式轉換成辨識資料庫可接受格式可參考以下兩個取代方法:
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去刷新手機那邊的資料:
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實作即可,黑名單功能跟來電辨識方式原理都ㄧ樣這邊就不多作介紹.
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:
有任何問題及指教歡迎與我聯絡。