一、开发iOS18的Controls控制组件
在 iOS18 中,你可以将应用的控件扩展到系统级别,使其出现在控制中心、锁定屏幕等位置。本文将详细介绍如何使用 WidgetKit 构建和定制控件,并让控件支持配置,最终将其添加到系统界面中。
1.1前期准备
在开始之前,确保你已经在 Xcode 中创建了一个 iOS 项目,并安装了最新版本的 Xcode。你还需要一些基本的 Swift 和 SwiftUI 知识。
1.2将控件添加到 Widget Bundle
1.2.1什么是 Widget Bundle?
Widget Bundle 是一种容器,允许你将多个控件组合在一起。通过将控件添加到 Widget Bundle,你可以更好地组织和管理这些控件,并方便地在应用中启用或禁用它们。
1.2.2创建一个 Widget Bundle
我们需要定义一个 WidgetBundle 来包含我们的控件。以下是具体的代码示例:
@main
struct ProductivityExtensionBundle: WidgetBundle {
var body: some Widget {
ChecklistWidget() // 添加清单控件
TaskCounterWidget() // 添加任务计数控件
TimerToggle() // 添加定时器切换控件
}
}
- @main 标记:表示这是应用的入口点。它将这个 WidgetBundle 注册为应用的一部分。
- ProductivityExtensionBundle:这是我们定义的 WidgetBundle 名称。
- var body: some Widget:这个属性表示该 WidgetBundle 中包含的控件列表。你可以在这里添加任意数量的控件。
1.2.3 控件的添加
在 body 中,我们添加了三个控件:
- ChecklistWidget:一个清单控件,用于显示任务列表。
- TaskCounterWidget:一个任务计数控件,用于显示当前任务的数量。
- TimerToggle:一个定时器切换控件,用于控制定时器的启动和停止。
1.3构建控件
1.3.1创建控件的基础结构
我们将使用 ControlWidget 协议来定义一个基础控件。这里我们以定时器切换控件为例进行说明。
struct TimerToggle: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "com.apple.Productivity.TimerToggle"
) {
ControlWidgetToggle(
"Work Timer",
isOn: TimerManager.shared.isRunning,
action: ToggleTimerIntent()
) { isOn in
Image(systemName: isOn
? "hourglass"
: "hourglass.bottomhalf.filled")
}
}
}
}
- struct TimerToggle: ControlWidget:定义了一个新的结构体 TimerToggle,它实现了 ControlWidget 协议。
- StaticControlConfiguration:这是一个静态配置,用于定义控件的外观和行为。
kind:指定控件的唯一标识符,这里使用了 "com.apple.Productivity.TimerToggle"。
ControlWidgetToggle:定义了一个切换控件,使用 isOn 属性表示当前的状态。
isOn:绑定到 TimerManager.shared.isRunning,表示定时器是否在运行。
action:指定了一个 ToggleTimerIntent 操作,当用户点击控件时触发该操作。
{ isOn in ... }:这是一个闭包,用于根据 isOn 的值动态更新控件的图标。
1.3.2控件的工作原理
这个控件的核心是一个切换按钮。根据定时器的运行状态(isOn),显示不同的图标(如沙漏或空心沙漏),并执行相应的操作(启动或停止定时器)。
1.3.3控件配置的关键点
- StaticControlConfiguration:静态配置,用于定义控件的结构。
- ControlWidgetToggle:切换控件,具有 isOn 属性和操作意图 action。
通过这种方式,你可以创建一个简单而功能强大的控件,并根据需要自定义其外观和行为。
1.4自定义控件外观
1.4.1指定不同的符号
为了增强用户体验,我们可以根据定时器的状态显示不同的符号(例如:定时器运行时显示沙漏,停止时显示半沙漏)。
struct TimerToggle: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "com.apple.Productivity.TimerToggle"
) {
ControlWidgetToggle(
"Work Timer",
isOn: TimerManager.shared.isRunning,
action: ToggleTimerIntent()
) { isOn in
Image(systemName: isOn
? "hourglass"
: "hourglass.bottomhalf.filled")
}
}
}
}
- Image(systemName:):这是一个 SwiftUI中的图像控件,用于显示系统图标。
isOn:根据 isOn 的值选择不同的图标。
systemName: isOn ? "hourglass" : "hourglass.bottomhalf.filled":如果定时器在运行,显示沙漏图标;否则显示半沙漏图标。
1.4.2添加自定义文本和颜色
你还可以为控件添加自定义文本和颜色,以提供更好的视觉效果和用户提示。
struct TimerToggle: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "com.apple.Productivity.TimerToggle"
) {
ControlWidgetToggle(
"Work Timer",
isOn: TimerManager.shared.isRunning,
action: ToggleTimerIntent()
) { isOn in
Label(isOn ? "Running" : "Stopped",
systemImage: isOn
? "hourglass"
: "hourglass.bottomhalf.filled")
}
.tint(.purple)
}
}
}
- Label:这是一个带有文本和图标的控件,用于显示控件的状态。
isOn ? "Running" : "Stopped":根据 isOn 的值选择显示文本,运行时显示“Running”,停止时显示“Stopped”。
systemImage:根据 isOn 的值选择不同的图标。 - .tint(.purple):设置控件的主题色为紫色,使其更具视觉吸引力。
通过这些自定义选项,你可以创建一个更加丰富和直观的控件。
1,5实现控件功能
1.5.1定义定时器切换逻辑
我们需要实现一个操作意图(Intent),用于处理定时器的启动和停止操作。
struct ToggleTimerIntent: SetValueIntent, LiveActivityIntent {
static let title: LocalizedStringResource = "Productivity Timer"
@Parameter(title: "Running")
var value: Bool // 定时器的运行状态
func perform() throws -> some IntentResult {
TimerManager.shared.setTimerRunning(value)
return .result()
}
}
- struct ToggleTimerIntent: SetValueIntent, LiveActivityIntent:定义了一个新的结构体 ToggleTimerIntent,它实现了 SetValueIntent 和 LiveActivityIntent 协议。
- @Parameter:这是一个参数注解,用于定义意图中的参数。
title: "Running":参数的标题。
var value: Bool:一个布尔值,表示定时器的运行状态。 - func perform() throws -> some IntentResult:定义了执行意图时的操作。
TimerManager.shared.setTimerRunning(value):调用 TimerManager 来设置定时器的运行状态。
return .result():返回一个结果,表示意图执行成功。
这个意图允许用户通过控件启动或停止定时器,并将状态更新到控件中。
1.5.2从应用内部刷新控件
当定时器状态发生变化时,我们需要刷新控件的显示,以确保其显示的状态与实际一致。
func timerManager(_ manager: TimerManager,
timerDidChange timer: ProductivityTimer) {
ControlCenter.shared.reloadControls(
ofKind: "com.apple.Productivity.TimerToggle"
)
}
- func timerManager(_ manager: TimerManager, timerDidChange timer: ProductivityTimer):这是一个回调函数,当定时器状态发生变化时调用。
- ControlCenter.shared.reloadControls(ofKind: "com.apple.Productivity.TimerToggle"):调用 ControlCenter 来刷新指定类型的控件,这里是 TimerToggle。
通过这种方式,你可以确保控件的显示始终与定时器的实际状态保持一致。
1.6值提供者与异步数据获取
1.6.1定义值提供者
为了提高控件的灵活性和响应性,我们可以使用值提供者(Value Provider)来动态获取控件的状态。
struct TimerValueProvider: ControlValueProvider {
func currentValue() async throws -> Bool {
try await TimerManager.shared.fetchRunningState()
}
let previewValue: Bool = false
}
- struct TimerValueProvider: ControlValueProvider:定义了一个新的结构体 TimerValueProvider,它实现了 ControlValueProvider 协议。
- func currentValue() async throws -> Bool:这是一个异步函数,用于获取当前的定时器状态。
try await TimerManager.shared.fetchRunningState():调用 TimerManager 的异步方法,获取定时器的运行状态。 - let previewValue: Bool = false:定义了一个预览值,当无法获取实际值时使用。
1.6.2异步获取状态
我们可以将值提供者集成到控件中,以实现异步数据获取。
struct TimerToggle: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "com.apple.Productivity.TimerToggle",
provider: TimerValueProvider()
) { isRunning in
ControlWidgetToggle(
"Work Timer",
isOn: isRunning,
action: ToggleTimerIntent()
) { isOn in
Label(isOn ? "Running" : "Stopped",
systemImage: isOn
? "hourglass"
: "hourglass.bottomhalf.filled")
}
.tint(.purple)
}
}
}
- provider: TimerValueProvider():使用我们定义的 TimerValueProvider 作为值提供者。
- { isRunning in ... }:这是一个闭包,当获取到定时器状态时执行,其中 isRunning 表示当前的定时器状态。
通过这种方式,你可以使控件动态响应定时器状态的变化,提供更好的用户体验。
1.7使控件可配置
1.7.1定义可配置的值提供者
我们可以进一步扩展值提供者,使其支持配置不同的定时器。
struct ConfigurableTimerValueProvider: AppIntentControlValueProvider {
func currentValue(configuration: SelectTimerIntent) async throws -> TimerState {
let timer = configuration.timer
let isRunning = try await TimerManager.shared.fetchTimerRunning(timer: timer)
return TimerState(timer: timer, isRunning: isRunning)
}
func previewValue(configuration: SelectTimerIntent) -> TimerState {
return TimerState(timer: configuration.timer, isRunning: false)
}
}
- struct ConfigurableTimerValueProvider: AppIntentControlValueProvider:定义了一个可配置的值提供者,实现了 AppIntentControlValueProvider 协议。
- func currentValue(configuration: SelectTimerIntent) async throws -> TimerState:这是一个异步函数,根据配置获取当前的定时器状态。
let timer = configuration.timer:获取配置中的定时器。
let isRunning = try await TimerManager.shared.fetchTimerRunning(timer: timer):获取指定定时器的运行状态。 - func previewValue(configuration: SelectTimerIntent) -> TimerState:定义了一个预览值,用于在无法获取实际值时使用。
1.7.2实现可配置的定时器控件
我们可以使用可配置的值提供者创建一个更加灵活的定时器控件。
struct TimerToggle: ControlWidget {
var body: some ControlWidgetConfiguration {
AppIntentControlConfiguration(
kind: "com.apple.Productivity.TimerToggle",
provider: ConfigurableTimerValueProvider()
) { timerState in
ControlWidgetToggle(
timerState.timer.name,
isOn: timerState.isRunning,
action: ToggleTimerIntent(timer: timerState.timer)
) { isOn in
Label(isOn ? "Running" : "Stopped",
systemImage: isOn
? "hourglass"
: "hourglass.bottomhalf.filled")
}
.tint(.purple)
}
}
}
- AppIntentControlConfiguration:这是一个支持应用意图的控件配置,允许根据用户配置动态更新控件。
- { timerState in ... }:这是一个闭包,根据获取到的定时器状态(timerState)来更新控件。
通过这种方式,你可以创建一个更加灵活和强大的控件,允许用户根据需要配置不同的定时器。
1.8自动提示用户配置
1.8.1自动提示用户配置
为了提高用户体验,我们可以自动提示用户对控件进行配置。
struct SomeControl: ControlWidget {
var body: some ControlWidgetConfiguration {
AppIntentControlConfiguration(
// 配置项
)
.promptsForUserConfiguration()
}
}
- .promptsForUserConfiguration():这是一个方法,调用它可以在控件首次使用时自动提示用户进行配置。
通过这种方式,你可以让用户在使用控件时更加方便地进行配置,提升应用的易用性。
1.9添加控件提示与描述
1.9.1添加操作提示和描述
为了让用户更好地理解控件的功能,我们可以为控件添加操作提示和描述。
struct TimerToggle: ControlWidget {
var body: some ControlWidgetConfiguration {
AppIntentControlConfiguration(
kind: "com.apple.Productivity.TimerToggle",
provider: ConfigurableTimerValueProvider()
) { timerState in
ControlWidgetToggle(
timerState.timer.name,
isOn: timerState.isRunning,
action: ToggleTimerIntent(timer: timerState.timer)
) { isOn in
Label(isOn ? "Running" : "Stopped",
systemImage: isOn
? "hourglass"
: "hourglass.bottomhalf.filled")
.controlWidgetActionHint(isOn ?
"Start" : "Stop")
}
.tint(.purple)
}
.displayName("Productivity Timer")
.description("Start and stop a productivity timer.")
}
}
- .controlWidgetActionHint:这是一个方法,用于为控件添加操作提示,如“Start”或“Stop”。
- .displayName("Productivity Timer"):设置控件的显示名称为“Productivity Timer”。
- .description("Start and stop a productivity timer."):添加控件的描述,说明其功能是启动和停止生产力定时器。
二、Xcode15.3 打包 HandyJSON 报错 Command SwiftCompile failed with a nonzero exit code
Xcode15.3 打包 HandyJSON 报错 《Command SwiftCompile failed with a nonzero exit code》
已经不在建议使用了
临时解决方案
1.方案一
降低Xode版本
2.方案二
暂时的解决方法是:
在 Pod 的 Target 中找到 HandyJSON, 然后设置Optimization Level为 None和No Optimization,
3.方案三
暂时需要使用的 可以使用这种方式拉取 pod 'HandyJSON', :git => 'github.com/Miles-Mathe…'
三、App Store 审核应用时出现包含 bitcode 的报错
问题描述:
在 Xcode 开发环境下,将采用网易云信相关产品适配的 iOS 客户端 SDK(例如 NERtcSDK 或 NIMSDK)开发的应用打包提交到苹果应用商店(App Store)审核时,出现包含 Bitcode 的报错,诸如如下报错:
Invalid Executable. The executable 'XXX.app/Frameworks/NERtcSDK.framework
NERtcSDK' contains bitcode.
其中,Xcode16 报错情况较多。
问题原因
Bitcode 是一种中间表示形式,在 Xcode 中打包提交到 App Store 审核时,如果出现包含 Bitcode 的报错,这通常意味着您的应用没有正确包含 Bitcode。Bitcode 是苹果的一项要求,它允许苹果在 App Store 中对您的应用进行进一步的优化。
当提交应用到 App Store 时出现与 Bitcode 相关的问题,您需要手动移除 framework 中的 Bitcode。framework 是指 macOS 和 iOS 项目中的一个软件框架,它是一种包含代码、资源和其他文件的包,用于实现特定的功能或服务。framework 通常用于提供应用程序的某些部分,如用户界面元素、数据处理功能或其他服务。
解决方法
工具介绍
xcrun bitcode_strip 是一个命令行工具,用于手动去除对应 framework 的 Bitcode,命令格式如下:
xcrun bitcode_strip -r ${framework_path} -o ${framework_path}
${framework_path} 是一个占位符,表示 framework 的二进制文件路径。在实际使用命令时,您需要将 ${framework_path} 替换为具体的文件路径。
使用示例
假设您有一个名为 NIMSDK.framework 的 framework,并且它位于 /path/to/~/NIMSDK.framework 路径,那么您可以按照以下方式处理:
1.通过 cd 命令进入到 NIMSDK.framework 的路径。
如果是通过 pod install 获取的 SDK,则进入 pods 文件夹。
2.执行以下命令检查 framework 是否包d
含 bitcode,返回 0 即为不包含。
otool -l NIMSDK | grep __LLVM | wc -l
3.如果检测结果不是 0,则继续执行以下命令移除 NIMSDK.framework 的 Bitcode。
xcrun bitcode_strip -r NIMSDK -o NIMSDK