1. 基本介绍
Widget Extension 创建看这里 -> iOS 14 小组件(1):WidgetExtension 创建及报错详解
这一篇介绍一下如何自定义样式以及和主工程 App 数据交互。
2. 效果展示
3. Widget Extension 自定义样式
3.1 结构介绍
我们先按照生成的 Widget Extension 文件,介绍一下他的流程。
@main 这里是主入口,这里可以设置小组件的 Provider(可以理解为控制器) 以及 WidgetEntryView(可以理解为主视图 View),以及长按后弹出框的 APP 信息设置。
Provider:控制器,这里可以用来做小组件的刷新操作
SimpleEntry: 这个是数据模型,Provider 里如果想更新数据到 WidgetEntryView,必须通过 SimpleEntry 来实现,当然命名随意了,但是这个必须继承 TimelineEntry。同时也可以新增参数,变量什么的,用来传递自己需要的数据类型。
WidgetEntryView: 这就是主视图了,在这里自定义页面用来显示在手机桌面。
3.2 自定义样式
struct Widget_Previews: PreviewProvider {
static var previews: some View {
WidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
更改 WidgetPreviewContext(family: .systemSmall),可以修改三种样式(systemSmall,systemMedium,systemLarge)。
3.3 自定义UI
iOS 14 小组件支持 Swift UI 来完成。
struct WidgetEntryView : View {
@Environment(\.widgetFamily) var family : WidgetFamily
var entry: Provider.Entry
@ViewBuilder
var body: some View {
switch family {
// 小号
case .systemSmall:
ZStack(alignment: .leading, content: {
Image("widgetImage")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/widgetImage"))
// 中号
case .systemMedium:
ZStack(alignment: .leading, content: {
Image("img1")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/img1"))
// 大号
case .systemLarge :
ZStack(alignment: .leading, content: {
Image("img2")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/img2"))
default:
ZStack(alignment: .leading, content: {
Image("widgetImage")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/WidgetImage"))
}
}
}
4. Widget Extension 点击交互及传值
4.1 Widget Extension 发送数据
这一点和之前的 Widget Extension 基本一致,还是以 URL 的形式发送交互参数,有 widgetURL,Link 两种发送方式。
数据发送方式 | 支持类型 |
---|---|
widgetURL | systemSmall,systemMedium,systemLarge |
Link | systemMedium,systemLarge |
import WidgetKit
import SwiftUI
import Intents
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) -> ()) {
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)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
struct WidgetEntryView : View {
@Environment(\.widgetFamily) var family : WidgetFamily
var entry: Provider.Entry
@ViewBuilder
var body: some View {
switch family {
// 小号
case .systemSmall:
ZStack(alignment: .leading, content: {
Image("widgetImage")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/widgetImage"))
// 中号
case .systemMedium:
ZStack(alignment: .leading, content: {
Image("img1")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/img1"))
// 大号
case .systemLarge :
ZStack(alignment: .leading, content: {
Image("img2")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/img2"))
default:
ZStack(alignment: .leading, content: {
Image("widgetImage")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/WidgetImage"))
}
}
}
@main
struct MyWidget: Widget {
let kind: String = "Widget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
WidgetEntryView(entry: entry)
}
.configurationDisplayName("Xcode Widget")
.description("iOS 14 的新的小组件")
}
}
struct Widget_Previews: PreviewProvider {
static var previews: some View {
WidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemLarge))
}
}
另外在这里附上 Link 写法,和 widgetURL 写法效果一致,有兴趣可以了解。
Link(destination: URL(string: "widget://linkTest")!){
ZStack(alignment: .leading, content: {
Image("widgetImage")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
})
}
4.2 APP 端接收数据
这里需要注意了,以前这种值传递是使用 AppDelegate 中的 openURL 方法来处理的。但是 iOS 14 的小组件数据交互只能用 SceneDelegate 来接收,交互事件会走到 SceneDelegate 中的 openURLContexts 方法中。
这里贴的示例是Objective-C 中的代码。
- (void)scene:(UIScene *)scene openURLContexts:(NSSet *)URLContexts
{
UIOpenURLContext * context = URLContexts.allObjects.firstObject;
NSLog(@"%@", context.URL);
}
5. 自定义多个不同样式 Widget Extension
在效果图中我们可以注意到,我们有很多个 Widget Extension,大中小三个为一组,各不相同,并且还可以有多个相同大小的小组件。
5.1 大中小不同样式的小组件
我们先看一下系统默认生成的代码,继承于 Widget 很显然不支持多个小组件,我们只能在 WidgetEntryView 中想办法处理,根据当前的 WidgetFamily 返回不同的小组件。
@main
struct MyWidget: Widget {
let kind: String = "Widget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
WidgetEntryView(entry: entry)
}
.configurationDisplayName("Xcode Widget")
.description("iOS 14 的新的小组件")
}
}
struct WidgetEntryView : View {
@Environment(\.widgetFamily) var family : WidgetFamily
var entry: Provider.Entry
@ViewBuilder
var body: some View {
switch family {
// 小号
case .systemSmall:
ZStack(alignment: .leading, content: {
Image("widgetImage")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/widgetImage"))
// 中号
case .systemMedium:
ZStack(alignment: .leading, content: {
Image("img1")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/img1"))
// 大号
case .systemLarge :
ZStack(alignment: .leading, content: {
Image("img2")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/img2"))
default:
ZStack(alignment: .leading, content: {
Image("widgetImage")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/WidgetImage"))
}
}
}
5.2 多组 Widget Extension 创建
上图部分其实只能生成一组 Widget,包含大中小三个样式,但是可能我们的需求会需要很多个相同大小的,这时候就需要使用 WidgetBundle 来处理了,和字面意思一样,组件包。下边为了避免粘贴代码块,导致看的不清晰,我直接贴完整代码。
- 注意:我们需要更新 @main 的位置到 WidgetBundle 上。
import WidgetKit
import SwiftUI
import Intents
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) -> ()) {
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)
}
}
struct ResponseObject {
let test1 : String
let test2 : String
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
// let responseObject : ResponseObject
}
struct WidgetEntryView : View {
@Environment(\.widgetFamily) var family : WidgetFamily
var entry: Provider.Entry
@ViewBuilder
var body: some View {
switch family {
// 小号
case .systemSmall:
ZStack(alignment: .leading, content: {
Image("widgetImage")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/widgetImage"))
// 中号
case .systemMedium:
ZStack(alignment: .leading, content: {
Image("img1")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/img1"))
// 大号
case .systemLarge :
ZStack(alignment: .leading, content: {
Image("img2")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/img2"))
default:
ZStack(alignment: .leading, content: {
Image("widgetImage")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/WidgetImage"))
}
}
}
//@main
struct MyWidget: Widget {
let kind: String = "Widget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
WidgetEntryView(entry: entry)
}
.configurationDisplayName("Xcode Widget")
.description("iOS 14 的新的小组件")
}
}
struct Widget_Previews: PreviewProvider {
static var previews: some View {
WidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemLarge))
}
}
@main
struct Widgets: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
MyWidget()
CustomWidget()
CustomWidget()
CustomWidget()
CustomWidget()
}
}
struct CustomWidget : Widget {
let kind: String = "CustomWidget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
CustomEntryView(entry: entry)
}
.configurationDisplayName("Xcode CustomWidget")
.description("iOS 14 的新的自定义小组件")
}
}
struct CustomEntryView : View {
@Environment(\.widgetFamily) var family : WidgetFamily
var entry: Provider.Entry
@ViewBuilder
var body: some View {
switch family {
// 小号
case .systemSmall:
ZStack(alignment: .leading, content: {
Image("img3")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/img3"))
// 中号
case .systemMedium:
ZStack(alignment: .leading, content: {
Image("img5")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/img5"))
// 大号
case .systemLarge :
ZStack(alignment: .leading, content: {
Image("img4")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.black)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/img4"))
default:
ZStack(alignment: .leading, content: {
Image("widgetImage")
.frame(width: 100, height: 100, alignment: .center)
HStack(alignment: .center, spacing: 15, content: {
Image("xcodeIcon")
VStack(alignment: .leading, spacing: 15, content: {
Text("Xcode Widget")
.foregroundColor(.white)
Text("iOS 14 的新的小组件")
.foregroundColor(.white)
.lineLimit(2)
})
})
}).widgetURL(URL(string: "widget://test/WidgetImage"))
}
}
}
组件包这里最多提供 5 个 Widget,也就是最多可以存在 15 个小组件。
6. 技术小结
- Widget Extension 其实并不难理解和使用,其实这篇文章已经包含了大部分 UI 设计思路了。
- Swift UI 熟练掌握,其实操作这个就很简单了。