Widget支持iOS 14以上系統(tǒng),UI必須為swift UI(不能調(diào)用UIKit橋接過來,會顯示一個黃色背景的紅色圓圈斜杠),其他可以是swift和OC混編
如果要全面理解widget,需要學習(沒時間的話只看自己需要的部分)
1.swift 官方中文教程
2.swift UI Apple官方文檔
但是如果只是完成業(yè)務(wù)需求,沒有太多時間,只看下面widget介紹
首先新建Widget Extention,步驟如下圖:

勾選Include Configuration Intent選項,會多生成一個.intentdefinition文件。


這個選項的作用是配置widget控件,比如系統(tǒng)自帶的天氣,長按顯示編輯小組件


這個背景色可以在Assets.xcassets/WidgetBackground中設(shè)置
交互按鈕的字體顏色在Assets.xcassets/AccentColor中設(shè)置

注意:如果勾選Include Configuration Intent選項,并且項目配置類前綴Class Prefix,則生成的.swift文件中ConfigurationIntent類,要手動添加類前綴
先看自動生成的.swift文件,這是一個時鐘的widget。編譯一下,看能不能成功。
可以看到里面都是struct:
struct ConfigureWidget: Widgetwidget入口,也負責整體項目配置
struct ConfigureWidgetEntryView : View負責UI相關(guān)
struct Provider: TimelineProvider數(shù)據(jù)的提供者
struct SimpleEntry: TimelineEntry數(shù)據(jù)模型
struct ConfigureWidget_Previews: PreviewProviderxcode熱重載配置文件
可以看到整體是一個MVVM結(jié)構(gòu)。
這幾個struct都可以重命名,對應(yīng)更改里面的配套名字就可以,比如更改SimpleEntry數(shù)據(jù)模型,那么傳遞的數(shù)據(jù)模型都需要更改。
1.入口和配置widget的不同樣式
入口是遵守Widget協(xié)議的struct
@main
struct ConfigureWidget: Widget {
let kind: String = "ConfigureWidget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
ConfigureWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
一個widget Extention只能有一個@main入口。
kind:widget的唯一標識
provider:數(shù)據(jù)源
return ConfigureWidgetEntryView 返回,UI界面
.configurationDisplayName("Your Widget")添加widget界面,顯示的標題
.description("This is an example widget.")添加widget界面,顯示的描述
.supportedFamilies([.systemSmall])添加widget界面,顯示可以添加的widget樣式,默認是small,medium,large三種樣式用一套適配,但是一般我們?nèi)N樣式,會分別適配。有三種方式達到分別適配的效果
1.在UI層配置三種模式顯示不同的UI
加載一個widget,然后根據(jù)小中大,匹配加載不同的UI,這種方式數(shù)據(jù)沒加載出來有可能會黑屏,定制化差,不推薦這種方式
struct ConfigureWidgetEntryView : View {
var entry: Provider.Entry
@Environment(\.widgetFamily) var family: WidgetFamily
@ViewBuilder
var body: some View {
switch family {
case .systemSmall: SmallView(entry:entry)
case .systemMedium: MediumView(entry:entry)
case .systemLarge: LargeView(entry:entry)
default: SmallView(entry:entry)
}
}
}
2.返回不同的widget支持不同的樣式
不同widget可以返回多個相同樣式,比如返回多個small樣式的widget,每個widget單獨加載,對應(yīng)一個單獨的UI,小中大可以添加個性化的添加widget話術(shù),推薦這種方式
@main
struct YourWidget : WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
SmallWidget()
SmallWidget2()
SmallWidget3()
MediumWidget()
LargeWidget()
}
}
3.新建多個Widget Extention
一個APP有幾個完全不同的widget,可以用這種方式,會有多個bundleID,不推薦
2.struct SimpleEntry: TimelineEntry數(shù)據(jù)模型
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
date是必須要實現(xiàn)的,因為要遵守TimelineEntry協(xié)議,他的作用是多久刷新一次數(shù)據(jù)。
另外自己要實現(xiàn)自己需要的數(shù)據(jù)模型。
configuration是用戶自定義小組件的配置模型,在.intentdefinition文件parameter中添加參數(shù)名,選擇相應(yīng)的類型,也可以自定義類型
3.struct ConfigureWidgetEntryView : View負責UI相關(guān)
struct ConfigureWidgetEntryView : View因為遵循View協(xié)議,所以在var body中用swift UI實現(xiàn)UI相關(guān)的代碼,每次TimelineProvider中刷新UI,也會走這里的代碼,所以這里會有一個數(shù)據(jù)模型的屬性,也就是自己自定義的數(shù)據(jù)模型屬性。
4.struct Provider: TimelineProvider數(shù)據(jù)的提供者
這里就是負責加載數(shù)據(jù),有下面三個方法
// 沒加載到數(shù)據(jù)前的占位控件,但是經(jīng)過測試并不會加載這的數(shù)據(jù)
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationIntent())
}
// 譯為屏幕快照,在選擇添加控件的地方展示的數(shù)據(jù)
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), configuration: configuration)
completion(entry)
}

// 根據(jù)時間軸,獲取網(wǎng)絡(luò)真實數(shù)據(jù)
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)
}
控制器有自己的生命周期,但widget需要定時獲取數(shù)據(jù);
默認例子中是1小時執(zhí)行一次getTimeline,每次加載5條SimpleEntry數(shù)據(jù),(60/5=12min),12分鐘更新一次UI
Calendar.current.date(byAdding: .second, value: 5, to: currentDate)意思是返回當前時間+5秒鐘的時間。
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
entries中的數(shù)據(jù)執(zhí)行完之后policy: .atEnd立刻completion(timeline)重新獲取數(shù)據(jù),執(zhí)行func getTimeline。
policy:.atEnd執(zhí)行完這個entries,立刻刷新
.never不刷新,可以調(diào)用WidgetCenter.shared.reloadAllTimelines()刷新所有widget,或者指定widget刷新
.after指定時間刷新
點擊事件:
systemSmall:只能是widgetURL方式跳轉(zhuǎn)
systemMedium、systemLarge:widgetURL、Link都可以
widgetURL:
var body: some View {
VStack{
}
.widgetURL(URL(string: "www.myWidget.com"))
}
Link包裹:
var body: some View {
Link(destination: URL(string: "www.myWidget.com")!){
VStack{
}
}
}
處理事件點擊,相當于處理URL Schemes,iOS14UI生命周期交給了scene
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
for context in URLContexts {
print(context.url)
}
}
和OC混編問題
如果原項目是OC,或者現(xiàn)在widget target項目里面加入OC文件,需要加入橋接文件,如果加入.intentdefinition的配置文件,需要在橋接文件中,引入#import "ConfigurationIntent.h",否則會報錯誤Cannot find type 'ConfigurationIntent' in scope。
如果需要用cocopod中的其他庫,需要添加新的target,加入在widget擴展中需要用到的庫,比如加入AFNetworking:
target 'WidgetExtension' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
pod 'AFNetworking'
# Pods for WidgetExtension
end
如果需要主項目中的文件,在需要用到的.m文件Target Membership中勾選widgetExtention,這樣widget小組件才能用這個OC 文件??赡苄枰恍┘嫒菪愿膭樱阂胍恍┤鄙俚念^文件,勾選其他關(guān)聯(lián)文件的Target Membership,添加widget關(guān)聯(lián)。勾選后編譯一下,沒問題后把用到的OC文件添加到橋接文件中聲明,這樣swiftUI才能調(diào)用原來OC主項目的文件
可以看下我寫的另一篇,記錄widget遇到的問題http://www.itdecent.cn/p/a4d61a880bac