个性化阅读
专注于IT技术分析

如何在iOS中实现T9搜索

本文概述

几年前, 我与我的iOS / Android团队一起开发了一个名为” BOG mBank-移动银行”的应用程序。该应用程序具有一项基本功能, 你可以使用手机银行功能为自己的手机后付款余额或任何联系人的手机余额充值。

在开发此模块时, 我们注意到在Android版本的应用程序中查找特定联系人比在iOS版本中查找联系人容易得多。为什么?这背后的关键原因是T9搜索, Apple设备缺少该搜索。

让我们解释一下T9的全部含义, 以及为什么它可能没有成为iOS的一部分, 以及iOS开发人员如何在必要时实现它。

什么是T9?

T9是用于手机的预测文本技术, 特别是那些包含物理3×4数字小键盘的手机。

数字键盘上的T9搜索插图

T9最初是由Tegic Communications开发的, 名称代表9键上的Text。

你可以猜测为什么T9可能从未进入iOS。在智能手机革命期间, T9输入变得过时, 因为现代智能手机采用触摸屏显示, 因此完全依靠全键盘。由于苹果在T9的鼎盛时期从未使用过带有物理键盘的手机, 并且也没有从事手机业务, 因此可以理解, iOS省略了该技术。

T9仍在某些不带触摸屏的廉价手机(所谓的功能手机)上使用。但是, 尽管大多数Android手机从未使用过物理键盘, 但现代Android设备仍支持T9输入, 可通过拼写要呼叫的联系人的姓名来拨打联系人。

T9预测性输入实例

在带有数字小键盘的手机上, 每当按下一个键(1-9)(在文本字段中)时, 该算法都会返回一个猜测值, 即对于该点所按下的键最有可能出现的字母。

Xcode屏幕截图

例如, 要输入单词” the”, 用户将按8, 然后按4, 再按3, 显示屏将显示” t”, 然后是” th”, 然后是” the”。如果打算使用不太常见的单词”前”(3673), 则预测算法可以选择”福特”。按下”下一个”键(通常为” *”键)可能会显示”剂量”, 最后显示”前”。如果选择了”之前”, 则用户下次按下序列3673时, 最有可能成为显示的第一个单词。但是, 如果要使用单词” Felix”, 则在输入33549时, 显示屏将显示” E”, 然后显示” De”, ” Del”, ” Deli”和” Felix”。

这是输入单词时更改字母的示例。

在iOS中以编程方式使用T9

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

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

要创建一个新项目, 请在Mac上打开你的Xcode应用程序, 然后选择”创建新的Xcode项目”, 然后为你的项目命名, 并选择要创建的应用程序的类型。只需选择” Single View App”, 然后按下一步。

Xcode屏幕截图

在下一个屏幕上, 你将看到需要提供的一些信息。

  • 产品名称:我命名为T9Search
  • 球队。在这里, 如果要在真实设备上运行此应用程序, 则必须具有开发者帐户。就我而言, 我将使用自己的帐户。

注意:如果你没有开发者帐户, 则也可以在Simulator上运行它。

  • 组织名称:我命名为srcmini
  • 组织标识符:我将其命名为” com.srcmini”
  • 语言:选择Swift
  • 取消选中”使用核心数据”, “包括单元测试”和”包括UI测试”

按下一步按钮, 我们准备开始。

简单架构

如你所知, 在创建新应用时, 你已经具有MainViewController类和Main.Storyboard。当然, 出于测试目的, 我们可以使用此控制器。

在开始设计之前, 首先创建所有必需的类和文件, 以确保我们已设置并运行所有内容以移至工作的UI部分。

在项目内的某个地方, 只需创建一个名为” PhoneContactsStore.swift”的新文件, 就我而言, 它看起来像这样。

T9搜索分页和架构

我们的首要任务是创建包含数字键盘输入的所有变体的地图。

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”的第一类。

你的文件应如下所示:

图片替代文字

首先, 在此类中, 我们需要确保我们有一个A-Z + 0-9的正则表达式过滤器。

私人让正则表达式=尝试! NSRegularExpression(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 {
         return String(format: "%@ %@", self.firstName, self.lastName)
     }
}

确保已覆盖哈希和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: "")
    }

现在, 我们需要另一种称为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让contactsStore = CNContactStore()

和:

fileprivate惰性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 {}
        }
    }
    

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

  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(
                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中易于实现。

但是你为什么要呢?苹果为什么不首先在iOS中包含T9支持?正如我们在介绍中所指出的那样, T9并不是当今电话的杀手feature, 而是更多的事后思考, 回溯到带有机械数字键盘的”笨拙”电话时代。

但是, 出于一致性考虑或为了提高可访问性和用户体验, 仍然有一些合理的原因为什么你应该在某些情况下实施T9搜索。更令人愉悦的是, 如果你是怀旧的人, 那么使用T9输入可以带回你上学时的美好回忆。

最后, 你可以在我的GitHub存储库中找到iOS中T9实施的完整代码。

赞(0)
未经允许不得转载:srcmini » 如何在iOS中实现T9搜索

评论 抢沙发

评论前必须登录!