Come implementare la ricerca T9 in iOS
Pubblicato: 2022-03-11Un paio di anni fa stavo lavorando a un'app chiamata "BOG mBank - Mobile Banking" con il mio team iOS/Android. C'è una funzione di base nell'app in cui puoi utilizzare la funzionalità di mobile banking per ricaricare il tuo saldo postpagato del tuo cellulare o il saldo del cellulare di qualsiasi contatto.
Durante lo sviluppo di questo modulo, abbiamo notato che era molto più facile trovare un contatto particolare nella versione Android dell'app che in quella iOS. Come mai? Il motivo principale alla base di ciò è la ricerca T9, che manca sui dispositivi Apple.
Spieghiamo di cosa tratta T9 e perché probabilmente non è diventato parte di iOS e come gli sviluppatori iOS possono implementarlo se necessario.
Cos'è T9?
T9 è una tecnologia di scrittura predittiva per telefoni cellulari, in particolare quelli che contengono un tastierino numerico fisico 3x4.
T9 è stato originariamente sviluppato da Tegic Communications e il nome sta per Text on 9 keys .
Puoi indovinare perché T9 probabilmente non è mai arrivato su iOS. Durante la rivoluzione degli smartphone, l'input T9 è diventato obsoleto, poiché i moderni telefoni smartphone facevano affidamento su tastiere complete, grazie ai loro display touchscreen. Dal momento che Apple non ha mai avuto telefoni con tastiere fisiche e non era nel settore della telefonia durante il periodo d'oro del T9, è comprensibile che questa tecnologia sia stata omessa da iOS.
T9 è ancora utilizzato su alcuni telefoni economici senza touchscreen (i cosiddetti feature phone). Tuttavia, nonostante il fatto che la maggior parte dei telefoni Android non sia mai stata dotata di tastiere fisiche, i moderni dispositivi Android dispongono del supporto per l'input T9, che può essere utilizzato per comporre i contatti scrivendo il nome del contatto che si sta tentando di chiamare.
Un esempio di input predittivo T9 in azione
Su un telefono con tastierino numerico, ogni volta che viene premuto un tasto (1-9) (quando si trova in un campo di testo), l'algoritmo restituisce un'ipotesi per quali lettere sono più probabili per i tasti premuti fino a quel punto.
Ad esempio, per inserire la parola "the", l'utente deve premere 8, quindi 4, quindi 3 e il display visualizzerà "t", quindi "th" e quindi "the". Se si intende la parola meno comune "anteriore" (3673), l'algoritmo predittivo può selezionare "Ford". Premendo il tasto "successivo" (in genere il tasto "*") potrebbe far apparire "dose" e infine "prima". Se è selezionato "fore", la prossima volta che l'utente premerà la sequenza 3673, sarà più probabile che fore sia la prima parola visualizzata. Se si intende la parola "Felix", tuttavia, quando si immette 33549, il display mostra "E", quindi "De", "Del", "Deli" e "Felix".
Questo è un esempio di una lettera che cambia durante l'inserimento delle parole.
Uso programmatico di T9 in iOS
Quindi, tuffiamoci in questa funzione e scriviamo un semplice esempio di input T9 per iOS. Prima di tutto, dobbiamo creare un nuovo progetto.
I prerequisiti necessari per il nostro progetto sono di base: Xcode e Xcode build tools installati sul tuo Mac.
Per creare un nuovo progetto, apri l'applicazione Xcode sul Mac e seleziona "Crea un nuovo progetto Xcode", quindi assegna un nome al progetto e scegli il tipo di applicazione da creare. Seleziona semplicemente "App visualizzazione singola" e premi Avanti.
Nella schermata successiva, come puoi vedere, ci saranno alcune informazioni che devi fornire.
- Nome prodotto: l'ho chiamato T9Search
- Squadra . Qui, se vuoi eseguire questa applicazione su un dispositivo reale, dovrai avere un account sviluppatore. Nel mio caso, userò il mio account per questo.
Nota: se non disponi di un account sviluppatore, puoi eseguirlo anche su Simulator.
- Nome organizzazione: l'ho chiamato Toptal
- Identificatore organizzazione: l'ho chiamato "com.toptal"
- Lingua: scegli Swift
- Deseleziona "Usa dati principali", "Includi unit test" e "Includi test interfaccia utente"
Premi il pulsante Avanti e siamo pronti per iniziare.
Architettura semplice
Come già sai, quando crei una nuova app, hai già la classe MainViewController
e Main.Storyboard
. A scopo di test, ovviamente, possiamo utilizzare questo controller.
Prima di iniziare a progettare qualcosa, creiamo prima tutte le classi e i file necessari per assicurarci di avere tutto impostato e funzionante per passare alla parte dell'interfaccia utente del lavoro.
Da qualche parte all'interno del tuo progetto, crea semplicemente un nuovo file chiamato " PhoneContactsStore.swift " Nel mio caso, assomiglia a questo.
Il nostro primo compito è creare una mappa con tutte le varianti degli input da tastiera numerica.
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" ]
Questo è tutto. Abbiamo implementato la mappa completa con tutte le varianti. Ora, procediamo con la creazione della nostra prima classe chiamata " PhoneContact ".
Il tuo file dovrebbe assomigliare a questo:
Innanzitutto, in questa classe, dobbiamo assicurarci di avere un filtro Regex da AZ + 0-9.
private let regex = try! NSRegularExpression(pattern: "[^ az()0-9+]", options: .caseInsensitive)
Fondamentalmente, l'utente ha proprietà predefinite che devono essere visualizzate:
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) } }
Assicurati di aver sovrascritto hash
e isEqual
per specificare la logica personalizzata per il filtro degli elenchi.
Inoltre, abbiamo bisogno del metodo di sostituzione per evitare di avere qualcosa tranne i numeri nella stringa.
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: "") }
Ora abbiamo bisogno di un altro metodo chiamato calculateT9
, per trovare i contatti relativi al fullname
o al numero di 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)) } }
Dopo aver implementato l'oggetto PhoneContact
, dobbiamo archiviare i nostri contatti da qualche parte nella memoria. A questo scopo creerò una nuova classe chiamata PhoneContactStore
.
Avremo due proprietà locali:
fileprivate let contactsStore = CNContactStore()
E:
fileprivate lazy var dataSource = Set<PhoneContact>()
Sto usando Set
per assicurarmi che non vi siano duplicati durante la compilazione di questa origine dati.
final class PhoneContactStore { fileprivate let contactsStore = CNContactStore() fileprivate lazy var dataSource = Set<PhoneContact>() static let instance : PhoneContactStore = { let instance = PhoneContactStore() return instance }() }
Come puoi vedere, questa è una classe Singleton, il che significa che la manteniamo in memoria fino a quando l'app non è in esecuzione. Per ulteriori informazioni sui singleton o sui modelli di progettazione, puoi leggere qui.
Ora siamo molto vicini al completamento della ricerca T9.
Mettere tutto insieme
Prima di accedere all'elenco dei contatti su Apple, devi prima chiedere il permesso.
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)) } }
Dopo aver autorizzato l'accesso ai contatti, possiamo scrivere il metodo per ottenere l'elenco dal 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 {} } }
Abbiamo già caricato l'elenco dei contatti in memoria, il che significa che ora possiamo scrivere un metodo semplice:
-
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 }
Questo è tutto. Abbiamo chiuso.
Ora possiamo usare la ricerca T9 all'interno 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) { } }
Implementazione del metodo di filtraggio:
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) }
Implementazione del metodo 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) }
Ed ecco l'ultima parte del nostro breve tutorial, l'implementazione di 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) } }
Avvolgendo
Questo conclude il nostro tutorial di ricerca T9 e, si spera, l'hai trovato semplice e facile da implementare in iOS.
Ma perché dovresti? E perché all'inizio Apple non ha incluso il supporto T9 in iOS? Come abbiamo sottolineato nell'introduzione, T9 non è certo una caratteristica killer sui telefoni di oggi: è più un ripensamento, un ritorno ai giorni dei telefoni "stupidi" con tastierino numerico meccanico.
Tuttavia, ci sono ancora alcuni validi motivi per cui dovresti implementare la ricerca T9 in determinati scenari, per motivi di coerenza o per migliorare l'accessibilità e l'esperienza dell'utente. In una nota più allegra, se sei un tipo nostalgico, giocare con l'input T9 potrebbe riportare bei ricordi dei tuoi giorni di scuola.
Infine, puoi trovare il codice completo per l'implementazione di T9 in iOS nel mio repository GitHub.