原文: Medium: 24 Swift Extensions for Cleaner Code

更高效地构建你的移动应用程序。

让代码更整齐的24个Swift扩展

在我看来,Swift 和 Objective-C 最好的功用之一便是扩展(extension)。它们使你能够不用通过继承或者覆写,就能够在任何类中增加新的办法,而且能够在整个项目中运用。

作为一名移动开发者,我一同从事 iOS 和 Android 的开发作业,我常常看到 Android 的功用和办法比 Swift 中的更简略、清晰、易懂。运用扩展,其间的一些办法能够移植到 Swift 中。通过这些新的(扩展)办法,就能够让 Swift 拥有简略、干净、易保护的代码。

我会偏向于运用 Swift 编程,可是这些扩展也能够移植到 Objective-C 中,或者直接和 Objective-C 一同运用,不需求转化。

String.trim() 和 Swift.trimmed

在 99% 的状况中,当我在 Swift 中裁剪 String 类型的字符串时,我想去掉空格和其他类似的符号(例如,换行和制表符)。

这个简略的扩展就能完成:

import Foundation
extension String {
    var trimmed: String {
        self.trimmingCharacters(in: .whitespacesAndNewlines)
    }
    mutating func trim() {
        self = self.trimmed
    }
}

用法:

var str1 = "  a b c d e   n"
var str2 = str1.trimmed
str1.trim() // a b c d e

Int.toDouble() 和 Double.toInt()

假如你的作业涉及到 optionals 可选值,这些办法或许会很有用。假如你有非可选的 Int,你能够用 Double(a) 转化它,其间 a 是一个整数变量。可是假如 a 是可选值,你就没法这样做。

让咱们为 IntDouble 增加扩展。

import Foundation
extension Int {
    func toDouble() -> Double {
        Double(self)
    }
}
extension Double {
    func toInt() -> Int {
        Int(self)
    }
}

用法:

let a = 15.78
let b = a.toInt()

String.toDate(…) 和 Date.toString(…)

String 中获取 Date 日期和格式化 Date 日期以显现它或发送到 API 是常见的任务。规范的转化方式需求三行代码。让咱们看看怎么使其更短。

import Foundation
extension String {
    func toDate(format: String) -> Date? {
        let df = DateFormatter()
        df.dateFormat = format
        return df.date(from: self)
    }
}
extension Date {
    func toString(format: String) -> String {
        let df = DateFormatter()
        df.dateFormat = format
        return df.string(from: self)
    }
}

用法:

let strDate = "2020-08-10 15:00:00"
let date = strDate.toDate(format: "yyyy-MM-dd HH:mm:ss")
let strDate2 = date?.toString(format: "yyyy-MM-dd HH:mm:ss")

Int.centsToDollars()

一些付出API(例如 Stripe)喜欢运用货币单位(美分)进行付出处理。它能够防止 FloatDouble 的不精确性。一同,运用这些类型来显现数值会更舒服。

这个扩展能够完成这种转化:

import Foundation
extension Int {
    // 美分转化为美元
    func centsToDollars() -> Double {
        Double(self) / 100
    }
}

用法:

let cents = 12350
let dollars = cents.centsToDollars()

String.asCoordinates()

一个地点在地球上的坐标至少需求两个数字–纬度和经度。另一个是海拔高度,但这只有在三维空间中才有含义,这在软件开发中并不常见。

从 API 中咱们能够得到两个独立的字段,或者一个字段的逗号分隔的值。这个扩展答应将这些字符串转化为 CLLocationCoordinate2D

import Foundation
import CoreLocation
extension String {
    var asCoordinates: CLLocationCoordinate2D? {
        let components = self.components(separatedBy: ",")
        if components.count != 2 { return nil }
        let strLat = components[0].trimmed
        let strLng = components[1].trimmed
        if let dLat = Double(strLat),
            let dLng = Double(strLng) {
            return CLLocationCoordinate2D(latitude: dLat, longitude: dLng)
        }
        return nil
    }
}

用法:

let strCoordinates = "41.6168, 41.6367"
let coordinates = strCoordinates.asCoordinates

String.asURL()

iOS 和 macOS 运用 URL 类型来处理链接。它更灵敏,它答应获取组件,它能够处理不同类型的 URLs。一同,咱们通常会输入它或从 API 字符串 String 中获取它。

将一种类型转化为另一种类型很简略,但这个扩展答应咱们处理可选类型或链式转化。


import Foundation
extension String {
    var asURL: URL? {
        URL(string: self)
    }
}

用法:

let strUrl = "https://medium.com"
let url = strUrl.asURL

UIDevice.vibrate()

iPhone 振动能够成为一种很帅的作用,用于设备的按钮点击和其他反馈。对于 iPhone 振动有一种特别的声响,由 AudioToolbox 结构处理。

将 AudioToolbox 加入到一切带有振动的 UIViewControllers 中是很烦人的,而且从逻辑上讲,振动更多的是一种设备功用(它不是来自扬声器而是来自设备自身),而不是播放声响。这个扩展能够将其简化为一行代码。

import UIKit
import AudioToolbox
extension UIDevice {
    static func vibrate() {
        AudioServicesPlaySystemSound(1519)
    }
}

用法:

UIDevice.vibrate()

String.width(…) 和 String.height(…)

iOS 能够运用提供的约束条件主动计算 UILabel 的巨细,但有时自己设置巨细很重要。

这个扩展答应咱们运用提供的 UIFont 来计算字符串的宽度和高度。

import UIKit
extension String {
    func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil)
        return ceil(boundingBox.height)
    }
    func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil)
        return ceil(boundingBox.width)
    }
}
extension NSAttributedString {
    func height(withConstrainedWidth width: CGFloat) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, context: nil)
        return ceil(boundingBox.height)
    }
    func width(withConstrainedHeight height: CGFloat) -> CGFloat {
        let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
        let boundingBox = boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, context: nil)
        return ceil(boundingBox.width)
    }
}

用法:

let text = "Hello, world!"
let textHeight = text.height(withConstrainedWidth: 100, font: UIFont.systemFont(ofSize: 16))

String.containsOnlyDigits

当你需求约束用户输入或验证来自 API 的数据时,下面的扩展对错常有用的。它查看字符串是否只包括数字。

import Foundation
extension String {
    var containsOnlyDigits: Bool {
        let notDigits = NSCharacterSet.decimalDigits.inverted
        return rangeOfCharacter(from: notDigits, options: String.CompareOptions.literal, range: nil) == nil
    }
}

用法:

let digitsOnlyYes = "1234567890".containsOnlyDigits
let digitsOnlyNo = "12345+789".containsOnlyDigits

String.isAlphanumeric

和之前的扩展一样,这个扩展查看 String 的内容,假如字符串不是空的而且只包括字母数字字符,则返回 true。这个扩展的倒置版别能够用来确认暗码是否含有非字母数字字符。

import Foundation
extension String {
    var isAlphanumeric: Bool {
        !isEmpty && range(of: "[^a-zA-Z0-9]", options: .regularExpression) == nil
    }
}

用法:

let alphanumericYes = "asd3kJh43saf".isAlphanumeric
let alphanumericNo = "Kkncs+_s3mM.".isAlphanumeric

字符串下标

Swift 5 有一种可怕的下标字符串的方式。例如,假如你想取得从5到10的字符,计算索引和偏移量是很费事的。这个扩展答应运用简略的 Ints 类型来完成这个目的。

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]
    }
}

用法:

let subscript1 = "Hello, world!"[7...]
let subscript2 = "Hello, world!"[7...11]

UIImage.squared

当你要求用户拍摄自己的相片或挑选一张现有的相片作为个人资料相片时,他们简直不会提供一张正方形的相片。一同,大多数用户界面都运用正方形或圆形。

这个扩展能够对提供的 UIImage 进行裁剪,使其成为一个完美的正方形。

import UIKit
extension UIImage {
    var squared: UIImage? {
        let originalWidth  = size.width
        let originalHeight = size.height
        var x: CGFloat = 0.0
        var y: CGFloat = 0.0
        var edge: CGFloat = 0.0
        if (originalWidth > originalHeight) {
            // landscape
            edge = originalHeight
            x = (originalWidth - edge) / 2.0
            y = 0.0
        } else if (originalHeight > originalWidth) {
            // portrait
            edge = originalWidth
            x = 0.0
            y = (originalHeight - originalWidth) / 2.0
        } else {
            // square
            edge = originalWidth
        }
        let cropSquare = CGRect(x: x, y: y, width: edge, height: edge)
        guard let imageRef = cgImage?.cropping(to: cropSquare) else { return nil }
        return UIImage(cgImage: imageRef, scale: scale, orientation: imageOrientation)
    }
}

这个扩展也能够作为一个办法来完成。由于正方形图画不是原始图画的特点,而是它的处理版别。假如你以为办法是一个更好的解决方案,只需将 var squared.UIImage? 替换为 func squared() -> UIImage?

用法:

let img = UIImage() // Must be a real UIImage
let imgSquared = img.squared // img.squared() for method

UIImage.resized(…)

在上传图片到你的服务器之前,你有必要保证图片的尺寸足够小。 iPhone 和 iPad 有十分强壮的摄像头,系统图库中的图片有或许十分大。

为了保证上传的 UIImage 图片不超过给定的尺寸巨细,例如 512 像素或 1024 像素,你能够运用这个扩展:

import UIKit
extension UIImage {
    func resized(maxSize: CGFloat) -> UIImage? {
        let scale: CGFloat
        if size.width > size.height {
            scale = maxSize / size.width
        }
        else {
            scale = maxSize / size.height
        }
        let newWidth = size.width * scale
        let newHeight = size.height * scale
        UIGraphicsBeginImageContext(CGSize(width: newWidth, height: newHeight))
        draw(in: CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return newImage
    }
}

用法:

let img2 = UIImage() // Must be a real UIImage
let img2Thumb = img2.resized(maxSize: 512)

上面两个扩展能够运用链式语法链接起来:

let img = UIImage() // Must be a real UIImage
let imgPrepared = img.squared?.resized(maxSize: 512)

Int.toString()

Java 中最有用的功用之一是 toString() 办法。它是一个肯定是一切类和类型的办法。Swift 答应运用字符串插值来完成类似的功用。”(someVar)“。但有一个差异——返回的变量是可选值类型。Swift 会在输出中加入 optional 这个词。Java 会直接崩溃,但 Kotlin 会漂亮地处理可选值类型:someVar?.toString() 会返回一个可选的字符串,假如 someVar 是空的(nil),则为 null(nil),否则为包括 String 类型的 var 变量。

不幸的是,Swift 不答应扩展 Any,所以咱们至少能够在 Int 类型上增加 toString() 办法。

import Foundation
extension Int {
    func toString() -> String {
        "(self)"
    }
}

用法:

let i1 = 15
let i1AsString = i1.toString()

Double.toString()

好像前面的比如,将 Double 类型转化为 String 类型也十分有用。但这种状况下,咱们将约束让它输出两个小数位。我不能说这个扩展适用于一切状况,但对于大多数场景,它都能很好地作业。

import Foundation
extension Double {
    func toString() -> String {
        String(format: "%.02f", self)
    }
}

用法:

let d1 = 15.67
let d1AsString = d1.toString()

Double.toPrice()

生成带有价格的 String 字符串只是格式化 Double 的另一种方式。这种算法并不通用,它取决于区域设置。但你能够把它作为一个总体思路,并根据你的应用进行调整。

import Foundation
extension Double {
    func toPrice(currency: String) -> String {
        let nf = NumberFormatter()
        nf.decimalSeparator = ","
        nf.groupingSeparator = "."
        nf.groupingSize = 3
        nf.usesGroupingSeparator = true
        nf.minimumFractionDigits = 2
        nf.maximumFractionDigits = 2
        return (nf.string(from: NSNumber(value: self)) ?? "?") + currency
    }
}

用法:

let dPrice = 16.50
let strPrice = dPrice.toPrice(currency: "€")

String.asDict

JSON 是一种交流或存储结构化数据的盛行格式。大多数 API 都喜欢运用 JSON。JSON 是一种 JavaScript 结构。Swift 有完全相同的数据类型—字典(dictionary)。

将一种类型转化为另一种类型对错常简略的技巧:

import Foundation
extension String {
    var asDict: [String: Any]? {
        guard let data = self.data(using: .utf8) else { return nil }
        return try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]
    }
}

用法:

let json = "{"hello": "world"}"
let dictFromJson = json.asDict

String.asArray

这个扩展与之前的一个扩展类似,但它将 JSON 数组转化为 Swift 数组。

import Foundation
extension String {
    var asArray: [Any]? {
        guard let data = self.data(using: .utf8) else { return nil }
        return try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [Any]
    }
}

用法:

let json2 = "[1, 2, 3]"
let arrFromJson2 = json2.asArray

String.asAttributedString

有时咱们需求对文本进行一些简略的独立于渠道的样式设计。一个比较常见的办法是运用简略的 HTML 来完成这一目的。

UILabel 能够显现带有粗体(<strong>)部分的文本、带下划线的文本、更大和更小的片段等。您只需求将 HTML 转化为 NSAttributedString,并将其分配给 UILabel.attributedText 即可。

这个扩展将帮助您完成第一个任务。

import Foundation
extension String {
    var asAttributedString: NSAttributedString? {
        guard let data = self.data(using: .utf8) else { return nil }
        return try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
    }
}

用法:

let htmlString = "<p>Hello, <strong>world!</string></p>"
let attrString = htmlString.asAttributedString

Bundle.appVersion

本系列的最后一个扩展能够从 Info.plist 文件中获取应用程序版别号。它能够用于:

  • 发送应用程序版别到 API。
  • 查看可用的更新。
  • 在设备屏幕上显现应用程序版别。
  • 在支持邮件中包括应用程序版别。

下面的扩展答应你在一行代码中获取应用程序版别(假如没有版别,则为零)。

import Foundation
extension Bundle {
    var appVersion: String? {
        self.infoDictionary?["CFBundleShortVersionString"] as? String
    }
    static var mainAppVersion: String? {
        Bundle.main.appVersion
    }
}

用法:

let appVersion = Bundle.mainAppVersion

总结

我希望这些扩展能帮助你使你的代码更简练。欢迎修正它们以满意你的要求,并将它们归入你的项目中。

假如你对更多有用的 Swift String 扩展感兴趣,你能够阅览我的其他文章:

10 个有用的 Swift 字符串扩展