1、基本介绍
iOS 14 中新增了 WidgetExtension,用来取代之前的 TodayExtension。相比之前的简单变了很多,但Widget Extension 只支持 SwiftUI。
2、WidgetExtension 创建
打开 File -> New -> Target -> Widget Extension
2.1、Live Activity 简介
通过调用Live Activity(实时活动) 来实现锁屏下方和 Dynamic Island(灵动岛)区域的功能。
只不过之前这个行为是 iOS 内置的,并且相关的UI 也都是固定的, 没有提供给我们接口去自定义它。
Live Activity 对应的 ActivityKit 就是 iOS 16.1 版本中引入的新库。
2.2、Include Configuration Intent
小组件提供用户可配置的属性,实现动态布局
3、结构介绍
@main 是主入口,这里可以设置小组件的 Provider(可以理解为控制器) 以及 WidgetEntryView(可以理解为主视图 View),以及长按后弹出框的 APP 信息设置。
Provider:控制器,这里可以用来做小组件的刷新操作
SimpleEntry: 这个是数据模型,Provider 里如果想更新数据到 WidgetEntryView,必须通过 SimpleEntry 来实现,但是这个必须继承 TimelineEntry。可以新增参数,变量,用来传递自己需要的数据类型。
WidgetEntryView: 主视图,在这里自定义页面用来显示在手机桌面。
3.1、 主函数入口
@main // 通过这个注解来声明这个结构体是主函数入口
struct WidgetDemo: Widget {
// 小组件的唯一标识
let kind: String = "WidgetDemo"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
WidgetDemoEntryView(entry: entry)
// 小组件跳转URL
.widgetURL(URL(string: "Scheme://openPage"))
}
// 小组件标题
.configurationDisplayName("My Widget")
// 小组件的描述
.description("This is an example widget.")
// 用户选择小部件、中版本或大版本的组件
.supportedFamilies([.systemMedium, .systemSmall, .systemLarge])
}
}
3.2、 provider
// 声明一个遵守TimelineProvider协议的结构体 实现里面的3个方法
struct Provider: IntentTimelineProvider {
// 占位视图,例如网络请求失败、发生未知错误、第一次展示小组件都会展示这个view
// 在编辑小组件的时候 会调用这个方法 返回一个SimpleEntry实例 然后用这个实例去创建 前面说到的自定义view 然后显示那个view
// 因为编辑小组件的时候只是一个样式展示 数据不需要真实的 所以用固定的数据初始化一个SimpleEntry对象直接返回就好了 不需要网络请求等异步处理
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationIntent())
}
// 这是第一次显示小组件的时候 调用这个方法 这里就是要显示真实的数据了 所以可以在代码块里 做网络请求的异步的操作
// 在completion回调中把实例好的entry对象传出去 之后系统就会用这个实例去初始化一个TestWidgetEntryView对象并显示
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), configuration: configuration)
completion(entry)
}
// 决定 Widget 何时刷新
// 这个方法返回一个时间线 用来描述小组件的刷新规则
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)
}
}
3.3、 SimpleEntry 数据模型
// 渲染 Widget 所需的数据模型,需要遵守TimelineEntry协议
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
3.4、 WidgetEntryView
// 渲染的view
struct WidgetDemoEntryView : View {
var entry: Provider.Entry
// 判断小组件的类型
@Environment(\.widgetFamily) var family: WidgetFamily
var body: some View {
switch family {
case .systemSmall:
VStack(alignment: .center, spacing: 0) {
Text("小")
Spacer().frame(height: 20)
Text(entry.date, style: .time)
}
case .systemMedium:
VStack(alignment: .center, spacing: 0) {
Text("中")
Spacer().frame(height: 20)
Text(entry.date, style: .time)
}
default:
VStack(alignment: .center, spacing: 0) {
Text("大")
Spacer().frame(height: 20)
Text(entry.date, style: .time)
}
}
}
}
3.5、预览
// 这个view是用来预览swiftUI的 点击xcode顶部菜单的 Editor->Canvas会显示实时画布 如果不需要预览 这段代码可以直接删除
struct WidgetDemo_Previews: PreviewProvider {
static var previews: some View {
WidgetDemoEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemLarge))
}
}
4、小组件点击交互及传值
4.1 Widget Extension 发送数据
以 URL 的形式发送交互参数,有 widgetURL,Link 两种发送方式。
widgetUrl 是针对整个小组件 点击小组件响应(如果有Link 就响应Link)
LinK 给元素添加点击事件, Link 对 systemSmall样式的组件不生效(systemSmall 样式的小组件只响应widgetUrl)
数据发送方式 | 支持类型 |
---|---|
widgetURL | systemSmall,systemMedium,systemLarge |
Link | systemMedium,systemLarge |
// 渲染的view
struct WidgetDemoEntryView : View {
var entry: Provider.Entry
// 判断小组件的类型
@Environment(\.widgetFamily) var family: WidgetFamily
var body: some View {
// Text(entry.date, style: .time)
switch family {
case .systemSmall:
VStack(alignment: .center, spacing: 0) {
Link(destination: URL(string: "Scheme://openTitlePage")!){
Text("小")
}
Spacer().frame(height: 20)
Text(entry.date, style: .time)
}.widgetURL(URL(string: "Scheme://openPage"))
case .systemMedium:
VStack(alignment: .center, spacing: 0) {
Link(destination: URL(string: "Scheme://openTitlePage")!){
Text("中")
}
Spacer().frame(height: 20)
Text(entry.date, style: .time)
}.widgetURL(URL(string: "Scheme://openPage"))
default:
VStack(alignment: .center, spacing: 0) {
Link(destination: URL(string: "Scheme://openTitlePage")!){
Text("大")
}
Spacer().frame(height: 20)
Text(entry.date, style: .time)
}.widgetURL(URL(string: "Scheme://openPage"))
}
}
}
Link(destination: URL(string: "Scheme://openPage")!){
Text("text")
}
4.2 APP 端接收数据
这里需要注意,以前这种值传递是使用 AppDelegate 中的 openURL 方法来处理的。但是 iOS 14 的小组件数据交互只能用 SceneDelegate 来接收,交互事件会走到 SceneDelegate 中的 openURLContexts 方法中。
// AppDelegate
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
NSLog(@"交互 = %@",url);
return YES;
}
// SceneDelegate
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts
{
NSLog(@"交互 = %@",URLContexts.allObjects.firstObject.URL);
}
5、和app本地数据进行交互 AppData
小组件和app交互是通过AppGroups的交互的
主项目和子项目都需要创建AppGroups
OC 需要创建桥接文件
创建WidgetKitManager.swift 第一次的话会自动配置
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
}
}
OC
#import "WidgetController.h"
#import "StudyOC-Swift.h"
@interface WidgetController ()
@end
@implementation WidgetController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self initWidget];
}
- (void)initWidget{
UIButton *btn = [UIButton new];
[btn setTitle:@"刷新" forState:UIControlStateNormal];
[btn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
btn.frame = CGRectMake(0, 100, 100, 100);
[btn addTarget:self action:@selector(clicktBtn1) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
- (void)clicktBtn1{
// [WidgetCenter];
//使用 Groups ID 初始化一个供 App Groups 使用的 NSUserDefaults 对象
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.zhahntest"];
//写入数据
[userDefaults setValue:@"123456789" forKey:@"userID"];
//读取数据
NSString *userIDStr = [userDefaults valueForKey:@"userID"];
NSLog(@"zzr123 = %@",userIDStr);
[[WidgetKitManager shareManager] reloadAllTimelines];
}
6、刷新机制
刷新分被动刷新和主动刷新
6.1、主动刷新
WidgetCenter.shared.reloadAllTimelines()
// 刷新指定的kind
// WidgetCenter.shared.reloadTimelines(ofKind: <#T##String#>)
6.2、被动刷新
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
7、要注意的点
7.1、getTimeline中的时间线设置
官方文档中有提到 每个小组件的一天的刷新次数、频率是有限制的 ,可能为了避免所有APP不停的刷新会影响手机性能吧。而且刷新不会很及时 比如我自己设置的3分钟的间隔,一般刷新的时间是在3~4分钟之间,如果时间设置的是十几秒的话就完全不刷新,但是官方文档又有说测试环境不限制,但是不知道为什么频率高了刷新直接无效。