Как реализовать поиск T9 в iOS
Опубликовано: 2022-03-11Пару лет назад я работал над приложением под названием «BOG mBank — Mobile Banking» со своей командой iOS/Android. В приложении есть базовая функция, с помощью которой вы можете использовать функцию мобильного банкинга для пополнения собственного баланса мобильного телефона с постоплатой или баланса мобильного телефона любого контакта.
При разработке этого модуля мы заметили, что найти конкретный контакт в Android-версии приложения намного проще, чем в iOS. Почему? Основной причиной этого является поиск T9, который отсутствует на устройствах Apple.
Давайте объясним, что такое T9, и почему он, вероятно, не стал частью iOS, и как iOS-разработчики могут реализовать его в случае необходимости.
Что такое Т9?
T9 — это технология интеллектуального ввода текста для мобильных телефонов, особенно тех, которые содержат физическую цифровую клавиатуру 3x4.
Первоначально T9 был разработан Tegic Communications, а его название расшифровывается как « Текст на 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, скорее всего, будет первым отображаемым словом. Однако, если предполагается слово «Феликс», при вводе 33549 на дисплее отображается «E», затем «De», «Del», «Deli» и «Felix».
Это пример изменения буквы при вводе слов.
Программное использование T9 в iOS
Итак, давайте углубимся в эту функцию и напишем простой пример ввода T9 для iOS. Прежде всего, нам нужно создать новый проект.
Предпосылки, необходимые для нашего проекта, являются основными: инструменты сборки Xcode и Xcode, установленные на вашем Mac.
Чтобы создать новый проект, откройте приложение Xcode на своем Mac и выберите «Создать новый проект Xcode», затем назовите свой проект и выберите тип создаваемого приложения. Просто выберите «Single View App» и нажмите «Далее».
На следующем экране, как вы можете видеть, будет некоторая информация, которую вам нужно предоставить.
- Название продукта: я назвал его T9Search
- Команда . Здесь, если вы хотите запустить это приложение на реальном устройстве, вам потребуется учетная запись разработчика. В моем случае я буду использовать для этого свою учетную запись.
Примечание. Если у вас нет учетной записи разработчика, вы также можете запустить ее в симуляторе.
- Название организации: я назвал ее Toptal
- Идентификатор организации: я назвал его «com.toptal».
- Язык: выберите Swift
- Снимите флажок «Использовать основные данные», «Включить модульные тесты» и «Включить тесты пользовательского интерфейса».
Нажмите кнопку «Далее», и мы готовы начать.
Простая архитектура
Как вы уже знаете, когда вы создаете новое приложение, у вас уже есть класс MainViewController
и Main.Storyboard
. В целях тестирования, конечно, мы можем использовать этот контроллер.
Прежде чем мы начнем что-то проектировать, давайте сначала создадим все необходимые классы и файлы, чтобы убедиться, что у нас все настроено и работает, чтобы перейти к части работы с пользовательским интерфейсом.
Где-то внутри вашего проекта просто создайте новый файл с именем « 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
, чтобы указать собственную логику для фильтрации списка.
Кроме того, нам нужен метод замены, чтобы в строке не было ничего, кроме чисел.
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: "") }
Теперь нам нужен еще один метод, называемый calculateT9
, для поиска контактов, связанных с fullname
или phonenumber
.

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
.
У нас будет два локальных свойства:
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 }() }
Как видите, это класс Singleton, что означает, что мы храним его в памяти до тех пор, пока приложение не запустится. Для получения дополнительной информации о синглтонах или шаблонах проектирования вы можете прочитать здесь.
Сейчас мы очень близки к завершению поиска 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 }
Вот и все. Мы сделали.
Теперь мы можем использовать поиск T9 внутри UIViewController
.
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 с самого начала не включила поддержку T9 в iOS? Как мы указывали во введении, T9 вряд ли можно назвать убийственной функцией современных телефонов — это скорее запоздалая мысль, возврат к дням «тупых» телефонов с механическими цифровыми блоками.
Однако все еще есть несколько веских причин, по которым вам следует реализовать поиск T9 в определенных сценариях, либо ради согласованности, либо для улучшения специальных возможностей и взаимодействия с пользователем. В более веселой ноте, если вы ностальгируете, игра с вводом T9 может вернуть приятные воспоминания о ваших школьных днях.
Наконец, вы можете найти полный код для реализации T9 в iOS в моем репозитории GitHub.