Cómo implementar la búsqueda T9 en iOS
Publicado: 2022-03-11Hace un par de años estaba trabajando en una aplicación llamada "BOG mBank - Banca móvil" con mi equipo de iOS/Android. Hay una función básica en la aplicación en la que puede usar la funcionalidad de banca móvil para recargar el saldo pospago de su propio teléfono celular o el saldo del teléfono celular de cualquier contacto.
Mientras desarrollábamos este módulo, notamos que era mucho más fácil encontrar un contacto en particular en la versión de Android de la aplicación que en iOS. ¿Por qué? La razón clave detrás de esto es la búsqueda T9, que no se encuentra en los dispositivos Apple.
Expliquemos de qué se trata T9, y por qué probablemente no se convirtió en parte de iOS, y cómo los desarrolladores de iOS pueden implementarlo si es necesario.
¿Qué es T9?
T9 es una tecnología de texto predictivo para teléfonos móviles, específicamente aquellos que contienen un teclado numérico físico de 3x4.
T9 fue desarrollado originalmente por Tegic Communications, y el nombre significa Texto en 9 teclas .
Puede adivinar por qué T9 probablemente nunca llegó a iOS. Durante la revolución de los teléfonos inteligentes, la entrada T9 quedó obsoleta, ya que los teléfonos inteligentes modernos dependían de teclados completos, cortesía de sus pantallas táctiles. Dado que Apple nunca tuvo teléfonos con teclados físicos y no estuvo en el negocio de los teléfonos durante el apogeo de T9, es comprensible que esta tecnología se omitió de iOS.
T9 todavía se usa en ciertos teléfonos económicos sin pantalla táctil (los llamados teléfonos con funciones). Sin embargo, a pesar del hecho de que la mayoría de los teléfonos Android nunca incluyeron teclados físicos, los dispositivos Android modernos cuentan con soporte para la entrada T9, que se puede usar para marcar contactos deletreando el nombre del contacto al que se intenta llamar.
Un ejemplo de entrada predictiva T9 en acción
En un teléfono con teclado numérico, cada vez que se presiona una tecla (1-9) (cuando está en un campo de texto), el algoritmo devuelve una estimación de qué letras son más probables para las teclas presionadas hasta ese punto.
Por ejemplo, para ingresar la palabra "the", el usuario presionaría 8, luego 4 y luego 3, y la pantalla mostraría "t", luego "th" y luego "the". Si se pretende usar la palabra menos común "fore" (3673), el algoritmo predictivo puede seleccionar "Ford". Presionar la tecla "siguiente" (típicamente la tecla "*") puede mostrar "dosis" y finalmente "adelante". Si se selecciona "fore", la próxima vez que el usuario presione la secuencia 3673, es más probable que fore sea la primera palabra que se muestre. Sin embargo, si se pretende usar la palabra "Felix", al ingresar 33549, la pantalla muestra "E", luego "De", "Del", "Deli" y "Felix".
Este es un ejemplo de una letra que cambia al ingresar palabras.
Uso programático de T9 en iOS
Entonces, profundicemos en esta función y escribamos un ejemplo sencillo de entrada T9 para iOS. En primer lugar, necesitamos crear un nuevo proyecto.
Los requisitos previos necesarios para nuestro proyecto son básicos: herramientas de compilación Xcode y Xcode instaladas en su Mac.
Para crear un nuevo proyecto, abra su aplicación Xcode en su Mac y seleccione "Crear un nuevo proyecto Xcode", luego asigne un nombre a su proyecto y elija el tipo de aplicación que se creará. Simplemente seleccione "Aplicación de vista única" y presione Siguiente.
En la siguiente pantalla, como puede ver, habrá cierta información que debe proporcionar.
- Nombre del producto: lo nombré T9Search
- equipo Aquí, si desea ejecutar esta aplicación en un dispositivo real, deberá tener una cuenta de desarrollador. En mi caso, usaré mi propia cuenta para esto.
Nota: Si no tiene una cuenta de desarrollador, también puede ejecutar esto en Simulator.
- Nombre de la organización: la llamé Toptal
- Identificador de la organización: lo nombré "com.toptal"
- Idioma: Elija Swift
- Desmarque "Usar datos básicos", "Incluir pruebas unitarias" e "Incluir pruebas de interfaz de usuario"
Presione el botón Siguiente y estamos listos para comenzar.
arquitectura sencilla
Como ya sabe, cuando crea una nueva aplicación, ya tiene la clase MainViewController
y Main.Storyboard
. Para fines de prueba, por supuesto, podemos usar este controlador.
Antes de comenzar a diseñar algo, primero creemos todas las clases y archivos necesarios para asegurarnos de que todo esté configurado y funcionando para pasar a la parte de la interfaz de usuario del trabajo.
En algún lugar dentro de su proyecto, simplemente cree un nuevo archivo llamado " PhoneContactsStore.swift ". En mi caso, se ve así.
Nuestra primera orden del día es crear un mapa con todas las variaciones 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" ]
Eso es todo. Hemos implementado el mapa completo con todas las variaciones. Ahora, procedamos a crear nuestra primera clase llamada " PhoneContact ".
Su archivo debería verse así:
Primero, en esta clase, debemos asegurarnos de tener un filtro Regex de AZ + 0-9.
private let regex = try! NSRegularExpression(pattern: "[^ az()0-9+]", options: .caseInsensitive)
Básicamente, el usuario tiene propiedades predeterminadas que deben mostrarse:
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) } }
Asegúrese de haber anulado hash
e isEqual
para especificar su lógica personalizada para el filtrado de listas.
Además, necesitamos tener el método de reemplazo para evitar tener nada más que números en la cadena.
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: "") }
Ahora necesitamos un método más llamado calculateT9
, para encontrar contactos relacionados con el nombre fullname
o el número de 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)) } }
Después de implementar el objeto PhoneContact
, necesitamos almacenar nuestros contactos en algún lugar de la memoria. Para este propósito, voy a crear una nueva clase llamada PhoneContactStore
.
Tendremos dos propiedades locales:
fileprivate let contactsStore = CNContactStore()
Y:
fileprivate lazy var dataSource = Set<PhoneContact>()
Estoy usando Set
para asegurarme de que no haya duplicación durante el llenado de esta fuente de datos.
final class PhoneContactStore { fileprivate let contactsStore = CNContactStore() fileprivate lazy var dataSource = Set<PhoneContact>() static let instance : PhoneContactStore = { let instance = PhoneContactStore() return instance }() }
Como puede ver, esta es una clase Singleton, lo que significa que la mantenemos en la memoria hasta que se ejecuta la aplicación. Para obtener más información sobre Singletons o patrones de diseño, puede leer aquí.
Ahora estamos muy cerca de finalizar la búsqueda de T9.
Poniendolo todo junto
Antes de acceder a la lista de contactos en Apple, primero debe solicitar permiso.
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)) } }
Después de haber autorizado el acceso a los contactos, podemos escribir el método para obtener la lista del 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 {} } }
Ya hemos cargado la lista de contactos en la memoria, lo que significa que ahora podemos escribir un método simple:
-
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 }
Eso es todo. Hemos terminado.
Ahora podemos usar la búsqueda T9 dentro 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) { } }
Implementación del 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) }
Implementación del 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) }
Y aquí está la última parte de nuestro breve tutorial, implementación de 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) } }
Terminando
Esto concluye nuestro tutorial de búsqueda de T9 y, con suerte, lo encontró sencillo y fácil de implementar en iOS.
Pero ¿por qué deberías? ¿Y por qué Apple no incluyó soporte T9 en iOS para empezar? Como señalamos en la introducción, T9 difícilmente es una función excelente en los teléfonos de hoy en día; es más una ocurrencia tardía, un recuerdo de los días de los teléfonos "tontos" con teclados numéricos mecánicos.
Sin embargo, todavía hay algunas razones válidas por las que debería implementar la búsqueda T9 en ciertos escenarios, ya sea por coherencia o para mejorar la accesibilidad y la experiencia del usuario. En una nota más alegre, si eres del tipo nostálgico, jugar con la entrada T9 podría traerte buenos recuerdos de tus días escolares.
Por último, puede encontrar el código completo para la implementación de T9 en iOS en mi repositorio de GitHub.