文字っぽいの。

文字を書いています。写真も混ざります。

SwiftPMでAssetsを含んだtargetを別のtargetで呼んでSwiftUI Previewするとクラッシュする。

わけわからんタイトルだけど、そのままなので仕方ない。

環境

  • 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 = {
        /* 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側で解決してほしいなー。