Como implementar a pesquisa T9 no iOS
Publicados: 2022-03-11Alguns anos atrás eu estava trabalhando em um aplicativo chamado “BOG mBank - Mobile Banking” com minha equipe iOS/Android. Há um recurso básico no aplicativo onde você pode usar a funcionalidade de mobile banking para recarregar seu próprio saldo pós-pago do celular ou o saldo do celular de qualquer contato.
Ao desenvolver este módulo, percebemos que era muito mais fácil encontrar um determinado contato na versão Android do aplicativo do que na versão iOS. Por quê? A principal razão por trás disso é a pesquisa T9, que está faltando nos dispositivos da Apple.
Vamos explicar o que é o T9 e por que ele provavelmente não se tornou parte do iOS, e como os desenvolvedores do iOS podem implementá-lo, se necessário.
O que é T9?
T9 é uma tecnologia de previsão de texto para telefones celulares, especificamente aqueles que contêm um teclado numérico físico 3x4.
T9 foi originalmente desenvolvido pela Tegic Communications, e o nome significa Texto em 9 teclas .
Você pode adivinhar por que o T9 provavelmente nunca chegou ao iOS. Durante a revolução dos smartphones, a entrada T9 tornou-se obsoleta, pois os smartphones modernos contavam com teclados completos, cortesia de suas telas sensíveis ao toque. Como a Apple nunca teve telefones com teclados físicos e não estava no ramo de telefonia durante o auge do T9, é compreensível que essa tecnologia tenha sido omitida do iOS.
O T9 ainda é usado em alguns telefones baratos sem tela sensível ao toque (os chamados telefones com recursos). No entanto, apesar do fato de a maioria dos telefones Android nunca apresentarem teclados físicos, os dispositivos Android modernos oferecem suporte para entrada T9, que pode ser usada para discar contatos soletrando o nome do contato que se está tentando chamar.
Um exemplo de entrada preditiva T9 em ação
Em um telefone com teclado numérico, cada vez que uma tecla (1-9) é pressionada (quando em um campo de texto), o algoritmo retorna uma estimativa de quais letras são mais prováveis para as teclas pressionadas até aquele ponto.
Por exemplo, para inserir a palavra “the”, o usuário pressionaria 8, depois 4 e depois 3, e a tela exibiria “t”, depois “th” e depois “the”. Se a palavra menos comum “fore” for pretendida (3673), o algoritmo preditivo pode selecionar “Ford”. Pressionar a tecla “próximo” (normalmente a tecla “*”) pode trazer “dose” e, finalmente, “fore”. Se “fore” for selecionado, então na próxima vez que o usuário pressionar a sequência 3673, fore será mais provável que seja a primeira palavra exibida. Se a palavra “Felix” for pretendida, no entanto, ao digitar 33549, o visor mostrará “E”, depois “De”, “Del”, “Deli” e “Felix”.
Este é um exemplo de mudança de letra ao inserir palavras.
Uso programático do T9 no iOS
Então, vamos mergulhar nesse recurso e escrever um exemplo fácil de entrada T9 para iOS. Primeiro de tudo, precisamos criar um novo projeto.
Os pré-requisitos necessários para o nosso projeto são básicos: Ferramentas de compilação Xcode e Xcode instaladas no seu Mac.
Para criar um novo projeto, abra seu aplicativo Xcode no seu Mac e selecione “Criar um novo projeto Xcode”, depois nomeie seu projeto e escolha o tipo de aplicativo a ser criado. Basta selecionar “Single View App” e pressionar Next.
Na próxima tela, como você pode ver, haverá algumas informações que você precisa fornecer.
- Nome do produto: eu o chamei de T9Search
- Equipe . Aqui, se você deseja executar este aplicativo em um dispositivo real, precisará ter uma conta de desenvolvedor. No meu caso, usarei minha própria conta para isso.
Observação: se você não tiver uma conta de desenvolvedor, também poderá executá-la no Simulador.
- Nome da Organização: Eu a chamei de Toptal
- Identificador da organização: chamei-o de “com.toptal”
- Idioma: Escolha Swift
- Desmarque "Usar dados principais", "Incluir testes de unidade" e "Incluir testes de interface do usuário"
Pressione o botão Avançar e estamos prontos para começar.
Arquitetura Simples
Como você já sabe, ao criar um novo aplicativo, você já tem a classe MainViewController
e Main.Storyboard
. Para fins de teste, é claro, podemos usar este controlador.
Antes de começarmos a projetar algo, vamos primeiro criar todas as classes e arquivos necessários para garantir que tudo esteja configurado e em execução para passar para a parte de interface do usuário do trabalho.
Em algum lugar dentro do seu projeto, basta criar um novo arquivo chamado “ PhoneContactsStore.swift ” No meu caso, fica assim.
Nossa primeira tarefa é criar um mapa com todas as variações de entradas de teclado numérico.
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" ]
É isso. Implementamos o mapa completo com todas as variações. Agora, vamos continuar criando nossa primeira classe chamada “ PhoneContact ”.
Seu arquivo deve ficar assim:
Primeiro, nesta classe, precisamos ter certeza de que temos um Regex Filter de AZ + 0-9.
private let regex = try! NSRegularExpression(pattern: "[^ az()0-9+]", options: .caseInsensitive)
Basicamente, o usuário possui propriedades padrão que precisam ser exibidas:
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) } }
Certifique-se de ter substituído hash
e isEqual
para especificar sua lógica personalizada para filtragem de lista.
Além disso, precisamos ter o método replace para evitar qualquer coisa, exceto números na string.
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: "") }
Agora precisamos de mais um método chamado calculateT9
, para encontrar contatos relacionados a fullname
ou 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)) } }
Após implementar o objeto PhoneContact
, precisamos armazenar nossos contatos em algum lugar da memória. Para isso, vou criar uma nova classe chamada PhoneContactStore
.
Teremos duas propriedades locais:
fileprivate let contactsStore = CNContactStore()
E:
fileprivate lazy var dataSource = Set<PhoneContact>()
Estou usando Set
para garantir que não haja duplicação durante o preenchimento desta fonte de dados.
final class PhoneContactStore { fileprivate let contactsStore = CNContactStore() fileprivate lazy var dataSource = Set<PhoneContact>() static let instance : PhoneContactStore = { let instance = PhoneContactStore() return instance }() }
Como você pode ver, esta é uma classe Singleton, o que significa que a mantemos na memória até que o aplicativo esteja em execução. Para obter mais informações sobre Singletons ou padrões de design, você pode ler aqui.
Estamos agora muito perto de ter a pesquisa T9 finalizada.
Juntando tudo
Antes de acessar a lista de contatos da Apple, você precisa primeiro pedir permissão.
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)) } }
Depois de autorizarmos o acesso aos contatos, podemos escrever o método para obter a lista do sistema.
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 {} } }
Já carregamos a lista de contatos na memória, o que significa que agora podemos escrever um método simples:
-
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 }
É isso. Acabamos.
Agora podemos usar a pesquisa T9 dentro de 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) { } }
Implementação do método de filtro:
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) }
Implementação do método Reload List:
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) }
E aqui está a última parte do nosso breve tutorial, implementação do 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) } }
Empacotando
Isso conclui nosso tutorial de pesquisa T9 e esperamos que você tenha achado simples e fácil de implementar no iOS.
Mas por que você deveria? E por que a Apple não incluiu suporte T9 no iOS para começar? Como apontamos na introdução, o T9 dificilmente é um recurso matador nos telefones de hoje - é mais uma reflexão tardia, um retrocesso aos dias dos telefones "burros" com teclados numéricos mecânicos.
No entanto, ainda existem alguns motivos válidos para implementar a pesquisa T9 em determinados cenários, seja por uma questão de consistência ou para melhorar a acessibilidade e a experiência do usuário. Em uma nota mais alegre, se você é do tipo nostálgico, brincar com a entrada T9 pode trazer de volta boas lembranças de seus dias de escola.
Por fim, você pode encontrar o código completo para implementação do T9 no iOS no meu repositório GitHub.