作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
George Vashakidze's profile image

George Vashakidze

George是一个非常积极和勤奋的手机开发者,拥有丰富的iOS和Android工作经验.

Expertise

Previously At

Bank of Georgia
Share

几年前,我开发了一款名为“BOG mBank - Mobile Banking” with my iOS/Android team. 在应用程序中有一个基本功能,你可以使用手机银行功能来充值你自己的手机后付余额或任何联系人的手机余额.

While developing this module, 我们注意到,在Android版本的应用中找到特定联系人要比在iOS版本中容易得多. Why? 这背后的关键原因是iOS的T9搜索,这在苹果设备中是缺失的.

What is T9 searching? What is T9 dialing? 让我们来解释一下T9是什么,为什么它可能没有成为iOS的一部分,以及如何 iOS developers can implement it if necessary.

What is T9?

T9 预测文本技术是用于手机的吗, 特别是那些包含物理3x4数字键盘的. 那些太年轻而不记得T9时代的人可以试试 a demo of a T9-based predictive text emulator.

数字键盘上的T9搜索说明

T9 was originally developed by Tegic Communications, and the name stands for Text on 9 keys.

你可以猜到为什么T9拨号可能永远不会出现在iOS上. During the smartphone revolution, T9 pad input became obsolete, 因为现代智能手机依赖全键盘, 多亏了他们的触屏显示器. 因为苹果从来没有任何带物理键盘的手机,而且在T9的全盛时期也没有涉足手机业务, iOS没有使用这项技术是可以理解的.

T9仍在某些没有触摸屏的廉价手机(所谓的功能手机)上使用。. However, 尽管事实上大多数安卓手机都没有物理键盘, 现代Android设备支持T9输入, 哪一个可以通过拼写要呼叫的联系人的名字来拨打联系人.

T9预测输入的一个实例

On a phone with a numeric keypad, 每次按下一个键(1-9)(当在文本字段中), 该算法返回对按到该点的键最有可能出现的字母的猜测.

Xcode screenshot

For example, to enter the word “the,,用户会按8,然后按4,然后按3, and the display would display “t,” then “th,” and then “the.如果要使用不太常见的单词“fore”(3673),预测算法可能会选择“Ford”.按下“next”键(通常是“*”键)可能会出现“dose”,最后是“fore”.” If “fore” is selected, 然后下一次用户按序列3673, Fore更有可能是第一个显示的单词. If the word “Felix” is intended, however, when entering 33549, the display shows “E,” then “De,” “Del,” “Deli,” and “Felix.”

这是一个输入单词时字母变化的例子.

Programmatic Use of T9 in iOS

所以,让我们深入研究这个功能,并编写一个简单的iOS T9输入示例. 首先,我们需要创建一个新项目.

我们的项目所需的先决条件是基本的: 在Mac上安装Xcode和Xcode构建工具.

To create a new project, open your Xcode application 然后选择“创建一个新的Xcode项目”,” then name your project, 并选择要创建的应用程序类型. 只需选择“单视图应用程序”并按下一步.

Xcode screenshot

在下一个屏幕上,正如您所看到的,将有一些您需要提供的信息.

  • Product Name: I named it T9Search
  • Team. Here, 如果您想在实际设备上运行此应用程序, 你必须有一个开发者账户. 在我的情况下,我将使用我自己的帐户.

Note: 如果您没有开发人员帐户,您也可以在模拟器上运行此程序.

  • Organization Name: I named it Toptal
  • Organization Identifier: I named it “com.toptal”
  • Language: Choose Swift
  • 取消选中“使用核心数据”、“包含单元测试”和“包含UI测试”

按下Next按钮,我们就可以开始了.

Simple Architecture

正如你已经知道的,当你创建一个新的应用程序时,你已经有 MainViewController class and Main.Storyboard. 当然,出于测试目的,我们可以使用这个控制器.

在我们开始设计之前, 让我们首先创建所有必要的类和文件,以确保我们已经设置好并运行一切,以移动到作业的UI部分.

在项目的某个地方,简单地创建一个名为“PhoneContactsStore.swift” In my case, it looks like this.

T9搜索存储板和架构

我们的第一项任务是创建一个包含所有数字键盘输入的地图.

import Contacts
import UIKit
fileprivate let T9Map = [
    " " : "0",
    “一个”:“2”,“b”:“2”,“c”:“2”,“d”:“3”,“e”:“3”,“f”:“3”,
    “g”:“4”,“h”:“4”,“我”:“4”、“j”:“5”,“k”:“5”,“l”:“5”, 
    “m”:“6”,“n”:“6”,“o”:“6”,“p”:“7”,“问”:“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"
]

That’s it. 我们已经实现了包含所有变化的完整地图. 现在,让我们继续创建第一个类,名为"PhoneContact.”

Your file should look like this:

image alt text

首先,在这个类中,我们需要确保我们有一个从a - z + 0-9的正则表达式过滤器.

private let regex = try! nsregulareexpression (pattern: "[^ a-z()0-9+]",选项: .caseInsensitive)

基本上,用户具有需要显示的默认属性:

var firstName    : String!
var lastName     : String!
var phoneNumber  : String!
var t9String     : String = ""
var image        : UIImage?
    
var fullName: String! {
    get {
         返回字符串(格式:"%@ %@"),self.firstName, self.lastName)
     }
}

Make sure you have overridden hash and isEqual 为列表过滤指定自定义逻辑.

此外,我们需要使用replace方法来避免字符串中除了数字之外的任何内容.

   override var hash: Int {
        get {
            return self.phoneNumber.hash
        }
    }
    
    覆盖函数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: "")
    }

Now we need one more method called calculateT9, to find contacts related to fullname or 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))
        }
    }

After implementing the PhoneContact 对象,我们需要将联系人存储在内存中的某个位置. 为此,我将创建一个名为 PhoneContactStore.

We will have two local properties:

filepprivate让contactstore = CNContactStore()

And:

fileprivate lazy var dataSource = Set()

I am using Set 以确保在填写此数据源期间没有重复.

final class PhoneContactStore {
    
    filepprivate让contactstore = CNContactStore()
    fileprivate lazy var dataSource = Set()
    
    让实例:PhoneContactStore = {
        let instance = PhoneContactStore()
        return instance
    }()
}

正如你所看到的,这是一个单例类,这意味着我们将它保存在内存中,直到应用程序运行. 有关单例或设计模式的更多信息,您可以阅读 here.

我们现在非常接近完成T9搜索.

Putting It All Together

在访问Apple上的联系人列表之前,您需要先获得许可.

class func hasAccess() -> Bool {
        let authorizationStatus = CNContactStore.authorizationStatus (: CNEntityType.contacts)
        return authorizationStatus == .authorized
    }
    
类func requestForAccess(_ completionHandler: @escaping (_ accessgranting: Bool), _ error : CustomError?) -> Void) {
        let authorizationStatus = CNContactStore.authorizationStatus (: 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))
        }
    }

在获得访问联系人的授权后,我们可以编写从系统获取列表的方法.

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{} //回退到早期版本
                
                try self.contactsStore.enumerateconcontacts (使用: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.组件(separatedBy: CharacterSet.decimalDigits.inverted)
                            phoneNum = NSArray(array: strArr).componentsJoined(by: "")
                            local.phoneNumber = phoneNum
                            local.calculateT9()
                            self.dataSource.insert(local)
                        }
                    }
                })
            } catch {}
        }
    }
    

我们已经将联系人列表加载到内存中, 这意味着我们现在可以编写一个简单的方法:

  1. findWith - t9String
  2. 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(
                使用:request, usingBlock: {(contact, ok) in
                count += 1;
            })
        } catch {}
        
        return count
}

That’s it. We are done.

Now we can use T9 search inside UIViewController.

filepprivate让celllidentifier = "contact_list_cell"

final类ViewController: UIViewController {
    
    @IBOutlet弱变量tableView: UITableView!
    @IBOutlet弱var searchBar: UISearchBar!
    
    fileprivate lazy var dataSource = [phoneconcontact]()
    filepprivate var searchString:字符串?
    filepprivate var searchchint9: 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 }
    }
    
    function filter(searchString: String, t9: Bool = true) {
       
    }
    
    函数reloadListSection(section: Int, animation: uitableviewwrowanimation = . .none) {
    }
}

Filter method implementation:

function 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)
    }

Reload List method implementation:

函数reloadListSection(section: Int, animation: uitableviewwrowanimation = . .none) {
        if self.tableView.numberOfSections <= section {
            self.tableView.beginUpdates()
            self.tableView.insertSections (IndexSet (integersIn: 0..

这是我们简短教程的最后一部分, UITableView implementation:

扩展ViewController: UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
    
    函数tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return tableView.dequeueReusableCell (withIdentifier:“ContactListCell”)!
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    函数tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.dataSource.count
    }
    
    函数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
        )
    }

    函数tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 55
    }
 
    函数searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        self.filter(searchString: searchText)
    }

}

Wrapping Up

我们的T9搜索教程到此结束, 希望你能发现它在iOS中很容易实现.

But why should you? 为什么苹果一开始没有在iOS中加入T9支持? 正如我们在引言中指出的, T9很难说是当今手机的杀手级功能——它更像是一个事后的想法, 这是对带有机械数字板的“哑巴”手机时代的回归.

However, 仍然有一些合理的理由说明为什么应该在某些场景中实现T9搜索, either for the sake of consistency, or to improve accessibility and user experience. On a more cheerful note, if you’re the nostalgic kind, 玩T9输入会让你回想起学生时代的美好回忆.

最后,你可以在我的网站上找到T9在iOS上实现的完整代码 GitHub repo.

Understanding the basics

  • What is predictive text?

    预测文本是一种输入技术,其中一个键或按钮代表许多字母, 比如旧手机上的数字键盘. 它还用于改善某些场景中的可访问性.

  • Why is T9 called that?

    T9代表9键上的文本,因为它依赖于9位数字键盘进行文本输入.

  • How do I use T9 on my keyboard?

    Here’s a quick example. 对于“你好”,你只需要按4-3-3 -5-6. 这些数字包含拼写“HELLO”的字母.”

Tags

Hire a Toptal expert on this topic.
Hire Now
George Vashakidze's profile image
George Vashakidze

Located in Tbilisi, Georgia

Member since June 21, 2018

About the author

George是一个非常积极和勤奋的手机开发者,拥有丰富的iOS和Android工作经验.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Previously At

Bank of Georgia

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.