这是我参与更文挑战的第1天,活动详情查看: 更文挑战
当 App 面向全球的朋友,在输入电话号码时,就不得不考虑如何将电话号码根据当地语言来格式化,如何展示各个国家的图标等问题了。在处理电话号码的问题时,可以使用PhoneNumberKit。
下面是我在源码阅读过程中收货。
不换行空格 "\u{00a0}"
我们经常需要过滤空白字符,其他一个特殊的字符需要考虑:"\u{00a0}" 不换行空格。
var spaceCharacterSet: CharacterSet = {
let characterSet = NSMutableCharacterSet(charactersIn: "\u{00a0}")
characterSet.formUnion(with: CharacterSet.whitespacesAndNewlines)
return characterSet as CharacterSet
}()
不换行空格有两个用途:
- 禁止合并连续的空格。例如 HTML 中会将连续的空白字符合并成一个。
- 禁止自动换行。编辑器一般会把自动换行放在空格字符处。但是,有些文本内容在排版时不适合被放在连续的一行行尾与下一行行首,例如:“100 km”。
详细资料可以查看维基百科。
NSRange 和 Range 相互转化
下面是 PhoneNumberKit 作者对 NSRange 和 Range 相互转化的方法:
extension String {
func nsRange(from range: Range<String.Index>) -> NSRange {
let utf16view = self.utf16
let from = range.lowerBound.samePosition(in: utf16view) ?? self.startIndex
let to = range.upperBound.samePosition(in: utf16view) ?? self.endIndex
return NSRange(location: utf16view.distance(from: utf16view.startIndex, to: from),
length: utf16view.distance(from: from, to: to))
}
func range(from nsRange: NSRange) -> Range<String.Index>? {
guard
let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex),
let from = String.Index(from16, within: self),
let to = String.Index(to16, within: self)
else { return nil }
return from..<to
}
}
平台的判断
由于平台的差异可能会导致 bug,或者要区别对待,所以需要在必要的情况下对平台进行判断。在 PhoneNumberKit 框架中,使用到了 canImport 来判断平台状况。
// 判断是有 ObjectiveC 运行环境
#if canImport(ObjectiveC)
// ……
#else
// ……
#endif
// 判断是否有 UIKit
#if canImport(UIKit)
// ...
#endif
更多的平台信息可以通过查看苹果文档。
| Platform condition | Valid arguments |
|---|---|
| os() | macOS, iOS, watchOS, tvOS, Linux, Windows |
| arch() | i386, x86_64, arm, arm64 |
| swift() | >= or < followed by a version number |
| compiler() | >= or < followed by a version number |
| canImport() | A module name |
| targetEnvironment() | simulator, macCatalyst |
在 PhoneNumberKit 中有两处为了避免 bug 而进行的平台判断:
// 用例一:
extension NSRegularExpression {
#if canImport(ObjectiveC)
func enumerateMatches(in string: String, options: NSRegularExpression.MatchingOptions = [], range: Range<String.Index>? = nil, using block: (NSTextCheckingResult?, NSRegularExpression.MatchingFlags, UnsafeMutablePointer<ObjCBool>) -> Swift.Void) {
let range = range ?? string.startIndex..<string.endIndex
let nsRange = string.nsRange(from: range)
self.enumerateMatches(in: string, options: options, range: nsRange, using: block)
}
#else
// FIX: block needs to be @escaping
func enumerateMatches(in string: String, options: NSRegularExpression.MatchingOptions = [], range: Range<String.Index>? = nil, using block: @escaping (NSTextCheckingResult?, NSRegularExpression.MatchingFlags, UnsafeMutablePointer<ObjCBool>) -> Swift.Void) {
let range = range ?? string.startIndex..<string.endIndex
let nsRange = string.nsRange(from: range)
self.enumerateMatches(in: string, options: options, range: nsRange, using: block)
}
#endif
}
// 用例二:
#if canImport(ObjectiveC)
let prefixPattern = String(format: "^(?:%@)", possibleNationalPrefix)
#else
// FIX: String format with %@ doesn't work without ObjectiveC (e.g. Linux)
let prefixPattern = "^(?:\(possibleNationalPrefix))"
#endif
Any way to test for the presence of the Objective-C runtime?
NSRegularExpression 的缓存
当我们的 App 需要经常用到正则 NSRegularExpression 时,就需要考虑对正则进行缓存来提高性能了。
PhoneNumberKit 中使用了 RegexManager 来缓存已创建的 NSRegularExpression,并使用自定义的队列进行保存和读取。
关键代码如下:
final class RegexManager {
// MARK: Regular expression pool
var regularExpresionPool = [String: NSRegularExpression]()
// concurrent 并行队列
private let regularExpressionPoolQueue = DispatchQueue(label: "com.phonenumberkit.regexpool",
attributes: .concurrent)
// MARK: Regular expression
func regexWithPattern(_ pattern: String) throws -> NSRegularExpression {
var cached: NSRegularExpression?
// 将数组的操作都放到了自己的 并行队列中,
// 读取使用的是 .sync
// 设置使用 .async(flags: .barrier)
self.regularExpressionPoolQueue.sync {
cached = self.regularExpresionPool[pattern]
}
if let cached = cached {
return cached
}
do {
// caseInsensitive 忽略大小写
let regex = try NSRegularExpression(pattern: pattern,
options: .caseInsensitive)
regularExpressionPoolQueue.async(flags: .barrier) {
self.regularExpresionPool[pattern] = regex
}
return regex
} catch {
throw PhoneNumberError.generalError
}
}
}
NSLocale
获取国家或地区代码的本地化字符串。
let name = (Locale.current as NSLocale).localizedString(forCountryCode: countryCode)
例如,在美国英语 (en_US) 语言环境中调用此方法,当 countryCode 为 “GB” 时,返回字符串“United Kingdom”。
苹果官网 localizedString(forCountryCode:)
数字转字符串
平时我将数字转字符串都是用的 "\(number)" 的方法,作者采用的是 public var description: String { get } 的方法。
代码如下:
var number: Int? = nil
debugPrint(number?.description ?? "empty")
number = 1
debugPrint(number?.description ?? "empty")
// 输出
"empty"
"1"
国家编码转 emoji
在选择国家的时候,通常都会在国家前面加上对应的图标,增加辨识度,方便用户选择。
如果使用图片来展示各个国家的图标,则会增加 App 包的大小。PhoneNumberKit 的作者采用了 emogi 的方式,通过国家的编码(county code)来找出对应的 emoji,这样即避免了图片造成的 App 安装变大,又提高的加载速度。
通过国家的编码(county code)找出对应的 emoji 的方法为:
static func getFlag(countryCode: String) -> String? {
var flag = ""
let flagBase = UnicodeScalar("🇦").value - UnicodeScalar("A").value
countryCode.uppercased().unicodeScalars.forEach {
if let scaler = UnicodeScalar(flagBase + $0.value) {
flag.append(String(describing: scaler))
}
}
guard flag.count == 1 else {
return nil
}
return flag
}
另外可以参考下面的文章: stackoverflow 上的讨论 维基百科
字符串的比较
不区分大小写比较两个字符串
不区分大小写比较两个字符串时,可以采用的先转换成全部大写或者小写后再比较。作者在通过搜索过滤国家列表的时候,就采用了这种方式:
public func updateSearchResults(for searchController: UISearchController) {
let searchText = searchController.searchBar.text ?? ""
filteredCountries = allCountries.filter { country in
country.name.lowercased().contains(searchText.lowercased()) ||
country.code.lowercased().contains(searchText.lowercased()) ||
country.prefix.lowercased().contains(searchText.lowercased())
}
tableView.reloadData()
}
另外,可以采用字符串的 caseInsensitiveCompare(_:) (Apple 文档)方法,这个方法其实是以 NSCaseInsensitiveSearch 作为唯一 option 调用 compare(_:options:) 的结果。作者在排序国家列表的时候采用了这种方式:
lazy var allCountries = phoneNumberKit
.allCountries()
.compactMap({ Country(for: $0, with: self.phoneNumberKit) })
.sorted(by: { $0.name.caseInsensitiveCompare($1.name) == .orderedAscending })
另外:当操作的字符串需要向用户展示时,需要调用 localizedCaseInsensitiveCompare(_:) 方法。
很奇怪作者为什么会采用两种方式,个人感觉全部使用
caseInsensitiveCompare方法即可。
忽略宽度或变音符号差异的方式比较字符串
处理文本时,尤其是拉丁文文本,以忽略大小写(大写或小写)、宽度(全角或半角)和变音符号(重音和半角)等差异的方式比较字符串时,可以使用 folding(options:locale:) (Apple 文档)从字符串中删除指定的字符区别来创建适合比较的字符串。
作者在对国家进行分组展示的时候使用到了 folding 方法(代码有改动):
let lhs = lName.folding(options: .diacriticInsensitive, locale: nil)
let rhs = rName.folding(options: .diacriticInsensitive, locale: nil)
if lhs?.first == rhs.first {
// 首字符一致
} else {
// 首字母不一致
}
根据用户设定获取 UIFont
在设置 Cell 的字体是,作者用到了 preferredFont(forTextStyle:) 。
// 根据用户设定的字体大小及粗细设置字体
@available(iOS 7.0, *)
open class func preferredFont(forTextStyle style: UIFontTextStyle) -> UIFont
设置字体代码如下:
cell.textLabel?.font = .preferredFont(forTextStyle: .callout)
cell.detailTextLabel?.font = .preferredFont(forTextStyle: .body)
NSCharacterSet
如何获取一个字符集的非集(不存在指定字符集的字符集)呢? 原来 NSCharacterSet 也有 inverted 方法(Apple 文档)。
用例:
let nonNumericSet: NSCharacterSet = {
var mutableSet = NSMutableCharacterSet.decimalDigit().inverted
mutableSet.remove(charactersIn: PhoneNumberConstants.plusChars)
mutableSet.remove(charactersIn: PhoneNumberConstants.pausesAndWaitsChars)
mutableSet.remove(charactersIn: PhoneNumberConstants.operatorChars)
return mutableSet as NSCharacterSet
}()
集合、字符串 allSatisfy
当判断一个集合、字符串中所有的元素是否都满足某个条件时,可以使用 allSatisfy(_:) (Apple 文档)。
extension String {
var isBlank: Bool {
return allSatisfy({ $0.isWhitespace })
}
}
每次阅读优秀的框架时,都有醍醐灌顶的感觉,谢谢那些优秀的开源作者。