原文: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、.systemBlue 等 UIColors 会在用户在浅色和深色模式之间切换时自动调整。但你可能想在用户切换设备的外观时添加额外的功能。这个计算的属性将允许你检查哪个外观是活动的,这样你就可以做出相应的反应。
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 扩展",并对我们的开发者同伴想出的所有创造性解决方案感到有趣。
欢迎在下面的评论中分享你最喜欢的扩展。😃