写在前面

1、iOS14后,苹果更新了扩展组件,引入了新的UI组件:WidgetKit 而舍弃了iOS14以下版本的Today Extension组件;

2、WidgetExtension 使用的是新的WidgetKit不同于Today Widget,它只能使用SwiftUI进行开发,所以需要SwiftUI和Swift基础;

3、Widget支持3种尺寸systemSmall (2x2)、 systemMedium (4x2)、 systemLarge(4x4)

1、创建 Widget

首先创建一个项目,取名MyApp;

然后创建Widget,File -> New -> Target

【iOS】记录widget开发流程及遇到的问题_ide

iOS -> Application Extension -> Widget Extension

【iOS】记录widget开发流程及遇到的问题_ide_02

输入项目名;

这里要注意下,Widget 分为 Static 和 Intent 两种模式。Intent 模式长按可编辑,Static 模式没有编辑选项。下图为 Intent 模式:

【iOS】记录widget开发流程及遇到的问题_swift_03

取名 MyWidget,这里先不选 Include Configuration Intent,不选则创建静态小组件,选中则创建可编辑小组件。

注:这里不能取名Widget,系统有这个文件,会报错。

【iOS】记录widget开发流程及遇到的问题_json_04

点击创建,有个弹窗,直接点击Activate

【iOS】记录widget开发流程及遇到的问题_json_05

现在可以运行下,在模拟器上会显示一个只有时间文本的小组件。

2、结构简述

MyWidget.swift 文件里有5个结构体

2.1、Provider

管理时间线的地方,数据绑定处理。

2.2、SimpleEntry

时间线入口,默认只生产一个date属性,需要自定义字段的话在这里声明。

2.3、MyWidgetEntryView

小组件视图布局在这里实现,处理数据展示到视图。

2.4、MyWidget

小组件加载入口,静态的调用StaticConfiguration,可编辑的调用IntentConfiguration。

2.5、MyWidget_Previews

视图预览。

3、Static 改为 Intent

3.1、添加 Intent 文件

我们创建的静态的,如果需要改为可编辑的,需要手动添加Intent文件。

File -> New -> File (cmd + N)

iOS -> Resource -> SiriKit Intent Definition File

【iOS】记录widget开发流程及遇到的问题_json_06

取名Custom.intentdefinition,这里Targets要选中小组件,默认没有选中。

【iOS】记录widget开发流程及遇到的问题_json_07

3.2、添加 Intent

取名MyIntent,Category选View,选中 user confirmation required,其他不选。

添加之后编译下项目(cmd + B)会生成一个 MyIntentIntent.swift 文件。MyIntent是我们自定义的Intent的名字。

【iOS】记录widget开发流程及遇到的问题_swift_08

文件路径

【iOS】记录widget开发流程及遇到的问题_xcode_09

3.3、修改 MyWidget.swift

3.3.1、Provider

TimelineProvider 改为 IntentTimelineProvider

struct Provider: TimelineProvider {
...
改为
struct Provider: IntentTimelineProvider {
...

getSnapshot 方法修改

func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
...
改为
func getSnapshot(for configuration:MyIntentIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
...

getTimeline 方法修改

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
...
改为
func getTimeline(for configuration:MyIntentIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
...
3.3.2 SimpleEntry

添加 configuration 属性

let configuration: MyIntentIntent

修改调用 SimpleEntry 的地方

Provider -> placeholder
SimpleEntry(date: Date(), configuration: MyIntentIntent())

Provider -> getSnapshot
let entry = SimpleEntry(date: Date(), configuration: configuration)

Provider -> getTimeline
let entry = SimpleEntry(date: entryDate, configuration: configuration)

MyWidget_Previews
MyWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: MyIntentIntent()))
3.3.3 MyWidget

StaticConfiguration 改为 IntentConfiguration

StaticConfiguration(kind: kind, provider: Provider()) { entry in
...
改为
IntentConfiguration(kind: kind, intent: MyIntentIntent.self, provider: Provider()) { entry in
...
3.3.4 MyWidget_Previews

上面已修改,参看3.3.2。

这里有个问题,因为是静态修改为可编辑的,点击编辑不会出现编辑界面。删除小组件重新添加才会有。

添加一个可选项试一下吧。

【iOS】记录widget开发流程及遇到的问题_json_10

在 MyWidgetEntryView 中获取参数值显示在屏幕上

Text(entry.configuration.parameter ?? "")

【iOS】记录widget开发流程及遇到的问题_json_11

【iOS】记录widget开发流程及遇到的问题_ide_12

4、自定义可选列表

【iOS】记录widget开发流程及遇到的问题_xcode_13

【iOS】记录widget开发流程及遇到的问题_ide_14

4.1、创建IntentHandle

添加 Target:iOS -> Application Extension -> Intents Extension

【iOS】记录widget开发流程及遇到的问题_ide_15

取名 IntentHandle。这个绑定Intent文件和数据模型的,这里要设置下 target 选中小组件,就可以在小组件中调用了,Intent 的 target 要选中 Intenthandle。MyWidget.swift文件不动。

【iOS】记录widget开发流程及遇到的问题_ide_16

【iOS】记录widget开发流程及遇到的问题_ide_17

4.2、创建列表文件并添加图片资源

创建MenuJson.swift文件,这里注意target选中小组件和handle

【iOS】记录widget开发流程及遇到的问题_json_18

添加图片

【iOS】记录widget开发流程及遇到的问题_ios_19

import Foundation

struct MenuJson: Codable {
let id: String
let name: String
let image: String

static func createMenuList() -> [MenuJson] {
var list = [MenuJson]()
list.append(.init(id: "1", name: "航拍", image: "aerial.jpg"))
list.append(.init(id: "2", name: "城市", image: "city.jpg"))
list.append(.init(id: "3", name: "人物", image: "figure.jpg"))
list.append(.init(id: "4", name: "宠物", image: "pet.jpg"))
return list
}
}

这里要选中小组件的target

【iOS】记录widget开发流程及遇到的问题_ide_20

4.3、在Intent文件中添加数据

4.3.1、添加Type

添加一个type属性,Type 选择 Add Type… ,Type取名MenuList。

【iOS】记录widget开发流程及遇到的问题_ide_21

Type展示名称可自定义

【iOS】记录widget开发流程及遇到的问题_ios_22

4.4、修改 IntentHandler 文件

添加代理 MyIntentIntentHandling

添加方法 provideTypeOptionsCollection

provideTypeOptionsCollection 中 绑定数据

class IntentHandler: INExtension, MyIntentIntentHandling {

func provideTypeOptionsCollection(for intent: MyIntentIntent, with completion: @escaping (INObjectCollection<MenuList>?, Error?) -> Void) {
let list = MenuJson.createMenuList().map { (item) -> MenuList in
.init(identifier: item.id, display: item.name)
}
completion(.init(items: list), nil)
}
...

4.5、修改 MyWidget 文件

struct Provider: IntentTimelineProvider {

let list = MenuJson.createMenuList().map { (item) -> MenuList in
.init(identifier: item.id, display: item.name)
}

func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(
date: Date(),
configuration: MyIntentIntent(),
menu: list[0]
)
}

func getSnapshot(for configuration:MyIntentIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {

var firstItem = list.filter { (item: MenuList) -> Bool in
item.identifier == configuration.type?.identifier
}
if firstItem.count == 0 {
firstItem = list
}

let entry = SimpleEntry(
date: Date(),
configuration: configuration,
menu: firstItem[0]
)
completion(entry)
}

func getTimeline(for configuration:MyIntentIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {

var firstItem = list.filter { (item: MenuList) -> Bool in
item.identifier == configuration.type?.identifier
}
if firstItem.count == 0 {
firstItem = list
}

let entry = SimpleEntry(
date: Date(),
configuration: configuration,
menu: firstItem[0]
)

let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}

struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: MyIntentIntent
let menu: MenuList
}

struct MyWidgetEntryView : View {
var entry: Provider.Entry

var body: some View {

let item = MenuJson.createMenuList().first { (subItem: MenuJson) -> Bool in
subItem.id == entry.menu.identifier
}
let defaultText = "剑舞鸿门能赦汉,船沉巨鹿竞亡秦。\n-- 清 • 严遂成"
ZStack {

Image(uiImage: UIImage(imageLiteralResourceName: item?.image ?? "figure.jpg"))
.resizable()
.aspectRatio(contentMode: .fit)
VStack {
Spacer()
Text(item?.name ?? defaultText)
}

}
.widgetURL(URL(string: "widget://tap"))
}
}

@main
struct MyWidget: Widget {
let kind: String = "MyWidget"

var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: MyIntentIntent.self, provider: Provider()) { entry in
MyWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}

struct MyWidget_Previews: PreviewProvider {
static var previews: some View {
let list = MenuJson.createMenuList().map { (item) -> MenuList in
.init(identifier: item.id, display: item.name)
}
MyWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: MyIntentIntent(), menu: list[0]))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}

注:

SwiftUI Image("")添加的图片只能放在Assets里面,直接拖入项目的图片需要调用UIImage来加载

Image(uiImage: UIImage(imageLiteralResourceName: item?.image ?? "figure.jpg"))