iOS에서 T9 검색을 구현하는 방법
게시 됨: 2022-03-11몇 년 전 저는 iOS/Android 팀과 함께 "BOG mBank - Mobile Banking"이라는 앱을 개발하고 있었습니다. 앱에는 모바일 뱅킹 기능을 사용하여 자신의 휴대전화 후불 잔액이나 연락처의 휴대전화 잔액을 충전할 수 있는 기본 기능이 있습니다.
이 모듈을 개발하는 동안 우리는 iOS 버전보다 Android 버전 앱에서 특정 연락처를 찾는 것이 훨씬 더 쉽다는 것을 알게 되었습니다. 왜요? 그 배후의 주요 이유는 Apple 장치에서 누락된 T9 검색입니다.
T9가 무엇인지, 왜 iOS의 일부가 되지 않았는지, 그리고 필요한 경우 iOS 개발자가 어떻게 구현할 수 있는지 설명하겠습니다.
T9은 무엇입니까?
T9 는 휴대폰, 특히 물리적 3x4 숫자 키패드가 포함된 휴대폰용 텍스트 예측 기술입니다.
T9은 원래 Tegic Communications에서 개발했으며 이름은 Text on 9 keys 의 약자입니다.
T9이 iOS에 출시되지 않은 이유를 짐작할 수 있습니다. 스마트폰 혁명 동안 T9 입력은 최신 스마트폰 전화기가 터치스크린 디스플레이 덕분에 풀 키보드에 의존함에 따라 쓸모없게 되었습니다. Apple은 물리적 키보드가 있는 전화기를 가지고 있지 않았고 T9의 전성기 동안 전화 사업에 참여하지 않았기 때문에 이 기술이 iOS에서 생략된 것은 이해할 수 있습니다.
T9는 여전히 터치스크린이 없는 일부 저렴한 휴대폰(소위 피처폰)에 사용됩니다. 그러나 대부분의 Android 휴대전화에는 물리적 키보드가 없었지만 최신 Android 기기는 T9 입력을 지원합니다. T9 입력은 전화를 걸고 있는 연락처의 이름을 철자하여 연락처에 전화를 걸 때 사용할 수 있습니다.
실행 중인 T9 예측 입력의 예
숫자 키패드가 있는 전화기에서 키(1-9)를 누를 때마다(텍스트 필드에 있을 때) 알고리즘은 해당 지점까지 눌린 키에 대해 어떤 문자가 가장 가능성이 높은지 추측해 반환합니다.
예를 들어, "the"라는 단어를 입력하려면 사용자가 8, 4, 3을 차례로 누르면 디스플레이에 "t", "th", "the"가 차례로 표시됩니다. 덜 일반적인 단어 "for"가 의도된 경우(3673), 예측 알고리즘은 "Ford"를 선택할 수 있습니다. "next" 키(일반적으로 "*" 키)를 누르면 "dose"가 표시되고 마지막으로 "fore"가 표시될 수 있습니다. "fore"를 선택하면 다음에 사용자가 시퀀스 3673을 누를 때 for가 첫 번째 단어가 표시될 가능성이 더 높습니다. 그러나 "Felix"라는 단어를 의도한 경우 33549를 입력하면 디스플레이에 "E"가 표시된 다음 "De", "Del", "Deli" 및 "Felix"가 표시됩니다.
단어를 입력할 때 글자가 바뀌는 예입니다.
iOS에서 프로그래밍 방식으로 T9 사용
따라서 이 기능에 대해 자세히 알아보고 iOS용 T9 입력의 쉬운 예를 작성해 보겠습니다. 우선 새 프로젝트를 만들어야 합니다.
우리 프로젝트에 필요한 전제 조건은 기본입니다: Mac에 설치된 Xcode 및 Xcode 빌드 도구.
새 프로젝트를 생성하려면 Mac에서 Xcode 애플리케이션을 열고 "새 Xcode 프로젝트 생성"을 선택한 다음 프로젝트 이름을 지정하고 생성할 애플리케이션 유형을 선택합니다. "Single View App"을 선택하고 다음을 누르기만 하면 됩니다.
다음 화면에서 볼 수 있듯이 제공해야 하는 몇 가지 정보가 있습니다.
- 상품명 : T9Search라고 명명했습니다.
- 팀 . 여기에서 실제 기기에서 이 애플리케이션을 실행하려면 개발자 계정이 있어야 합니다. 제 경우에는 제 계정을 사용하겠습니다.
참고: 개발자 계정이 없는 경우 시뮬레이터에서도 실행할 수 있습니다.
- 조직 이름: Toptal이라고 이름을 지정했습니다.
- 조직 식별자: 이름을 "com.toptal"로 지정했습니다.
- 언어: Swift 선택
- "핵심 데이터 사용", "단위 테스트 포함" 및 "UI 테스트 포함"을 선택 취소합니다.
다음 버튼을 누르면 시작할 준비가 됩니다.
단순한 아키텍처
이미 알고 있듯이 새 앱을 만들 때 이미 MainViewController
클래스와 Main.Storyboard
가 있습니다. 물론 테스트 목적으로 이 컨트롤러를 사용할 수 있습니다.
디자인을 시작하기 전에 먼저 필요한 모든 클래스와 파일을 만들어 작업의 UI 부분으로 이동하기 위해 모든 것을 설정하고 실행하도록 합시다.
프로젝트 내부 어딘가에 " PhoneContactsStore.swift "라는 새 파일을 생성하기만 하면 됩니다. 제 경우에는 다음과 같습니다.
우리의 첫 번째 업무는 숫자 키보드 입력의 모든 변형으로 맵을 만드는 것입니다.
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" ]
그게 다야 모든 변형이 포함된 전체 지도를 구현했습니다. 이제 " PhoneContact "라는 첫 번째 클래스를 생성해 보겠습니다.
파일은 다음과 같아야 합니다.
먼저 이 수업에서 AZ + 0-9의 정규식 필터가 있는지 확인해야 합니다.
private let regex = try! NSRegularExpression(pattern: "[^ az()0-9+]", options: .caseInsensitive)
기본적으로 사용자에게는 표시해야 하는 기본 속성이 있습니다.
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) } }
목록 필터링을 위한 사용자 정의 논리를 지정하려면 hash
및 isEqual
을 재정의했는지 확인하십시오.
또한 문자열에 숫자를 제외한 어떤 것도 포함되지 않도록 하려면 replace 메서드가 필요합니다.
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: "") }
이제 fullname
phonenumber
calculateT9
가 하나 더 필요합니다.
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)) } }
PhoneContact
개체를 구현한 후 연락처를 메모리 어딘가에 저장해야 합니다. 이를 위해 PhoneContactStore
라는 새 클래스를 만들 것입니다.
두 개의 로컬 속성이 있습니다.
fileprivate let contactsStore = CNContactStore()
그리고:
fileprivate lazy var dataSource = Set<PhoneContact>()
이 데이터 소스를 채우는 동안 중복이 없는지 확인하기 위해 Set
을 사용하고 있습니다.

final class PhoneContactStore { fileprivate let contactsStore = CNContactStore() fileprivate lazy var dataSource = Set<PhoneContact>() static let instance : PhoneContactStore = { let instance = PhoneContactStore() return instance }() }
보시다시피 이것은 Singleton 클래스입니다. 즉, 앱이 실행될 때까지 메모리에 보관됩니다. 싱글톤 또는 디자인 패턴에 대한 자세한 내용은 여기를 참조하세요.
이제 T9 검색이 거의 완료되었습니다.
함께 모아서
Apple의 연락처 목록에 액세스하기 전에 먼저 권한을 요청해야 합니다.
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)) } }
연락처에 대한 액세스 권한을 부여한 후 시스템에서 목록을 가져오는 방법을 작성할 수 있습니다.
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 {} } }
이미 연락처 목록을 메모리에 로드했으므로 이제 간단한 메서드를 작성할 수 있습니다.
-
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 }
그게 다야 우리는 끝났습니다.
이제 UIViewController
내에서 T9 검색을 사용할 수 있습니다.
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) { } }
필터 메소드 구현:
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) }
리로드 목록 메소드 구현:
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) }
다음은 간단한 튜토리얼 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) } }
마무리
이것으로 T9 검색 튜토리얼을 마치며 iOS에서 간단하고 쉽게 구현할 수 있기를 바랍니다.
하지만 왜 그래야 합니까? 그리고 왜 Apple은 처음부터 iOS에 T9 지원을 포함하지 않았습니까? 소개에서 지적했듯이 T9는 오늘날 휴대폰의 킬러 기능이 아닙니다. 기계식 번호 패드가 있는 "멍청한" 휴대폰의 시절로 되돌아가는 것에 더 가깝습니다.
그러나 일관성을 위해 또는 접근성 및 사용자 경험을 개선하기 위해 특정 시나리오에서 T9 검색을 구현해야 하는 몇 가지 유효한 이유가 있습니다. 좀 더 유쾌하게 말하면 향수를 불러일으키는 타입이라면 T9 입력을 가지고 노는 것이 학창시절의 좋은 추억을 불러일으킬 수 있습니다.
마지막으로 내 GitHub 리포지토리에서 iOS의 T9 구현을 위한 전체 코드를 찾을 수 있습니다.