10个实用的Swift字符串扩展

1,474 阅读2分钟

电子邮件验证、类型转换等。

原文:Medium: 10 Useful Swift String Extensions

Photo by Zan on Unsplash

string 是所有编程语言中最基本的数据类型之一。无论是用于控制台应用程序、网络服务、基于 GUI 的应用程序,还是游戏开发,它都是编程的重要组成部分。Swift 语言提供了 String 类,它可以处理大多数常见的文本操作——大多数但不是全部。

我从 2014 年开始用 Swift 写代码,我几乎不记得有哪个项目没有使用 String 扩展。这里我将分享我使用最多的扩展。其中有些是我自己写的,还有一部分是我从不同的开源代码中借来的。所有的例子都能在 Swift 5 中工作,但其中大部分也能兼容旧版本。

如果你还是一名 Android 开发者,你可能会对我的文章 10 个实用的 Kotlin 字符串扩展 感兴趣。

Photo by Markus Spiske on Unsplash

1. MD5 哈希值计算

第一个有用的扩展提供了计算字符串的 MD5 哈希值。当你使用网络服务或检查文件是否正确时,它很有用。该扩展不使用任何外部依赖,而是使用本地 CommonCrypto 框架,因此它需要链接一个 bridge 头文件。

#import <CommonCrypto/CommonCrypto.h>

扩展本身很简单。这里假设你想要将 MD5 作为一个字符串返回(例如,作为一个参数传递给服务器)。如果你需要获取 data 类型的数据,你可以少走一步,直接返回哈希变量。

import Foundation

extension String {
    var md5: String? {
        let length = Int(CC_MD5_DIGEST_LENGTH)
        
        guard let data = self.data(using: String.Encoding.utf8) else { return nil }
        
        let hash = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> [UInt8] in
            var hash: [UInt8] = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
            CC_MD5(bytes.baseAddress, CC_LONG(data.count), &hash)
            return hash
        }
        
        return (0..<length).map { String(format: "%02x", hash[$0]) }.joined()
    }
}

如果你要把它添加到一个外部框架中,请在 var 变量之前添加 public 关键字。

如何使用

let password: String = "your password"
guard let passwordMD5 = password.md5 else {
    showError("Can't calculate MD5 of your password")
    return
}

因为在这个例子中,你得到 nil 的可能性极小,所以这个用法也很有意义:

let passwordMD5 = password.md5!

Photo by Vladislav Klapin on Unsplash

2. 本地化字符串

如果你正在开发一款支持多语言的应用程序,你可能需要在代码中嵌入一些文本字符串,这些字符串将以用户的语言出现在屏幕上。编辑 storyboards 或 XIB 文件不需要编写任何代码——但动态字符串需要。

原生的方式是使用 NSLocalizedString 函数。例如:

NSLocalizedString("string_id", comment: "")

它总是可以正常工作,通常也不会产生任何问题,但它看起来有点丑陋。这个扩展是一种语法糖:

import Foundation

extension String {
    var localized: String {
        NSLocalizedString(self, comment: "")
    }
}

如何使用

"string_id".localized

Photo by Nery Montenegro on Unsplash

3. 整数下标

在任何编程语言中,对字符串的典型操作都涉及到获取其中的一部分--或者像程序员说的那样,截取一个片段(slice)。它可以是前几个字符,后几个字符,或者是字符串中间的某些部分。

在 Swift 中,最让我惊讶的事情之一就是总是难以得到子字符串(和数组片段--但这是另一个话题)。你不能像很多语言一样,直接写出 "Hello, world"[0...4] 这样的东西。但这样会很舒服吧?

那我们需要什么呢?

let str = "Hello, world"
print(str[...4]) // "Hello"
print(str[..<5]) // "Hello"
print(str[7...]) // "world"
print(str[3...4] + str[2]) // "lol"

这段代码看起来简单易懂。但如果你尝试编译它,你会看到这个错误:

'subscript(_:)' is unavailable: cannot subscript String with an integer range, use a String.Index range instead.

传统上来说,它也有解决方案,而我们的解决方案是写一个 String 类的扩展。

import Foundation

extension String {
    subscript (i: Int) -> Character {
        return self[index(startIndex, offsetBy: i)]
    }
    
    subscript (bounds: CountableRange<Int>) -> Substring {
        let start = index(startIndex, offsetBy: bounds.lowerBound)
        let end = index(startIndex, offsetBy: bounds.upperBound)
        if end < start { return "" }
        return self[start..<end]
    }
    
    subscript (bounds: CountableClosedRange<Int>) -> Substring {
        let start = index(startIndex, offsetBy: bounds.lowerBound)
        let end = index(startIndex, offsetBy: bounds.upperBound)
        if end < start { return "" }
        return self[start...end]
    }
    
    subscript (bounds: CountablePartialRangeFrom<Int>) -> Substring {
        let start = index(startIndex, offsetBy: bounds.lowerBound)
        let end = index(endIndex, offsetBy: -1)
        if end < start { return "" }
        return self[start...end]
    }
    
    subscript (bounds: PartialRangeThrough<Int>) -> Substring {
        let end = index(startIndex, offsetBy: bounds.upperBound)
        if end < startIndex { return "" }
        return self[startIndex...end]
    }
    
    subscript (bounds: PartialRangeUpTo<Int>) -> Substring {
        let end = index(startIndex, offsetBy: bounds.upperBound)
        if end < startIndex { return "" }
        return self[startIndex..<end]
    }
}

它的工作原理和上面的例子几乎一模一样,只是它需要一些类型转换。当然,子串返回的是 Substring 类型的实例,而不是 String 类型。而当你索引一个字符时,它会返回一个 Character 类型的实例。这两种类型都可以被转码为 String

let str = "Hello, world"
let strHello = String(str[...4])
let strWorld = String(str[7...])

Photo by Mick Haupt on Unsplash

4. 检查内容

很多时候我需要检查一个 String 字符串是否包含数字。或者只包含数字。例如,用户名通常只包含数字和字母,但密码应该包含更多的字符类型。姓名应该只包含字母。

在提交给服务器之前,有几个辅助函数(包装成扩展)对输入数据的验证非常有用。

import Foundation

extension String {
    var containsOnlyDigits: Bool {
        let notDigits = NSCharacterSet.decimalDigits.inverted
        return rangeOfCharacter(from: notDigits, options: String.CompareOptions.literal, range: nil) == nil
    }
    
    var containsOnlyLetters: Bool {
        let notLetters = NSCharacterSet.letters.inverted
        return rangeOfCharacter(from: notLetters, options: String.CompareOptions.literal, range: nil) == nil
    }
    
    var isAlphanumeric: Bool {
        let notAlphanumeric = NSCharacterSet.decimalDigits.union(NSCharacterSet.letters).inverted
        return rangeOfCharacter(from: notAlphanumeric, options: String.CompareOptions.literal, range: nil) == nil
    }
}

该文件包含三个常见的扩展,但你也可以根据需求为其他字符集编写类似的扩展。一般的规则是得到一个包含所有允许字符的字符集(或多个字符集的联合)。然后反转这个字符集,并检查字符串是否包含其中的任何符号。

如何使用

let a1 = "12345".containsOnlyDigits // true
let a2 = "a12345".containsOnlyDigits // false
let b1 = "abcde".containsOnlyLetters // true
let b2 = "abcde1".containsOnlyLetters // false
let c1 = "abcde12345".isAlphanumeric // true
let c2 = "abcde.12345".isAlphanumeric // false

Photo by Kon Karampelas on Unsplash

5. 检查字符串是否是有效的电子邮件地址

说到输入数据验证,我们不能避免检查电子邮件地址。大多数应用程序都会要求用户提供电子邮件地址,用于验证、订阅、发送收据等多种用途。

我在网上找到了许多解决方案。它们背后的想法都是一样的:我们创建一个正则表达式,并评估包含电子邮件地址(或不包含它)的字符串。不同的是正则表达式本身。

电子邮件地址总是包含两部分:left@right,其中左边是你的标识符(例如姓名),右边是完整的域名。允许使用哪个域名是一个选择问题。例如,不清楚是否允许使用 user@localhost 这样的电子邮件。对于大多数用例来说,它是不允许的,但如果你写一个本地使用的控制台应用程序,它可以是一个有效的地址。

这个解决方案对大多数情况都有效。

import Foundation

extension String {
    var isValidEmail: Bool {
        let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"

        let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
        return emailTest.evaluate(with: self)
    }
}

备选版本(由 Tromgy 建议):

extension String {
    func matches(_ expression: String) -> Bool {
        if let range = range(of: expression, options: .regularExpression, range: nil, locale: nil) {
            return range.lowerBound == startIndex && range.upperBound == endIndex
        } else {
            return false
        }
    }
    
    var isValidEmail: Bool {
        matches("[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}")
    }
}

这个方案的优点是简化了正则表达式的工作。这两种方案都是有效的。

如何使用

let approved = "test@test.com".isValidEmail // true
let rejected = "12345".isValidEmail // false

电话号码也可以用同样的方式验证,但需要一个外部库--比如 这个库

Photo by Rima Kruciene on Unsplash

6. 保存和检索本地设置

很多时候,我们需要在不创建文件的情况下存储一些数据--例如,用户名或用户 ID。一个典型的保存和加载数据的方法是通过 UserDefaults 类,以前叫做 NSUserDefaults

你需要了解它的内容:

  • 当应用程序重新启动时,之前存储在 UserDefaults 中的数据还是有的。
  • 卸载应用会清除 UserDefaults 中的数据。
  • 你可以在任何时刻从 UserDefaults 中读取数据,也可以从任何线程中读取数据。
  • 在将数据保存到 UserDefaults 后,你应该调用 synchronize() 方法(只适用于 iOS 11 及更早版本)

UserDefaults 的典型用法:

let value: String? = UserDefaults.standard.string(forKey: "key")

如果存在一个与 key 关联的值,你就得到它;如果没有,你就得到 nil

保存就比较复杂了:

UserDefaults.standard.set("value", forKey: "key")
UserDefaults.standard.synchronize() // iOS 11 and earlier

还有删除:

UserDefaults.standard.removeObject(forKey: "key")
UserDefaults.standard.synchronize() // iOS 11 and earlier

让我们使用 Swift 扩展来简化这个过程。这些扩展有两个目标。

  1. 它应该缩短代码,让它更清晰。
  2. 如果值没有保存,它应该返回 null
import Foundation

extension Int {
    init?(key: String) {
        guard UserDefaults.standard.value(forKey: key) != nil else { return nil }
        self.init(UserDefaults.standard.integer(forKey: key))
    }
    
    func store(key: String) {
        UserDefaults.standard.set(self, forKey: key)
        UserDefaults.standard.synchronize()
    }
}

extension Bool {
    init?(key: String) {
        guard UserDefaults.standard.value(forKey: key) != nil else { return nil }
        self.init(UserDefaults.standard.bool(forKey: key))
    }
    
    func store(key: String) {
        UserDefaults.standard.set(self, forKey: key)
        UserDefaults.standard.synchronize()
    }
}

extension Float {
    init?(key: String) {
        guard UserDefaults.standard.value(forKey: key) != nil else { return nil }
        self.init(UserDefaults.standard.float(forKey: key))
    }
    
    func store(key: String) {
        UserDefaults.standard.set(self, forKey: key)
        UserDefaults.standard.synchronize()
    }
}

extension Double {
    init?(key: String) {
        guard UserDefaults.standard.value(forKey: key) != nil else { return nil }
        self.init(UserDefaults.standard.double(forKey: key))
    }
    
    func store(key: String) {
        UserDefaults.standard.set(self, forKey: key)
        UserDefaults.standard.synchronize()
    }
}

extension Data {
    init?(key: String) {
        guard let data = UserDefaults.standard.data(forKey: key) else { return nil }
        self.init(data)
    }
    
    func store(key: String) {
        UserDefaults.standard.set(self, forKey: key)
        UserDefaults.standard.synchronize()
    }
}

extension String {
    init?(key: String) {
        guard let str = UserDefaults.standard.string(forKey: key) else { return nil }
        self.init(str)
    }
    
    func store(key: String) {
        UserDefaults.standard.set(self, forKey: key)
        UserDefaults.standard.synchronize()
    }
}

extension Array where Element == Any {
    init?(key: String) {
        guard let array = UserDefaults.standard.array(forKey: key) else { return nil }
        self.init()
        self.append(contentsOf: array)
    }
    
    func store(key: String) {
        UserDefaults.standard.set(self, forKey: key)
        UserDefaults.standard.synchronize()
    }
}

extension Dictionary where Key == String, Value == Any {
    mutating func merge(dict: [Key: Value]) {
        for (k, v) in dict {
            updateValue(v, forKey: k)
        }
    }
    
    init?(key: String) {
        guard let dict = UserDefaults.standard.dictionary(forKey: key) else { return nil }
        self.init()
        self.merge(dict: dict)
    }
    
    func store(key: String) {
        UserDefaults.standard.set(self, forKey: key)
        UserDefaults.standard.synchronize()
    }
}

UserDefaults 支持的类型有好几种,但任何可序列化的类型都可以进行类似的扩展。

如何使用

let age = 25
age.store(key: "age")
print(Int(key: "age")) // Optional(25)
print(Float(key: "age")) // Optional(25.0)
print(String(key: "age")) // Optional("25")
print(String(key: "age1")) // nil

let dict: [String: Any] = [
  "name": "John",
  "surname": "Doe",
  "occupation": "Swift developer",
  "experienceYears": 5,
  "age": 32
]
dict.store(key: "employee")
print(Dictionary(key: "employee"))
// Optional(["name": John, "occupation": Swift developer, "age": 32, "experienceYears": 5, "surname": Doe])

如你所见,如果有必要的话,它会自动转换类型。

我必须指出这些扩展有一个缺点。它们在每次执行 store 操作时都会保存更改,这会对磁盘或闪存进行不必要的写入。另一方面,你永远不会忘记同步更新。是否使用这些扩展取决于你。

注:UserDefaults.standard.synchronize() 操作从 iOS 12 开始就不需要了(由 David Such 指出)。

Photo by Markus Spiske on Unsplash

7. 从 String 字符串中解析 JSON 数据

JavaScript 对象符号(JSON)格式变得如此流行,以至于没有多少 Swift 应用程序避免它。即使你不直接做,一些框架也会在下面做。

JSON 源码不过是一个字符串。所以我们可以写一个 String 类的扩展来解析它,把它变成一个类型为 [String: Any] 的字典。

Swift 有一个 JSON 解析的标准类--JSONONSerialization。但它使用的是 Data,而不是 String。这就是为什么我们要创建两个扩展。DataString

Data+json.swift

import Foundation

extension Data {
    init?(json: Any) {
        guard let data = try? JSONSerialization.data(withJSONObject: json, options: .fragmentsAllowed) else { return nil }
        self.init(data)
    }
    
    func jsonToDictionary() -> [String: Any]? {
        (try? JSONSerialization.jsonObject(with: self, options: .allowFragments)) as? [String: Any]
    }
    
    func jsonToArray() -> [Any]? {
        (try? JSONSerialization.jsonObject(with: self, options: .allowFragments)) as? [Any]
    }
}

String+json.swift

import Foundation

extension String {
    init?(json: Any) {
        guard let data = Data(json: json) else { return nil }
        self.init(decoding: data, as: UTF8.self)
    }
    
    func jsonToDictionary() -> [String: Any]? {
        self.data(using: .utf8)?.jsonToDictionary()
    }
    
    func jsonToArray() -> [Any]? {
        self.data(using: .utf8)?.jsonToArray()
    }
}

如何使用

let dict: [String: Any] = [
  "name": "John",
  "surname": "Doe",
  "age": 31
]
print(dict)
// ["surname": "Doe", "name": "John", "age": 31]
let json = String(json: dict)
print(json)
// Optional("{\"surname\":\"Doe\",\"name\":\"John\",\"age\":31}")

let restoredDict = json?.jsonToDictionary()
print(restoredDict)
// Optional(["name": John, "surname": Doe, "age": 31])

Photo by Suzanne D. Williams on Unsplash

8. 转换

Swift 转换本身相当简单,但有时将它们作为扩展来呈现会更适宜。例如,如果你从 Kotlin(Android 到 iOS)转换代码,你可能会看到 toIntOrNull() 这样的函数。让我们用 Swift 来写它们。

import Foundation

extension String {
    func toInt() -> Int {
        Int(self)!
    }
    
    func toIntOrNull() -> Int? {
        Int(self)
    }
}

这里的逻辑非常简单。该扩展非常短,但如果你需要将大量的 Android 代码转换为 iOS 代码,它可以很有用。同样,你也可以添加 toDouble()toString() 等扩展。

如何使用

print("10".toInt())
// 10

print("15".toIntOrNull())
// Optional(15)

print("5.5".toInt())
// CRASH!

print("5a".toInt())
// CRASH!

Photo by Robert Katzki on Unsplash

9. 从字符串中提取颜色

Swift 没有漂亮的方式把十六进制字符串转换颜色,所以很明显我们可以用扩展来实现。

首先,颜色在 iOS 和 Mac 中的表示方式是不同的。UIKit 提供了 UIColor 类,而 Cocoa 则有几乎相同的 NSColor 类。为了让代码通用,我们将引入一个别名 UniColor

#if os(iOS) || os(tvOS)
import UIKit
typealias UniColor = UIColor
#else
import Cocoa
typealias UniColor = NSColor
#endif

private extension Int {
    func duplicate4bits() -> Int {
        return (self << 4) + self
    }
}

extension UniColor {
    convenience init?(hexString: String) {
        self.init(hexString: hexString, alpha: 1.0)
    }

    fileprivate convenience init?(hex3: Int, alpha: Float) {
        self.init(red:   CGFloat( ((hex3 & 0xF00) >> 8).duplicate4bits() ) / 255.0,
                            green: CGFloat( ((hex3 & 0x0F0) >> 4).duplicate4bits() ) / 255.0,
                            blue:  CGFloat( ((hex3 & 0x00F) >> 0).duplicate4bits() ) / 255.0,
                            alpha: CGFloat(alpha))
    }

    fileprivate convenience init?(hex6: Int, alpha: Float) {
        self.init(red:   CGFloat( (hex6 & 0xFF0000) >> 16 ) / 255.0,
                            green: CGFloat( (hex6 & 0x00FF00) >> 8 ) / 255.0,
                            blue:  CGFloat( (hex6 & 0x0000FF) >> 0 ) / 255.0, alpha: CGFloat(alpha))
    }

    convenience init?(hexString: String, alpha: Float) {
        var hex = hexString

        if hex.hasPrefix("#") {
            hex = String(hex[hex.index(after: hex.startIndex)...])
        }

        guard let hexVal = Int(hex, radix: 16) else {
            self.init()
            return nil
        }

        switch hex.count {
            case 3: self.init(hex3: hexVal, alpha: alpha)
            case 6: self.init(hex6: hexVal, alpha: alpha)
            default: self.init()
                        return nil
        }
    }

    convenience init?(hex: Int) {
        self.init(hex: hex, alpha: 1.0)
    }

    convenience init?(hex: Int, alpha: Float) {
        if (0x000000 ... 0xFFFFFF) ~= hex {
            self.init(hex6: hex, alpha: alpha)
        } else {
            self.init()
            return nil
        }
    }
}

extension String {
    func toColor() -> UniColor? {
        UniColor(hexString: self)
    }
}

extension Int {
    func toColor(alpha: Float = 1.0) -> UniColor? {
        UniColor(hex: self, alpha: alpha)
    }
}

如何使用

let strColor = "#ff0000" // Red color
let color = strColor.toColor()
var red: CGFloat = 0.0
var green: CGFloat = 0.0
var blue: CGFloat = 0.0
var alpha: CGFloat = 0.0
color?.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
print(red, green, blue, alpha)
// 1.0 0.0 0.0 1.0

Photo by Nick Hillier on Unsplash

10. 将字符分组

最后一个扩展对银行卡号和其他需要分组的数据的格式化很有帮助。它也可以用于对大数的数字进行分组,但有一个特殊的类。其他可能的用途是将固定宽度的电子表格转换为逗号分隔的电子表格,游戏机游戏,以及许多其他用途。

这个扩展的想法是在原始字符串的每 n 个字符后插入一些字符或字符串。例如,如果我们得到卡号 1234567890123456,我们通常希望将其显示为 1234 5678 9012 3456

import Foundation

extension String {
    mutating func insert(separator: String, every n: Int) {
        self = inserting(separator: separator, every: n)
    }
    
    func inserting(separator: String, every n: Int) -> String {
        var result: String = ""
        let characters = Array(self)
        stride(from: 0, to: count, by: n).forEach {
            result += String(characters[$0..<min($0+n, count)])
            if $0+n < count {
                result += separator
            }
        }
        return result
    }
}

如何使用

var cardNumber = "1234567890123456"
cardNumber.insert(separator: " ", every: 4)
print(cardNumber)
// 1234 5678 9012 3456

let pin = "7690"
let pinWithDashes = pin.inserting(separator: "-", every: 1)
print(pinWithDashes)
// 7-6-9-0

总结

我希望这些扩展能为你节省时间,帮助你让你的代码更简洁。

下次再见。祝你编码愉快!