わけわからんタイトルだけど、そのままなので仕方ない。
環境
- Xcode: 13.2.1
- Swift: 5.5.2
背景
iOSDC 2021でこの発表をみて、SwiftPM中心のプロジェクト構成にしようとする人は多いだろう。
fortee.jp
自分もその一人で、その中ではまった問題。
SwiftPM中心のプロジェクト構成にするとMulti Module化がしやすくなるので、機能ごとや画面ごと(プロジェクトの設計によるが)にModule(Package.swiftでいうTargets)を分割していく。
そうすると自然と .xcassets
も分割されていく。上記発表ではBundleに Bundle.module
を指定しましょうという話がされていて、基本的にはそれでうまくいく。しかし、SwiftUIのPreviewを使おうとすると特定条件でPreviewができなくなる。
条件とエラー
下記のコードをまとめたリポジトリはこちら
github.com
こういう 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を利用すると普通に表示できる。
つまり、
.xcassets
を含んだTargetを別のTargetで利用してPreviewする:NG
.xcassets
を含んだTargetをProject内のコードでPreviewする:OK
となっている。
原因
SwiftUI Previewer crashes while in swift package that depends on another's packages Bundle.module reference - Using Swift - Swift Forums
Bundle.module
がPackageのPreview時には生成されずにNot foundでクラッシュするらしい。
対策
このエラーで調べると出てくるけど、自分でBundle.module
に相当するものを書いてあげれば動く。
public let imageBundle = Bundle.myModule
private class CurrentBundleFinder {}
extension Foundation.Bundle {
static var myModule: Bundle = {
let bundleNameIOS = "LocalPackages_TargetName"
let bundleNameMacOs = "PackageName_TargetName"
let candidates = [
Bundle.main.resourceURL,
Bundle(for: CurrentBundleFinder.self).resourceURL,
Bundle.main.bundleURL,
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する場所が別になるので地味に不便。
諦めて全部ビルドしてシミュレーターで確認してしまう。ミニアプリと大体同じ解決方法。
Asset系を1つのTarget(Module)にまとめてしまう
筋が悪い感はすごいが、ワークアラウンドをTarget毎に書かなくても良くなる。ただし、どのAssetがどのTargetで利用されているか分からなくなるので、将来的に負債になることが明白。あまり取りたくない選択肢。
FontやColorだけならまだいいかなぁ……という感覚。
まとめ
実は別の解決方法があるかもしれないので、知っている人がいたら教えてほしい。できれば、Xcode側で解決してほしいなー。