Comment implémenter la recherche T9 dans iOS
Publié: 2022-03-11Il y a quelques années, je travaillais sur une application appelée "BOG mBank - Mobile Banking" avec mon équipe iOS/Android. Il existe une fonctionnalité de base dans l'application où vous pouvez utiliser la fonctionnalité de banque mobile pour recharger votre propre solde postpayé de téléphone portable ou le solde de téléphone portable de n'importe quel contact.
Lors du développement de ce module, nous avons remarqué qu'il était beaucoup plus facile de trouver un contact particulier dans la version Android de l'application que dans celle d'iOS. Pourquoi? La principale raison derrière cela est la recherche T9, qui est absente des appareils Apple.
Expliquons ce qu'est T9, et pourquoi il n'est probablement pas devenu une partie d'iOS, et comment les développeurs iOS peuvent l'implémenter si nécessaire.
Qu'est-ce que le T9 ?
T9 est une technologie de texte prédictif pour les téléphones mobiles, en particulier ceux qui contiennent un clavier numérique physique 3x4.
T9 a été développé à l'origine par Tegic Communications, et le nom signifie Text on 9 keys .
Vous pouvez deviner pourquoi T9 n'est probablement jamais arrivé sur iOS. Au cours de la révolution des smartphones, la saisie T9 est devenue obsolète, car les smartphones modernes reposaient sur des claviers complets, grâce à leurs écrans tactiles. Étant donné qu'Apple n'a jamais eu de téléphones avec des claviers physiques et n'était pas dans le secteur de la téléphonie à l'apogée du T9, il est compréhensible que cette technologie ait été omise d'iOS.
Le T9 est encore utilisé sur certains téléphones bon marché sans écran tactile (appelés feature phones). Cependant, malgré le fait que la plupart des téléphones Android n'ont jamais été dotés de claviers physiques, les appareils Android modernes prennent en charge l'entrée T9, qui peut être utilisée pour composer des contacts en épelant le nom du contact que l'on essaie d'appeler.
Un exemple d'entrée prédictive T9 en action
Sur un téléphone avec un pavé numérique, chaque fois qu'une touche (1-9) est enfoncée (dans un champ de texte), l'algorithme renvoie une estimation des lettres les plus probables pour les touches enfoncées jusqu'à ce point.
Par exemple, pour saisir le mot « the », l'utilisateur doit appuyer sur 8 puis 4 puis 3, et l'écran affiche « t », puis « th », puis « the ». Si le mot moins courant « avant » est voulu (3673), l'algorithme prédictif peut sélectionner « Ford ». Appuyer sur la touche « suivant » (généralement la touche « * ») peut faire apparaître « dose » et enfin « avant ». Si "fore" est sélectionné, alors la prochaine fois que l'utilisateur appuiera sur la séquence 3673, fore sera plus susceptible d'être le premier mot affiché. Si le mot "Felix" est voulu, cependant, lors de la saisie de 33549, l'écran affiche "E", puis "De", "Del", "Deli" et "Felix".
Ceci est un exemple d'une lettre changeant lors de la saisie de mots.
Utilisation programmatique de T9 dans iOS
Alors, plongeons dans cette fonctionnalité et écrivons un exemple simple d'entrée T9 pour iOS. Tout d'abord, nous devons créer un nouveau projet.
Les prérequis nécessaires à notre projet sont basiques : Xcode et les outils de build Xcode installés sur votre Mac.
Pour créer un nouveau projet, ouvrez votre application Xcode sur votre Mac et sélectionnez "Créer un nouveau projet Xcode", puis nommez votre projet et choisissez le type d'application à créer. Sélectionnez simplement "Application Single View" et appuyez sur Suivant.
Sur l'écran suivant, comme vous pouvez le voir, il y aura des informations que vous devrez fournir.
- Nom du produit : je l'ai nommé T9Search
- Équipe . Ici, si vous souhaitez exécuter cette application sur un appareil réel, vous devrez avoir un compte développeur. Dans mon cas, j'utiliserai mon propre compte pour cela.
Remarque : Si vous n'avez pas de compte développeur, vous pouvez également l'exécuter sur Simulator.
- Nom de l'organisation : Je l'ai nommée Toptal
- Identifiant de l'organisation : je l'ai nommé "com.toptal"
- Langue : Choisissez Swift
- Décochez "Utiliser les données de base", "Inclure les tests unitaires" et "Inclure les tests d'interface utilisateur"
Appuyez sur le bouton Suivant et nous sommes prêts à commencer.
Architecture simplifiée
Comme vous le savez déjà, lorsque vous créez une nouvelle application, vous avez déjà la classe MainViewController et Main.Storyboard . À des fins de test, bien sûr, nous pouvons utiliser ce contrôleur.
Avant de commencer à concevoir quelque chose, créons d'abord toutes les classes et tous les fichiers nécessaires pour nous assurer que tout est configuré et fonctionne pour passer à la partie interface utilisateur du travail.
Quelque part dans votre projet, créez simplement un nouveau fichier appelé " PhoneContactsStore.swift " Dans mon cas, il ressemble à ceci.
Notre premier ordre du jour est de créer une carte avec toutes les variantes d'entrées du clavier numérique.
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" ]C'est ça. Nous avons implémenté la carte complète avec toutes les variantes. Passons maintenant à la création de notre première classe appelée " PhoneContact ".
Votre fichier devrait ressembler à ceci :
Tout d'abord, dans cette classe, nous devons nous assurer que nous avons un filtre Regex de AZ + 0-9.
private let regex = try! NSRegularExpression(pattern: "[^ az()0-9+]", options: .caseInsensitive)
Fondamentalement, l'utilisateur a des propriétés par défaut qui doivent être affichées :
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) } } Assurez-vous que vous avez remplacé hash et isEqual pour spécifier votre logique personnalisée pour le filtrage de liste.
De plus, nous devons avoir la méthode replace pour éviter d'avoir autre chose que des nombres dans la chaîne.
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: "") } Maintenant, nous avons besoin d'une autre méthode appelée calculateT9 , pour trouver des contacts liés au fullname ou au numéro 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)) } } Après avoir implémenté l'objet PhoneContact , nous devons stocker nos contacts quelque part dans la mémoire. Pour cela, je vais créer une nouvelle classe appelée PhoneContactStore .
Nous aurons deux propriétés locales :
fileprivate let contactsStore = CNContactStore()
Et:
fileprivate lazy var dataSource = Set<PhoneContact>()
J'utilise Set pour m'assurer qu'il n'y a pas de duplication lors du remplissage de cette source de données.
final class PhoneContactStore { fileprivate let contactsStore = CNContactStore() fileprivate lazy var dataSource = Set<PhoneContact>() static let instance : PhoneContactStore = { let instance = PhoneContactStore() return instance }() }Comme vous pouvez le voir, il s'agit d'une classe Singleton, ce qui signifie que nous la gardons en mémoire jusqu'à ce que l'application soit en cours d'exécution. Pour plus d'informations sur les singletons ou les modèles de conception, vous pouvez lire ici.
Nous sommes maintenant sur le point d'avoir finalisé la recherche T9.
Mettre tous ensemble
Avant d'accéder à la liste des contacts sur Apple, vous devez d'abord demander la permission.
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)) } }Après avoir autorisé l'accès aux contacts, nous pouvons écrire la méthode pour obtenir la liste du système.
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 {} } }Nous avons déjà chargé la liste de contacts dans la mémoire, ce qui signifie que nous pouvons maintenant écrire une méthode 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 }C'est ça. Nous avons fini.
Nous pouvons maintenant utiliser la recherche T9 dans 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) { } }Implémentation de la méthode de filtrage :
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) }Implémentation de la méthode 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) } Et voici la dernière partie de notre bref tutoriel, l'implémentation 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) } }Emballer
Ceci conclut notre tutoriel de recherche T9, et j'espère que vous l'avez trouvé simple et facile à mettre en œuvre dans iOS.
Mais pourquoi devriez-vous? Et pourquoi Apple n'a-t-il pas inclus le support T9 dans iOS pour commencer ? Comme nous l'avons souligné dans l'introduction, le T9 n'est pas une fonctionnalité qui tue sur les téléphones d'aujourd'hui - c'est plutôt une réflexion après coup, un retour à l'époque des téléphones "stupides" avec des pavés numériques mécaniques.
Cependant, il existe encore quelques raisons valables pour lesquelles vous devriez implémenter la recherche T9 dans certains scénarios, soit par souci de cohérence, soit pour améliorer l'accessibilité et l'expérience utilisateur. Sur une note plus joyeuse, si vous êtes du genre nostalgique, jouer avec l'entrée T9 pourrait vous rappeler de bons souvenirs de vos années d'école.
Enfin, vous pouvez trouver le code complet pour l'implémentation de T9 dans iOS sur mon dépôt GitHub.
