Post

Swift Native Type Extensions|Elegant Namespace Implementation for Cleaner Code

Discover how to create elegant native type extensions in Swift by encapsulating methods with namespace functionality, improving code organization and maintainability for iOS developers.

Swift Native Type Extensions|Elegant Namespace Implementation for Cleaner Code

点击这里查看本文章简体中文版本。

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

This post was translated with AI assistance — let me know if anything sounds off!


[Swift] An Elegant Native Type Extension Method

Encapsulate extension methods to provide Namespace functionality

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

https://www.swift.org/

The exact source of this method is unclear; it was learned from a skilled colleague’s code.

Native Type Extensions

In daily iOS/Swift development, we often need to extend native APIs and write our own helpers.

The following example extends UIColor, allowing it to convert to a 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))
        }
    }
}

After directly extending UIColor, the access method is as follows:

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

Question

As we add more custom extensions, the access interface can become cluttered, for example:

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() // Custom extension method
color.toHexString() // Custom extension method
color.withAlphaComponent(...)
color.setFill(...)
color.setToBlue() // Custom extension method

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

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

Our custom extensions and native methods are mixed together, making them hard to distinguish. Additionally, as the project grows and more packages are used, extension name conflicts may occur. For example, if two packages both add a getCMYK() method to the UIColor extension, it will cause issues.

Custom Extension Namespace Container

We can use Swift Protocols, Computed Properties, and Generics features to encapsulate extension methods, giving them Namespace functionality.

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
// Declare a generic container ExtensionContainer<Base>:
public struct ExtensionContainer<Base> {
    public let base: Base
    public init(_ base: Base) {
        self.base = base
    }
}

// Define the interface for AnyObject, Class (Reference) Type implementations:
// For example, NSXXX classes in Foundation
public protocol ExtensionCompatibleObject: AnyObject {}
// Define the interface for Struct (Value) Type implementations:
public protocol ExtensionCompatible {}

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

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

Extending Native Types

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

Usage

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

Example 2. URL .queryItems Extension

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

Combining with Builder Pattern

Additionally, we can combine this packaging method with the 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

If you have any questions or feedback, feel free to contact me.


Buy me a beer

This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.

Improve this page on Github.

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