为有牺牲多壮志,敢教日月换新天。

iOS18适配:ControlWidgetToggle和ControlWidgetButton

热烈欢迎,请直接点击!!!

进入博主App Store主页,下载使用各个作品!!!

注:博主将坚持每月上线一个新app!!

支持原创,博客园原文链接: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】

6、了解应用扩展的工作原理:https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionOverview.html#//apple_ref/doc/uid/TP40014214-CH2-SW2

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下载

posted @ 2024-08-17 09:53  为敢技术  阅读(230)  评论(0编辑  收藏  举报