🦔

Swift Package: 異なるモジュールのAssets画像を使う

2023/08/25に公開1

Swift Packageでマルチモジュール構成にしている際、異なるモジュールの画像リソースを使いたい場合があると思います。例えば、以下の画像のような構成で、ModuleAにあるMedia.xcassetsの画像をModuleBで使いたい場合です。このような時、リソースを読み込めるパターンと読み込めないパターンがあるのが分かったのでまとめます。


ローカルパッケージの例

まず、前提として、別モジュールのリソースを取得するにはBundleを指定すれば良いです。その際、モジュールのBundle IdentifierはPackage名-モジュールのTarget名-resourcesとなるようです。

let bundle = Bundle(identifier: "SamplePackages-ModuleA-resources")

❌リソースを読み込めないパターン

パターン1 Bundleを直接指定するパターン

ModuleB/ModuleBView.swift
import ModuleA
import SwiftUI

struct ModuleBView: View {
    var body: some View {
        VStack {
            Image("dot", bundle: Bundle(identifier: "SamplePackages-ModuleA-resources"))
                .frame(width: 32, height: 32)
        }
    }
}

パターン2 BundleをComputed Propertyで指定するパターン

ModuleB/ModuleBView.swift
import ModuleA
import SwiftUI

struct ModuleBView: View {
    var body: some View {
        VStack {
            Image("dot", bundle: moduleABundle)
                .frame(width: 32, height: 32)
        }
    }
    
    var moduleABundle: Bundle? {
        return Bundle(identifier: "SamplePackages-ModuleA-resources")
    }
}

パターン3 Bundleをinitで初期化するパターン

ModuleB/ModuleBView.swift
import ModuleA
import SwiftUI

struct ModuleBView: View {
    let moduleABundle: Bundle?

    init() {
        moduleABundle = Bundle(identifier: "SamplePackages-ModuleA-resources")
    }

    var body: some View {
        VStack {
            Image("dot", bundle: moduleABundle)
                .frame(width: 32, height: 32)
        }
    }
}

これらのパターンでは、Viewの初期化あるいは最初の描画のタイミングでは別モジュールのBundleの用意が間に合っていないことでBundleがnilになってしまうことが問題のようです。

⭕️リソースを読み込めるパターン

パターン4 onAppear()でBundleを取得する

ModuleB/ModuleBView.swift
struct ModuleBView: View {
    @State var moduleABundle: Bundle? = nil

    var body: some View {
        VStack {
            Image("dot", bundle: moduleABundle)
                .frame(width: 32, height: 32)
        }
        .onAppear {
            moduleABundle = Bundle(identifier: "SamplePackages-ModuleA-resources")
        }
    }
}

onAppear()でBundleを取得した場合は、別モジュールのBundleの用意が間に合い、画像リソースを表示できます。ただし、onAppear()より前の最初の描画では間に合わないためコンソールに警告が出力されます。

No image named 'リソース名' found in asset catalog for [Bundleのパス]

パターン5 ModuleAにリソースへのアクセス手段を用意する

ModuleA/ModuleAImage.swift
import SwiftUI

public enum ModuleAImage {
    public static let bundle = Bundle.module

    public static let dotImage = Image("dot", bundle: .module)
    public static let dashImage = Image("dash", bundle: .module)
}
ModuleB/ModuleBView.swift
struct ModuleBView: View {
    var body: some View {
        VStack {
            Image("dot", bundle: ModuleAImage.bundle)
                .frame(width: 32, height: 32)
            ModuleAImage.dashImage
                .frame(width: 32, height: 32)
        }
    }
}

ModuleA側にAssetsリソースへのアクセス手段を準備する手法が一番自然で安定して動きます。コンソールに警告も出力されません。おそらく、ModuleAのインタフェースを経由するようにするとプロセスが切り替わってリソースを取得できるようになるのだと思います。

Discussion

uhooiuhooi

私は基本的にパターン5でリソースを取得していますー!
bundle を元に他のモジュールのリソースを引っ張ったことがあまりなく、タイミングによって nil になるのは知らなかったので勉強になりました🙏