記事

Swiftで実現|iOS向けWhoscall風の電話番号識別と着信表示機能

iOS開発者向けに、SwiftでWhoscallのような電話番号識別と着信表示を自作する方法を解説。電話番号の自動判別とラベル表示で着信管理を効率化し、ユーザー体験を向上させます。

Swiftで実現|iOS向けWhoscall風の電話番号識別と着信表示機能

本記事は AI による翻訳をもとに作成されています。表現が不自然な箇所がありましたら、ぜひコメントでお知らせください。

記事一覧


自分の電話は自分で識別(Swift)

iOSで自作するWhoscallの着信識別・電話番号ラベル機能

起源

ずっとWhoscallの熱心なユーザーで、Androidスマホを使っていた頃から利用していました。見知らぬ着信情報を非常にリアルタイムで表示でき、その場で通話するかどうかをすぐに判断できます。後にAppleの陣営に移り、最初のAppleスマホはiPhone 6(iOS 9)でしたが、その時はWhoscallの使用が非常に使いづらく、電話番号をコピーしてアプリで検索しなければなりませんでした。後にWhoscallは見知らぬ電話のデータベースをローカルにインストールするサービスを提供し、リアルタイム認識の問題は解決しましたが、簡単にスマホの連絡先が乱れてしまいました。

iOS 10以降、Appleが電話識別機能(Call Directory Extension)の権限を開発者に開放してから、whoscallは現時点での体験においてAndroid版とほとんど遜色なく、むしろAndroid版を超えている部分もあります(Android版は広告が多いですが、開発者の立場からは理解できます)。

用途?

Call Directory Extension は何ができるのでしょうか?

  1. 電話 発信 識別タグ

  2. 電話 着信 識別ラベル

  3. 通話履歴 識別ラベル

  4. 電話 拒否 ブラックリスト設定

制限?

  1. ユーザーは「設定」>「電話」>「通話のブロックと識別」から手動でアプリを有効にする必要があります。

  2. オフラインデータベース方式でのみ電話番号を識別可能です(リアルタイムで着信情報を取得してAPIを呼び出して照会することはできず、あらかじめ番号と名前の対応を端末内データベースに書き込む必要があります)
    そのためWhoscallは定期的にプッシュ通知でユーザーにアプリを開いて着信識別データベースを更新するよう促します

  3. 数量の上限は?現在のところ情報は見つかっていませんが、ユーザーのスマホ容量に依存し特別な上限はないと思われます。ただし、認識リストやブロックリストの数が多い場合は、分割して処理・書き込みを行う必要があります。

  4. ソフトウェア制限:iOS バージョンは10以上が必要です。

「設定」->「電話」->「通話ブロックと識別」

「設定」->「電話」->「通話のブロックと識別」

利用シーンは?

  1. 通信アプリやオフィス用通信アプリでは、アプリ内に相手の連絡先があっても、実際には携帯電話の連絡先に番号を登録していない場合があります。この機能は、同僚や上司からの着信が知らない番号として扱われてしまい、着信を見逃すのを防ぐことができます。

  2. 敝サイト( 結婚吧 )や私の( 591房屋交易 )では、ユーザーが店舗や家主と連絡する際に発信する電話番号はすべて当社の転送番号であり、転送センターを経由して目的の電話番号に転送されます。大まかな流れは以下の通りです:

ユーザーが発信する電話はすべて転送センターの代表番号(#内線)であり、実際の電話番号はわかりません。一方で個人情報の保護になり、他方で何人が店舗に連絡したか(効果測定)がわかり、どこで見て発信したか(例:ウェブは#1234、アプリは#5678)も把握できます。また、無料サービスを提供し、通信費用は当社が負担します。

しかし、この方法には避けられない問題があります。それは電話番号が乱雑になることです。誰にかけたのか、または店舗からの折り返し電話かを識別できず、ユーザーは発信者が誰かわかりません。電話識別機能を使うことで、この問題を大幅に解決し、ユーザー体験を向上させることができます!

完成した画面のスクリーンショット:

[結婚吧 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

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

Call Directory Extension 電話識別機能の動作フロー:

開始:

さあ、実際にやってみましょう!

1.iOSプロジェクトにCall Directory Extensionを追加する

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

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

選択 Call Directory Extension

Call Directory Extension を選択する

Extension名を入力

Extension名を入力してください

Schemeを追加してデバッグを便利にする

デバッグを便利にするためにSchemeを追加してもよいです。

目次の下にCall Directory Extensionのフォルダとプログラムが表示されます

目次の下に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がその構造体を読み取って識別データベースに書き込む必要があります。流れは以下の通りです:

つまり、自分たちのデータベースファイルを別途管理し、Extensionに読み込ませて電話機に書き込む必要がある

つまり、自分たちのデータベースファイルを別途管理し、それをExtensionに読み込ませて端末に書き込む必要があります。

いわゆる識別データやファイルはどのような形ですか?

実際には辞書構造で、例えば:[“電話”:”王大明”] のようなものです。

ローカルにあるファイルは一部のローカル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: "あなたのExtension間の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からも読み取れる場所であることです。

補足 — 電話番号の形式:

  1. 台湾の市外局番および携帯番号は、先頭の0を外して886に置き換えます:例 0255667788 -> 886255667788

  2. 電話番号の形式は数字のみの文字列で、「-」、「,」、「#」などの記号を含まないでください。

  3. 市外局番の電話番号に内線番号を含める場合は、記号を付けずにそのまま後ろに続けてください:例 0255667788,0718 -> 8862556677880718

  4. 一般的なiOS電話番号フォーマットを識別データベースが受け入れ可能な形式に変換するには、以下の2つの置換方法を参考にしてください:

1
2
3
4
5
6
7
8
9
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に通知して端末側のデータを更新する必要があります:

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("リロードに失敗しました")
            
            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 のみで、携帯電話データを処理するときの動作を行います。デフォルトのサンプルで既に構築されているため、あまり変更する必要はありません:

  1. addAllBlockingPhoneNumbers :ブラックリスト番号を一括で追加する処理

  2. addOrRemoveIncrementalBlockingPhoneNumbers :ブラックリスト番号をインクリメンタルに追加または削除する処理

  3. addAllIdentificationPhoneNumbers :着信識別番号を一括で追加する処理

  4. addOrRemoveIncrementalIdentificationPhoneNumbers :着信識別番号をインクリメンタルに追加または削除する処理

私たちは上記の機能実装を完了すれば十分です。ブラックリスト機能と着信識別の仕組みは同じなので、ここでは詳しく説明しません。

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) {
    // データストアから識別する電話番号とその識別ラベルを取得します。電話番号が多数ある場合、パフォーマンスとメモリ使用量を最適化するために、
    // 一度に一部の番号だけを読み込み、読み込んだ各バッチで割り当てられたオブジェクトを解放するためにautoreleasepoolを使用することを検討してください。
    //
    // 番号は数値的に昇順で提供する必要があります。
    //        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) {
    // データストアから識別する電話番号(およびその識別ラベル)の変更を取得します。電話番号が多数ある場合、パフォーマンスとメモリ使用量を最適化するために、
    // 一度に一部の番号だけを読み込み、読み込んだ各バッチで割り当てられたオブジェクトを解放するためにautoreleasepoolを使用することを検討してください。
    //        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のリロードに失敗します!)

完成!

ここまでで完了です!実装は非常に簡単です!

ヒント:

  1. 「設定」「電話」「通話のブロックと識別」でアプリを開くとずっと読み込み中だったり、開いても番号を識別できない場合は、番号が正しいか、ローカルで管理している.jsonデータが正しいか、reload extensionが成功しているかをまず確認してください。 また、再起動を試してみて、それでも解決しない場合は、call directory extensionのSchemeでビルドしてエラーメッセージを確認してください。

  2. この機能で最も難しいのはプログラムではなく、ユーザーに手動で設定をオンにしてもらうことです。具体的な方法と案内はwhoscallを参考にしてください:

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

Whoscall

ご質問やご意見がございましたら、こちらからご連絡ください

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


🍺 Buy me a beer on PayPal

👉👉👉 Follow Me On Medium! (1,053+ Followers) 👈👈👈

本記事は Medium にて初公開されました(こちらからオリジナル版を確認)。ZMediumToMarkdown による自動変換・同期技術を使用しています。

Improve this page on Github.

本記事は著者により CC BY 4.0 に基づき公開されています。