Jak zaimplementować wyszukiwanie T9 w iOS
Opublikowany: 2022-03-11Kilka lat temu wraz z moim zespołem iOS/Android pracowałem nad aplikacją o nazwie „BOG mBank - Mobile Banking”. W aplikacji dostępna jest podstawowa funkcja, dzięki której możesz korzystać z funkcji bankowości mobilnej, aby doładować swoje saldo abonamentowe telefonu komórkowego lub saldo telefonu komórkowego dowolnego kontaktu.
Podczas tworzenia tego modułu zauważyliśmy, że o wiele łatwiej jest znaleźć konkretny kontakt w wersji aplikacji na Androida niż na iOS. Czemu? Głównym powodem tego jest wyszukiwanie T9, którego brakuje w urządzeniach Apple.
Wyjaśnijmy, o co chodzi w T9 i dlaczego prawdopodobnie nie stał się częścią iOS oraz jak programiści iOS mogą go zaimplementować w razie potrzeby.
Co to jest T9?
T9 to technologia przewidywania tekstu dla telefonów komórkowych, w szczególności tych, które zawierają fizyczną klawiaturę numeryczną 3x4.
T9 został pierwotnie opracowany przez Tegic Communications, a jego nazwa oznacza Tekst na 9 klawiszach .
Możesz się domyślić, dlaczego T9 prawdopodobnie nigdy nie trafił na iOS. Podczas rewolucji smartfonów wejście T9 stało się przestarzałe, ponieważ nowoczesne smartfony opierały się na pełnych klawiaturach dzięki ekranom dotykowym. Ponieważ Apple nigdy nie miał telefonów z fizycznymi klawiaturami i nie było w branży telefonicznej podczas rozkwitu T9, zrozumiałe jest, że ta technologia została pominięta w iOS.
T9 jest nadal używany w niektórych niedrogich telefonach bez ekranu dotykowego (tak zwanych telefonach z funkcjami). Jednak pomimo faktu, że większość telefonów z Androidem nigdy nie była wyposażona w fizyczne klawiatury, nowoczesne urządzenia z Androidem obsługują wejście T9, które można wykorzystać do wybierania kontaktów poprzez przeliterowanie nazwy kontaktu, do którego próbuje się zadzwonić.
Przykład przewidywanego wprowadzania danych T9 w działaniu
W telefonie z klawiaturą numeryczną za każdym razem, gdy zostanie naciśnięty klawisz (1-9) (w polu tekstowym), algorytm zwraca odgadnięcie, jakie litery są najbardziej prawdopodobne dla klawiszy naciśniętych do tego momentu.
Na przykład, aby wprowadzić słowo „the”, użytkownik nacisnąłby 8, potem 4, a następnie 3, a na wyświetlaczu pojawiłoby się „t”, następnie „th”, a następnie „the”. Jeśli zamierzone jest mniej popularne słowo „fore” (3673), algorytm predykcyjny może wybrać „Ford”. Naciśnięcie klawisza „dalej” (zazwyczaj klawisza „*”) może wywołać „dawkę”, a na koniec „fore”. Jeśli wybrano „fore”, to następnym razem, gdy użytkownik naciśnie sekwencję 3673, z większym prawdopodobieństwem będzie pierwszym wyświetlanym słowem. Jeśli jednak słowo „Felix” jest zamierzone, podczas wprowadzania 33549 na wyświetlaczu pojawi się „E”, a następnie „De”, „Del”, „Deli” i „Felix”.
To jest przykład litery zmieniającej się podczas wpisywania słów.
Programowe użycie T9 w iOS
Zanurzmy się więc w tę funkcję i napiszmy prosty przykład wejścia T9 dla iOS. Przede wszystkim musimy stworzyć nowy projekt.
Wymagania wstępne potrzebne do naszego projektu są podstawowe: narzędzia do budowania Xcode i Xcode zainstalowane na komputerze Mac.
Aby utworzyć nowy projekt, otwórz aplikację Xcode na komputerze Mac i wybierz „Utwórz nowy projekt Xcode”, a następnie nazwij swój projekt i wybierz typ aplikacji, która ma zostać utworzona. Po prostu wybierz „Aplikacja pojedynczego widoku” i naciśnij Dalej.
Na następnym ekranie, jak widać, będzie kilka informacji, które musisz podać.
- Nazwa produktu: nazwałem go T9Search
- Zespół . Tutaj, jeśli chcesz uruchomić tę aplikację na prawdziwym urządzeniu, będziesz musiał mieć konto programisty. W moim przypadku użyję do tego własnego konta.
Uwaga: jeśli nie masz konta programisty, możesz uruchomić to również w Symulatorze.
- Nazwa organizacji: nazwałem ją Toptal
- Identyfikator organizacji: nazwałem go „com.toptal”
- Język: wybierz Swift
- Odznacz „Użyj danych podstawowych”, „Dołącz testy jednostkowe” i „Dołącz testy interfejsu użytkownika”
Naciśnij przycisk Dalej i jesteśmy gotowi do startu.
Prosta architektura
Jak już wiesz, kiedy tworzysz nową aplikację, masz już klasę MainViewController
i Main.Storyboard
. Do celów testowych oczywiście możemy użyć tego kontrolera.
Zanim zaczniemy coś projektować, najpierw utwórzmy wszystkie niezbędne klasy i pliki, aby upewnić się, że wszystko jest skonfigurowane i uruchomione, aby przejść do części UI zadania.
Gdzieś w swoim projekcie po prostu utwórz nowy plik o nazwie „ PhoneContactsStore.swift ” W moim przypadku wygląda to tak.
Naszym pierwszym zadaniem jest stworzenie mapy ze wszystkimi odmianami wprowadzanych z klawiatury numerycznej.
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" ]
Otóż to. Zaimplementowaliśmy kompletną mapę ze wszystkimi odmianami. Teraz przejdźmy do tworzenia naszej pierwszej klasy o nazwie „ PhoneContact ”.
Twój plik powinien wyglądać tak:
Najpierw w tej klasie musimy upewnić się, że mamy filtr Regex od AZ + 0-9.
private let regex = try! NSRegularExpression(pattern: "[^ az()0-9+]", options: .caseInsensitive)
Zasadniczo użytkownik ma domyślne właściwości, które należy wyświetlić:
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) } }
Upewnij się, że zastąpiłeś hash
i isEqual
, aby określić niestandardową logikę filtrowania listy.
Ponadto musimy mieć metodę replace , aby uniknąć w łańcuchu czegokolwiek poza liczbami.
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: "") }
Teraz potrzebujemy jeszcze jednej metody zwanej calculateT9
, aby znaleźć kontakty związane z fullname
i nazwiskiem lub 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)) } }
Po zaimplementowaniu obiektu PhoneContact
musimy przechowywać nasze kontakty gdzieś w pamięci. W tym celu stworzę nową klasę o nazwie PhoneContactStore
.
Będziemy mieć dwie lokalne nieruchomości:
fileprivate let contactsStore = CNContactStore()
I:
fileprivate lazy var dataSource = Set<PhoneContact>()
Używam Set
, aby upewnić się, że podczas wypełniania tego źródła danych nie ma duplikatów.
final class PhoneContactStore { fileprivate let contactsStore = CNContactStore() fileprivate lazy var dataSource = Set<PhoneContact>() static let instance : PhoneContactStore = { let instance = PhoneContactStore() return instance }() }
Jak widać, jest to klasa Singleton, co oznacza, że przechowujemy ją w pamięci do czasu uruchomienia aplikacji. Więcej informacji na temat Singletonów lub wzorców projektowych można znaleźć tutaj.
Jesteśmy teraz bardzo blisko zakończenia wyszukiwania T9.
Kładąc wszystko razem
Zanim uzyskasz dostęp do listy kontaktów w Apple, musisz najpierw poprosić o pozwolenie.
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)) } }
Po autoryzacji dostępu do kontaktów możemy napisać metodę pobrania listy z systemu.
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 {} } }
Listę kontaktów wczytaliśmy już do pamięci, co oznacza, że możemy teraz napisać prostą metodę:
-
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 }
Otóż to. Skończyliśmy.
Teraz możemy użyć wyszukiwania T9 wewnątrz 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) { } }
Implementacja metody filtrowania:
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) }
Implementacja metody 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) }
A oto ostatnia część naszego krótkiego samouczka, implementacja 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) } }
Zawijanie
To kończy nasz samouczek wyszukiwania T9 i mam nadzieję, że uznałeś go za prosty i łatwy do wdrożenia w iOS.
Ale dlaczego miałbyś? I dlaczego Apple nie włączyło obsługi T9 do iOS na początku? Jak wskazaliśmy we wstępie, T9 nie jest zabójczą funkcją w dzisiejszych telefonach – to raczej refleksja, powrót do czasów „głupich” telefonów z mechanicznymi klawiaturami numerycznymi.
Jednak nadal istnieje kilka uzasadnionych powodów, dla których warto zaimplementować wyszukiwanie T9 w niektórych scenariuszach, czy to ze względu na spójność, czy też w celu poprawy dostępności i wygody użytkownika. Co więcej, jeśli jesteś typem nostalgicznym, zabawa z wejściem T9 może przywrócić miłe wspomnienia z czasów szkolnych.
Na koniec, w moim repozytorium GitHub możesz znaleźć kompletny kod implementacji T9 w iOS.