创建主工程就不必讲了
1 创建小组件
创建子工程 [new Target ] 选择 [ Widger Extension]
小组件入口是WidgetBundle文件,可以进行多个小组件的调试
TestWidget2文件是主要操作,小组件使用swiftUI布局,使用 AppIntent进行事件处理,TestDataUserDefalut 用的userdefault 和主程序数据同步,下面详细讲解
dart
//
// TestWidget2.swift
// TestSwift6Demo
//
// Created by ITHPNB04296 on 11/3/25.
//
import WidgetKit
import SwiftUI
import AppIntents
struct TestWidget2:Widget{
let kind:String = "TestWidget2"
var body: some WidgetConfiguration{
StaticConfiguration(kind: kind, provider: MyWidget2Provider()) {entry in
TestWidget2View(entry: entry ,ganmeStatus: entry.state)
.containerBackground(for: ContainerBackgroundPlacement.widget) {
//背景色
Color.yellow
}
}
.configurationDisplayName("TestWidget2")
.description("show Testwidget2")
.supportedFamilies([.systemMedium])
}
}
struct TestWidget2View:View {
var entry: MyWidget2Provider.Entry
@Environment(\.widgetFamily) var family:WidgetFamily
var ganmeStatus:Test2State
// var selectedCharacter:CharacterDetail
var body: some View {
VStack{
Text("\(ganmeStatus.date.description)")
Button("\(ganmeStatus.state)") {
print("点击按钮1 直接就跳转到主APP了")
}
if entry.running{
createRunningView()
}else{
createStaticView()
}
}
}
func createRunningView()->some View{
//点击一次更新了一次
return VStack{
Button("点击按钮2", intent: Test2Intent())
//该Text 无法设置停止时间
// Text(entry.date, style: Text.DateStyle.timer)
// .font(Font.system(size: 15.0))
// .fontWeight(.bold)
// .multilineTextAlignment(.center)
// .contentTransition(.numericText(countsDown: entry.state.duration > 0))
//新Text样式,可以设置停止时间
Text(timerInterval: entry.daterange,pauseTime: entry.pauseTime,countsDown: entry.countsDown,showsHours: entry.showsHours)
}
}
func createStaticView()->some View{
return VStack{
Button("点击按钮2", intent: Test2Intent())
Text(entry.date, style: Text.DateStyle.date)
.font(Font.system(size: 15.0))
.fontWeight(.bold)
.multilineTextAlignment(.center)
//过渡动画 countsDown 参数用来控制是否向下滚动
.contentTransition(.numericText(countsDown: entry.state.duration > 0))
}
}
}
//添加按钮点击事件行为
struct Test2Intent:AppIntent{
static var title: LocalizedStringResource = "XXX"
//点击事件函数是异步的可以做一些耗时操作,比如网络请求等
func perform() async throws -> some IntentResult {
//也可以通过App group 和主程序共享数据
//点击事件更新
let isrunning = TestDataUserDefalut().get()
TestDataUserDefalut().set(!isrunning)
return .result()
}
}
struct Test2State{
var state:String
var date:Date{
return Date()
}
var duration:Int = 10
}
struct MyWidget2Entry:TimelineEntry{
var date: Date
//默认区间是0
var daterange:ClosedRange<Date> = Date()...Date()
//默认没有开启
var running:Bool {
//添加状态控制
return TestDataUserDefalut().get()
}
/// The relevance of a widget's content to the user.
var relevance: TimelineEntryRelevance?
//添加适配新的TimerText
//停止时间
var pauseTime: Date = Date()
//倒计时
var countsDown = false
var showsHours = true
//携带其他额外数据
var state:Test2State
}
struct MyWidget2Provider: TimelineProvider{
//没有数据时候占位,仅初始化时候调用一次,数据没加载上来时候,添加占位数据
func placeholder(in context: Context) -> MyWidget2Entry {
return MyWidget2Entry(date: Date(), state: Test2State(state: "init"))
}
//生成预览照,给系统组件库使用
func getSnapshot(in context: Context, completion: @escaping @Sendable (MyWidget2Entry) -> Void) {
let date = Date()
let entry:MyWidget2Entry
print("getSnapshot")
entry = MyWidget2Entry(date: date, state: Test2State(state:"perview"))
completion(entry)
}
//实时更新时间线,每次点击按钮时候调用两次 ? 调用两次没理解
func getTimeline(in context: Context, completion: @escaping (Timeline<MyWidget2Entry>) -> Void) {
let date = Date()
let endtime:Date = Calendar.current.date(byAdding: .minute, value: 10, to: date)!
let entry = MyWidget2Entry(date: date, daterange: date...endtime,countsDown: true, state: Test2State(state:"start"))
//下一次更新时候
let nextUpdateDate = Calendar.current.date(byAdding: .second, value: 1, to: date)!
//时间线,\
/*
atend -当时间线中的所有条目都已显示完毕后,才会重新加载时间线。固定时间点加载,比如天气
never - 不会自动刷新小组件内容,只有主app主动去刷新数据
after(Date) - 指定一个未来的日期和时间
*/
let timeline = Timeline(entries: [entry], policy: TimelineReloadPolicy.after(nextUpdateDate))
print("时间线方法1更新了 ")
completion(timeline)
}
}
//更新共享数据
struct TestDataUserDefalut{
func get()->Bool{
let sharedDefaults = UserDefaults(suiteName: "group.testwidget1.demo")
let isrunning = sharedDefaults?.value(forKey: "sharedRunning") as? Bool ?? false
return isrunning
}
func set(_ state:Bool){
let sharedDefaults = UserDefaults(suiteName: "group.testwidget1.demo")
sharedDefaults?.set(state, forKey: "sharedRunning")
sharedDefaults?.synchronize()
}
}
小组件和主工程进行数据同步,也就是进程间通信
***配置 App Groups
- 打开 Xcode,选择你的主 App 目标(Target)。
- 在 Signing & Capabilities 中,点击 + Capability。
- 搜索 App Groups 并添加。
- 创建一个新的 App Group(如 group.com.yourcompany.shared)。
- 在 Widget Extension / App Clips 目标中重复上述步骤,选择相同的 App Group。***
我的demo使用plist文件也就是userdefault 同步数据当然也可以使用数据库同步数据,我看同事使用SwiftData(ios 17之后才能使用)同步数据 swiftData官方文档 ,我选择的appgroup 名字是 "group.testwidget1.demo",这个不是包名,TestWidget2 里面的 TestDataUserDefalut 就是小程序同步数据代码,下面看一下主程序App同步代码,添加包括刷新通知等,讲解比较详细
dart
class ViewController: UIViewController {
lazy var stateLab:UILabel = {
let size = UIScreen.main.bounds.size
let labwidth = 100.0
let labheight = 50.0
let lab = UILabel(frame: CGRectMake((size.width - labwidth) / 2.0, (size.height - labheight) / 2.0, labwidth, labheight))
view.addSubview(lab)
lab.backgroundColor = UIColor.green
lab.font = UIFont.systemFont(ofSize: 28)
lab.textColor = UIColor.red
lab.textAlignment = .center
return lab
}()
func updateState(_ state:Bool){
DispatchQueue.main.async {
self.stateLab.text = state ? "打开" : "关闭"
}
}
func addTest(){
/*
配置 App Groups
1. 打开 Xcode,选择你的主 App 目标(Target)。
2. 在 Signing & Capabilities 中,点击 + Capability。
3. 搜索 App Groups 并添加。
4. 创建一个新的 App Group(如 group.com.yourcompany.shared)。
5. 在 Widget Extension / App Clips 目标中重复上述步骤,选择相同的 App Group。
*/
let appGroup = "group.testwidget1.demo"
let sharedDefaults = UserDefaults(suiteName: appGroup)
let isrunning = sharedDefaults?.value(forKey: "sharedRunning") as? Bool ?? false
updateState(isrunning)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let sharedDefaults = UserDefaults(suiteName: "group.testwidget1.demo")
let isrunning = sharedDefaults?.value(forKey: "sharedRunning") as? Bool ?? false
sharedDefaults?.set(!isrunning, forKey: "sharedRunning")
print("\(!isrunning)")
updateState(!isrunning)
//通知小组件刷新
notifiUpdateWidget()
}
func notifiUpdateWidget(){
//通知小组件刷新,刷新所有小组件
WidgetCenter.shared.reloadAllTimelines()
//刷新指定的小组件,小组件那边注册的kind
// WidgetCenter.shared.reloadTimelines(ofKind: "TestWidget1")
//获取配置去刷新小组件
// WidgetCenter.shared.getCurrentConfigurations { result in
// guard case .success(let success) = result else {
// return
// }
//
if let widget = success.first(
where:{ widget in
let intent = widget.configuration as ConfigurationAppIntent
return intent?.character == ""
}
){
WidgetCenter.shared.reloadTimelines(ofKind: widget.kind)
}
// }
}
override func viewDidLoad() {
super.viewDidLoad()
addTest()
}
}