2020 年 6 月 22 日,蘋(píng)果召開(kāi)了第一次線上的開(kāi)發(fā)者大會(huì) - WWDC20。這次發(fā)布會(huì)上宣布了ARM架構(gòu)Mac芯片(
拳打Intel)、iOS 14 ATT(腳踢Facebook),可謂是一次載入史冊(cè)(我是爸爸)的發(fā)布會(huì)了,當(dāng)然還發(fā)布了被稱(chēng)為下一個(gè)頂級(jí)流量入口的Widget
。踩著八月的尾巴,本次我們就來(lái)探究一下Widget。
本文會(huì)從
Widget初窺
和Widget開(kāi)發(fā)
兩個(gè)維度和章節(jié)來(lái)探究一下Widget,
其中初窺
章節(jié)會(huì)帶您簡(jiǎn)單的了解一下Widget,適合應(yīng)用決策者閱讀;
開(kāi)發(fā)
章節(jié)會(huì)帶著您一步一步的完成設(shè)計(jì)開(kāi)發(fā)Widget,適合程序員閱讀。
Widget初窺
一、Widget是什么
In iOS 14, we have a dramatic new Home screen experience, one that is much more dynamic and personalized, with a focus on widgets.
The content is the focus.
This is very important:widgets are not mini-apps.
Think of this as more projecting content from your app onto the Home screen rather than full mini-apps filled with tiny little buttons.
一句話來(lái)說(shuō):Widget不是迷你應(yīng)用程序。而是一種新的主屏幕體驗(yàn),能快速提供用戶(hù)關(guān)心的內(nèi)容是重點(diǎn)
二、Widget的特點(diǎn)
-
Glanceable
一目了然 -
Relevant
早韭晚菘 -
Personalized
量體裁衣
要設(shè)計(jì)一個(gè)優(yōu)秀的Widget,就要先了解Widget的全部特點(diǎn),了然于胸
針對(duì)Apple提出的Glanceable
、Relevant
、Personalized
分別用一個(gè)成語(yǔ)來(lái)形容就是一目了然
、早韭晚菘
、量體裁衣
簡(jiǎn)單來(lái)說(shuō)下這幾個(gè)特點(diǎn)
1、Glanceable | 一目了然,一覽無(wú)余
一個(gè)優(yōu)秀的Widget要一目了然,一覽無(wú)余。
普通人每天進(jìn)入“主屏幕”的次數(shù)超過(guò)90次,但是在主屏幕僅停留幾分鐘,就切換到其他App了。
所以Widget一定要充分利用狹小的屏幕展示最核心的信息,并且要簡(jiǎn)潔明了。設(shè)計(jì)新穎,便于快速瀏覽,高效是一個(gè)優(yōu)秀Widget的核心。
用戶(hù)不用思考這個(gè)Widget怎么使用,不需要點(diǎn)擊任何按鈕就可以獲得最關(guān)心的信息。
2、Relevant | 早韭晚菘
蘋(píng)果希望Widget可以和用戶(hù)緊密結(jié)合,與用戶(hù)的行為所關(guān)聯(lián),比如早上起床,用戶(hù)希望看一下天氣;中午恰飯,用戶(hù)希望有人推薦下附近的美食;晚高峰的時(shí)候,用戶(hù)希望了解一下行車(chē)路線;晚安的時(shí)候,希望記錄下次日的行程。
為此,蘋(píng)果系統(tǒng)提供了一個(gè)叫Smart Stacks(智能疊放)的功能,Smart Stacks是一個(gè)Widgets的集合。系統(tǒng)會(huì)根據(jù)每個(gè)人的習(xí)慣,自動(dòng)顯示用戶(hù)當(dāng)前時(shí)間點(diǎn)最需要的Widget。
<div style="align:center"><img src="https://guojunliu.github.io/images/widget/3.gif"/></div>
3、Personalized 量體裁衣
3.1 大小
Widget要能為用戶(hù)提供個(gè)性化的服務(wù),比如天氣Widget,需要能為不同的用戶(hù)提供不同細(xì)節(jié)的天氣情況。
為此Apple提供三種不同大小的小部件
systemSmall
systemMedium
systemLarge
其中systemSmall大小為2*2 Icon
,systemMedium大小為4*2 Icon
,systemLarge大小為4*4 Icon
,具體的顯示效果如下
3.2 個(gè)性化配置
另一方面Widget需要能為不同城市的用戶(hù)提供當(dāng)?shù)氐奶鞖馇闆r。
為此Apple在創(chuàng)建Widget時(shí)為開(kāi)發(fā)者提供了兩種類(lèi)型:
StaticConfiguration
:對(duì)于沒(méi)有用戶(hù)可配置屬性的窗口小部件,也就是用戶(hù)無(wú)需配置,展示的內(nèi)容只和用戶(hù)信息有關(guān)系。例如,顯示一般市場(chǎng)信息的股市窗口小部件,或顯示趨勢(shì)頭條的新聞窗口小部件。IntentConfiguration
:對(duì)于具有用戶(hù)可配置屬性的窗口小部件,也就是支持用戶(hù)配置及用戶(hù)意圖的推測(cè)。您使用SiriKit自定義意圖來(lái)定義屬性。例如,需要一個(gè)城市的郵政編碼的天氣小部件,或者需要一個(gè)跟蹤號(hào)的包裹跟蹤小部件。
需要說(shuō)明的是,IntentConfiguration并不需要編寫(xiě)代碼,只需要簡(jiǎn)單的配置,Xcode 會(huì)自動(dòng)幫你生成對(duì)應(yīng)的代碼和類(lèi)型。
3.3 黑暗模式
此外Widget還支持系統(tǒng)的黑暗模式
三、Widget的本質(zhì)
Widget的本質(zhì)是一系列靜態(tài)視圖堆疊而成的集合,不同的時(shí)間點(diǎn)展示不同的視圖
這里要引入Widget的核心Timeline
顧名思義,Timeline就是一條時(shí)間線,在對(duì)應(yīng)的時(shí)間點(diǎn)發(fā)生對(duì)應(yīng)的事件
許多Widget具有可預(yù)測(cè)的時(shí)間點(diǎn),在這些時(shí)間點(diǎn)更新其內(nèi)容是有意義的。例如,顯示天氣信息的小部件可能會(huì)在一整天內(nèi)每小時(shí)更新一次溫度。股市窗口小部件可以在公開(kāi)市場(chǎng)時(shí)間頻繁更新其內(nèi)容,但周末則不用完全更新。通過(guò)提前計(jì)劃這些時(shí)間,生成不同的視圖放入時(shí)間線中,WidgetKit會(huì)在適當(dāng)?shù)臅r(shí)間到來(lái)時(shí)自動(dòng)刷新您的窗口小部件。
這也決定了Widget基本上不能實(shí)時(shí)更新
另外值得一提的是,WidgetKit會(huì)把 Timelines 所定義的Views 結(jié)構(gòu)信息緩存到磁盤(pán),然后在刷新的時(shí)候才通過(guò) JIT 的方式來(lái)渲染。這使得系統(tǒng)可以在極低電量開(kāi)銷(xiāo)下為眾多 Widgets 處理 Timelines 信息。
四、用戶(hù)交互
不好意思,沒(méi)有交互?。?!
為了實(shí)現(xiàn)以上的特點(diǎn),Apple也移除限制了Widget的一些功能
- 不能交互
- 不能播放動(dòng)畫(huà)
- 不能播放視頻
- 不支持滾動(dòng)
- 不支持主動(dòng)刷新視圖
唯一支持的只有用戶(hù)點(diǎn)擊Widget喚起主App
其中點(diǎn)擊喚起主App有兩種方案,分別是:
- widgetURL
- Link
widgetURL喚起App的點(diǎn)擊區(qū)域是Widget的所有區(qū)域,這種方案適合簡(jiǎn)單元素,單一邏輯的小部件
對(duì)于systemSmall類(lèi)型的小部件,只支持widgetURL喚起方式
針對(duì)systemMedium
和systemLarge
還可以使用更細(xì)分的Link喚起方式,這種喚起方式能讓小部件通過(guò)不同元素的點(diǎn)擊喚起App的不同頁(yè)面,讓開(kāi)發(fā)者有更多的施展空間
舉個(gè)簡(jiǎn)單的例子,widgetURL可應(yīng)用于天氣小部件,博客小部件,點(diǎn)擊直達(dá)App;
Link可用于備忘錄和日歷小部件,點(diǎn)擊不同的備忘錄和日期直接跳轉(zhuǎn)到對(duì)應(yīng)的備忘錄詳情和待辦詳情頁(yè)面
Widget初窺總結(jié)
Widget的出現(xiàn)猶如在一潭死水的iOS桌面上泛起了一片漣漪,一定會(huì)有很多App來(lái)爭(zhēng)奪這塊肥肉一般的流量入口。
但是仔細(xì)研究一下會(huì)發(fā)現(xiàn),Apple這次推出的Widget非??酥疲](méi)有非常激進(jìn),
俗話說(shuō):喜歡是放肆,但愛(ài)就是克制。這里不得不再次引用Apple在Widget介紹中出現(xiàn)頻率最高的話widgets are not mini-apps
,因?yàn)閃idget在設(shè)計(jì)之初就是為了能使用最少的成本,向用戶(hù)提供最核心的信息。為了盡可能的減少用戶(hù)成本(電量,網(wǎng)絡(luò)等)和提高用戶(hù)體驗(yàn),Apple在技術(shù)層面上做了很多限制,限制了非常多的功能,大大削弱了Widget的地位和重要程度,也降低了開(kāi)發(fā)者實(shí)現(xiàn)的熱情和積極性
其實(shí)每年Apple更新的新技術(shù)只有很少的一部分能應(yīng)用到App上,希望這次的Widget能有動(dòng)力讓大家結(jié)合自己的App,給自己的App帶來(lái)更多的流量,也能給用戶(hù)帶來(lái)更好的體驗(yàn)。
Widget開(kāi)發(fā)
重頭戲來(lái)啦,接下來(lái)讓我們一步一步設(shè)計(jì)編寫(xiě)出優(yōu)秀的小部件吧
開(kāi)始之前,首先我們要介紹下Widget的開(kāi)發(fā)語(yǔ)言,Apple特別指定了小部件只能使用SwiftUI來(lái)開(kāi)發(fā)
一、SwiftUI
現(xiàn)在iOS主流的開(kāi)發(fā)語(yǔ)言還是Objective-C,那Apple為什么要選擇2019 WWDC發(fā)布迄今為止只有一年的SwiftUI呢?
首先,從一開(kāi)始就將小部件實(shí)現(xiàn)多平臺(tái)化是Apple的一個(gè)目標(biāo),SwiftUI在跨設(shè)備展示的能力上是一把大殺器;
其次SwiftUI還使自動(dòng)布局和暗模式等功能變得非常容易,降低了適配等開(kāi)發(fā)成本,對(duì)不需要太多元素的小部件來(lái)說(shuō),SwiftUI重點(diǎn)關(guān)注布局的特點(diǎn)無(wú)疑是最合適的;
從另一方面來(lái)講,只有使用 SwiftUI 才能達(dá)到我們上邊說(shuō)的對(duì)于 Widget 的限制。如果可以使用 Objective-C UIKit 的話,我們強(qiáng)大的開(kāi)發(fā)者可能會(huì)想出無(wú)數(shù)的黑科技來(lái)忽略Apple真的小部件的限制。比如開(kāi)發(fā)無(wú)法使用 UIViewRepresentable 來(lái)橋接 UIKit;
最后Apple也夾帶了自己的私心,Apple今年已經(jīng)將 Swift 語(yǔ)言和 SwiftUI 的重要程度提升到了一個(gè)新的高度,Swift已經(jīng)可以獨(dú)立于Foundtion框架,那么對(duì)應(yīng)的SwiftUI也應(yīng)該不依賴(lài)于UIKit框架了,強(qiáng)行使用SwiftUI可以使開(kāi)發(fā)人員盡可能容易地將其學(xué)習(xí)其內(nèi)容并應(yīng)用于iOS,iPadOS和macOS,
畢竟5月份卡位第20位Objective-C在6月份已經(jīng)跌出了前20
這里要重點(diǎn)說(shuō)明一下,Widget只要使用任何 UIKit 的元素就會(huì)直接 Crash
二、將小部件目標(biāo)添加到您的應(yīng)用
窗口小部件擴(kuò)展模板提供了創(chuàng)建窗口小部件的起點(diǎn)。單個(gè)小部件擴(kuò)展可以包含多種小部件。例如,一個(gè)體育應(yīng)用程序可能有一個(gè)顯示團(tuán)隊(duì)信息的小部件,另一個(gè)顯示游戲時(shí)間表的小部件。一個(gè)小部件擴(kuò)展可以包含兩個(gè)小部件。盡管建議將所有窗口小部件包含在一個(gè)窗口小部件擴(kuò)展中,但如有必要,可以添加多個(gè)擴(kuò)展。
在Xcode中打開(kāi)您的應(yīng)用程序項(xiàng)目,然后選擇“文件”>“新建”>“目標(biāo)”。
從“應(yīng)用程序擴(kuò)展”組中,選擇“窗口小部件擴(kuò)展”,然后單擊“下一步”。
輸入您的包名。
如果窗口小部件提供了用戶(hù)可配置的屬性,請(qǐng)選中
Include Configuration Intent
復(fù)選框。
單擊完成。
重點(diǎn):小部件不僅支持Swift項(xiàng)目,同樣也支持Objective-C項(xiàng)目,OC小伙伴不用擔(dān)心啦
創(chuàng)建完小部件之后,我們會(huì)多出一個(gè)SmileEverydayWidget.swift文件,這已經(jīng)是一個(gè)可以run起來(lái)的小部件了,因?yàn)槲覀兘酉聛?lái)要逐個(gè)方法來(lái)分析,所以先將文件全文展示如下
//
// SmileEverydayWidget.swift
// SmileEverydayWidget
//
// Created by steve on 2020/8/28.
//
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
public typealias Entry = SimpleEntry
public func snapshot(with context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date())
completion(entry)
}
public func timeline(with 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)
}
}
struct SimpleEntry: TimelineEntry {
public let date: Date
}
struct PlaceholderView : View {
var body: some View {
Text("Placeholder View")
}
}
struct SmileEverydayWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.date, style: .time)
}
}
@main
struct SmileEverydayWidget: Widget {
private let kind: String = "SmileEverydayWidget"
public var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider(), placeholder: PlaceholderView()) { entry in
SmileEverydayWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
struct SmileEverydayWidget_Previews: PreviewProvider {
static var previews: some View {
/*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/
}
}
這里的概念和代碼比較多,接下來(lái)我們一個(gè)一個(gè)來(lái)解釋
三、Widget API
首先我們從帶有main
字段的方法來(lái)說(shuō)起,
@main
struct SmileEverydayWidget: Widget {
private let kind: String = "com.steve.liu.smileEverydayWidget"
public var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider(), placeholder: PlaceholderView()) { entry in
SmileEverydayWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
大家都知道帶有main
標(biāo)識(shí)的方法都是程序的入口
這段代碼使用SwiftUI聲明了一個(gè)名為SmileEverydayWidget的小部件,其中StaticConfiguration
是小部件的初始化方法,它有幾個(gè)參數(shù):
kind
provider
placeholder
其中
kind
是標(biāo)識(shí)小部件的字符串,并且應(yīng)描述小部件所代表的內(nèi)容。即小部件的包名
provider
為時(shí)間線提供者
PlaceholderView
為占位視圖
同時(shí)也提供了一些方法,例如
-
configurationDisplayName()
設(shè)置小部件顯示的名稱(chēng) -
description()
設(shè)置小部件的描述 -
supportedFamilies()
設(shè)置小部件支持的尺寸
這里有一個(gè)重點(diǎn),為了使某個(gè)應(yīng)用程序的窗口小部件出現(xiàn)在窗口小部件庫(kù)中,用戶(hù)必須在安裝該應(yīng)用程序后至少啟動(dòng)一次包含該窗口小部件的應(yīng)用程序。
四、WidgetEntryView
WidgetEntryView就是使用SwiftUI布局的小部件視圖
struct SmileEverydayWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.date, style: .time)
}
}
例如這個(gè)小部件視圖就簡(jiǎn)單的展示了當(dāng)前的時(shí)間
接下來(lái)我們可以將默認(rèn)的布局更改為我們自己想要的布局,例如我在設(shè)置了顯示文本的字體和小部件的背景圖
struct SmileEverydaWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
return Text(entry.message)
.background(Image(entry.backgroundImageStr))
.font(.callout)
}
}
五、PlaceholderView
每種小部件都需要提供占位符UI。
占位符UI是窗口小部件的默認(rèn)內(nèi)容。
它應(yīng)該代表您的小部件類(lèi)型,但僅此而已。
此用戶(hù)界面中不應(yīng)有任何用戶(hù)數(shù)據(jù)。
想象一下
如果在用戶(hù)的主屏幕上出現(xiàn)如下的場(chǎng)景,那么你的小部件離被移除可能已經(jīng)不遠(yuǎn)了
六、TimelineEntry
我們知道小部件是按照時(shí)間線來(lái)展示的,TimelineEntry
時(shí)間線上的一個(gè)個(gè)條目
struct SimpleEntry: TimelineEntry {
public let date: Date
}
TimelineEntry有一個(gè)必須有的屬性就是date
,也就是這個(gè)條目在時(shí)間線上的具體時(shí)間
另外開(kāi)發(fā)者可以在TimelineEntry里自定義各種屬性,用來(lái)給小部件視圖提供數(shù)據(jù)
例如我在TimelineEntry里自定義了message
和backgroundImageStr
屬性,用來(lái)顯示小部件上的文字和背景圖片
struct SimpleEntry: TimelineEntry {
public let date: Date
public let message: String
public let backgroundImageStr : String
}
七、TimelineProvider
TimelineProvider
是一個(gè)提供了上述我們所說(shuō)的TimelineEntry
集合的對(duì)象
我們來(lái)看下具體的代碼:
struct Provider: TimelineProvider {
public typealias Entry = SimpleEntry
public func snapshot(with context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date())
completion(entry)
}
public func timeline(with 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)
}
}
其中包含兩個(gè)方法
- snapshot()
- timeline()
1、snapshot 快照
為了在小部件庫(kù)中顯示小部件,WidgetKit 要求提供者提供預(yù)覽快照, 即snapshot()
,這個(gè)方法里主要提供了一些示例數(shù)據(jù),最好是真實(shí)數(shù)據(jù)
,用于時(shí)間線不能展示
的時(shí)候展示給用戶(hù)
需要說(shuō)明的是,快照是系統(tǒng)需要快速顯示單個(gè)條目的位置。
因此,您的擴(kuò)展程序必須盡快返回視圖,因?yàn)檫@樣做時(shí),用戶(hù)會(huì)在iOS上漂亮的Widget Gallery中看到真正的Widget。
這不是我們?cè)谠O(shè)計(jì)時(shí)必須提供的屏幕截圖或圖像。這是用戶(hù)在iOS,iPadOS和macOS上真正的小部件體驗(yàn)。
在大多數(shù)情況下,時(shí)間軸的第一個(gè)條目和快照可以作為同一條目返回,因此,在“小工具庫(kù)”中看到的就是用戶(hù)將其添加到設(shè)備中時(shí)得到的內(nèi)容。
例如我們?cè)赪idget Gallery中添加電池小部件時(shí),小部件此時(shí)在Widget Gallery中展示的就是當(dāng)前設(shè)備電池信息的實(shí)時(shí)數(shù)據(jù)的快照,而不是一些虛假的數(shù)據(jù),這個(gè)時(shí)候小部件的數(shù)據(jù)是什么樣子,用戶(hù)添加到主屏幕上之后小部件的數(shù)據(jù)就是什么樣子,從而提高用戶(hù)的體驗(yàn)。
對(duì)比
小部件里有兩個(gè)比較類(lèi)似的概念,PlaceholderView
和snapshot
,都是一種占位解決方案,不同的是PlaceholderView
是在主屏幕上無(wú)法快速獲取數(shù)據(jù)時(shí)的一種占位視圖,不至于顯示loading或者白屏給用戶(hù)看;而snapshot
主要用于Widget Gallery
中,用來(lái)提高用戶(hù)體驗(yàn)的,一般來(lái)說(shuō),snapshot
就是時(shí)間線的第一幀
2、timeline
在請(qǐng)求初始快照后,WidgetKit調(diào)用timeline
以請(qǐng)求提供者的常規(guī)時(shí)間軸。時(shí)間軸由一個(gè)或多個(gè)時(shí)間軸條目TimelineEntry
以及一個(gè)重載策略ReloadPolicy
組成,該重載策略通知WidgetKit何時(shí)請(qǐng)求后續(xù)時(shí)間軸。
關(guān)于重載策略,提供了以下幾種策略
-
atEnd
: 是指 Timeline 執(zhí)行到最后一個(gè)時(shí)間片的時(shí)候再刷新。 -
atAfter
: 是指在某個(gè)時(shí)間以后有規(guī)律的刷新 -
never
:是指以后不需要刷新了。什么時(shí)候需要重新刷新需要 App 重新告知 Widget
根據(jù)上邊的分析,我們可以將TimelineProvider
改造如下
struct Provider: TimelineProvider {
public typealias Entry = SimpleEntry
public func snapshot(with context: Context, completion: @escaping (SimpleEntry) -> ()) {
let date = Date()
let message = "蒹葭蒼蒼,白露為霜。所謂伊人,在水一方。"
let backgroundImageStr = "bg7"
let entry = SimpleEntry(date: date, message: message, backgroundImageStr: backgroundImageStr)
completion(entry)
}
public func timeline(with context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
var currentDate = Date()
var nextUpdateDate = Calendar.current.date(byAdding: .second, value: 3, to: currentDate)!
let message = "蒹葭蒼蒼,白露為霜。所謂伊人,在水一方。\n溯洄從之,道阻且長(zhǎng);溯游從之,宛在水中央。\n蒹葭凄凄,白露未晞。所謂伊人,在水之湄。"
let backgroundImageStr = "bg"
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
var currentDateStr = ""
var nextUpdateDateStr = ""
let longUuid = UUID().uuidString
let range: Range = longUuid.range(of: "-")!
let location: Int = longUuid.distance(from: longUuid.startIndex, to: range.lowerBound)
let uuid = longUuid.prefix(location)
for i in 1 ..< 10 {
var msg = ""
currentDate = nextUpdateDate;
nextUpdateDate = Calendar.current.date(byAdding: .second, value: 3, to: currentDate)!
currentDateStr = formatter.string(from: currentDate)
nextUpdateDateStr = formatter.string(from: nextUpdateDate)
msg.append(message)
msg.append("\n時(shí)間軸ID " + uuid);
msg.append("\n時(shí)間軸第" + String(i+1) + "個(gè)視圖")
msg.append("\n本次視圖開(kāi)始時(shí)間 " + currentDateStr)
msg.append("\n下次視圖開(kāi)始時(shí)間 " + nextUpdateDateStr)
let entry = SimpleEntry(date: currentDate, message: msg, backgroundImageStr: backgroundImageStr+String(i))
entries.append(entry)
print(String(i))
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
這段代碼里我們提供了一個(gè)古詩(shī)詞的快照
并生成了一個(gè)時(shí)間線,其中時(shí)間線里包含了10個(gè)entry,且每個(gè)entry 間隔10s
entry文本拼接顯示了
- 展示文本(詩(shī)經(jīng))
- 時(shí)間軸ID
- 本次視圖開(kāi)始時(shí)間
- 下次視圖開(kāi)始時(shí)間
下邊我們來(lái)看一下真實(shí)run出來(lái)的效果
到此為止我們就有了一個(gè)可以在主屏幕上展示的小部件了
并且能根據(jù)時(shí)間線展示不同的視圖了。
七、Reload Timeline 使小部件保持最新
為了使我們的小部件能隨時(shí)提供最新的,而不是過(guò)期的信息,我們需要不時(shí)的對(duì)小部件進(jìn)行更新。
我們已經(jīng)知道了小部件的本質(zhì)是一系列的視圖堆疊,那么更新小部件就是更新這些視圖。
比如一個(gè)有三個(gè)視圖的小部件,預(yù)測(cè)了現(xiàn)在和未來(lái)3小時(shí)的天氣預(yù)報(bào),這個(gè)小部件顯示步驟如下:
但是有一個(gè)很重要的問(wèn)題就是時(shí)間線是我們預(yù)測(cè)出來(lái)的,是預(yù)測(cè)就會(huì)有偏差。比如天氣預(yù)報(bào),預(yù)報(bào)2小時(shí)后有雨,但是隨著天氣的變化,2小時(shí)后變成晴天了,這個(gè)時(shí)候我們?nèi)绻桓滦〔考系臅r(shí)間線,就會(huì)在2小時(shí)后給用戶(hù)提供錯(cuò)誤的信息。
為此我們需要在有信息變化的時(shí)候重新顯示新的視圖,如下圖:
為了是我們的小部件信息準(zhǔn)確無(wú)誤,首先我們需要了解下小部件是如何刷新的
很不幸,Widget 的刷新完全由 WidgetCenter控制
。開(kāi)發(fā)者無(wú)法通過(guò)任何 API 去主動(dòng)刷新 Widget 的頁(yè)面,只能告知 WidgetCenter,Timeline 需要刷新了
所以我們不能直接刷新小部件的視圖,而是要通過(guò)生成一個(gè)新的時(shí)間線來(lái)替換舊的時(shí)間線,Reload Timeline 并不是直接刷新 Widget,而是 WidgetCenter 重新向 Widget 請(qǐng)求下一階段的數(shù)據(jù)。
其中Reload Timeline
分為兩種方式
- System Reloads
- App Reloads
1、System Reloads
這個(gè)行為由系統(tǒng)主動(dòng)發(fā)起,會(huì)調(diào)用一次 Reload Timeline 向 Widget 請(qǐng)求下一階段刷新的數(shù)據(jù)。系統(tǒng)除了會(huì)按時(shí)發(fā)起 System Reloads 之外,還會(huì)動(dòng)態(tài)決策每個(gè)不同的 TimeLine 的 System Reloads 的頻次。比如被點(diǎn)擊次數(shù)很大程度上直接決定了 System Reloads 的頻率,點(diǎn)擊率越高,更新頻次越快,當(dāng)然還有一些由于設(shè)備環(huán)境變化觸發(fā)的行為也會(huì)觸發(fā) System Reloads,比如設(shè)備時(shí)間進(jìn)行了變更。
很顯然這種方案不能很好的解決我們上邊的問(wèn)題
2、App Reloads
這種行為指的是App主動(dòng)通知小部件,你需要更新信息了。這里邊根據(jù)App的當(dāng)前的前后臺(tái)狀態(tài)又分為兩種方式
- 應(yīng)用在前臺(tái)運(yùn)行
- 應(yīng)用在后臺(tái)運(yùn)行
當(dāng)應(yīng)用在前臺(tái)運(yùn)行的時(shí)候,App 可以直接使用WidgetCenter的 API 來(lái) Reload Timeline;而當(dāng)應(yīng)用處于后臺(tái)時(shí),可以使用后臺(tái)推送(Background Notification)來(lái) Reload Timeline。
除了這些,給Timeline設(shè)定合適的刷新策略也是很重要的手段
合理的組合使用這些刷新機(jī)制,能夠極大的提高Widget信息的準(zhǔn)確性
八、交互
前邊我們說(shuō)過(guò),widget和app交互有兩種方式SwiftUI widgetURL API
和SwiftUI Link API
這兩種方式的本質(zhì)都是URL Schemes
,只要監(jiān)聽(tīng)SceneDelegate
的scene:openURLContexts:
就可以了
由于Schemes大家都太熟悉了,關(guān)于如何高效快速準(zhǔn)確的傳遞參數(shù),這里就不展開(kāi)講了。
九、設(shè)計(jì)漂亮的小部件
如果你已經(jīng)看到了這里,并且已經(jīng)理解了上述的講解,你已經(jīng)具備了開(kāi)發(fā)小部件的能力。
那么有哪些關(guān)鍵點(diǎn)能給自己的小部件錦上添花呢?
去除額外的App信息
:系統(tǒng)會(huì)在小部件下方自動(dòng)顯示你的應(yīng)用名稱(chēng),因此你無(wú)需在內(nèi)容中重復(fù)App的名稱(chēng),Icon,而是要通過(guò)顏色,布局和圖像來(lái)聯(lián)系您的App
簡(jiǎn)潔的描述
。小部件庫(kù)中顯示的描述可以幫助人們理解每個(gè)小部件的功能。從動(dòng)作動(dòng)詞開(kāi)始描述通常效果很好;例如,“查看當(dāng)前天氣狀況和位置預(yù)測(cè)”或“跟蹤即將舉行的活動(dòng)和會(huì)議”。避免包含不必要的短語(yǔ)來(lái)引用窗口小部件本身,例如“此窗口小部件顯示...”,“使用此窗口小部件...”或“添加此窗口小部件”。
舒適的信息密度
:一覽無(wú)余。當(dāng)內(nèi)容顯得稀疏時(shí),小部件可能看起來(lái)是多余的;當(dāng)內(nèi)容太密集時(shí),小部件將無(wú)法瀏覽。如果要包含很多信息,請(qǐng)避免讓小部件成為難以解析的項(xiàng)的拼貼。尋求整理內(nèi)容的方法,以便人們可以立即掌握關(guān)鍵部分,并以更長(zhǎng)的時(shí)間查看相關(guān)細(xì)節(jié)。您可能還考慮創(chuàng)建一個(gè)較大的小部件,并尋找可以用圖形替換文本而又不會(huì)失去清晰度的位置。
明智地使用顏色
:豐富,美麗的色彩吸引眼球,但它們絕不能阻止人們一眼就吸收小部件的信息。使用顏色可以增強(qiáng)小部件的外觀,而不會(huì)與小部件的內(nèi)容競(jìng)爭(zhēng)。
使用系統(tǒng)字體,支持系統(tǒng)功能
:例如 支持黑暗模式;使用SF Pro和使用系統(tǒng)字體;文本可縮放。
設(shè)計(jì)一個(gè)真實(shí)的預(yù)覽以顯示在小部件庫(kù)中
:突出顯示小部件的外觀和功能可幫助人們做出明智的決定,并鼓勵(lì)他們添加小部件。您可以在小部件預(yù)覽中顯示真實(shí)數(shù)據(jù),但是如果數(shù)據(jù)生成或加載所需的時(shí)間太長(zhǎng),請(qǐng)顯示真實(shí)的模擬數(shù)據(jù)。
設(shè)計(jì)占位符內(nèi)容,以幫助人們識(shí)別您的小部件
。小部件在加載數(shù)據(jù)時(shí)顯示占位符內(nèi)容。通過(guò)將UI的靜態(tài)部分與代表實(shí)際內(nèi)容的半透明形狀結(jié)合起來(lái),可以創(chuàng)建有效的預(yù)覽。例如,您可以使用不同寬度的矩形來(lái)建議文本行,并使用圓環(huán)或正方形代替字形和圖像。
圖片適配屏幕尺寸
:確保圖片在大部件和小部件下都不會(huì)壓縮
十圍之木,始生如蘗
簡(jiǎn)單的總結(jié)一下
一個(gè)優(yōu)秀的小部件是完全可以提高用戶(hù)體驗(yàn),成為很好的流量入口,給App帶來(lái)巨大的商業(yè)價(jià)值。
但是要設(shè)計(jì)一個(gè)優(yōu)秀的小部件也并非易事。
本文拋磚引玉,希望大家能設(shè)計(jì)出更多優(yōu)秀的小部件。
本次的Widget指北到這里就結(jié)束了,萬(wàn)字不易,多多傳播。
喜歡我你就關(guān)注我,
有話說(shuō)你就評(píng)論我,
都不干你就點(diǎn)個(gè)贊
Demo
參考