自分の電話番号を自分で識別する(Swift)
iOSで自作するWhoscall風の着信識別・電話番号表示機能
起源
ずっとWhoscallの忠実なユーザーで、Androidスマホを使っていた時から利用していました。知らない着信情報を即座に表示でき、その場で通話するかどうかを判断できます。その後Appleに乗り換え、最初のiPhoneはiPhone 6(iOS 9)でしたが、その時はWhoscallの使用が非常に不便で、電話番号をコピーしてアプリで検索しなければなりませんでした。後にWhoscallは未知の電話番号データベースを端末にインストールするサービスを提供し、即時識別の問題は解決しましたが、簡単にスマホの連絡先が混乱してしまいました!
iOS 10以降、Appleが電話識別機能(Call Directory Extension)の権限を開発者に開放したことで、whoscallは現状の体験においてAndroid版とほとんど差がなく、むしろAndroid版を超えることもあります(Android版は広告が多いですが、開発者の立場からは理解できます)。
用途?
Call Directory Extension は何ができるのでしょうか?
-
電話 発信 識別マーク
-
電話 着信 識別マーク
-
通話履歴 識別ラベル
-
電話 拒否 ブラックリスト設定
制限?
-
ユーザーは手動で「設定」→「電話」→「通話のブロックと識別」からあなたのAPPを有効にする必要があります。
-
電話番号の識別はオフラインのデータベース方式のみ対応(リアルタイムで着信情報を取得してAPIを呼び出すことはできず、事前に番号と名前の対応を端末内データベースに書き込む必要があります)
そのためWhoscallは定期的にプッシュ通知でユーザーにアプリを開いて着信識別データベースを更新するよう促します -
数量の上限?現在のところ情報は見つかっていませんが、ユーザーの端末容量に依存し特別な上限はないと思われます。ただし、識別リストやブロックリストの数が多い場合は、分割して処理・書き込みする必要があります!
-
ソフトウェア制限:iOS バージョンは 10 以上が必要

「設定」->「電話」->「通話のブロックと識別」
応用シーンは?
-
通信アプリやオフィス向け通信アプリでは、アプリ内に相手の連絡先があっても、実際には携帯電話の連絡先に番号を登録していない場合があります。この機能を使うことで、同僚や上司からの着信が不明な電話と判断されてしまい、取り逃がすのを防げます。
-
当サイト(結婚吧)や私の(591房屋交易)では、ユーザーが店舗や家主と連絡を取る際にかける電話番号はすべて当社の転送番号であり、転送センターを経由して目的の電話に転送されます。大まかな流れは以下の通りです:

ユーザーが発信する電話はすべて転送センターの代表番号(#内線番号)であり、実際の電話番号はわかりません。一方で個人情報の保護になり、また何人が店舗に連絡したか(効果の評価)が把握でき、どこで見て発信したかも分かります(例:ウェブでは#1234、アプリでは#5678表示)。さらに無料サービスの提供も可能で、通話料金は当社が負担します。
しかし、この方法には避けられない問題があります。それは電話番号が混乱することです。誰にかけたのか、または店舗からの折り返し電話かが分からず、ユーザーが着信者を認識できません。電話番号識別機能を使うことで、この問題を大幅に解決し、ユーザー体験を向上させることができます!
完成した画面のスクリーンショット:

電話番号入力時や着信時に直接識別結果が表示され、通話履歴リストでも乱雑にならずに下部に識別結果が表示されることが確認できます。
Call Directory Extension 電話識別機能の動作フロー:

開始:
さあ、実際にやってみましょう!
1.iOSプロジェクトにCall Directory Extensionを追加する

Xcode -> ファイル -> 新規作成 -> ターゲット

Call Directory Extension を選択する

Extensionの名前を入力してください

Scheme を追加してデバッグを便利にすることもできます。

目次の下に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のバンドル識別子を入力", completionHandler: { (status, error) in
if status == .enabled {
// 有効化されている
} else if status == .disabled {
// 無効化されている
} else {
// 不明、サポートされていない
}
})
}
前述したように、着信識別の仕組みはローカルで識別データベースを管理する必要があります。では、本題のこの機能をどう実現するかです。
残念ながら、Call Directory Extensionに直接データを書き込むことはできません。そのため、対応するデータ構造を別途管理し、Call Directory Extensionがその構造を読み取って識別データベースに書き込む必要があります。流れは以下の通りです:

つまり、自分たちのデータベースファイルを別途管理し、それをExtensionに読み込ませて端末に書き込む必要があります。
いわゆる識別データやファイルはどのような形でしょうか?
実際には辞書の構造で、例えば:[“電話”:”王大明”] のようなものです。
ローカルに存在するファイルは一部のローカル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ファイルの書き込み完了
}
}
}
}
}
ただの一般的なローカルファイルの管理ですが、注意すべきはディレクトリがExtensionからも読み取れる場所であることです。
補足 — 電話番号の形式:
-
台湾の市外局番と携帯番号は先頭の0を外し、886に置き換えます:例 0255667788 -> 886255667788
-
電話番号の形式は数字のみの文字列で、「-」、「,」、「#」などの記号を含めないでください。
-
市外局番付きの電話番号で内線番号も識別する場合は、記号を付けずにそのまま後ろに続けてください:例 0255667788,0718 -> 8862556677880718
-
一般的なiOS電話番号フォーマットを識別データベースで受け入れ可能な形式に変換するには、以下の2つの置換方法を参考にしてください:
var newNumber = "0255667788,0718"
if let regex = try? NSRegularExpression(pattern: "^0{1}") {
// 先頭の0を886に置換
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("リロード失敗")
switch error.code {
case .unknown:
print("エラーは不明です")
case .noExtensionFound:
print("エクステンションが見つかりません")
case .loadingInterrupted:
print("読み込みが中断されました")
case .entriesOutOfOrder:
print("エントリの順序が正しくありません")
case .duplicateEntries:
print("重複したエントリがあります")
case .maximumEntriesExceeded:
print("最大エントリ数を超えました")
case .extensionDisabled:
print("エクステンションは無効です")
case .currentlyLoading:
print("現在読み込み中です")
case .unexpectedIncrementalRemoval:
print("予期しない段階的削除です")
}
} else if let error = errorOrNil {
print("リロードエラー: \(error)")
} else {
print("リロード成功")
}
}
}
以上の方法でExtensionに更新を通知し、実行結果を取得します。(この時、Call Directory Extension内のbeginRequestが呼び出されます。続きをご覧ください)
主なiOSプロジェクトのコードはここまでです!
3.Call Directory Extension のプログラムの修正を開始する
Call Directory Extension フォルダを開き、すでに作成されているファイル CallDirectoryHandler.swift を見つけます。
能実装できるメソッドは beginRequest のみで、携帯電話のデータを処理する際の動作です。デフォルトのサンプルはすでに用意されているため、あまり手を加える必要はありません:
-
addAllBlockingPhoneNumbers :ブラックリスト番号を一括で追加する処理
-
addOrRemoveIncrementalBlockingPhoneNumbers :ブラックリスト番号のインクリメンタル追加・削除を処理する
-
addAllIdentificationPhoneNumbers :着信識別番号を一括で追加する処理
-
addOrRemoveIncrementalIdentificationPhoneNumbers :着信識別番号のインクリメンタル追加・削除を処理する
私たちは上記の機能実装を完了すれば十分です。ブラックリスト機能と着信識別の仕組みは同じなので、ここでは詳しく説明しません。
private func fetchAll(context: CXCallDirectoryExtensionContext) {
if let dir = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "あなたのクロスExtension,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) {
// データストアから識別する電話番号とそのラベルを取得します。多数の電話番号がある場合は、最適なパフォーマンスとメモリ使用のために、
// 一度に一部の番号のみを読み込み、読み込んだ各バッチでautorelease poolを使用して割り当てられたオブジェクトを解放することを検討してください。
//
// 電話番号は数値の昇順で提供する必要があります。
// 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) {
// 識別する電話番号セットの変更(およびそのラベル)をデータストアから取得します。多数の電話番号がある場合は、最適なパフォーマンスとメモリ使用のために、
// 一度に一部の番号のみを読み込み、読み込んだ各バッチでautorelease poolを使用して割り当てられたオブジェクトを解放することを検討してください。
// 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)
// 次回のインクリメンタル読み込みのために、最新の識別エントリセットをデータストアに記録します...
}
当サイトのデータ量は多くなく、ローカルのデータ構造も非常にシンプルなため、インクリメンタル更新はできません。そこでここでは すべて新規追加の方法を統一して使用します。もしインクリメンタル方式にする場合は、必ず先に古いデータを削除してください(このステップは非常に重要で、さもないとextensionのリロードに失敗します!)
完成!
ここまでで完了です!実装はとても簡単です!
ヒント:
-
「設定」「電話」「通話封鎖と識別」でAPPを有効にした際にずっと読み込み中だったり、有効化後に番号を認識できない場合は、まず番号が正しいか、本地で管理している.jsonデータが正しいか、reload extensionが成功しているかを確認してください。
それでも解決しない場合は、再起動を試すか、call directory extensionのSchemeをビルドしてエラーメッセージを確認してください。 -
この機能の最も難しい点はプログラムではなく、ユーザーに手動で設定をオンにしてもらうことです。具体的な方法や案内はwhoscallを参考にしてください:

*Post は Medium から ZMediumToMarkdown によって変換されました。



コメント