1.创建项目
正常的创建项目流程,我使用的是Object-C语言。
创建项目完成后引入Widget Extension。
File -> New -> target-> Widget Extension ->Next
由于是加入一个新的Target,所以Widget的名字不能与项目名相同,也不能起成“Widget”(因为Widget是一个已有的类名),删除时不能只是删除文件还要在项目的Targets中删除,起已经删除过一次的名字会报找不到文件的错误。如果 Widget 支持用户配置属性(例如天气组件,用户可以选择城市),就需要勾选Include Configuration Intent这个选项。
创建后,会自动生成5个struct和自带的方法。
2.探究自带的方法用法
预览视图-Previews
代码运行的预览视图是SwiftUI新特性,会将运行成果显示在右边的视图上且支持热更新,但是会很卡,它不是Widget的必须部分,可以直接将其删除或注释。
struct WidgetView_Previews: PreviewProvider {
static var previews: some View {
WidgetViewEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemMedium))
}
}
需要注意:Widget只支持3种尺寸systemSmall (2x2)、 systemMedium (4x2)、 systemLarge(4x4)
同时,一个APP可以支持多个不同的Widget。
数据提供-Provider
Provider是Widget最重要的部分,它决定了小组件的placeholder/getSnapshot/getTimeline这三种数据的显示。在项目创建时勾选了Include Configuration Intent后的话,Provider继承自IntentTimelineProvider支持用户自主编辑,没有勾选则继承自TimelineProvider不支持用户自主编辑。
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date())
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date())
completion(entry)
}
func getTimeline(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)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
官方的示例代码的意思是:显示从现在开始的5个小时的每个小时的时间,再显示完之后又重新运行一次getTimeline。所以,我们只需要控制刷新时间的Calendar.Component的Value与entries中元素的个数,并设置TimeLine的 policy 就可以控制Widget的刷新时间,次数和方法。但是,这个刷新次数是有苹果官方是有限制的,5分钟刷新一次是极限,低于5分钟刷新官方会觉得刷新次数太多,高频率的刷新也会导致耗电量的增加。
Timeline里面有三种方式:atEnd,after(date),never
atEnd: timeline 中最后一个 entry 显示后更新。timelines 方法会重新调用。
after(date): 指定日期,重新更新timeline。
never:系统不会自动更新,除非我们主动通过 Widget Center Api 来更新。
例子:实现一个按秒刷新的时钟,为了每一秒尽可能的准确刷新就应该向entries提供0-299这300秒的300个时间数据,View展示时转换成具体到秒的字符串展示即可,运行一个周期后再次获取5分钟的时间数据。但是我测试了之后,发现在刷新了一段时间之后,小组件就不在刷新了,所以,如果要做定时器还是有问题。
var currentDate = Date()
// 每5分钟刷新一次
let refreshDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
var arr:[SimpleEntry] = []
var tempDate = Date()
for idx in 0...300 {
tempDate = Calendar.current.date(byAdding: .second, value: idx, to: currentDate)!
let tempEntry = SimpleEntry(date: tempDate, configuration: configuration)
arr.append(tempEntry)
}
let timeline = Timeline(entries: arr, policy: .after(refreshDate))
completion(timeline)
也可以主动刷新
//刷新所有 widget
WidgetCenter.shared.reloadAllTimelines
//或者
//刷新某一个widget. xxxx 是该widget的 identifier
WidgetCenter.shared.reloadTimelines(ofKind: "xxxx")
宿主App的数据有更新时,可以主动刷新 widget UI。具体怎么做呢?对于宿主App是 OC 写的项目,需要下面两步:
建立桥接文件
一般在新建 swift 文件时 Xcode 会自动弹出是否建立 bridge 文件的弹框,选择创建。
也可以自己新建一个 Header file 文件,然后指定 Targets-> Swift Compiler -> Object-C Bridging Header -> Header文件
建立swift文件
WidgetCenter是 swift 的类,这里我们新建一个 swift 文件作为我们的刷新工具类, 里面代码如下:
// 导入 widget kit 库
import WidgetKit
//声明 14以上的系统才可用此 api
@available(iOS 14.0, *)
//定义 oc 方法
@objcMembers final class WidgetKitHelper : NSObject {
class func reloadAllWidgets() {
// arm64架构真机以及模拟器可以使用
#if arch(arm64) || arch(i386) || arch(x86_64)
WidgetCenter.shared.reloadAllTimelines()
#endif
}
}
刷新的时候,直接调用
[WidgetKitHelper reloadAllWidgets]
数据共享
我们刷新 widget
是因为有数据更新了所以要刷新 UI。那 widget
如何取出宿主App 的数据进行刷新呢?也需要两步:
建立
App Group
给App Group
定义一个唯一标识比如:group.com.yourcompany.xxx
,同时,开发者证书的Capabilities
这一项也要把App Group
勾选上在
widget
中取出数据
UserDefaults(suiteName: "group.com.yourcompany.xxx").object(forKey: "xxxxxx")
下面是写组件UI的地方
UI用的是SwiftUI,具体语法可以参考这里https://gitee.com/TheAlgorithms/SwiftUI#HStack
struct WidgetViewEntryView : View {
var entry: Provider.Entry
static let taskDateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
return formatter
}()
var body: some View {
Link(destination: URL(string: "widgetDemo://799")!){
VStack {
Text("\(entry.date, formatter: Self.taskDateFormat)")
}
.frame(height: 20)
}
Link(destination: URL(string: "widgetDemo://798")!){
VStack {
Text("I'm a Button")
}
.frame(height: 20)
}
}
}
通过Link标记控件 点击对应控件,跳转到appdeleget里,根据传过来的url参数,执行对应的操作。需要注意,systemSmall,小的Widget不支持这种link点击,小的Widget点击之后 直接跳转进去app。
-(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options{
NSLog(@"%@",url.absoluteString);
if ([url.scheme isEqualToString:@"widgetDemo://799"]){
//执行跳转后的操作
}
return YES;
}
最后是小组件用户手动配置数据
创建小组件的时候,要勾选这个配置项,intentdefinition的配置页面,在下面的地方增加一个title属性,String类型,注意右边的四个选项只需要勾选第2个。
回到代码实现页面,我们只要修改下WidgetEntryView的body里面的Text内容,从entry里面获取到configuration配置的title属性:
var body: some View {
// Text(entry.date, style: .time)
Text(entry.configuration.title == nil ? "没有值" : entry.configuration.title!)
}
然后 就可以实现小组件显示用户输入文字。