[Swift] 優雅なネイティブ型拡張方法
拡張メソッドを独自にラップして、Namespace機能を持たせる

実際の出典は不明で、優秀な同僚のコードから学んだものです。
ネイティブ型の拡張
日常の 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の命名衝突が発生する可能性があります。例えば、二つのライブラリが両方とも UIColor を拡張し、同じく getCMYK() と名付けた場合、問題が生じます。
カスタム拡張 Namespace コンテナ
SwiftのProtocol、計算プロパティ、ジェネリクスの特徴を活用して、拡張メソッドを自分でカプセル化し、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 }
}
}
let url = URL(string: "https://zhgchg.li?a=b&c=d")!
url.zhg.queryParameters // ["c": "d", "a": "b"]
Builder Pattern の統合
また、このラッピング方法を Builder Pattern と組み合わせて操作することもできます:
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によって変換されました。



コメント