1 安装

1.1 使用命令行

1
2
3
4
5
$ git clone https://github.com/giginet/Scipio.git
$ cd Scipio
$ swift run -c release scipio --help
# Add reference .build/release/scipio to the PATH variable.
$ export PATH=/path/to/scipio:$PATH

1.2 作为Package使用

推荐使用这种方式,较少命令行中参数,在Swift代码中EntryPoint配置方便。

2 准备您应用程序的所有依赖项

2.1 创建一个新的Swift包来描述依赖关系

1
2
3
$ mkdir MyAppDependencies
$ cd MyAppDependencies
$ swift package init

2.2 编辑 Package.swift 以描述应用程序的依赖关系下一步

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
// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "MyAppDependencies",
platforms: [
// Specify platforms to build
.iOS(.v14),
],
products: [],
dependencies: [
// Add dependencies
.package(url: "https://github.com/onevcat/APNGKit.git", exact: "2.2.1"),
],
targets: [
.target(
name: "MyAppDependency",
dependencies: [
// List all dependencies to build
.product(name: "APNGKit", package: "APNGKit"),
]),
]
)

3 手动Rswift generate到项目中

如何找到R文件:R.generated.swift

任意找到一个_R,点击定义,FileShow in Finder

4a 命令行打包

1
2
3
4
5
6
7
8
$ scipio prepare path/to/MyAppDependencies
> 🔁 Resolving Dependencies...
> 🗑️ Cleaning MyAppDependencies...
> 📦 Building APNGKit for iOS
> 🚀 Combining into XCFramework...
> 📦 Building Delegate for iOS
> 🚀 Combining into XCFramework...
> ❇️ Succeeded.

4b 自定义打包配置

4b.1 创建可执行包

1
2
3
4
5
6
7
8
$ mkdir my-build-tool
$ cd my-build-tool
$ swift package init --type executable
Creating executable package: my-build-tool
Creating Package.swift
Creating .gitignore
Creating Sources/
Creating Sources/main.swift

4b.2 编辑Package

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
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "my-build-tool",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(
url: "https://github.com/giginet/Scipio.git",
revision: "0.15.0" // Use the latest version
),
],
targets: [
.executableTarget(
name: "my-build-tool",
dependencies: [
.product(name: "ScipioKit", package: "Scipio"),
],
path: "Sources"
),
]
)

4b.3 实现构建脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Foundation
import ScipioKit

@main
struct EntryPoint {
private static let myPackageDirectory = URL(fileURLWithPath: "/path/to/MyPackage")

static func main() async throws {
let runner = Runner(
mode: .prepareDependencies,
options: .init(
baseBuildOptions: .init(
buildConfiguration: .release,
isSimulatorSupported: true
)
)
)

try await runner.run(
packageDirectory: myPackageDirectory,
frameworkOutputDir: .default
)
}
}

4b.4 命令行打包

1
$ swift run -c release my-build-tool

引用

  1. https://github.com/giginet/Scipio.git
  2. https://github.com/mac-cain13/R.swift/blob/main/Plugins/RswiftGeneratePublicResources/RswiftGeneratePublicResources.swift

示例代码🔗https://github.com/gewill/BlogCodes/tree/main/Localizable%20in%20SwiftPM

在处理SwiftPM中本地化时,尝试了几种方案。先说结论Rswift preferredLanguage方案最佳。

方案一:Local

在SwiftUI中使用local可行,但是在SwiftPM会被宿主应用中覆写。不过也是小问题,只要命名规范,按照模块页面功能前缀来的话,一般也不会出现key重复的问题。

这里也是用到了Rswift自动生成的key,避免复制粘贴字符串类型的key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SwiftUI view			
Section {
Text("Change locale").font(.title)
Text("Will be overwrite by host app!").foregroundColor(.pink)
Button(action: {
viewModel.locale = Locale(identifier: Language.en.rawValue)
}, label: {
Text("Change locale english")
})
Button(action: {
viewModel.locale = Locale(identifier: Language.zh_Hans.rawValue)
}, label: {
Text("Change locale chinese simplified")
})
Text(LocalizedStringKey(R.string.localizable.hello_world.key.description))
} header: {
Text("Change locale")
}
.environment(\.locale, viewModel.locale)

方案二:Rswift preferredLanguage

目前是比较完善的方案。配合 AppLocale 可以全局切换语言。

利用Rswift可处理key和bundle的问题,还优化了SwiftUI.Text的使用体验,直接使用init即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class AppLocale {
let preferredLanguage = CurrentValueSubject<Language, Never>(.en)
var preferredString: _R.string {
R.string(preferredLanguages: [preferredLanguage.value.rawValue])
}

static var shared = AppLocale()
private init() {}
}

// SwiftUI view
Section {
Text("Preferred Languages \(viewModel.preferredLanguage.displayTitle)")
Picker("Preferred Languages", selection: $viewModel.preferredLanguage) {
ForEach(Language.allCases) {
Text($0.displayTitle)
}
}
.pickerStyle(.segmented)
Text(AppLocale.shared.preferredString.localizable.hello_world)
} header: {
Text("Change R.string Preferred Languages")
}

最轻量级集成方式在ViewModel订阅AppLocale.shared.preferredLanguage,更新self.objectWillChange.send(),即可响应语言切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class HomeViewModel: ObservableObject {
init() {
AppLocale.shared.preferredLanguage
.removeDuplicates()
.sink(receiveValue: { _ in
guard let self else { return }
self.objectWillChange.send()
})
.store(in: &cancelables)
}
}

struc HomeView: View {
@StateObject var viewModel = HomeViewModel()

var body: some View {
Text(AppLocale.shared.preferredString.localizable.hello_world)
}
}

方案三:liamnichols / xcstrings-tool

可用,但是仅支持 iOS16+。有个小坑SwiftPM集成时,官方教程的有错误,正确的git地址为:

1
2
3
4
// 1. Add the xcstrings-tool Package dependency
.package(url: "https://github.com/liamnichols/xcstrings-tool.git", from: "0.1.0")
// 2. Or use the repo is essentially a mirror of the main repository however the xcstrings-tool command line interface is a binary dependency that significantly simplifies your build graph and improves compile times.
.package(url: "https://github.com/liamnichols/xcstrings-tool-plugin.git", from: "0.1.0")

具体参考官方的示例:https://github.com/liamnichols/xcstrings-tool-demo

serverTrustEvaluationFailed 错误

1
2
3
4
5
6
7
8
Printing description of error:
▿ AFError
▿ serverTrustEvaluationFailed : 1 element
▿ reason : ServerTrustFailureReason
▿ noRequiredEvaluator : 1 element
- host : "***"
(lldb) po error.debugDescription
"Server trust evaluation failed due to reason: A ServerTrustEvaluating value is required for host *** but none was found."

原因是配置了白名单

1
2
3
4
5
6
7
8
9
10
11
var evaluators: [String: ServerTrustEvaluating] = [:]

let evaluators: [String: ServerTrustEvaluating] = [
"*.yourdomain.com": PinnedCertificatesTrustEvaluator()
]
// 白名单 allHostsMustBeEvaluated: true
let serverTrust = ServerTrustManager(allHostsMustBeEvaluated: true,
evaluators: evaluators)

session = Alamofire.Session(configuration: configuration,
serverTrustManager: serverTrust)

可把 allHostsMustBeEvaluated 改为 false,只对指定的host开启。

原理 TLS Server Trust

https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#evaluating-server-trusts-with-servertrustmanager-and-servertrustevaluating

在与服务器和 Web 服务通信时使用安全的 HTTPS 连接是保护敏感数据的重要步骤。默认情况下,Alamofire 会收到与 URLSession 相同的自动 TLS 证书和证书链验证。虽然这保证了证书链的有效性,但它并不能防止中间人 (MITM) 攻击或其他潜在漏洞。为了减轻中间人攻击,处理敏感客户数据或财务信息的应用程序应使用 Alamofire ServerTrustEvaluating 协议提供的证书或公钥固定。

使用 ServerTrustManager 和 ServerTrustEvaluating 评估服务器信任

该协议 ServerTrustEvaluating 提供了一种执行任何类型的服务器信任评估的方法。它只有一个要求:

1
func evaluate(_ trust: SecTrust, forHost host: String) throws

此方法提供从基础 URLSession 接收 SecTrust 的值和主机 String ,并提供执行各种评估的机会。

包括许多不同类型的信任评估器,提供对评估过程的可组合控制:

  1. DefaultTrustEvaluator :使用默认服务器信任评估,同时允许您控制是否验证质询提供的主机。
  2. RevocationTrustEvaluator :检查收到的证书的状态,以确保其未被吊销。由于它需要网络请求开销,因此通常不会对每个请求执行此操作。
  3. PinnedCertificatesTrustEvaluator :使用提供的证书来验证服务器信任。如果其中一个固定的证书与其中一个服务器证书匹配,则认为服务器信任有效。此赋值器还可以接受自签名证书。
  4. PublicKeysTrustEvaluator :使用提供的公钥来验证服务器信任。如果其中一个固定的公钥与其中一个服务器证书公钥匹配,则认为服务器信任有效。
  5. CompositeTrustEvaluator :计算值数组,仅当所有 ServerTrustEvaluating 值都成功时才成功。例如,此类型可用于组合 RevocationTrustEvaluator 和 PinnedCertificatesTrustEvaluator 。
  6. DisabledTrustEvaluator :此评估程序应仅在调试方案中使用,因为它会禁用所有评估,而这些评估将始终将任何服务器信任视为有效。此评估器绝不应在生产环境中使用!

ServerTrustManager

负责 ServerTrustManager 存储值到特定主机的 ServerTrustEvaluating 内部映射。这允许 Alamofire 使用不同的评估器评估每个主机。

1
2
3
4
5
6
7
8
let evaluators: [String: ServerTrustEvaluating] = [
// 默认情况下,应用程序捆绑包中包含的证书会自动固定。
"cert.example.com": PinnedCertificatesTrustEvaluator(),
// 默认情况下,会自动使用应用程序包中包含的证书中的公钥。
"keys.example.com": PublicKeysTrustEvaluator(),
]

let manager = ServerTrustManager(evaluators: evaluators)

这将 ServerTrustManager 具有以下行为:

  1. cert.example.com 将始终使用启用默认和主机验证的证书固定,因此需要满足以下条件才能使 TLS 握手成功:
    1. 证书链必须有效。
    2. 证书链必须包含其中一个固定的证书。
    3. 质询主机必须与证书链的叶证书中的主机匹配。
  2. keys.example.com 将始终使用启用默认和主机验证的公钥固定,因此需要满足以下条件才能使 TLS 握手成功:
    1. 证书链必须有效。
    2. 证书链必须包含其中一个固定的公钥。
    3. 质询主机必须与证书链的叶证书中的主机匹配。
  3. 对其他主机的请求将产生错误,因为 ServerTrustManager 默认情况下需要评估所有主机。

测试用例

Alamofire 项目中测试用例:Tests/TLSEvaluationTests.swift

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
47
48
49
50
51
private enum TestCertificates {
static let rootCA = TestCertificates.certificate(filename: "expired.badssl.com-root-ca")
static let intermediateCA1 = TestCertificates.certificate(filename: "expired.badssl.com-intermediate-ca-1")
static let intermediateCA2 = TestCertificates.certificate(filename: "expired.badssl.com-intermediate-ca-2")
static let leaf = TestCertificates.certificate(filename: "expired.badssl.com-leaf")

// 从给定的文件名创建证书:SecCertificate对象。
static func certificate(filename: String) -> SecCertificate {
let filePath = Bundle.test.path(forResource: filename, ofType: "cer")!
let data = try! Data(contentsOf: URL(fileURLWithPath: filePath))
let certificate = SecCertificateCreateWithData(nil, data as CFData)!

return certificate
}
}

func testThatExpiredCertificateRequestFailsWhenPinningLeafPublicKeyWithCertificateChainValidation() {
// Given
// 这里直接从证书提取公钥
let keys = [TestCertificates.leaf].af.publicKeys
let evaluators = [expiredHost: PublicKeysTrustEvaluator(keys: keys)]

let manager = Session(configuration: configuration,
serverTrustManager: ServerTrustManager(evaluators: evaluators))

let expectation = expectation(description: "\(expiredURLString)")
var error: AFError?

// When
manager.request(expiredURLString)
.response { resp in
error = resp.error
expectation.fulfill()
}

waitForExpectations(timeout: timeout)

// Then
XCTAssertNotNil(error, "error should not be nil")
XCTAssertEqual(error?.isServerTrustEvaluationError, true)

if case let .serverTrustEvaluationFailed(reason)? = error {
if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) {
XCTAssertTrue(reason.isTrustEvaluationFailed, "should be .trustEvaluationFailed")
} else {
XCTAssertTrue(reason.isDefaultEvaluationFailed, "should be .defaultEvaluationFailed")
}
} else {
XCTFail("error should be .serverTrustEvaluationFailed")
}
}

引子

onevcat on Twitter / X

大家的 iOS 项目都是怎么做 CI 的?Xcode Cloud 不够用啊(太贵了)

看你的提交频率了…我自己的话低配就够了。另外就是找一款合适的虚拟机,尽量别自己折腾环境和升级啥的 挺烦的…我是tart+GitHub Action 还挺满意

Xcode 的 Test 结果可以直接显示在 GitHub 里吗?如果再能把截图都同步过去就完美了。

https://github.com/kishikawakatsumi/xcresulttool

Fastlane

不知道有没有帮助,可以参考下,

如何用极狐GitLab 为 iOS App 创建自动化CI/CD?详细教程来了

💡 GitLab Actions + Fastlane,GitLab Actions仅为触发器。

Tart

https://github.com/cirruslabs/tart

直接下载

下载带Xcode的完整镜像

1
2
3
brew install cirruslabs/cli/tart
tart clone ghcr.io/cirruslabs/macos-sonoma-xcode:latest sonoma-xcode
tart run sonoma-xcode

从头开始创建 macOS 虚拟机映像

Tart 可以从 *.ipsw 文件创建虚拟机。你可以在这里下载特定的 *.ipsw 文件,也可以使用 latest 代替 *.ipsw 的路径来下载最新的可用版本:

1
2
tart create --from-ipsw=latest sonoma-vanilla
tart run sonoma-vanilla

自托管 CI

创建 .cirrus.yml 文件

1
2
3
4
5
6
7
8
9
10
11
task:
name: hello
macos_instance:
# can be a remote or a local virtual machine
# image: ghcr.io/cirruslabs/macos-sonoma-base:latest
image: sonoma-xcode
hello_script:
- echo "Hello from within a Tart VM!"
- echo "Here is my CPU info:"
- sysctl -n machdep.cpu.brand_string
- sleep 15

运行该task

将上述 .cirrus.yml 文件放入存储库的根目录中,并使用以下命令运行它:

1
2
brew install cirruslabs/cli/cirrus
cirrus run

从 Tart VM 中检索工件

在许多情况下,需要从 Tart 虚拟机中检索特定文件或文件夹。例如,以下 .cirrus.yml 配置定义了一个任务,该任务构建二进制 tart 文件并通过 artifacts 指令公开它:

1
2
3
4
5
6
7
task:
name: Build
macos_instance:
image: ghcr.io/cirruslabs/macos-sonoma-xcode:latest
build_script: swift build --product tart
binary_artifacts:
path: .build/debug/tart

运行 Cirrus CLI 时,将 --artifacts-dir 定义 artifacts 写入主机上提供的本地目录:

1
cirrus run --artifacts-dir artifacts

请注意,所有检索到的工件都将以关联的任务名称和 artifacts 指令名称为前缀。对于上面的示例, tart 二进制文件将保存到 $PWD/artifacts/Build/binary/.build/debug/tart .

整合方案

第一步:GitHub Actions

GitHub Actions 触发 cirrus run

1
2
3
4
5
6
7
8
9
10
11
12
name: learn-github-actions
run-name: ${{ github.actor }} is learning GitHub Actions
on: [push]
jobs:
check-bats-version:
runs-on: self-hosted
steps:
- run: cd ~/git/ci
- run: git stash
- run: git checkout main
- run: git pull
- run: cirrus run --artifacts-dir artifacts

第二步:cirrus

这一步使用tart来管理并隔离运行环境。

cirrustart虚拟机中执行脚本: xcodebuild 打包或者测试。fastlane最好。

1
2
3
4
5
6
7
8
9
10
11
12
13
task:
name: xcode test
macos_instance:
# can be a remote or a local virtual machine
# image: ghcr.io/cirruslabs/macos-sonoma-base:latest
image: sonoma-xcode
hello_script:
- echo "Hello from within a Tart VM!"
- echo "Here is my CPU info:"
- sysctl -n machdep.cpu.brand_string
build_script: xcodebuild -scheme DemoApp -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.2' clean test
binary_artifacts:
path: .build/debug/tart

GitHub Actions 部分log

1
2
3
4
5
6
7
8
9
10
Run cirrus run
'xcode test' task
pull virtual machine
clone virtual machine
boot virtual machine
syncing working directory
'hello' script
'build' script
'binary' artifacts
'xcode test' task succeeded in 03:17!

第三步:Fastlane

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
47
48
49
50
51
default_platform(:ios)

ipa_dir = "fastlane_build/"
ipa_name = "AppName" + Time.new.strftime("%Y-%m-%d_%H:%M:%S")

commit = last_git_commit
message = commit[:message]
short_hash = commit[:abbreviated_commit_hash]

# 更新内容
changelog = %(by Will
git branch: #{git_branch}
git short_hash: #{short_hash}
git message: #{message}
)

before_all do
app_store_connect_api_key(
key_id: "",
issuer_id: "",
key_filepath: "",
duration: 1200,
in_house: false,
)
end

platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
increment_build_number(
build_number: latest_testflight_build_number + 1,
)
build_app(
scheme: "AppName",
# Debug、Release
configuration: "Release",
clean: true,
# 导出方式 app-store、ad-hoc、enterprise、development
export_method: "app-store",
export_xcargs: "-allowProvisioningUpdates", # enable automatic signing
# ipa的存放目录
output_directory: ipa_dir,
# 输出ipa的文件名为当前的build号
output_name: ipa_name,
)
upload_to_testflight(
skip_waiting_for_build_processing: true,
changelog: changelog,
)
end
end

总结

总的流程就是

  1. GitHub Actions触发
  2. cirrus管理Tart
  3. Fastlane执行打包或者测试

相比Jenkins,全局都是配置文件,更干净简洁,没有繁琐的UI。当然也稍微增加了门槛,上手就要求熟悉配置文件。

Introducing

Open Chinese Convert (OpenCC, 開放中文轉換) is an opensource project for conversions between Traditional Chinese, Simplified Chinese and Japanese Kanji (Shinjitai). It supports character-level and phrase-level conversion, character variant conversion and regional idioms among Mainland China, Taiwan and Hong Kong. This is not translation tool between Mandarin and Cantonese, etc.

Features

Strictly differentiate between 「one simplified to many traditionals」 and 「one simplified to many variants」.
Completely compatible with different variants and can realize dynamic substitution.
Strictly scrutinize one-simplified-to-multiple-traditional entries, and the principle is 「if it can be divided, then it will be divided」.
Support Mainland China, Taiwan, Hong Kong, different variants and regional customary word conversion, such as 「裏」「裡」、「鼠標」「滑鼠」.

If you have any questions or suggestions, you can contact them through Email.

Download OpenCCman on the App Store

Privacy policy

This App does not collect or upload any private information.

介绍

中文简繁转换开源项目,支持词汇级别的转换、异体字转换和地区习惯用词转换(中国大陆、台湾、香港、日本新字体)。不提供普通话与粤语的转换。

特点

严格区分「一简对多繁」和「一简对多异」。
完全兼容异体字,可以实现动态替换。
严格审校一简对多繁词条,原则为「能分则不合」。
支持中国大陆、台湾、香港异体字和地区习惯用词转换,如「裏」「裡」、「鼠標」「滑鼠」。

应用使用有任何问题或建议,欢迎邮件联系:531sunlight@gmail.com

App Store下载 OpenCCman

隐私政策

本App不进行任何隐私信息收集或上传。

介紹

中文簡繁轉換開源項目,支持詞彙級別的轉換、異體字轉換和地區習慣用詞轉換(中國大陸、臺灣、香港、日本新字體)。不提供普通話與粵語的轉換。

特點

嚴格區分「一簡對多繁」和「一簡對多異」。
完全兼容異體字,可以實現動態替換。
嚴格審校一簡對多繁詞條,原則爲「能分則不合」。
支持中國大陸、臺灣、香港異體字和地區習慣用詞轉換,如「裏」「裡」、「鼠標」「滑鼠」。

應用使用有任何問題或建議,歡迎郵件聯繫:531sunlight@gmail.com

App Store下載 OpenCCman

隱私政策

本App不進行任何隱私信息收集或上傳。

第一步:定位问题

页面 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 预览的工作原理

介绍

Preview Chinese light

ConnectUI 是专为应用程序开发人员和发布者构建的强大销售分析和报告平台。

借助 ConnectUI,您可以轻松跟踪和分析您的应用销售、收入、下载和更新。

应用使用有任何问题或建议,欢迎邮件联系:531sunlight@gmail.com

App Store下载 ConnectUI

隐私政策

本App不进行任何隐私信息收集或上传。

Introducing

Preview English light

ConnectUI is a powerful sales analytics and reporting platform built specifically for app developers and publishers.

With ConnectUI, you can easily track and analyze your app sales, revenues, downloads, and updates.

For any questions or suggestions, please feel free to contact us via email at 531sunlight@gmail.com.

Download ConnectUI on the App Store

Privacy policy

This App does not collect or upload any private information.

0%