わけわからんタイトルだけど、そのままなので仕方ない。
環境
- Xcode: 13.2.1
- Swift: 5.5.2
背景
iOSDC 2021でこの発表をみて、SwiftPM中心のプロジェクト構成にしようとする人は多いだろう。
自分もその一人で、その中ではまった問題。
SwiftPM中心のプロジェクト構成にするとMulti Module化がしやすくなるので、機能ごとや画面ごと(プロジェクトの設計によるが)にModule(Package.swiftでいうTargets)を分割していく。
そうすると自然と .xcassets
も分割されていく。上記発表ではBundleに Bundle.module
を指定しましょうという話がされていて、基本的にはそれでうまくいく。しかし、SwiftUIのPreviewを使おうとすると特定条件でPreviewができなくなる。
条件とエラー
下記のコードをまとめたリポジトリはこちら
こういう Package.swift
構成になっている。
let package = Package( name: "ImageAssetPackageSample", platforms: [.iOS(.v15)], products: [ .library( name: "ImageAssetPackageSample", targets: ["ImageAssetPackageSample"]), ], dependencies: [ ], targets: [ .target( name: "ImageAssetPackageSample", dependencies: ["ImageAsset"]), .target( name: "ImageAsset", dependencies: []) ] )
この中で、ImageAssetが.xcassets
を持っていて、
public struct ImageAsset { public static let blueSquare: UIImage = UIImage(named: "BlueSquare", in: Bundle.module, compatibleWith: nil)! }
というコードで画像が得られるようになっている。ここで、ImageAssetPackageSample
というTargetでSwiftUIのViewを作ってPreviewをすると
unable to find bundle named ImageAssetPackageSample_ImageAsset ---------------------------------------- CrashReportError: XCPreviewAgent crashed (中略) ImageAsset/resource_bundle_accessor.swift:27: Fatal error: unable to find bundle named ImageAssetPackageSample_ImageAsset (後略)
というエラーでクラッシュしてPreviewができない。一方でImageAsset
を依存に含んだImageAssetPackageSample
を利用して、Project側のアプリコードでSwiftUI Previewを利用すると普通に表示できる。
つまり、
となっている。
原因
Bundle.module
がPackageのPreview時には生成されずにNot foundでクラッシュするらしい。
対策
ワークアラウンドなコードを書く
このエラーで調べると出てくるけど、自分でBundle.module
に相当するものを書いてあげれば動く。
public let imageBundle = Bundle.myModule private class CurrentBundleFinder {} extension Foundation.Bundle { static var myModule: Bundle = { /* The name of your local package, prepended by "LocalPackages_" for iOS and "PackageName_" for macOS. You may have same PackageName and TargetName*/ let bundleNameIOS = "LocalPackages_TargetName" let bundleNameMacOs = "PackageName_TargetName" let candidates = [ /* Bundle should be present here when the package is linked into an App. */ Bundle.main.resourceURL, /* Bundle should be present here when the package is linked into a framework. */ Bundle(for: CurrentBundleFinder.self).resourceURL, /* For command-line tools. */ Bundle.main.bundleURL, /* Bundle should be present here when running previews from a different package (this is the path to "…/Debug-iphonesimulator/"). */ Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent(), Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent(), ] for candidate in candidates { let bundlePathiOS = candidate?.appendingPathComponent(bundleNameIOS + ".bundle") let bundlePathMacOS = candidate?.appendingPathComponent(bundleNameMacOs + ".bundle") if let bundle = bundlePathiOS.flatMap(Bundle.init(url:)) { return bundle } else if let bundle = bundlePathMacOS.flatMap(Bundle.init(url:)) { return bundle } } fatalError("unable to find bundle") }() }
Providing XCAssets Folder in Swift… | Apple Developer Forums
ただし、Multi Module化して、細かく画面を分けてTargetが分割されると、無限にこのコードを書いていく事になって不毛感がすごい。
ミニアプリを作る
Target内でPreviewせずに、Project側で参照してPreviewすることはできるので、Module毎にPreview用のミニアプリを作っていく。SwiftPM中心のプロジェクト構成にしていれば、Projectを増やすことが容易なのでわりと許容できる対応な気もするが、コードを書いている場所とPreviewする場所が別になるので地味に不便。
SwiftUI Previewを使わない
諦めて全部ビルドしてシミュレーターで確認してしまう。ミニアプリと大体同じ解決方法。
Asset系を1つのTarget(Module)にまとめてしまう
筋が悪い感はすごいが、ワークアラウンドをTarget毎に書かなくても良くなる。ただし、どのAssetがどのTargetで利用されているか分からなくなるので、将来的に負債になることが明白。あまり取りたくない選択肢。
FontやColorだけならまだいいかなぁ……という感覚。
まとめ
実は別の解決方法があるかもしれないので、知っている人がいたら教えてほしい。できれば、Xcode側で解決してほしいなー。