我们在 Livefront 使用的 10 个 Swift 扩展

122 阅读7分钟

原文:10 Swift Extensions We Use at Livefront

让我们说实话,Swift 和 Apple 的框架并不具备我们为 Apple 设备构建最佳软件所需的所有功能。幸运的是,Swift 支持扩展,所以我们可以添加我们需要的缺失部分,使我们的生活更轻松。

如果你是 Swift 的新手,在继续之前请参考文档,了解更多关于扩展的信息。

在这篇文章中,我将重点介绍为现有类型添加额外功能的扩展。扩展还可以为协议添加默认实现,为协议类型添加约束,等等。

当创建你自己的扩展时,我建议创建少量的单元测试来测试实现,以确保你能得到期望的结果。为了使下面的 gists 相对简短,我没有包括我们的单元测试。

你可以在我的 GitHub 页面上找到本文中使用的 Xcode Playground

以下是我们在 Livefront 使用的众多扩展中的 10 个。

1. UIView - 约束

UIView 中添加约束条件:

import PlaygroundSupport
import UIKit

// Extension #1 - A helper method to add a view to another with top, left, bottom, and right constraints.
extension UIView {

    /// Add a subview, constrained to the specified top, left, bottom and right margins.
    ///
    /// - Parameters:
    ///   - view: The subview to add.
    ///   - top: Optional top margin constant.
    ///   - left: Optional left (leading) margin constant.
    ///   - bottom: Optional bottom margin constant.
    ///   - right: Optional right (trailing) margin constant.
    ///
    func addConstrained(subview: UIView,
                        top: CGFloat? = 0,
                        left: CGFloat? = 0,
                        bottom: CGFloat? = 0,
                        right: CGFloat? = 0) {
        subview.translatesAutoresizingMaskIntoConstraints = false
        addSubview(subview)

        if let top = top {
            subview.topAnchor.constraint(equalTo: topAnchor, constant: top).isActive = true
        }
        if let left = left {
            subview.leadingAnchor.constraint(equalTo: leadingAnchor, constant: left).isActive = true
        }
        if let bottom = bottom {
            subview.bottomAnchor.constraint(equalTo: bottomAnchor, constant: bottom).isActive = true
        }
        if let right = right {
            subview.trailingAnchor.constraint(equalTo: trailingAnchor, constant: right).isActive = true
        }
    }
}

// Implementation
class ViewController: UIViewController {

    let newView = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBlue

        newView.backgroundColor = .systemTeal
        view.addConstrained(subview: newView, top: 50, left: 100, right: -100)
    }
    
}

let viewController = ViewController()
PlaygroundPage.current.liveView = viewController

与其努力记住将 translatesAutoresizingMaskIntoConstraints 设置为 false,将视图添加到父视图中,并设置所有单独的约束,这个辅助方法将为你执行所有这些操作。这个方法允许你为父视图设置顶部、前部、尾部和底部的约束。如果你省略了其中的一个约束参数,该约束的值将为零,将视图固定在父视图的边缘。如果你想完全覆盖父视图,请省略所有的约束参数。

2. Date - UTC 日期

在 UTC 时区从一个字符串中创建一个 Date 对象:

import Foundation

// Extension #2 - Create a date object from a date string with the UTC timezone.
//Inspired by: https://developer.apple.com/library/archive/qa/qa1480/_index.html
extension Date {
    /// Returns a date from the provided string.
    ///
    /// - Parameter utcString: The string used to create the date.
    ///
    /// - Returns: A date from the provided string.
    ///
    static func utcDate(from utcString: String) -> Date? {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.timeZone = TimeZone(abbreviation: "UTC")!
        return formatter.date(from: utcString)
    }
}

// Implementation
let utcDateString = "2021-04-03T14:00:00.000Z"
let utcDate = Date.utcDate(from: utcDateString) //Playgrounds will show this in the machine's timezone.
print(utcDate!)

REST API 通常会返回一个 UTC 时区的日期字符串。上面的静态方法允许你将该字符串转换为一个 Date 对象。如果你在自己的项目中遇到这个扩展的问题,请确保 dateFormat 与你所接收的日期字符串的格式相匹配。

3. String - 检索 URLs

从一个字符串中检索有效的 URLs

import Foundation

// Extension #3 - Retrieves valid URLs from a given string.
//Credit - Thanks to Paul Hudson for the core functionality on this extension.
//Source - https://www.hackingwithswift.com/example-code/strings/how-to-detect-a-url-in-a-string-using-nsdatadetector
extension String {
    /// Searches through a string to find valid URLs.
    /// - Returns: An array of found URLs.
    func getURLs() -> [URL] {
        var foundUrls = [URL]()
        guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
            return foundUrls
        }

        let matches = detector.matches(
            in: self,
            options: [],
            range: NSRange(location: 0, length: self.utf16.count)
        )

        for match in matches {
            guard let range = Range(match.range, in: self),
                  let retrievedURL = URL(string: String(self[range])) else { continue }
            foundUrls.append(retrievedURL)
        }

        return foundUrls
    }
}

// Implementation
let unfilteredString = "To get the best search results, go to https://www.google.com, www.duckduckgo.com, or www.bing.com"
let urls = unfilteredString.getURLs()

当你在一个给定的字符串中有多个 URL 时,这个辅助方法是非常方便的。我强烈建议写一些单元测试,以确保这个方法为你的特定 JSON 响应检索到预期的 URL

4. UIStackView - 移除视图

删除 UIStackView 的所有子视图

import UIKit

// Extension #4 - Removes all views from a UIStackView.
extension UIStackView {
    /// Removes all arranged subviews and their constraints from the view.
    func removeAllArrangedSubviews() {
        arrangedSubviews.forEach {
            self.removeArrangedSubview($0)
            NSLayoutConstraint.deactivate($0.constraints)
            $0.removeFromSuperview()
        }
    }
}

// Implementation
let view1 = UIView()
let view2 = UIView()
let view3 = UIView()

let stackView = UIStackView()

//Add subviews to stackView
stackView.addArrangedSubview(view1)
stackView.addArrangedSubview(view2)
stackView.addArrangedSubview(view3)

//Confirm stackView contains 3 views
stackView.arrangedSubviews.count    //3
//Remove views from stackView
stackView.removeAllArrangedSubviews()

//Confirm stackView doesn't contain any subviews now
stackView.arrangedSubviews.count    //0

当从 UIStackView 中移除视图时,有几个步骤需要执行,比如从堆栈视图本身移除,停用任何约束,以及完全从父视图中移除。这个辅助方法将为你处理所有这些步骤。

5. Bundle - App Version 和 Build number

注:SwifterSwift/UIApplicationExtensions.Swift 中已包含该扩展。

检索应用程序的版本和构建号

import Foundation

// Extension #5 - retrieve the app version # and build #.
//Inspired by https://stackoverflow.com/questions/25965239/how-do-i-get-the-app-version-and-build-number-using-swift
extension Bundle {
    /// Retrieve the app version # from Bundle
    var releaseVersionNumber: String? {
        return infoDictionary?["CFBundleShortVersionString"] as? String
    }

    /// Retrieve the build version # from Bundle
    var buildVersionNumber: String? {
        return infoDictionary?["CFBundleVersion"] as? String
    }
}

// Implementation
let releaseVersionNumber = Bundle.main.releaseVersionNumber
let buildVersionNumber = Bundle.main.buildVersionNumber

这是应该包含在 Bundle 中的一个功能。与其试图记住晦涩的字典 key,这些计算的属性将有助于检索应用程序的版本和构建号。许多应用程序在他们的设置菜单中包括版本号。

6. Calendar -- 上一年

获取上一年的整数

import Foundation

// Extension #6 - Get the prior year as an integer
extension Calendar {
    /// Returns the prior year as an integer.
    ///
    /// - Returns: Returns last year's year as an integer.
    func priorYear() -> Int {
        guard let priorYear = date(byAdding: .year, value: -1, to: Date()) else {
            return component(.year, from: Date()) - 1
        }
        return component(.year, from: priorYear)
    }
}

//Implementation
let priorYearAsNumber = Calendar.current.priorYear()

这个方法很简单。该方法将以整数形式返回上一年的数据。

7. UIStackView - 便捷初始化方法

注:SwifterSwift/UIApplicationExtensions.Swift 中已包含该扩展。

便捷初始化方法,使创建更容易

import PlaygroundSupport
import UIKit

// Extension #7 - Make UIStackView creation a lot easier.
extension UIStackView {

    /// `UIStackView` convenience initializer for creating a stack view with arranged subviews, an
    /// axis and spacing.
    ///
    /// - Parameters:
    ///   - alignment: The alignment of the arranged subviews perpendicular to the stack view’s
    ///                axis.
    ///   - arrangedSubviews: The subviews to arrange in the `UIStackView`.
    ///   - axis: The axis that the subviews should be arranged around.
    ///   - distribution: The distribution of the arranged views along the stack view’s axis.
    ///   - spacing: The spacing to place between each arranged subview. Defaults to 0.
    ///
    convenience init(alignment: UIStackView.Alignment = .fill,
                     arrangedSubviews: [UIView],
                     axis: NSLayoutConstraint.Axis,
                     distribution: UIStackView.Distribution = .fill,
                     spacing: CGFloat = 0) {
        arrangedSubviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
        self.init(arrangedSubviews: arrangedSubviews)
        self.alignment = alignment
        self.axis = axis
        self.distribution = distribution
        self.spacing = spacing
    }
}

// Implementation
let view1 = UIView()
view1.backgroundColor = .systemPink
let view2 = UIView()
view2.backgroundColor = .systemOrange
let view3 = UIView()
view3.backgroundColor = .systemTeal

let stackView = UIStackView(alignment: .leading,
                            arrangedSubviews: [view1, view2, view3],
                            axis: .vertical,
                            distribution: .fill,
                            spacing: 20)

let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
view.backgroundColor = .systemBlue
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
    view1.heightAnchor.constraint(equalToConstant: 50),
    view1.widthAnchor.constraint(equalToConstant: 150),
    view2.heightAnchor.constraint(equalToConstant: 50),
    view2.widthAnchor.constraint(equalToConstant: 150),
    view3.heightAnchor.constraint(equalToConstant: 50),
    view3.widthAnchor.constraint(equalToConstant: 150),
    stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])

PlaygroundPage.current.liveView = view

记住要在 UIStackView 上设置哪些属性是很有挑战性的。这个便捷初始化方法将常见的属性作为它的参数。初始化器还将每个视图上的 translatesAutoresizingMaskIntoConstraints 设置为 false

8. UIColor - Hex

获取一个 UIColor 的 Hex 值

import UIKit

// Extension #8 - generates a string with the hex color value.
//Inspired by: https://stackoverflow.com/a/26341062
extension UIColor {
    // MARK: - Helper Functions
    /// Returns the hex string for this `UIColor`. For example: `#FFFFFF` or `#222222AB` if the alpha value is included.
    ///
    /// - Parameter includeAlpha: A boolean indicating if the alpha value should be included in the returned hex string.
    ///
    /// - Returns: The hex string for this `UIColor`. For example: `#FFFFFF` or
    ///            `#222222AB` if the alpha value is included.
    ///
    func hexString(includeAlpha: Bool = false) -> String {
        let components = cgColor.components
        let red: CGFloat = components?[0] ?? 0.0
        let green: CGFloat = components?[1] ?? 0.0
        let blue: CGFloat = components?[2] ?? 0.0
        let alpha: CGFloat = components?[3] ?? 0.0
        let hexString = String.init(
            format: "#%02lX%02lX%02lX%02lX",
            lroundf(Float(red * 255)),
            lroundf(Float(green * 255)),
            lroundf(Float(blue * 255)),
            lroundf(Float(alpha * 255))
        )
        return includeAlpha ? hexString : String(hexString.dropLast(2))
    }
}

// Implementation
let whiteColor = UIColor(displayP3Red: 1, green: 1, blue: 1, alpha: 1)

let whiteHexString = whiteColor.hexString() //#FFFFFF
let blackColor = UIColor(displayP3Red: 0, green: 0, blue: 0, alpha: 1)

let blackHexString = blackColor.hexString() //#000000

这个方法将检索 UIColor 的十六进制值并以字符串形式返回。如果你想为一个用户保存和持久化一个颜色值,这可能非常有用。这样你只需要保存十六进制字符串,而不是三个整数的 RGB 值。

9. UIViewController - 深色模式

检查是否启用了深色模式

import UIKit

// Extension #9
extension UIViewController {
    /// Gets a flag indicating whether or not the UI is in dark mode.
    public var isDarkMode: Bool {
        if #available(iOS 12.0, *) {
            return traitCollection.userInterfaceStyle == .dark
        }
        return false
    }
}

诸如.label.systemBlueUIColors 会在用户在浅色和深色模式之间切换时自动调整。但你可能想在用户切换设备的外观时添加额外的功能。这个计算的属性将允许你检查哪个外观是活动的,这样你就可以做出相应的反应。

10. UICollectionView - Last IndexPath

注:SwifterSwift/UIApplicationExtensions.Swift 中已包含该扩展。

获取一个 collectionView 的最后索引路径

import PlaygroundSupport
import UIKit

// Extension #10 - get the last valid indexPath in a UICollectionView.
extension UICollectionView {

    /// Validates whether an `IndexPath` is a valid index path for an item in a collection view.
    ///
    /// - Parameter indexPath: The index path to validate.
    /// - Returns: `true` if the index path represents an item in the collection view or false
    ///     otherwise.
    ///
    func isValid(_ indexPath: IndexPath) -> Bool {
        guard indexPath.section < numberOfSections,
              indexPath.item < numberOfItems(inSection: indexPath.section)
        else {
            return false
        }

        return true
    }

    /// Provides the last valid `indexPath` in the collection view.
    /// - Parameter section: The section used to provide the last `indexPath`.
    /// - Returns: the last valid `indexPath` in the collection view or nil if not a valid `indexPath`.
    func lastIndexPath(in section: Int) -> IndexPath? {
        let lastIndexPath = IndexPath(row: numberOfItems(inSection: section) - 1, section: section)
        guard isValid(lastIndexPath) else { return nil }
        return lastIndexPath
    }
}

// Implementation
class CollectionViewController: UICollectionViewController {

    let items = Array(1...100)

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
    }

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items.count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        cell.backgroundColor = .systemBlue
        return cell
    }
}

let collectionViewController = CollectionViewController(collectionViewLayout: UICollectionViewFlowLayout())
let lastIndexPath = collectionViewController.collectionView.lastIndexPath(in: 0)
lastIndexPath?.section  //0
lastIndexPath?.row      //99
PlaygroundPage.current.liveView = collectionViewController

最后,这里有一个添加到 UICollectionView 的方法,它将返回最后的有效 indexPath。这是另一个感觉上应该已经存在于 UIKit 中的功能之一。虽然这可以通过获取 collectionView 中的项目数并在视图控制器中减去一个来实现,但通过扩展来添加它还是比较安全的。

总结

我想说的是,创建一个项目而不添加一个扩展几乎是不可能的。通过扩展添加功能使 Swift 更加强大,并允许你以一种安全的方式创建新的功能。我鼓励你在网上搜索 "Swift 扩展",并对我们的开发者同伴想出的所有创造性解决方案感到有趣。

欢迎在下面的评论中分享你最喜欢的扩展。😃