以前的文章介绍了iOS14的Widget小组件的一些特性,以及如何建立静态的Widget(StaticConfiguration)。咱们知道苹果对于Widget表现的足够克制,致使iOS的Widget的交互能力很弱,只能展现静态的东西,甚至连滚动列表、输入文字都不能够。虽然能够经过时间线来作部分更新逻辑,可是对于用户来讲仍是静态的,并且一种Widget能够重复添加,若是不能用户不能自定义配置,这种能够重复添加的功能也就失去了意义。根据服务端状态来提供可配置Widget,能够带给用户更好的服务和更多元的交互体验。html
为了节省开发时间,网路部分桥接了主工程网络组件,UI上简单实现了一下主工程里九宫格的效果,美观上还有待调整。这次主要实现动态可配置。swift
先看下目前实现的动态配置小组件的Demo效果api
编辑状态下:xcode
左:Samll,右:Medium
复制代码
如下是选择配置项的页面:markdown
左:静态配置,数据是本地写死的,更新的话,须要发版。
右:按照服务端接口动态配置的可选项
复制代码
建立动态配置的小组件,分为两种情:网络
新建Widget扩展的时候勾选了Include Configuration Intent
,系统会自动建立出与Widget扩展名称相同的 xxx.intentdefinition
配置文件,而且建立了一个能够供Widget使用的自定义意图Configuration
,Widget文件中的Provider
、SimpleEntry
、WidgetConfiguration
等都自动作了相应的配置,能够直接用。app
现有的静态的Widget,改为可配置的。异步
第一种自动化程度比较高,新手作也不容易出错。下边咱们着重讲一下第二种状况。 静态Widget改为动态配置的小组件三个关键步骤:编辑器
一、在工程目录中, 新建文件,选择SiriKit Intent Definition File
,记得勾选member of the Targetide
二、点击新建的xxx.intentdefinition
文件,Xcode显示了一个空的意图定义编辑器,点击+
添加一个自定义意图(Custom Intent),选择New Intent
,命名为LJZDMultibleConfigurationIntent
,记住这个名称,很重要!
三、将Category设置为View,并选择“Intent is eligible for widgets”复选框,以代表小部件可使用该Intent。
注意:
一、注意自定义意图的名称 LJZDMultibleConfigurationIntent,Xcode编译的时候,会自动生成相关的自定义意图类相关的代码。
二、后边引用的自定义意图的类名、协议都是根据这个名称生成的。不是根据xxx.intentdefinition文件名生成的。
复制代码
四、在Parameters
下,点击+
添加参数,这里分为三大类:System Types、Enums、Types
Enums:枚举类型,提供静态的可选项,可选项数据是提早写好的,要修改须要发版。
Types:提供动态类型的可选项,能够从接口获取数据。
复制代码
选择Types
类型以后,意图定义编辑器里会在CUSTOM INTENTS
下边新增一个模块:TYPES
。Types里默认有两个参数Identifier
,displayString
,能够按需增长新的属性。
苹果的Code alone里没有增长额外属性,他是经过`Identifier`来找到具体的数据模型。
我这里新增了Widget须要的icon地址、跳转主App具体页面的路由地址、是否可用等属性,知足了UI和跳转的逻辑。
接口请求回来,直接建立对应的可选项意图模型就能够了。
复制代码
以上就是配置意图文件的部分。
意图文件建立成功后,编译工程,会自动生成一些代码,后边使用意图的过程当中,会用到这些代码
建立动态配置的Widget第二步,新增一个IntentsExtension的target。
这里有三个关键步骤:
一、在Intents扩展的target里,找到Supported Intents配置,增长前面配置的自定义意图名称LJZDMultibleConfigurationIntent
。
二、工程目录里找到xxx.intentdefinition
文件,勾选文件的Target Membership
,把xxx.intentdefinition
文件添加到IntentExtension的target里。
重要:
必定要在File inspector中,验证intent定义文件,也就是 xxx.intentdefinition 文件包含在应用程序、小部件扩展和intent扩展里
复制代码
三、在Xcode中添加了名称为IntentHandler
的意图扩展后,工程会建立一个IntentHandler.swift
的文件,生成一个IntentHandler
类,里面提供了处理数据的handler,在这里须要实现 LJZDMultibleConfigurationIntent
类的 LJZDMultibleConfigurationIntentHandling
协议方法,这个方法里去作获取配置数据的操做。
下边是LJZDMultibleConfigurationIntentHandling
协议的部分代码,这些代码是添加自定义意图后,编译工程才会生成。
也能够找到自定义意图文件LJZDMultibleConfiguration
, 直接去Xcode生成的
LJZDMultibleConfigurationIntent
文件中查完整代码。
...
@available(iOS 12.0, macOS 10.16, watchOS 5.0, *) @available(tvOS, unavailable)
@objc(LJZDMultibleConfigurationIntentHandling)
public protocol LJZDMultibleConfigurationIntentHandling: NSObjectProtocol {
/*!
@abstract Dynamic options methods - provide options for the parameter at runtime
@discussion Called to query dynamic options for the parameter and this intent in its current form.
@param intent The input intent
@param completion The response block contains options for the parameter
*/
@available(iOS 14.0, macOS 10.16, watchOS 7.0, *)
@objc(provideMultChannelsOptionsCollectionForLJZDMultibleConfiguration:withCompletion:)
func provideMultChannelsOptionsCollection(for intent: LJZDMultibleConfigurationIntent, with completion: @escaping (INObjectCollection<MultChannel>?, Error?) -> Swift.Void)
....
复制代码
首先咱们对比实现Widget的一组结构体的关键协议
协议 | WidgetConfiguration | TimelineProvider | getSnapshot | getTimeline |
---|---|---|---|---|
静态 Widget | StaticConfiguration | TimelineProvider | func getSnapshot(in context: Self.Context, completion: @escaping (Self.Entry) -> Void) | func getTimeline(in context: Self.Context, completion: @escaping (Timeline<Self.Entry>) -> Void) |
可配置 Widget | IntentConfiguration | IntentTimelineProvider | func getSnapshot(for configuration: Self.Intent, in context: Self.Context, completion: @escaping (Self.Entry) -> Void) | func getTimeline(for configuration: Self.Intent, in context: Self.Context, completion: @escaping (Timeline<Self.Entry>) -> Void) |
把静态Widget修改为可配置的Widget:
一、修改 Widget 入口函数的 Configuration ,StaticConfiguration
-> IntentConfiguration
二、修改 TimelineProvider
协议 TimelineProvider
-> IntentTimelineProvider
三、修改对应协议的必要方法
按照顺序修改对应的协议及方法,就能够正常跑起来了。
至此,静态Widget修改为可选配置Widget就算完成了!
苹果要求,Widget的视图只能经过SwiftUI编写,可是也不是全部的SwiftUI都能用。更不能使用UIViewRepresentable
或者 NSViewRepresentable
包装的 UIKit 和 AppKit 视图。具体可使用的SwiftUI视图,能够参考苹果的开发文档
Present your app’s content in widgets with SwiftUI views
单独说一下Widget里加载图片的问题,咱们先看下SwiftUI文档中Image加载图片的方式:
这里没有直接从网络获取图片的方式,只能用UIImage去加载网络图片,而后经过init(uiImage: UIImage)
的方式建立 Image
平时咱们开发中利用UIImage
加载网络图片都是异步操做,给imageView一个占位图,而后去异步请求图片信息,接口请求成功,再把图片数据渲染回去。可是到了Widget这里是行不通的,只能同步加载图片。
获取配置数据的流程如上图,同步处理图片的操做须要在渲染视图以前,因而就放到了getTimeline的方法中,icon图片较小,实际体验速度仍是挺快的。
当用户点击Widget的时候,能够直接跳转到主App的某个相关页面。苹果提供了两种方式:
* For all widgets, add the widgetURL(_:) view modifier to a view in your widget’s view hierarchy.
If the widget’s view hierarchy includes more than one widgetURL modifier, the behavior is undefined.
* For widgets using systemMedium or systemLarge, add one or more Link controls to your widget’s view hierarchy.
You can use both widgetURL and Link controls. If the interaction targets a Link control, the system uses the URL in that control.
For interactions anywhere else in the widget, the system uses the URL specified in the widgetURL view modifier.
复制代码
说明一下主要区别:
widgetURL
,可是能够有多个Link
控件。widgetURL
,以最后一个设置的为准。small
类型的Widget也是能够添加Link
控件的,至于苹果为何没有提到,个人理解是small
类型的Widget已经很小了,不建议作复杂的视图展现,使用一个widgetURL
彻底能知足使用。Widget经过widgetURL
、Link
控件打开主App,系统都会把URL
传递到onOpenURL(perform:), application(_:open:options:)
方法或者application(_:open:)
方法。
若是二者没有使用,系统会传递一个NSUserActivity
对象给onContinueUserActivity(_:perform:)
, application(_:continue:restorationHandler:)
, 或者application(_:continue:restorationHandler:)
来响应。
NSUserActivity
对象包含了与用户交互的Widget的详细信息,若是Widget是可配置的,也会包含自定义意图的配置信息。若是主App是用Swift写的,用 WidgetCenter.UserInfoKey
来获取,若是是OC写的,用WGWidgetUserInfoKeyKind
和WGWidgetUserInfoKeyFamily
来获取。
Extension程序跟主App进行数据通讯,须要配置好App Groups
,通讯方式以下:
具体使用区别算是老生常谈的问题了,这里就再也不赘述了。具体用法能够点击跳转到官方文档查看。
选择Widget-extension对应的target,Xcode运行,就能够把小组件会直接添加到设备的桌面上。可是若是你的supportedFamilies
没有指定哪种的话,你是没有办法debug的。或者当你的Widget-extension使用WidgetBundle
支持多种Widget的时候,运行不起来,由于系统并不知道你要调试哪种Widget。咱们能够经过设置Widget-extension的环境变量来解决这个问题。
找到Widget-extension的Edit Scheme,添加环境变量。
_XCWidgetKind
对应的 WidgetBundle
里的须要调试的Widget
_XCWidgetFamily
对应Widget的尺寸,能够设置为 samll
,medium
,large
_XCWidgetDefaultView
对应Widget显示的默认视图,能够设置为 timeline
,snapshot
,placeholder
运行主App的时候,也能够选择Attach To Process
的方式调试扩展程序。
Xcode菜单栏选择Debug->Attach To Process by Pid or Name
,而后填上须要调试的扩展的 Targe
名称
这个问题颇有迷惑性,在实践的过程当中,偶尔发生,偶尔又没有,新建工程,勾选Configuration Intent并无什么问题,问题是现有工程的接入问题:
针对问题静态Widget改可配置,后文教程提到。现有的OC工程接入可配置Widget,关键点仍是OC-Swift的混编,添加Bridging-Header文件后,就找不到Intent了。在Intent配置页面,配置好Intent以后,编译一下,系统会自动生成XXXIntent
类,继承自INIntent
。这里有两个关键点:
Intent类是xcode自动生成的,自动生成代码,须要找到生成规则,增长OC-Swift混编头文件后,系统默认的生成代码语言为Automatic
,Widget只支持Swift方式,在WidgetExtension的Target下,BuildSettings
找到Intent Definition Compiler
,指定语言为Swift。
好比名称是LJHotkeyConfiguration
,自动生成的类名是LJHotkeyConfigurationIntent
若是使用过程当中,仍是没法定位到自动生成的类文件,须要使用重启大法
,关闭工程,从新打开就能够了
工程从新打开后,颜色也正常了,也能够定位到类文件
以上是实现一个Widget须要用到的结构体、协议以及他们之间的依赖关系。小组件功能简单,也足够“小”,可是涉及的知识点仍是挺多的。但愿此文对你们有帮助。
Widgets Code-along, part 1: The adventure begins
Widgets Code-along, part 2: Alternate timelines