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.
点击这里查看本文章简体中文版本。
點擊這裡查看本文章正體中文版本。
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
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.
This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.