ZhgChg.Li

Swift|優雅な原生型拡張でNamespace機能を実現|効率的なコード管理術

Swiftの原生型を拡張しNamespace機能を持たせる方法を解説。拡張メソッドを自作で整理し、コードの可読性と保守性を大幅に向上させる実践テクニックを紹介します。

Swift|優雅な原生型拡張でNamespace機能を実現|効率的なコード管理術
本記事は AI による翻訳です。お気づきの点があればお知らせください。

[Swift] 優雅なネイティブ型拡張方法

拡張メソッドを独自にカプセル化し、Namespace機能を持たせる

<https://www.swift.org/>

https://www.swift.org/

やり方の実際の出典は不明で、優秀な同僚のコードから学びました。

ネイティブ型の拡張

日常の iOS/Swift 開発では、ネイティブ API を拡張したり、自分のヘルパーを作成したりすることがよくあります。

以下は UIColor の拡張の例です。UIColor を拡張して HEX カラー文字列に変換できるようにします:

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) を行った後のアクセス方法は以下の通りです:

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

問題

自分で定義した拡張方法が増えると、アクセスインターフェースが混乱し始めます。例えば:

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機能を持たせることができます。

// ジェネリックコンテナ 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)
    }
}

ネイティブ型の拡張

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

使用

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

例2. URL .queryItems 拡張

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 パターンと組み合わせて操作することもできます:

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 によって変換しました。

GitHub で編集
この記事を改善
本記事は Medium で初公開
オリジナルを読む
この記事をシェア
リンクをコピー · SNS でシェア
ZhgChgLi
著者

ZhgChgLi

An iOS, web, and automation developer from Taiwan 🇹🇼 who also loves sharing, traveling, and writing.

コメント