Swift|優雅な原生型拡張でNamespace機能を実現|効率的なコード管理術
Swiftの原生型を拡張しNamespace機能を持たせる方法を解説。拡張メソッドを自作で整理し、コードの可読性と保守性を大幅に向上させる実践テクニックを紹介します。
本記事は AI による翻訳をもとに作成されています。表現が不自然な箇所がありましたら、ぜひコメントでお知らせください。
記事一覧
[Swift] 優雅なネイティブ型拡張方法
拡張メソッドを独自にカプセル化し、Namespace機能を持たせる
やり方の実際の出典は不明で、優秀な同僚のコードから学びました。
ネイティブ型の拡張
日常の iOS/Swift 開発では、ネイティブ API を拡張したり、自分のヘルパーを作成したりすることがよくあります。
以下は UIColor の拡張の例です。UIColor を拡張して HEX カラー文字列に変換できるようにします:
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 {
/// UIColorを16進数の文字列に変換します。
/// - Returns: 16進数の文字列(例: "#RRGGBB" または "#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 // RGB空間で表現できない色
}
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 モジュール
public extension UIColor {
func getCMYK() {
// ...
}
}
// B モジュール
// 'getCMYK()' の再宣言は無効です
public extension UIColor {
func getCMYK() {
// ...
}
}
私たちが独自に拡張したメソッドとネイティブのメソッドがすべて混ざってしまい、区別が難しくなります。また、プロジェクトの規模が大きくなり、参照するライブラリが増えると、Extensionの命名衝突が発生する可能性があります。例えば、2つのライブラリが両方ともUIColorのExtensionにgetCMYK()というメソッドを持っている場合、問題が起きます。
カスタム拡張ネームスペースコンテナ
Swiftのプロトコル、計算プロパティ、ジェネリクスの特性を活用して、拡張メソッドを独自にカプセル化し、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
}
}
// AnyObject、クラス(参照型)用のプロトコルを定義:
// 例として Foundation の NSXXX クラス
public protocol ExtensionCompatibleObject: AnyObject {}
// 構造体(値型)用のプロトコルを定義:
public protocol ExtensionCompatible {}
// カスタム Namespace 計算プロパティ:
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 {
/// UIColorを16進数の文字列に変換します。
/// - Returns: 16進数文字列(例: "#RRGGBB" または "#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 // 色をRGB空間で表現できませんでした
}
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 パターンと組み合わせて操作することもできます:
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
ご質問やご意見がありましたら、こちらからご連絡ください 。
Post を Medium から ZMediumToMarkdown によって変換しました。
本記事は Medium にて初公開されました(こちらからオリジナル版を確認)。ZMediumToMarkdown による自動変換・同期技術を使用しています。
