前言
由于公司需要,需要在桌面上增加一个小组件,让用户实时的看到盘口或者仓位信息,让我调研一番,之前没做过,经过学习现在记录一下。小组件是iOS14之后才有的功能,所以小组件功能必须要是iOS14以上。
小组件创建
-
创建一个新的target,点击左下角的"+"号,然后选中Widget Extension,点击Next,填好新target的名字,点击Finish,然后点击Active按钮。
注意:
Include ConFiguration Intent
这个选项勾选的意思就是我们创建的组件是可以被用户自定义编辑的
完成后,新的target会出现在这里。
-
这里能看到新的target的相关代码,新创建出来的文件是有默认代码的,这个@main就是小组件target的入口。
-
现在可以先运行下看看效果,图二可以看到,默认的样式是有三种的,小、中、大,截图的是中样式,iOS15之后又多了一种超大模式。
代码分析
- @main对应是小组件的入口,每一个小组件有且只能有一个入口。
@main
struct firstWidget: Widget {
//小组件的标识
let kind: String = "firstWidget"
var body: some WidgetConfiguration {
//Provider()是数据源,给小组件提供数据的,放在数组中,这里等于是对数组的遍历,entry是遵循TimelineEntry协议的结构体对象,是数组中的元素。
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
// 这里会获取到下面在Provider里getTimeline方法里放在数组中的entry对象
// 小组件的UI样式
firstWidgetEntryView(entry: entry)
}
//这里是小组件的标题,添加组件(上面截图)的时有展示
.configurationDisplayName("My Widget")
//这是对小组件的说明,添加组件(上面截图)的时候有展示
.description("This is an example
widget.")
//小组件支持的样式,iOS14有三种,小、中、大,iOS15之后有四种,多了一个超大样式。
.supportedFamilies([.systemSmall,.system
Medium,.systemLarge])
}
}
- 我们现在看看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(timeIntervalSinceNow: -12*60*60),
configuration: configuration)
completion(entry)
}
方法三:最终要的方法,获取显示数据
//小组件的内容变化都依赖于TimeLine,本质是TimeLine驱动的一连串静态视图,它其实就是一个元素为TimelineEntry的数组,如果只有一个元素,那么小组件内容是一成不变的,如果需要随着时间变化,就需要给每个元素设置合适的时间,系统会在指定的时间使用元素来驱动视图
//决定widget何时刷新
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
//页面刷新的所有数据都来自这个数组,每次刷新从这个数组里取出一个数据刷新页面
var entries: [SimpleEntry] = []
let currentDate = Date()
for hourOffset in 0 ..< 5 {
//数组里有很多元素,我们先展示一个元素后,什么时候展示下一个元素,由byAdding: .hour控制,默认是1小时
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration)
entries.append(entry)
}
//atEnd:所有的元素显示完毕后刷新,只要还有没显示完的元素,就不会刷新当前的Timeline
//never:永远不会刷新TimeLine,如果最后一个元素显示完毕,就不动了
//after:是指定下次刷新的时间,系统会在这个时间对TimeLine进行刷新
//上面可以看做是刷新的最早时间(比如说atEnd,数组中的元素刷新一遍后,立马会从头继续刷新;afte:到指定时间时立即刷新),但是因为刷新时机是由系统统一控制的,频繁的请求刷新TimeLine,可能会被系统限制反而达不到理想的刷新效果,刷新时机就会比设置的时间延后
//系统决定每个不同的TimeLine的刷新频次,超过频次的刷新请求不会生效,高频使用的小组件会获得更多的刷新频次
//这个方法的调用时机是系统说了算的,我们只能等系统刷新的时候在这里请求数据,然后刷新样式。不能轮询,因为轮询下来数据后,就是为了刷新组件样式,但是因为刷新频率太高的话就会被系统忽略,刷新无效。
//系统的刷新频率从上看到大概是每天70次,刷新时机是5分钟,但是我写demo看了下,是15分钟就会刷新一次
let timeline = Timeline(entries: entries, policy: .atEnd)
//这里就要去刷新组件的样式了
completion(timeline)
}
- Provider提供的数据源中的元素必须是遵循了TimelineEntry的元素。
struct MyNetDataWidgetEntry: TimelineEntry {
let date: Date
let configuration : MyNetDataWidgetIntent
//里面放我们要给小组件提供提供的数据,DLData是我自己创建的数据模型
let mode : DLData
}
- 数据有了,我们看看展示的UI在哪
struct firstWidgetEntryView : View {
//Provider提供的数据
var entry: Provider.Entry
var body: some View {
//创建小组件对应的view,默认就一个text
Text(entry.date, style: .time)
}
}
多组件
上面的是单个组件的的创建方法,如果我们要创建多个组件,也是可以的。
- 第一步是创建一个swift文件,注意target要选中你创建的小组件target。
默认生成的代码不是这样的,需要对它进行修改,改成下面这样子,添加main标识,注意:把之前的main标识去掉
import SwiftUI
import WidgetKit
@main
struct manyWidgets: WidgetBundle {
var body: some Widget {
//添加多个组件
firstWidget()
secondWidget()
}
}
-
我们看下组件预览,已经有6种样式了,我是添加了两个组件
可编辑的组件
-
如果你像我上面那样创建的本身就是可编辑组件的话,就会在目录看到这样一个文件
默认的名字就是Configuration,对应我们刚才代码中的
let configuration: ConfigurationIntent
,如果我们希望每个组件都有自己的编辑参数,就需要修改这个名字,它对应组件一,然后点击左下角“+”号在创建一个文件,修改名字,添加参数,对应组件二。
- 在代码中使用这个title
struct firstWidgetEntryView : View {
var entry: Provider.Entry
var body: some View { Text(entry.configuration.title ?? "")
}
}
运行程序后,看到了上面设置的默认值,长按组件选择编辑,设置新的标题后返回桌面就看到了新的标题。
- 如果你最开始创建的是一个不可编辑的组件,看不到
1
中所显示的文件,那就commadn+n键创建文件,剩下的就一样了。
组件点击跳转主项目
点击有两种方式,widgetUrl和Link,widgetUrl是作用于整个组件的,Link是作用于部分view。需要注意的是:组件的small样式是不能添加Link的,它只有一个widgetUrl,不管点击哪里,都响应的是widgetUrl,并且点击组件的任何地方,都会打开主app
- 每个组件只能添加一个widgetUrl,作用范围是整个组件。
GeometryReader{geo in
//这里就是小组件的UI内容
Text(entry.configuration.title
?? "")
}.widgetURL(URL(string: "jump://first")!)
然后在主项目的AppDelegate中的openUrl方法中获取获取到url,执行想要的操作。
if ([url.absoluteString isEqualToString:@"jump://first"]) {
NSLog(@"点击了整个first组件");
}
2.如果是想要点击局部,就需要使用Link啦
VStack(alignment:.leading,spacing:10){
Text(entry.configuration.title
??"").frame(height:50).backgrou
nd(Color.blue)
Link(destination: URL(string:
"jump://red")!){
Text("").frame(width: 100,height:30).background(Color.red)
}
Link(destination: URL(string: "jump://orange")!){
Text("").frame(width: 100, height: 30).background(Color.orange)
}
Spacer()
}
然后在主项目的AppDelegate中的openUrl方法中获取获取到url,执行想要的操作。
主项目主动刷新组件
app杀死状态:小组件的刷新时机是由系统控制的,并且每天的刷新次数有限,有网友测试说是75次,小组件内部的代码主动刷新的话,也不一定会刷新,由系统控制,它觉得你刷新过于频繁了,就禁止你刷新。
app在前台或者后台保活:是可以随时刷新组件的,并且次数不受限制。
- 创建一个swift文件
import Foundation
import WidgetKit
@objc
@available(iOS 14.0, *)
class WidgetKitManager: NSObject {
@objc
static let shareManager = WidgetKitManager()
/// MARK: 刷新所有小组件
@objc
func reloadAllTimelines() {
#if arch(arm64) || arch(i386) || arch(x86_64)
WidgetCenter.shared.reloadAllTimelines()
#endif
}
/// MARK: 刷新单个小组件
/*
kind: 小组件Configuration 中的kind
*/
@objc
func reloadTimelines(kind: String) {
#if arch(arm64) || arch(i386) || arch(x86_64)
WidgetCenter.shared.reloadTimelines(ofKind: kind)
#endif
}
}
- 在想要刷新的地方调用方法刷新组件
[WidgetKitManager.shareManager reloadAllTimelines];