Post

Swift 原生类型扩展|打造 Namespace 容器避免命名冲突,提升模组化维护性

iOS 开发中扩充原生类型常导致命名冲突与介面混乱,透过 Swift 泛型容器与协议封装扩展方法,实现 Namespace 功能,有效区隔自订与原生 API,提升专案模组化与维护效率。

Swift 原生类型扩展|打造 Namespace 容器避免命名冲突,提升模组化维护性

Click here to view the English version of this article.

點擊這裡查看本文章正體中文版本。

基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。


[Swift] 一个优雅的 原生类型扩展方式

自行封装扩充方法,使其有 Namespace 的功能

<https://www.swift.org/>{:target="_blank"}

https://www.swift.org/

做法实际出处不详,是从厉害同事的 Code 上学到的。

原生类型扩展

在日常的 iOS/Swift 开发中,我们常常需要对原生 API 进行扩充、撰写自己的 Helper。

以下以扩充 UIColor 为例,我们希望扩充 UIColor 使其能转换成 HEX Color String:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
extension UIColor {
    /// Convert a UIColor to a hex string representation.
    /// - Returns: A hex string (e.g., "#RRGGBB" or "#RRGGBBAA").
    func toHexString(includeAlpha: Bool = false) -> String? {
        var red: CGFloat = 0
        var green: CGFloat = 0
        var blue: CGFloat = 0
        var alpha: CGFloat = 0

        guard self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else {
            return nil // Color could not be represented in RGB space
        }

        if includeAlpha {
            return String(format: "#%02X%02X%02X%02X",
                          Int(red * 255),
                          Int(green * 255),
                          Int(blue * 255),
                          Int(alpha * 255))
        } else {
            return String(format: "#%02X%02X%02X",
                          Int(red * 255),
                          Int(green * 255),
                          Int(blue * 255))
        }
    }
}

直接对 UIColor 进行扩充 (Extension) 后的存取方式如下:

1
2
let color = UIColor.blue
color.toHexString() // #0000ff

问题

当我们自定义的扩充方式越来越多会让存取介面开始变得混乱,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let color = UIColor.blue
color.getRed(...)
color.getWhite(...)
color.getHue(...)
color.getCMYK() // 自行扩充的方法
color.toHexString() // 自行扩充的方法
color.withAlphaComponent(...)
color.setFill(...)
color.setToBlue() // 自行扩充的方法

// A Module
public extension UIColor {
  func getCMYK() {
    // ...
  }
}

// B Module
// Invalid redeclaration of 'getCMYK()'
public extension UIColor {
  func getCMYK() {
    // ...
  }
}

我们自行扩充的方法跟原生提供的方法全部混合在一起,难以区分;另外专案规模越大、引用的套件越多也有可能遇到 Extension 命名冲突,例如两个套件都对 UIColor Extension 也都叫 getCMYK() 就会有问题。

自订扩展 Namespace 容器

我们可以运用 Swift Protocol, Computed Property 与 泛型的特型来自行封装扩充方法,使其有 Namespace 的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 宣告一个泛型容器 ExtensionContainer<Base>:
public struct ExtensionContainer<Base> {
    public let base: Base
    public init(_ base: Base) {
        self.base = base
    }
}

// 定义 for AnyObject, Class(Reference) Type 实现的接口:
// 例如 Foundation 中的 NSXXX 类别
public protocol ExtensionCompatibleObject: AnyObject {}
// 定义 for Struct(Value) Type 实现的接口:
public protocol ExtensionCompatible {}

// 自订义 Namespace Computed Property:
public extension ExtensionCompatibleObject {
    var zhg: ExtensionContainer<Self> {
        return ExtensionContainer(self)
    }
}

public extension ExtensionCompatible {
    var zhg: ExtensionContainer<Self> {
        return ExtensionContainer(self)
    }
}

扩展原生类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
extension UIColor: ExtensionCompatibleObject {}

extension ExtensionContainer where Base: UIColor {
    /// Convert a UIColor to a hex string representation.
    /// - Returns: A hex string (e.g., "#RRGGBB" or "#RRGGBBAA").
    func toHexString(includeAlpha: Bool = false) -> String? {
        var red: CGFloat = 0
        var green: CGFloat = 0
        var blue: CGFloat = 0
        var alpha: CGFloat = 0

        guard self.base.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else {
            return nil // Color could not be represented in RGB space
        }

        if includeAlpha {
            return String(format: "#%02X%02X%02X%02X",
                          Int(red * 255),
                          Int(green * 255),
                          Int(blue * 255),
                          Int(alpha * 255))
        } else {
            return String(format: "#%02X%02X%02X",
                          Int(red * 255),
                          Int(green * 255),
                          Int(blue * 255))
        }
    }
}

使用

1
2
let color = UIColor.blue
color.zhg.toHexString() // #0000ff

范例 2. URL .queryItems 扩充

1
2
3
4
5
6
7
8
9
10
extension URL: ExtensionCompatible {}

extension ExtensionContainer where Base == URL {
    
    var queryParameters: [String: String]? {
        URLComponents(url: base, resolvingAgainstBaseURL: true)?
            .queryItems?
            .reduce(into: [String: String]()) { $0[$1.name] = $1.value }
    }
}

结合 Builder Pattern

另外我们也可以将此封装方式结合 Builder Pattern 操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
final class URLBuilder {
    private var components: URLComponents

    init(base: URL) {
        self.components = URLComponents(url: base, resolvingAgainstBaseURL: true)!
    }

    func setQueryParameters(_ parameters: [String: String]) -> URLBuilder {
        components.queryItems = parameters.map { .init(name: $0.key, value: $0.value) }
        return self
    }

    func setScheme(_ scheme: String) -> URLBuilder {
        components.scheme = scheme
        return self
    }

    func build() -> URL? {
        return components.url
    }
}

extension URL: ExtensionCompatible {}

extension ExtensionContainer where Base == URL {
    func builder() -> URLBuilder {
        return URLBuilder(base: base)
    }
}

let url = URL(string: "https://zhgchg.li")!.zhg.builder().setQueryParameters(["a": "b", "c": "d"]).setScheme("ssh").build()
// ssh://zhgchg.li?a=b&c=d

有任何问题及指教欢迎 与我联络


Buy me a beer

本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。

Improve this page on Github.

This post is licensed under CC BY 4.0 by the author.