Swift Package: 異なるモジュールのAssets画像を使う
Swift Packageでマルチモジュール構成にしている際、異なるモジュールの画像リソースを使いたい場合があると思います。例えば、以下の画像のような構成で、ModuleA
にあるMedia.xcassets
の画像をModuleB
で使いたい場合です。このような時、リソースを読み込めるパターンと読み込めないパターンがあるのが分かったのでまとめます。
ローカルパッケージの例
まず、前提として、別モジュールのリソースを取得するにはBundleを指定すれば良いです。その際、モジュールのBundle IdentifierはPackage名-モジュールのTarget名-resources
となるようです。
let bundle = Bundle(identifier: "SamplePackages-ModuleA-resources")
❌リソースを読み込めないパターン
パターン1 Bundleを直接指定するパターン
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で指定するパターン
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で初期化するパターン
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を取得する
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にリソースへのアクセス手段を用意する
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)
}
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
私は基本的にパターン5でリソースを取得していますー!
bundle を元に他のモジュールのリソースを引っ張ったことがあまりなく、タイミングによって
nil
になるのは知らなかったので勉強になりました🙏