簡介
新的 WidgetKit 框架和 SwiftUI 關(guān)于 widget 新的 api,創(chuàng)建的 widget能滿足 iOS绣张,iPadOS答渔,和 macOS 平臺(tái),能放在 Home screen 的任意位置侥涵。而且還擁有智能堆棧 Smart Stack 沼撕,集成siri的智能化推薦能力宋雏,能根據(jù)你使用時(shí)間,位置等因素端朵,來智能顯示組件好芭。
- Widget使用SwiftUI視圖顯示其內(nèi)容,可視化能力強(qiáng)
- 一個(gè)App 可以提供多種樣式的 Widget,可更具不同情況來顯示不同類型的Widget,用戶可以選擇將其最關(guān)心的放置在最重要的位置上冲呢,以便最方便的獲取信息舍败。
- Widget和之前的TodayWidget是一個(gè)獨(dú)立運(yùn)行的程序,需要在項(xiàng)目中進(jìn)行App Groups的設(shè)置才能使其與主程序互通數(shù)據(jù)敬拓。
- 目前存在的問題邻薯,widget不支持連續(xù)的實(shí)時(shí)更新,只能通過timeline來設(shè)置時(shí)間軸進(jìn)行更新乘凸。
- Widget 只有大中小3種尺寸(systemLarge厕诡、systemMedium、systemSmall)营勤。
項(xiàng)目構(gòu)建
添加Widget到原工程
打開你的 Xcode 工程, 并且選擇 File > New > Target灵嫌,在 Application Extension 中選擇 Widget Extension,輸入 Widget 的名字葛作,如果 Widget 提供了用戶可配置的屬性寿羞,請(qǐng)選中“ Include Configuration Intent ”復(fù)選框。
在這里簡單介紹下Widget的配置信息
小部件擴(kuò)展模板提供了一個(gè)符合小部件協(xié)議的初始小部件實(shí)現(xiàn)赂蠢。Widget的body屬性決定了該Widget是否具有用戶可配置的屬性绪穆。
創(chuàng)建widget時(shí),包含配置意圖「Include Configuration Intent」復(fù)選框決定了Xcode使用哪種配置虱岂。當(dāng)選擇這個(gè)復(fù)選框時(shí)玖院,Xcode使用將使用默認(rèn)設(shè)置進(jìn)行配置。
Configuration有兩種配置可供選擇:
1.StaticConfiguration: 對(duì)于一個(gè)沒有用戶可配置屬性的Widget第岖∧丫「例如,顯示一般市場信息的股票市場Widget蔑滓,或顯示趨勢標(biāo)題的新聞Widget郊酒。」
2.IntentConfiguration烫饼。對(duì)于一個(gè)具有用戶可配置屬性的Widget來說,你可以使用SiriKit自定義意圖來定義屬性试读。您使用 SiriKit 自定義意圖來定義屬性杠纵。「例如钩骇,一個(gè)天氣Widget需要一個(gè)城市的郵政編碼或郵政編碼比藻,或者一個(gè)包裹跟蹤Widget需要一個(gè)跟蹤號(hào)碼铝量。」
以下代碼創(chuàng)建一個(gè)常規(guī)的StaticConfiguration银亲,不可配置的狀態(tài)的 Widget:
struct TestWidget: Widget {
let kind: String = "TestWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind,provider: Provider()) { entry in
TestWidget EntryView(entry: entry)
}
.configurationDisplayName("name")
.description("des")
.supportedFamilies([.systemMedium])
}
}
kind:識(shí)別Widget的字符串慢叨。這是您選擇的標(biāo)識(shí)符,應(yīng)描述Widget所代表的內(nèi)容务蝠。
Provider:符合TimelineProvider的對(duì)象拍谐。一個(gè)符合TimelineProvider的對(duì)象,它能產(chǎn)生一個(gè)時(shí)間線馏段,告訴WidgetKit何時(shí)渲染W(wǎng)idget轩拨。時(shí)間線包含一個(gè)你定義的自定義TimelineEntry類型。時(shí)間線條目標(biāo)識(shí)了你希望WidgetKit更新Widget內(nèi)容的日期院喜。在自定義類型中包含你的Widget的視圖需要渲染的屬性亡蓉。
Placeholder:一個(gè) SwiftUI 視圖,WidgetKit 用來在第一次渲染W(wǎng)idget喷舀。占位符是您的Widget的通用表示砍濒,沒有特定的配置或數(shù)據(jù)。
Content Closure(內(nèi)容閉合):一個(gè)包含SwiftUI視圖的封閉硫麻。WidgetKit調(diào)用它來渲染W(wǎng)idget的內(nèi)容爸邢,從提供者那里傳遞一個(gè)TimelineEntry參數(shù)。
Custom Intent(自定義配置)庶香。一個(gè)定義用戶可配置屬性的自定義意圖甲棍。
包括顯示名稱(name)、描述(description)和Widget支持的系列(families)赶掖。
Provider 時(shí)間線配置(Provide Timeline Entries)
struct Provider: TimelineProvider {
public typealias Entry = SimpleEntry
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)
}
}
Provider會(huì)生成由時(shí)間線條目組成的時(shí)間線感猛,每個(gè)條目都指定了更新小組件內(nèi)容的日期和時(shí)間。
- placeholder :系統(tǒng)讓你的 view 自動(dòng)渲染一個(gè)占位圖奢赂。
- getSnapshot:快照預(yù)覽為了在小部件庫中顯示小部件陪白,在你添加widget時(shí)候,預(yù)覽的樣式膳灶。當(dāng)部件還沒有從服務(wù)器獲取狀態(tài)時(shí)咱士,Provider通過顯示一個(gè)空狀態(tài)來實(shí)現(xiàn)getSnapshot方法。
- getTimeline:在請(qǐng)求初始快照后轧钓,WidgetKit調(diào)用getTimeline(for:with:completion:)向提供者請(qǐng)求一個(gè)常規(guī)的時(shí)間線序厉。時(shí)間線由一個(gè)或多個(gè)時(shí)間線條目和一個(gè)重載策略組成,告知WidgetKit何時(shí)請(qǐng)求后續(xù)時(shí)間線毕箍。
Widget的刷新策略
Timeline里面有三種方式:atEnd弛房,after(date),never
- atEnd: timeline 中最后一個(gè) entry 顯示后更新而柑。timelines 方法會(huì)重新調(diào)用文捶。
- after(date): 指定日期荷逞,重新更新timeline。
- never:系統(tǒng)不會(huì)自動(dòng)更新粹排,除非我們主動(dòng)通過 Widget Center Api 來更新种远。
保持組件狀態(tài)為最新(Keeping a Widget Up To Date)
Widget使用SwiftUI視圖來顯示它們的內(nèi)容。WidgetKit在一個(gè)單獨(dú)的過程中代表您渲染視圖顽耳。因此坠敷,即使小組件在屏幕上,小組件擴(kuò)展也不會(huì)持續(xù)活躍斧抱。盡管widget并不總是處于活動(dòng)狀態(tài)常拓,但有幾種方法可以使其內(nèi)容保持最新。
為可預(yù)測的事件生成一個(gè)時(shí)間軸
定義widget時(shí)辉浦,實(shí)現(xiàn)一個(gè)自定義的TimelineProvider弄抬。WidgetKit從你的provider那里獲取一個(gè)時(shí)間線,并使用它來跟蹤何時(shí)更新widget宪郊。時(shí)間線是一個(gè)TimelineEntry對(duì)象的數(shù)組掂恕。時(shí)間線中的每個(gè)條目都有日期和時(shí)間,以及小組件顯示其視圖所需的附加信息弛槐。除了時(shí)間線條目懊亡,時(shí)間線還指定了一個(gè)刷新策略,該策略告訴WidgetKit何時(shí)請(qǐng)求新的時(shí)間線.
下面是蘋果官網(wǎng)給的一個(gè)顯示角色健康水平的游戲小部件的例子乎串。
當(dāng)健康水平低于100%時(shí)店枣,角色以每小時(shí)25%的速度恢復(fù)。例如叹誉,當(dāng)角色的健康水平為25%時(shí)鸯两,需要3小時(shí)才能完全恢復(fù)到100%。下圖顯示了WidgetKit如何從provider那里請(qǐng)求時(shí)間線长豁,在時(shí)間線條目中指定的每個(gè)時(shí)間渲染小部件钧唐。
func getTimeline(for configuration: CharacterSelectionIntent, with context: Self.Context, completion: @escaping (Timeline<Self.Entry>) -> ()) {
let selectedCharacter = characher(for: configuration)
let endDate = selectedCharacter.fullHealthDate
let oneHour: TimeInterval = 60 * 60
var currentDate = Date()
var entries: [SimpleEntry] = []
while currentDate < endDate {
let relevance = TimelineEntryRelevance(score: Float(selectedCharacter.healthLevel))
let entry = SimpleEntry(date: currentDate, character: selectedCharacter, relevance: relevance)
currentDate += oneHour
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
當(dāng)WidgetKit最初請(qǐng)求時(shí)間軸時(shí),provider會(huì)創(chuàng)建一個(gè)有四個(gè)條目的時(shí)間軸匠襟。第一個(gè)條目代表當(dāng)前的時(shí)間(Now)钝侠,之后是每小時(shí)一次的三個(gè)條目。在刷新策略設(shè)置為默認(rèn)的atEnd的情況下酸舍,WidgetKit會(huì)在時(shí)間線條目中的最后一個(gè)日期之后請(qǐng)求一個(gè)新的時(shí)間線帅韧。當(dāng)時(shí)間線中的每個(gè)日期到達(dá)時(shí),WidgetKit會(huì)調(diào)用小組件的內(nèi)容閉包顯示結(jié)果啃勉。在最后一個(gè)時(shí)間線條目過后忽舟,WidgetKit會(huì)重復(fù)這個(gè)過程,要求提供者提供一個(gè)新的時(shí)間線。由于角色的健康度已經(jīng)達(dá)到了100%萧诫,提供者會(huì)以當(dāng)前時(shí)間的單一條目和刷新策略設(shè)置為never來回應(yīng)。在這種設(shè)置下枝嘶,WidgetKit不會(huì)要求另一條時(shí)間線帘饶,直到應(yīng)用程序使用WidgetCenter告訴WidgetKit請(qǐng)求新的時(shí)間線。
當(dāng)時(shí)間線改變時(shí)通知WidgetKit
當(dāng)某件事情影響到小組件的當(dāng)前時(shí)間線時(shí)群扶,或者說當(dāng)你要刷新Widget的時(shí)候及刻,App可以告訴WidgetKit請(qǐng)求新的時(shí)間線。App可以告訴WidgetKit重新加載時(shí)間線并更新小組件的內(nèi)容竞阐。要重載特定類型的widget,使用WidgetCenter
WidgetCenter.shared.reloadTimelines(ofKind: "com.testWidget")
kind參數(shù)包含與用于創(chuàng)建widget的WidgetConfiguration的值相同的字符串。當(dāng)然也只有一個(gè)widget的話也可以用reloadAllTimelines舅桩。
如果widget具有用戶可配置的屬性靶草,那么通過使用WidgetCenter來驗(yàn)證是否存在具有適當(dāng)設(shè)置的widget,從而避免不必要的重新加載幕垦。
WidgetCenter.shared.getCurrentConfigurations { result in
guard case .success(let widgets) = result else { return }
if let widget = widgets.first(
where: { widget in
let intent = widget.configuration as? SelectCharacterIntent
return intent?.character == characterThatReceivedHealingPotion
}
) {
WidgetCenter.shared.reloadTimelines(ofKind: widget.kind)
}
}
如果只有一個(gè)Widget丢氢,你可以使用WidgetCenter來重新加載所有widget的時(shí)間線。
WidgetCenter.shared.reloadAllTimelines()
Link WidgetUrl
從 Widget 跳轉(zhuǎn)到 App 指定界面先改,只需要用 SwiftUI Link 的方式疚察,View 的外層包上一個(gè) Link,destination 是設(shè)定好的 url仇奶,就能實(shí)現(xiàn)跳轉(zhuǎn)了貌嫡。
Link(destination: URL(string: topJump.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)!) {
HStack() {
Spacer()
.frame(width: Len(15))
// titile
Text(topDesc)
.fontWeight(.regular)
.font(.system(size: Len(14)))
.foregroundColor(Color(red: 51/255, green: 51/255, blue: 51/255, opacity: 1.0))
Image(triangleIcon)
.padding(.trailing, 4.0)
.frame(width: Len(8), height: Len(8))
Spacer()
}
}
widgetURL:在View上加一個(gè)widgetURL ,URL是設(shè)定好的 url该溯,就能實(shí)現(xiàn)跳轉(zhuǎn)了
HStack() {
Spacer()
.frame(width: UMEWLen(15))
// titile
Text(newBean.topDesc)
.fontWeight(.regular)
.font(.system(size: Len(14)))
.foregroundColor(Color(red: 51/255, green: 51/255, blue: 51/255, opacity: 1.0))
Image(triangleIcon)
.padding(.trailing, 4.0)
.frame(width: Len(8), height: Len(8))
Spacer()
}.widgetURL(URL(string: topJump.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)!)
Widget bundles
有時(shí)候我們會(huì)有多個(gè)樣式不同種類的 Widget岛抄,就需要用 @WidgetBundleBuilder 把多個(gè) Widget 放在一起
@main
struct MainBundle: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
oneWidget()
twoWidget()
threeWidget()
}
}
Intent
在你的Xcode項(xiàng)目中,選擇File > New File并選擇SiriKit Intent Definition File朗伶。點(diǎn)擊 "下一步 "并在提示時(shí)保存文件弦撩。Xcode創(chuàng)建一個(gè)新的.intenttdefinition文件,并將其添加到項(xiàng)目中论皆。
Xcode從意圖定義「intent definition file」文件中生成代碼益楼。要在一個(gè)目標(biāo)「target」中使用這些代碼。
-將intent定義文件作為目標(biāo)的一個(gè)成員点晴。
-通過添加 intent 的類名到 target 屬性的 Supported Intents 部分來指定要包含在 target 中的特定 intents感凤。
稍后會(huì)上傳demo和自己遇到的一些坑
[NewWidget]https://github.com/moneyYouCai/NewWidget-iOS14