🔨

Asset Catalog の type-safe コードを public import アクセスできるようにする Workaround

2025/01/25に公開

Xcode 15から搭載されたAsset Catalogをtype-safeにアクセスできる機能便利ですよね。これでSwiftGenを利用せずに標準ツールを用いた選択肢が提供されました。

まだご存知ない方はぜひWWDCの動画を見てください。
https://developer.apple.com/jp/videos/play/wwdc2023/10165/?time=231
https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes#Asset-Catalogs

しかし、ここでコード生成されるtype-safeにリソースアクセスできるSwiftファイル(GeneratedAssetSymbols.swift)は現状internal修飾子がつけられています。そのため、リソースがあるPackageをImportして外部からアクセスすることができません。これはプロジェクトをマルチモジュールで構成する際に、Asset丸ごと単位の粒度で切り出している人にとって障壁となるでしょう。

1つの選択肢として、画面を構成するFeature内にリソースを配置してinternalにアクセスできる構成に変更するということが検討できます。
本記事ではそれは行わず、生成されたAsset CatalogのコードをpublicにアクセスできるようにするPolyfillを紹介します。

Polyfill 方法

https://github.com/shimastripe/PolyfillGeneratedAssetCatalogExample

ユースケースとして、exampleという画像を外部パッケージからUIKit.UIColorSwiftUI.Colorで今回のtype-safeな書き方でアクセスできるようにしたいです。

let uiColor = UIColor(resource: .example)
let swiftUIColor = Color(.example)

以下のようなPFColorResourceをリソースが置かれたPackageに定義することで、外部パッケージでImportしても、標準APIと同じ書き方のまま参照することができます。

public enum PFColorResource {
    // 継続的にアップデートするのはここ!!
    case example

    var mapping: ColorResource {
        switch self {
        // 継続的にアップデートするのはここ!!
        case .example: .example
    }
}

#if canImport(UIKit)
@available(iOS 11.0, tvOS 11.0, *)
@available(watchOS, unavailable)
public extension UIKit.UIColor {
    convenience init(resource: PFColorResource) {
        #if !os(watchOS)
        self.init(resource: resource.mapping)
        #else
        self.init()
        #endif
    }
}
#endif

#if canImport(SwiftUI)
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension SwiftUI.Color {
    init(_ resource: PFColorResource) {
        self.init(resource.mapping)
    }
}
#endif

Point

  • UIColorColorのextensionでColorResoureと全く同じ関数のインターフェースをPFColorResourceに対して用意する。
    • mappingというinternalプロパティでinternalにしかさわれないColorResourceに変換する。
  • 色の追加・削除・名前変更時に、caseの更新を手動で対応する。
    • これだけがWorkaroundに必要なコスト。
    • caseに記載する名前はGeneratedAssetSymbols.swiftを開いて標準のコード生成からコピーすると楽

GeneratedAssetSymbols.swift を覗く方法

    convenience init(resource: PFColorResource) {
        #if !os(watchOS)
        // ↓ resource 部分をクリックするとコードジャンプできる!
        self.init(resource: resource.mapping)
        #else
        self.init()
        #endif
    }

コードのresource部分を cmd+clickすると簡単にコード生成ファイルにアクセスできます。コメントつけておくと忘れずに済んでいいと思います。

Pros / Cons

  • Asset Catalogに定義された画像やカラーが削除・名前変更されるとビルドが適切に落ちる。
    • type-safeにすることで防ぎたい事故をコンパイラで保証する仕組みは無事担保されている。
  • 標準SDKと同じinit()でリソースにアクセスできる!!
    • 将来公式publicサポートされた際、このワークアラウンドを消すだけで済む。
    • App本体で標準のAPIで先に対応しておいて、後々Packageにリソースを移す際にもこの方法を用いればコード側の変更を最小限に抑えられる(import XXXを足すだけで済むはず)
  • でも、リソースのcase書き出すのがダルいよ
    • GeneratedAssetSymbols.swiftからコピーすると楽だよ!!
    • VSCodeとかでcaseの行をcmd+Dで一括選択して複数行編集するといいよ!!

(マクロでmappingの自動生成とかもできるんじゃないかと思うんですが、それならもうSwiftGenを継続すればいいかなと...)

以上、なるべくライブラリを剥がして標準SDKを活用してプロジェクト構成を改善していきたい人向けのTipsでした。

Discussion