SwiftPMのpreviewでBundle.moduleにアクセスしてクラッシュした話

# 概要

* Swift Package Manager の Preview で Bundle.module にアクセスしてクラッシュした状況と、手元の環境で解決できた方法の話です

## 環境
* Xcode 13.1
* macOS 11.3.1

# 何があったのか？

* SwiftPM を使って AppMain と Assets のターゲットを作成しました
* Assets の Resources の中に、Assets.xcassets を配置し、`aka` の Color Set を追加しました
* [SwiftGen](https://github.com/SwiftGen/SwiftGen)を利用して、`Asset.aka.color` で追加した `aka` にアクセスできるようにしました
* AppMain 内の preview で、`Asset.aka.color` を使って View を表示させようとしました

![スクリーンショット 2021-11-13 22.05.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/5ed099aa-9a0c-893c-b88b-89079a15d924.png)

* 上の画像が期待する結果ですが、クラッシュしたと言われました↓

![スクリーンショット 2021-11-13 22.23.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/6403ba72-8e60-518e-8ebf-8115da25fe53.png)

* エラーはこちらです
* Libraries_Assets.bundle にアクセスできなくなっているようです

> Assets/resource_bundle_accessor.swift:27: Fatal error: unable to find bundle named Libraries_Assets

* SwiftGen の生成したコードを確認すると、Bundle.module を使って bundle にアクセスしようとしているようです↓(一部抜粋)

```swift
public extension ColorAsset.Color {
  @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
  convenience init?(asset: ColorAsset) {
    let bundle = BundleToken.bundle
    #if os(iOS) || os(tvOS)
    self.init(named: asset.name, in: bundle, compatibleWith: nil)
    #elseif os(macOS)
    self.init(named: NSColor.Name(asset.name), bundle: bundle)
    #elseif os(watchOS)
    self.init(named: asset.name)
    #endif
  }
}

private final class BundleToken {
  static let bundle: Bundle = {
    #if SWIFT_PACKAGE
    return Bundle.module
    #else
    return Bundle(for: BundleToken.self)
    #endif
  }()
}
```

https://github.com/SwiftGen/SwiftGen/blob/stable/Sources/TestUtils/Fixtures/Generated/XCAssets/swift5/all.swift

* Bundle.module のパスを確認すると確かに、Libraries_Assets.bundle は存在しないようです
* 正しいパスを指定して、Bundle をロードさせてやればよさそうです

# 対処法
* Bundle(for: BundleToken.self) を確認した所、2階層上に、Libraries_Assets.bundle を確認しました
* そこで、Preview の場合は、Bundle.module ではなく、パスを指定して Bundle をロードさせることにしました
* NSStringFromClass を使って、モジュール名を取得しました
* Bundle が入っているはずのパスをモジュール名で検索して、Bundle の取得を試みています

```swift
internal final class MyBundleToken {
    private static let previewBundlePath: URL = {
        let typeName = type(of: MyBundleToken())
        // "\(ModuleName).MyBundleToken"
        let nsClassString = NSStringFromClass(typeName)
        guard let bundleName = nsClassString.components(separatedBy: ".").first else {
            fatalError("failed get bundle name: \(nsClassString)")
        }

        let searchUrl: URL = Bundle(for: MyBundleToken.self)
            .bundleURL
            .deletingLastPathComponent()
            .deletingLastPathComponent()
        let list: [URL]
        do {
            list = try FileManager.default.contentsOfDirectory(at: searchUrl, includingPropertiesForKeys: nil)
        } catch let error {
            fatalError("failed get contents: \(searchUrl)")
        }
        let firstMatch: URL? = list.first { (url: URL) in
            let name = url.lastPathComponent
            return name.contains("_\(bundleName).bundle")
        }
        guard let res = firstMatch else {
            let jointed = list.map { $0.relativeString }
                .joined(separator: "\n")
            fatalError("no url contains: _\(bundleName).bundle\n\(jointed)")
        }
        return res
    }()
    static let bundle: Bundle = {
        #if SWIFT_PACKAGE
        if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
            return Bundle(url: previewBundlePath)!
        } else {
            return Bundle.module
        }
        #else
        return Bundle(for: MyBundleToken.self)
        #endif
    }()
}

```

* 合わせて swiftgen.yml も更新します
* bundle を先ほど作成した `MyBundleToken.bundle` に差し替えます
* パラメータの詳細は[こちら](https://github.com/SwiftGen/SwiftGen/blob/stable/Documentation/templates/xcassets/swift5.md)

```yaml
xcassets:
  inputs:
    - Modules/Sources/Assets/Resources/Assets.xcassets
  outputs:
    - templateName: swift5
      output: Modules/Sources/Assets/Generated.swift
      params:
        publicAccess: true
        bundle: MyBundleToken.bundle
```

* 再度、SwiftGen でコードを生成すると、確かに MyBundleToken.bundle になっている事を確認できます

```swift
public extension ColorAsset.Color {
  @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
  convenience init?(asset: ColorAsset) {
    let bundle = MyBundleToken.bundle
    #if os(iOS) || os(tvOS)
    self.init(named: asset.name, in: bundle, compatibleWith: nil)
    #elseif os(macOS)
    self.init(named: NSColor.Name(asset.name), bundle: bundle)
    #elseif os(watchOS)
    self.init(named: asset.name)
    #endif
  }
}
```

* 手元の環境ではこれで問題が解決できました

# まとめ
* Swift Package Manager の Preview で Bundle.module にアクセスしてクラッシュした状況と、手元の環境で解決できた方法の話でした
* SwiftGen はパラメータが細かく指定できるので、簡単に問題を解決することができました、とても便利です