概要
- Swift Package Manager の Preview で Bundle.module にアクセスしてクラッシュした状況と、手元の環境で解決できた方法の話です
環境
- Xcode 13.1
- macOS 11.3.1
何があったのか?
- SwiftPM を使って AppMain と Assets のターゲットを作成しました
- Assets の Resources の中に、Assets.xcassets を配置し、
aka
の Color Set を追加しました -
SwiftGenを利用して、
Asset.aka.color
で追加したaka
にアクセスできるようにしました - AppMain 内の preview で、
Asset.aka.color
を使って View を表示させようとしました
- 上の画像が期待する結果ですが、クラッシュしたと言われました↓
- エラーはこちらです
- Libraries_Assets.bundle にアクセスできなくなっているようです
Assets/resource_bundle_accessor.swift:27: Fatal error: unable to find bundle named Libraries_Assets
- SwiftGen の生成したコードを確認すると、Bundle.module を使って bundle にアクセスしようとしているようです↓(一部抜粋)
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
}()
}
- Bundle.module のパスを確認すると確かに、Libraries_Assets.bundle は存在しないようです
- 正しいパスを指定して、Bundle をロードさせてやればよさそうです
対処法
- Bundle(for: BundleToken.self) を確認した所、2階層上に、Libraries_Assets.bundle を確認しました
- そこで、Preview の場合は、Bundle.module ではなく、パスを指定して Bundle をロードさせることにしました
- NSStringFromClass を使って、モジュール名を取得しました
- Bundle が入っているはずのパスをモジュール名で検索して、Bundle の取得を試みています
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
に差し替えます - パラメータの詳細はこちら
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 になっている事を確認できます
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 はパラメータが細かく指定できるので、簡単に問題を解決することができました、とても便利です