修复 SwiftUI Preview 项目中无法运行的问题

第一步:定位问题

页面 Scheme Target Preview是否成功
空白View App 失败
空白View Package 成功
空白View + import Core Package 成功
空白View + import Core + 调用Font Package 失败

定位到时CommonCore的错误

第二步:分析解决问题

分析CommonProductCore依赖没有真正引入。

故拆分Common为四个Product,直接对外暴露,解决了Preview无法运行的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import PackageDescription

let package = Package(
name: "Common",
platforms: [
.iOS(.v14)
],
products: [
.library(
name: "Core",
// type: .dynamic,
targets: ["Core"]
),
],

有两个细节要调整:

  1. 全局Package移除 type: .dynamic,让SPM决定使用什么类型。
  2. Fork ProgressHUD, 移除 type: .static。并在Common Package.swift 中引用fork
1
2
// .package(url: "https://github.com/relatedcode/ProgressHUD.git", from: "13.7.2")
.package(url: "https://github.com/gewill/ProgressHUD.git", branch: "devlop")

为什么改为静态库?

静态库可以避免Core引用的重复的问题。

1
2
3
4
5
6
Showing Recent Messages
Swift package target 'Core' is linked as a static library by 'AppCommoms' and 'Core', but cannot be built dynamically because there is a package product with the same name.

Swift package target 'Core' is linked as a static library by 'Onboarding' and 'Core', but cannot be built dynamically because there is a package product with the same name.

Swift package target 'Core' is linked as a static library by 'AppSDK' and 'Core', but cannot be built dynamically because there is a package product with the same name.

第三步:解决跨Package引用资源文件的问题

SwiftUI Preview 位置特殊处理

由于Preview特殊机制,实际上Bundle(for: BundleFinder.**self**).resourceURL,位置在 /Users/will/Library/Developer/Xcode/DerivedData/App-gvnjpztkrklpjvfegxsksaoxjaaf/Build/Intermediates.noindex/Previews/Onboarding/Products/Debug-iphonesimulator/PackageFrameworks/Onboarding_-570F2A58E471CBF3_PackageProduct.framework

需要往上跳两级目录。故在AppCommomsBundleBundle备选中添加SwiftUI Preview目录即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private class BundleFinder {}

class AppCommomsBundle {
static var module: Bundle = {
// Bundle name should be like this "ProductName_TargetName"
let bundleName = "AppCommoms_AppCommoms"
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: BundleFinder.self).resourceURL,

// SwiftUI Preview
Bundle(for: BundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent(),

// For command-line tools.
Bundle.main.bundleURL
] + Bundle.allBundles.map { $0.bundleURL }

for candidate in candidates {
let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
return bundle
}
}
return Bundle(for: BundleFinder.self)
// fatalError("unable to find bundle named \(bundleName)")
}()
}

使用Bundle.module

还有一种更简单的方案就是用Apple推荐的Bundle.module,也是Rswift使用的public let R = _R(bundle: Bundle.module)

Access a resource in codein page link
Always use Bundle.module when you access resources. A package shouldn’t make assumptions about the exact location of a resource.

具体原因见Xcode Build生成的资源文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import class Foundation.Bundle
import class Foundation.ProcessInfo
import struct Foundation.URL

private class BundleFinder {}

extension Foundation.Bundle {
/// Returns the resource bundle associated with the current Swift module.
static let module: Bundle = {
let bundleName = "Onboarding_Onboarding"

let overrides: [URL]
#if DEBUG
// The 'PACKAGE_RESOURCE_BUNDLE_PATH' name is preferred since the expected value is a path. The
// check for 'PACKAGE_RESOURCE_BUNDLE_URL' will be removed when all clients have switched over.
// This removal is tracked by rdar://107766372.
if let override = ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_PATH"]
?? ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_URL"] {
overrides = [URL(fileURLWithPath: override)]
} else {
overrides = []
}
#else
overrides = []
#endif

let candidates = overrides + [
// 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: BundleFinder.self).resourceURL,

// For command-line tools.
Bundle.main.bundleURL,
]

for candidate in candidates {
let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
return bundle
}
}
fatalError("unable to find bundle named Onboarding_Onboarding")
}()
}

参考文章

  1. Bundling resources with a Swift package
  2. Static Library vs Dynamic Library in iOS
  3. Moya Package.swift
  4. 构建稳定的预览视图 —— SwiftUI 预览的工作原理