iOS18适配:ControlWidgetToggle和ControlWidgetButton
支持原创,博客园原文链接:https://www.cnblogs.com/strengthen/p/18362397
文末可以有demo下载。
首先查看WWDC2024的官方视频:
WWDC2024将 App 控件扩展到系统级别:https://developer.apple.com/cn/videos/play/wwdc2024/10157/
There are two types of controls: buttons and toggles. Buttons perform discrete actions, which can include launching your app, while toggles change a piece of boolean state, like turning something on or off. 【翻译为】:
控件有两种类型:按钮和切换按钮。按钮执行离散操作,包括启动应用,而切换按钮则更改布尔状态,例如打开或关闭某些功能。
官方文档:Adding refinements and configuration to controls -https://developer.apple.com/documentation/WidgetKit/Adding-refinements-and-configuration-to-controls
详细步骤:
1、点击链接:https://developer.apple.com/download/applications/ ,输入Apple开发者账号,下载Xcode 16.5 beta安装包。
2、下载iOS 18.1 beta 2 模拟器运行时:https://developer.apple.com/download/all/,下载:【iOS 18.1 beta 2 Simulator Runtime】,也可以通过安装Xcode16后,启动Xcode16时,进行安装iOS18模拟器运行时。
3、Xcode 16安装模拟器运行时,请参考另一篇博文: https://www.cnblogs.com/strengthen/p/18348016
4、如果无法打开Xcode16,请升级你的mac OS系统,升级为mac OS 14.5以上或者升级为mac OS 15 Beta::https://developer.apple.com/download/,下载macOS 15.1 beta 2。
5、新建Widget Extension:选择Xcode16,屏幕左上角【File】-【New】-【Target】-【Widget Extension】
App Extension (应用扩展): 这是一个扩展应用功能的组件,可以在其他应用的上下文中提供特定的功能,如一个Share Extension可以让用户在Safari或Photos等其他应用内分享内容到你的应用。
Containing App (宿主应用): 这是指包含了上述扩展的主应用程序。也就是说,应用扩展是这个主应用的一部分,以扩展形式分发,并且与它同被打包到一个应用包中。
Host App (托管应用或主机应用): 这是指在当前上下文中调用或运行该应用扩展的应用。例如,如果用户正在使用Safari并且触发了一个Share Extension来分享一个网页,那么Safari就是这个应用扩展的host app。
(6.1)、app extension主要与其host app进行通信,其通信方式类似于事务处理:host app发出请求,app extension发出响应。
(6.2)、app extension与其宿主应用之间没有直接通信,当app extension运行时,宿主应用甚至不会运行。宿主应用和host app不通信。
(6.3)、虚线表示app extension与其宿主应用之间可用的有限交互。app extension及其宿主应用,可以通过访问私有定义的共享容器中的共享数据进行通信。
(6.4)、在后台,系统使用进程间通信来确保host app和app extension能够协同工作。在代码中无需考虑这种底层通信机制,因为使用的是系统提供的更高级的 API。
7、 创建App Group共享存储对象,用于app extension和app进行共享数据。
8、WWDC2024代码不够详细,直接上代码。
WidgetExtensionBundle.swift文件:
import WidgetKit import SwiftUI // 标记:表示这是应用的入口点。它将这个 WidgetBundle 注册为应用的一部分。 @main // Widget Bundle 是一种容器,允许你将多个控件组合在一起,以更好地组织和管理这些控件,并方便地在应用中启用或禁用它们。 // WidgetExtensionBundle这是我们定义的 WidgetBundle 名称。 struct WidgetExtensionBundle: WidgetBundle { // body属性表示该 WidgetBundle 中包含的控件列表。你可以在这里添加任意数量的控件。 var body: some Widget { // 类型一:Toggles:切换控件,切换开关状态。 WidgetToggle() // 类型二:Buttons:执行离散操作,打开App。 WidgetButton() } }
WidgetToggle.swift文件:
import Foundation import WidgetKit import SwiftUI import AppIntents import UIKit import Combine import Intents // 使用 ControlWidget 协议来定义一个基础控件,定义了一个新的结构体 WidgetToggle,它实现了 ControlWidget 协议。 struct WidgetToggle: ControlWidget { // body属性表示该 WidgetToggle 中包含的控件列表。 var body: some ControlWidgetConfiguration { // 这是一个静态配置,用于定义控件的结构:外观和行为。 StaticControlConfiguration( // 指定控件的唯一标识符 kind: "com.apple.ControlWidgetToggle", // 使用定义的 TimerValueProvider 作为值提供者。 provider: TimerValueProvider() ) { isRunning in // :这是一个闭包,当获取到定时器状态时执行,其中 isRunning 表示当前的定时器状态。通过这种方式,你可以使控件动态响应定时器状态的变化,提供更好的用户体验。 // 定义了一个切换控件,使用 isOn 属性表示当前的状态。 isOn 属性和操作意图 action。 ControlWidgetToggle( "iNFC", // 绑定到 TimerManager.shared.isRunning,表示定时器是否在运行。 isOn: isRunning, // 指定了一个 ToggleTimerIntent 操作,当用户点击控件时触发该操作。 action: ToggleTimerIntent() ){ isOn in // 这是一个闭包,用于根据 isOn 的值动态更新控件的图标。 // 图像控件,用于显示系统图标。根据 isOn 的值选择不同的图标。 // Image(systemName: isOn ? "hourglass" : "hourglass.bottomhalf.filled") // 这是一个带有文本和图标的控件,用于显示控件的状态。 Label(isOn ? "Running" : "Stopped", // 根据 isOn 的值选择显示文本,运行时显示“Running”,停止时显示“Stopped”。 systemImage: isOn // 根据 isOn 的值选择不同的图标。 ? "hourglass.bottomhalf.filled" : "hourglass") // 这是一个方法,用于为控件添加操作提示,如“Start”或“Stop”。 .controlWidgetActionHint(isOn ? "Start" : "Stop") } // 设置控件的主题色为紫色,使其更具视觉吸引力。 .tint(.purple) } // 设置控件的显示名称为“iNFC”。 .displayName("iNFC") // 添加控件的描述。 .description("The most powerful NFC application") } } // 定义了一个新的结构体 TimerValueProvider,它实现了 ControlValueProvider 协议。 struct TimerValueProvider: ControlValueProvider { // 这是一个异步函数,用于获取当前的定时器状态。 func currentValue() async throws -> Bool { // 获取定时器的运行状态。 return TimerManager.shared.fetchRunningState() } // 定义了一个预览值,当无法获取实际值时使用。 var previewValue: Bool { return false } } // 操作意图(Intent),用于处理定时器的启动和停止操作。定义了一个新的结构体,它实现了 SetValueIntent 和 LiveActivityIntent 协议。 struct ToggleTimerIntent: SetValueIntent, LiveActivityIntent { // 本地化字符串资源 static var title: LocalizedStringResource = "WidgetToggle" // Parameter:这是一个参数注解,用于定义意图中的参数。 // title:参数的标题。 @Parameter(title: "Toggle") // 一个布尔值,表示定时器的运行状态。 var value: Bool // 定义了执行意图时的操作。 func perform() async throws -> some IntentResult { // 切换保存开关状态,调用 TimerManager 来设置定时器的运行状态。 TimerManager.shared.setTimerRunning(value) // 保存数据到Group App容器,传递给主应用 if let appGroupDefaults = UserDefaults(suiteName: "group.com.apple.iNFC") { if appGroupDefaults.bool(forKey: "widgetExtensionData") { appGroupDefaults.set(false, forKey: "widgetExtensionData") } else { appGroupDefaults.set(true, forKey: "widgetExtensionData") } } // 返回一个结果,表示意图执行成功。 return .result() } }
TimerManager.swift文件:
import SwiftUI import Combine class TimerManager: ObservableObject { static let shared = TimerManager() @Published var isRunning = false private init() {} func setTimerRunning(_ value: Bool) { isRunning = value } func fetchRunningState() -> Bool { return isRunning } }
WidgetButton.swift文件:
import Foundation import WidgetKit import SwiftUI import AppIntents import UIKit import Combine // 使用 ControlWidget 协议来定义一个基础控件,定义了一个新的结构体 WidgetToggle,它实现了 ControlWidget 协议。 struct WidgetButton: ControlWidget { // body属性表示该 WidgetButton 中包含的控件列表。 var body: some ControlWidgetConfiguration { // // 这是一个静态配置,用于定义控件的结构:外观和行为。 StaticControlConfiguration( kind: "com.apple.ControlWidgetButton" ) { // 定义了一个打开容器App的控件, ControlWidgetButton(action: OpenContainerAction()) { Label("WidgetButton", systemImage: "paperplane") } actionLabel: { isActive in if isActive { Label("WidgetButton", systemImage: "hourglass") } } } .displayName("iNFC") .description("The most powerful NFC application") } } struct OpenContainerAction: AppIntent { // // 本地化字符串资源 static let title: LocalizedStringResource = "WidgetButton" // 定义了执行意图时的操作。 func perform() async throws -> some IntentResult & OpensIntent { // 保存数据到Group App容器,传递给主应用 if let appGroupDefaults = UserDefaults(suiteName: "group.com.apple.iNFC") { if appGroupDefaults.bool(forKey: "widgetExtensionData") { appGroupDefaults.set(false, forKey: "widgetExtensionData") } else { appGroupDefaults.set(true, forKey: "widgetExtensionData") } } // 重要:打开容器App的操作 return .result(opensIntent: OpenURLIntent(URL(string: "iNFC://")!)) } }
Github链接:demo下载