iOS桌面小插件Widget Extension如何实现个性化定制?

2026-05-22 18:471阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1764个文字,预计阅读时间需要8分钟。

iOS桌面小插件Widget Extension自iOS14后出现,基于SwiftUI构建。旧项目新建时可能遇到堆栈错误,需将插件target的开发SDK版本设置为14.0以上,并在File - Target - Widget Extension中配置。

iOS桌面小插件 Widget Extension
  • 这个插件时iOS14以后才出现的,基于SwiftUI
  • 旧项目新建时可能一堆错误,其中一个时要把插件target 开发sdk版本设置为14.0以上
新建target
  • File - Target - Widget Extension

项目结构
  • @main 这里是主入口,这里可以设置小组件的 Provider以及 WidgetEntryView,以及长按后弹出框的 APP 信息设置。
  • Provider:控制器,这里可以用来做小组件的刷新操作
  • SimpleEntry: 这个是数据模型,Provider 里如果想更新数据到 WidgetEntryView,必须通过 SimpleEntry 来实现,当然命名随意了,但是这个必须继承 TimelineEntry。同时也可以新增参数,变量什么的,用来传递自己需要的数据类型。
  • WidgetEntryView: 这就是主视图了,在这里自定义页面用来显示在手机桌面。

import WidgetKit import SwiftUI import Intents // 控制器,类似Controller,这里可以用来做小组件的刷新操作 struct Provider: IntentTimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), configuration: ConfigurationIntent()) } func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), configuration: configuration) completion(entry) } func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = SimpleEntry(date: entryDate, configuration: configuration) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } // 数据模型,数据显示在View上必须经过这里 struct SimpleEntry: TimelineEntry { let date: Date let configuration: ConfigurationIntent } // View,小组件的界面 struct WidgetExtensionEntryView : View { var entry: Provider.Entry var body: some View { Text(entry.date, style: .time) } } // 程序入口,初始化相关信息,如Provider,View等 @main struct WidgetExtension: Widget { let kind: String = "WidgetExtension" var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in WidgetExtensionEntryView(entry: entry) } .configurationDisplayName("小组件") .description("This is an 测试一下 widget.") } } // 自定义样式 struct WidgetExtension_Previews: PreviewProvider { static var previews: some View { // 设置小组件尺寸 systemSmall systemMedium systemLarge WidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent())) .previewContext(WidgetPreviewContext(family: .systemSmall)) } } 自定义UI 自定义小组件尺寸

// 自定义样式 struct WidgetExtension_Previews: PreviewProvider { static var previews: some View { // 设置小组件尺寸 systemSmall systemMedium systemLarge WidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent())) .previewContext(WidgetPreviewContext(family: .systemSmall)) } } HStack、VStack、ZStack

  • HStack、VStack相当于UIStackView,H是水平方向,V是竖直方向。ZStack可以理解为相对于屏幕里外方向,也就是相当于以前superView和subView的方式。

// View,小组件的界面 struct WidgetExtensionEntryView : View { var entry: Provider.Entry var body: some View { // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fit) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }) }) } } 传递数据

  • 通过widgetURL 和Link
  • 在主应用添加 URL Types

// View,小组件的界面 struct WidgetExtensionEntryView : View { var entry: Provider.Entry var body: some View { // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fit) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }) }).widgetURL(URL(string: "widgetExtensionDemo://test1")) } }

  • 接受数据

  • 只能用SceneDelegate来接受数据,AppDelegate不行。

  • SceneDelegate

  • SceneDelegate中相应事件

- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts{ NSLog(@"%s",__FUNCTION__); UIOpenURLContext * context = URLContexts.allObjects.firstObject; NSLog(@"%@", context.URL); } 适配不同尺寸小组件


// View,小组件的界面 struct WidgetExtensionEntryView : View { @Environment(\.widgetFamily) var family:WidgetFamily var entry: Provider.Entry var body: some View { switch family { case .systemSmall: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fill) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }) }).widgetURL(URL(string: "widgetExtensionDemo://test1")) case .systemMedium: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fill) // 水平 HStack(alignment: .top, spacing: 5, content: { // 左侧图 Image("1").resizable().aspectRatio(contentMode: .fit).frame(width: 200, height: 80, alignment: .leading).cornerRadius(10.0).foregroundColor(.blue) // 垂直 VStack(alignment: .trailing, spacing: 5, content: { // 右侧文字 Text("zh组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }).foregroundColor(.gray) }) }).widgetURL(URL(string: "widgetExtensionDemo://test2")) case .systemLarge: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fill) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").aspectRatio(contentMode: .fit).cornerRadius(10.0).frame(width: 200, height: 100, alignment: .leading) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }).foregroundColor(.blue) }).widgetURL(URL(string: "widgetExtensionDemo://test3")) default: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fit) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }) }).widgetURL(URL(string: "widgetExtensionDemo://test1")) } } } 更多小组件创建

  • 重写@main入口

// 更多小组件 @main struct Widgets:WidgetBundle { init() { } @WidgetBundleBuilder var body: some Widget{ // 最多创建5次,也就是15个小组件 WidgetExtension() CustomWidget() CustomWidget() CustomWidget() CustomWidget() } } struct CustomWidget:Widget { var kind:String="自定义组件" var body: some WidgetConfiguration{ IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in CustomEntryView(entry:entry) } .configurationDisplayName("自定义更多组件") .description("ios14自定义更多小组件") } } // 自定义Ui struct CustomEntryView:View { @Environment(\.widgetFamily) var family:WidgetFamily var entry: Provider.Entry @ViewBuilder var body: some View { switch family { case .systemSmall: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fill) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }) }).widgetURL(URL(string: "widgetExtensionDemo://test1")) case .systemMedium: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fill) // 水平 HStack(alignment: .top, spacing: 5, content: { // 左侧图 Image("1").resizable().aspectRatio(contentMode: .fit).frame(width: 200, height: 80, alignment: .leading).cornerRadius(10.0).foregroundColor(.blue) // 垂直 VStack(alignment: .trailing, spacing: 5, content: { // 右侧文字 Text("zh组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }).foregroundColor(.gray) }) }).widgetURL(URL(string: "widgetExtensionDemo://test2")) case .systemLarge: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fill) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").aspectRatio(contentMode: .fit).cornerRadius(10.0).frame(width: 200, height: 100, alignment: .leading) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }).foregroundColor(.blue) }).widgetURL(URL(string: "widgetExtensionDemo://test3")) default: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fit) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }) }).widgetURL(URL(string: "widgetExtensionDemo://test1")) } } }

参考1
参考2

本文共计1764个文字,预计阅读时间需要8分钟。

iOS桌面小插件Widget Extension自iOS14后出现,基于SwiftUI构建。旧项目新建时可能遇到堆栈错误,需将插件target的开发SDK版本设置为14.0以上,并在File - Target - Widget Extension中配置。

iOS桌面小插件 Widget Extension
  • 这个插件时iOS14以后才出现的,基于SwiftUI
  • 旧项目新建时可能一堆错误,其中一个时要把插件target 开发sdk版本设置为14.0以上
新建target
  • File - Target - Widget Extension

项目结构
  • @main 这里是主入口,这里可以设置小组件的 Provider以及 WidgetEntryView,以及长按后弹出框的 APP 信息设置。
  • Provider:控制器,这里可以用来做小组件的刷新操作
  • SimpleEntry: 这个是数据模型,Provider 里如果想更新数据到 WidgetEntryView,必须通过 SimpleEntry 来实现,当然命名随意了,但是这个必须继承 TimelineEntry。同时也可以新增参数,变量什么的,用来传递自己需要的数据类型。
  • WidgetEntryView: 这就是主视图了,在这里自定义页面用来显示在手机桌面。

import WidgetKit import SwiftUI import Intents // 控制器,类似Controller,这里可以用来做小组件的刷新操作 struct Provider: IntentTimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), configuration: ConfigurationIntent()) } func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), configuration: configuration) completion(entry) } func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = SimpleEntry(date: entryDate, configuration: configuration) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } // 数据模型,数据显示在View上必须经过这里 struct SimpleEntry: TimelineEntry { let date: Date let configuration: ConfigurationIntent } // View,小组件的界面 struct WidgetExtensionEntryView : View { var entry: Provider.Entry var body: some View { Text(entry.date, style: .time) } } // 程序入口,初始化相关信息,如Provider,View等 @main struct WidgetExtension: Widget { let kind: String = "WidgetExtension" var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in WidgetExtensionEntryView(entry: entry) } .configurationDisplayName("小组件") .description("This is an 测试一下 widget.") } } // 自定义样式 struct WidgetExtension_Previews: PreviewProvider { static var previews: some View { // 设置小组件尺寸 systemSmall systemMedium systemLarge WidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent())) .previewContext(WidgetPreviewContext(family: .systemSmall)) } } 自定义UI 自定义小组件尺寸

// 自定义样式 struct WidgetExtension_Previews: PreviewProvider { static var previews: some View { // 设置小组件尺寸 systemSmall systemMedium systemLarge WidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent())) .previewContext(WidgetPreviewContext(family: .systemSmall)) } } HStack、VStack、ZStack

  • HStack、VStack相当于UIStackView,H是水平方向,V是竖直方向。ZStack可以理解为相对于屏幕里外方向,也就是相当于以前superView和subView的方式。

// View,小组件的界面 struct WidgetExtensionEntryView : View { var entry: Provider.Entry var body: some View { // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fit) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }) }) } } 传递数据

  • 通过widgetURL 和Link
  • 在主应用添加 URL Types

// View,小组件的界面 struct WidgetExtensionEntryView : View { var entry: Provider.Entry var body: some View { // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fit) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }) }).widgetURL(URL(string: "widgetExtensionDemo://test1")) } }

  • 接受数据

  • 只能用SceneDelegate来接受数据,AppDelegate不行。

  • SceneDelegate

  • SceneDelegate中相应事件

- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts{ NSLog(@"%s",__FUNCTION__); UIOpenURLContext * context = URLContexts.allObjects.firstObject; NSLog(@"%@", context.URL); } 适配不同尺寸小组件


// View,小组件的界面 struct WidgetExtensionEntryView : View { @Environment(\.widgetFamily) var family:WidgetFamily var entry: Provider.Entry var body: some View { switch family { case .systemSmall: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fill) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }) }).widgetURL(URL(string: "widgetExtensionDemo://test1")) case .systemMedium: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fill) // 水平 HStack(alignment: .top, spacing: 5, content: { // 左侧图 Image("1").resizable().aspectRatio(contentMode: .fit).frame(width: 200, height: 80, alignment: .leading).cornerRadius(10.0).foregroundColor(.blue) // 垂直 VStack(alignment: .trailing, spacing: 5, content: { // 右侧文字 Text("zh组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }).foregroundColor(.gray) }) }).widgetURL(URL(string: "widgetExtensionDemo://test2")) case .systemLarge: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fill) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").aspectRatio(contentMode: .fit).cornerRadius(10.0).frame(width: 200, height: 100, alignment: .leading) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }).foregroundColor(.blue) }).widgetURL(URL(string: "widgetExtensionDemo://test3")) default: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fit) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }) }).widgetURL(URL(string: "widgetExtensionDemo://test1")) } } } 更多小组件创建

  • 重写@main入口

// 更多小组件 @main struct Widgets:WidgetBundle { init() { } @WidgetBundleBuilder var body: some Widget{ // 最多创建5次,也就是15个小组件 WidgetExtension() CustomWidget() CustomWidget() CustomWidget() CustomWidget() } } struct CustomWidget:Widget { var kind:String="自定义组件" var body: some WidgetConfiguration{ IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in CustomEntryView(entry:entry) } .configurationDisplayName("自定义更多组件") .description("ios14自定义更多小组件") } } // 自定义Ui struct CustomEntryView:View { @Environment(\.widgetFamily) var family:WidgetFamily var entry: Provider.Entry @ViewBuilder var body: some View { switch family { case .systemSmall: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fill) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }) }).widgetURL(URL(string: "widgetExtensionDemo://test1")) case .systemMedium: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fill) // 水平 HStack(alignment: .top, spacing: 5, content: { // 左侧图 Image("1").resizable().aspectRatio(contentMode: .fit).frame(width: 200, height: 80, alignment: .leading).cornerRadius(10.0).foregroundColor(.blue) // 垂直 VStack(alignment: .trailing, spacing: 5, content: { // 右侧文字 Text("zh组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }).foregroundColor(.gray) }) }).widgetURL(URL(string: "widgetExtensionDemo://test2")) case .systemLarge: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fill) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").aspectRatio(contentMode: .fit).cornerRadius(10.0).frame(width: 200, height: 100, alignment: .leading) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }).foregroundColor(.blue) }).widgetURL(URL(string: "widgetExtensionDemo://test3")) default: // 深度布局,屏幕深度 ZStack(alignment: .center, content: { // 背景图 Image("2").resizable().aspectRatio(contentMode: .fit) // 水平 HStack(alignment: .center, spacing: 5, content: { // 左侧图 Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0) // 垂直 VStack(alignment: .center, spacing: 5, content: { // 右侧文字 Text("小组件1").foregroundColor(.blue) Text("小组件2").foregroundColor(.blue).lineLimit(2) }) }) }).widgetURL(URL(string: "widgetExtensionDemo://test1")) } } }

参考1
参考2