iOSでT9検索を実装する方法
公開: 2022-03-11数年前、私はiOS/Androidチームと一緒に「BOGmBank-MobileBanking」というアプリに取り組んでいました。 アプリには基本的な機能があり、モバイルバンキング機能を使用して、自分の携帯電話の後払い残高または連絡先の携帯電話残高を補充できます。
このモジュールの開発中に、iOSバージョンよりもAndroidバージョンのアプリの方が特定の連絡先を見つけるのがはるかに簡単であることに気付きました。 なんで? この背後にある主な理由は、AppleデバイスにないT9検索です。
T9とは何か、なぜT9がiOSの一部にならなかったのか、そして必要に応じてiOS開発者がどのように実装できるのかを説明しましょう。
T9とは何ですか?
T9は、携帯電話、特に物理的な3x4テンキーを含む携帯電話向けの予測テキストテクノロジーです。
T9はもともとTegicCommunicationsによって開発されたもので、名前は9キーのテキストを表します。
T9がおそらくiOSに到達しなかった理由を推測できます。 スマートフォン革命の間、T9入力は時代遅れになりました。これは、現代のスマートフォン電話がタッチスクリーンディスプレイのおかげでフルキーボードに依存していたためです。 Appleは物理的なキーボードを備えた電話を持っていなかったし、T9の全盛期には電話ビジネスに参加していなかったので、このテクノロジーがiOSから省略されたことは理解できます。
T9は、タッチスクリーンのない特定の安価な電話(いわゆるフィーチャーフォン)で引き続き使用されています。 ただし、ほとんどのAndroidスマートフォンには物理キーボードが搭載されていませんが、最近のAndroidデバイスではT9入力がサポートされており、通話しようとしている連絡先の名前を入力することで連絡先にダイヤルできます。
動作中のT9予測入力の例
テンキーを備えた電話では、キー(1〜9)が押されるたびに(テキストフィールド内の場合)、アルゴリズムは、そのポイントまでに押されたキーの可能性が最も高い文字の推測を返します。
たとえば、「the」という単語を入力するには、ユーザーは8、4、3の順に押すと、ディスプレイに「t」、「th」、「the」の順に表示されます。 あまり一般的ではない単語「fore」が意図されている場合(3673)、予測アルゴリズムは「Ford」を選択する場合があります。 「次へ」キー(通常は「*」キー)を押すと、「投与量」が表示され、最後に「前」が表示される場合があります。 「fore」が選択されている場合、次にユーザーがシーケンス3673を押すと、foreが最初に表示される単語である可能性が高くなります。 ただし、「Felix」という単語を意図している場合は、33549と入力すると、ディスプレイに「E」、「De」、「Del」、「Deli」、「Felix」の順に表示されます。
これは、単語を入力しているときに文字が変わる例です。
iOSでのT9のプログラムによる使用
それでは、この機能を詳しく調べて、iOS用のT9入力の簡単な例を書いてみましょう。 まず、新しいプロジェクトを作成する必要があります。
私たちのプロジェクトに必要な前提条件は基本です: MacにインストールされたXcodeおよびXcodeビルドツール。
新しいプロジェクトを作成するには、MacでXcodeアプリケーションを開き、[新しいXcodeプロジェクトを作成する]を選択してから、プロジェクトに名前を付け、作成するアプリケーションの種類を選択します。 「シングルビューアプリ」を選択し、「次へ」を押すだけです。
次の画面で、ご覧のとおり、提供する必要のある情報がいくつかあります。
- 製品名: T9Searchという名前を付けました
- チーム。 ここで、このアプリケーションを実際のデバイスで実行する場合は、開発者アカウントが必要になります。 私の場合、これには自分のアカウントを使用します。
注:開発者アカウントをお持ちでない場合は、シミュレーターでも実行できます。
- 組織名: Toptalと名付けました
- 組織識別子: 「com.toptal」という名前を付けました
- 言語: Swiftを選択
- [コアデータを使用する]、[単体テストを含める]、[UIテストを含める]のチェックを外します
[次へ]ボタンを押すと、開始する準備が整います。
シンプルなアーキテクチャ
すでにご存知のように、新しいアプリを作成するときは、 MainViewController
クラスとMain.Storyboard
がすでにあります。 もちろん、テスト目的で、このコントローラーを使用できます。
何かを設計する前に、まず必要なすべてのクラスとファイルを作成して、ジョブのUI部分に移動するためのすべてのセットアップと実行が完了していることを確認しましょう。
プロジェクト内のどこかに、「 PhoneContactsStore.swift 」という名前の新しいファイルを作成するだけです。私の場合は、次のようになります。
私たちの最初の仕事は、数字キーボード入力のすべてのバリエーションを備えたマップを作成することです。
import Contacts import UIKit fileprivate let T9Map = [ " " : "0", "a" : "2", "b" : "2", "c" : "2", "d" : "3", "e" : "3", "f" : "3", "g" : "4", "h" : "4", "i" : "4", "j" : "5", "k" : "5", "l" : "5", "m" : "6", "n" : "6", "o" : "6", "p" : "7", "q" : "7", "r" : "7", "s" : "7", "t" : "8", "u" : "8", "v" : "8", "w" : "9", "x" : "9", "y" : "9", "z" : "9", "0" : "0", "1" : "1", "2" : "2", "3" : "3", "4" : "4", "5" : "5", "6" : "6", "7" : "7", "8" : "8", "9" : "9" ]
それでおしまい。 すべてのバリエーションを含む完全なマップを実装しました。 それでは、「 PhoneContact 」という最初のクラスの作成に進みましょう。
ファイルは次のようになります。
まず、このクラスでは、AZ+0-9の正規表現フィルターがあることを確認する必要があります。
private let regex = try! NSRegularExpression(pattern: "[^ az()0-9+]", options: .caseInsensitive)
基本的に、ユーザーには表示する必要のあるデフォルトのプロパティがあります。
var firstName : String! var lastName : String! var phoneNumber : String! var t9String : String = "" var image : UIImage? var fullName: String! { get { return String(format: "%@ %@", self.firstName, self.lastName) } }
リストフィルタリングのカスタムロジックを指定するには、 hash
とisEqual
をオーバーライドしていることを確認してください。
また、文字列に数字以外のものが含まれないようにするには、replaceメソッドが必要です。
override var hash: Int { get { return self.phoneNumber.hash } } override func isEqual(_ object: Any?) -> Bool { if let obj = object as? PhoneContact { return obj.phoneNumber == self.phoneNumber } return false } private func replace(str : String) -> String { let range = NSMakeRange(0, str.count) return self.regex.stringByReplacingMatches(in: str, options: [], range: range, withTemplate: "") }
次に、 fullname
またはphonenumber
に関連する連絡先を検索するために、 calculateT9
と呼ばれるもう1つのメソッドが必要です。
func calculateT9() { for c in self.replace(str: self.fullName) { t9String.append(T9Map[String(c).localizedLowercase] ?? String(c)) } for c in self.replace(str: self.phoneNumber) { t9String.append(T9Map[String(c).localizedLowercase] ?? String(c)) } }
PhoneContact
オブジェクトを実装した後、連絡先をメモリのどこかに保存する必要があります。 この目的のために、 PhoneContactStore
という新しいクラスを作成します。
2つのローカルプロパティがあります。
fileprivate let contactsStore = CNContactStore()
と:
fileprivate lazy var dataSource = Set<PhoneContact>()
このデータソースの入力中に重複がないことを確認するために、 Set
を使用しています。
final class PhoneContactStore { fileprivate let contactsStore = CNContactStore() fileprivate lazy var dataSource = Set<PhoneContact>() static let instance : PhoneContactStore = { let instance = PhoneContactStore() return instance }() }
ご覧のとおり、これはシングルトンクラスです。つまり、アプリが実行されるまでメモリに保持されます。 シングルトンまたはデザインパターンの詳細については、こちらをご覧ください。

これで、T9検索の完了に非常に近づきました。
すべてを一緒に入れて
Appleの連絡先リストにアクセスする前に、まず許可を求める必要があります。
class func hasAccess() -> Bool { let authorizationStatus = CNContactStore.authorizationStatus(for: CNEntityType.contacts) return authorizationStatus == .authorized } class func requestForAccess(_ completionHandler: @escaping (_ accessGranted: Bool, _ error : CustomError?) -> Void) { let authorizationStatus = CNContactStore.authorizationStatus(for: CNEntityType.contacts) switch authorizationStatus { case .authorized: self.instance.loadAllContacts() completionHandler(true, nil) case .denied, .notDetermined: weak var wSelf = self.instance self.instance.contactsStore.requestAccess(for: CNEntityType.contacts, completionHandler: { (access, accessError) -> Void in var err: CustomError? if let e = accessError { err = CustomError(description: e.localizedDescription, code: 0) } else { wSelf?.loadAllContacts() } completionHandler(access, err) }) default: completionHandler(false, CustomError(description: "Common Error", code: 100)) } }
連絡先へのアクセスを許可した後、システムからリストを取得するメソッドを記述できます。
fileprivate func loadAllContacts() { if self.dataSource.count == 0 { let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactThumbnailImageDataKey, CNContactPhoneNumbersKey] do { let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor]) request.sortOrder = .givenName request.unifyResults = true if #available(iOS 10.0, *) { request.mutableObjects = false } else {} // Fallback on earlier versions try self.contactsStore.enumerateContacts(with: request, usingBlock: {(contact, ok) in DispatchQueue.main.async { for phone in contact.phoneNumbers { let local = PhoneContact() local.firstName = contact.givenName local.lastName = contact.familyName if let data = contact.thumbnailImageData { local.image = UIImage(data: data) } var phoneNum = phone.value.stringValue let strArr = phoneNum.components(separatedBy: CharacterSet.decimalDigits.inverted) phoneNum = NSArray(array: strArr).componentsJoined(by: "") local.phoneNumber = phoneNum local.calculateT9() self.dataSource.insert(local) } } }) } catch {} } }
連絡先リストはすでにメモリにロードされています。つまり、簡単なメソッドを記述できます。
-
findWith - t9String
-
findWith - str
class func findWith(t9String: String) -> [PhoneContact] { return PhoneContactStore.instance.dataSource.filter({ $0.t9String.contains(t9String) }) } class func findWith(str: String) -> [PhoneContact] { return PhoneContactStore.instance .dataSource.filter({ $0.fullName.lowercased() .contains(str.lowercased()) }) } class func count() -> Int { let request = CNContactFetchRequest(keysToFetch: []) var count = 0; do { try self.instance.contactsStore.enumerateContacts( with: request, usingBlock: {(contact, ok) in count += 1; }) } catch {} return count }
それでおしまい。 完了です。
これで、 UIViewController
内でT9検索を使用できます。
fileprivate let cellIdentifier = "contact_list_cell" final class ViewController: UIViewController { @IBOutlet weak var tableView: UITableView! @IBOutlet weak var searchBar: UISearchBar! fileprivate lazy var dataSource = [PhoneContact]() fileprivate var searchString : String? fileprivate var searchInT9 : Bool = true override func viewDidLoad() { super.viewDidLoad() self.tableView.register( UINib( nibName: "ContactListCell", bundle: nil ), forCellReuseIdentifier: "ContactListCell" ) self.searchBar.keyboardType = .numberPad PhoneContactStore.requestForAccess { (ok, err) in } } func filter(searchString: String, t9: Bool = true) { } func reloadListSection(section: Int, animation: UITableViewRowAnimation = .none) { } }
フィルタメソッドの実装:
func filter(searchString: String, t9: Bool = true) { self.searchString = searchString self.searchInT9 = t9 if let str = self.searchString { if t9 { self.dataSource = PhoneContactStore.findWith(t9String: str) } else { self.dataSource = PhoneContactStore.findWith(str: str) } } else { self.dataSource = [PhoneContact]() } self.reloadListSection(section: 0) }
リストメソッドの実装をリロードします。
func reloadListSection(section: Int, animation: UITableViewRowAnimation = .none) { if self.tableView.numberOfSections <= section { self.tableView.beginUpdates() self.tableView.insertSections(IndexSet(integersIn:0..<section + 1), with: animation) self.tableView.endUpdates() } self.tableView.reloadSections(IndexSet(integer:section), with: animation) }
そして、これが私たちの簡単なチュートリアルの最後の部分であるUITableView
の実装です。
extension ViewController: UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return tableView.dequeueReusableCell(withIdentifier: "ContactListCell")! } func numberOfSections(in tableView: UITableView) -> Int { return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.dataSource.count } func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { guard let contactCell = cell as? ContactListCell else { return } let row = self.dataSource[indexPath.row] contactCell.configureCell( fullName: row.fullName, t9String: row.t9String, number: row.phoneNumber, searchStr: searchString, img: row.image, t9Search: self.searchInT9 ) } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 55 } func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { self.filter(searchString: searchText) } }
まとめ
これでT9検索チュートリアルは終了です。iOSで簡単に実装できることを願っています。
しかし、なぜあなたはすべきですか? そして、なぜAppleはiOSにT9サポートを最初から含めなかったのですか? 冒頭で指摘したように、T9は今日の電話のキラー機能ではありません。これは、機械式のナンバーパッドを備えた「ダム」電話の時代への逆戻りです。
ただし、一貫性を保つため、またはアクセシビリティとユーザーエクスペリエンスを向上させるために、特定のシナリオでT9検索を実装する必要がある正当な理由はまだいくつかあります。 もっと陽気な話ですが、もしあなたが懐かしい人なら、T9入力で遊んでみると、学生時代の懐かしい思い出がよみがえるかもしれません。
最後に、iOSでのT9実装の完全なコードは、私のGitHubリポジトリにあります。