2020 年 6 月 22 日,蘋果召開了第一次線上的開發者大會 - WWDC20。此次發佈會上宣佈了ARM架構Mac芯片(拳打Intel)、iOS 14 ATT(腳踢Facebook),可謂是一次載入史冊(我是爸爸)的發佈會了,固然還發布了被稱爲下一個頂級流量入口的
Widget
。ios踩着八月的尾巴,本次咱們就來探究一下Widget。git
本文會從
Widget初窺
和Widget開發
兩個維度和章節來探究一下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.github
一句話來講:Widget不是迷你應用程序。而是一種新的主屏幕體驗,能快速提供用戶關心的內容是重點swift
Glanceable
一目瞭然Relevant
早韭晚菘Personalized
量體裁衣要設計一個優秀的Widget,就要先了解Widget的所有特色,瞭然於胸api
針對Apple提出的Glanceable
、Relevant
、Personalized
分別用一個成語來形容就是一目瞭然
、早韭晚菘
、量體裁衣
緩存
簡單來講下這幾個特色markdown
一個優秀的Widget要一目瞭然,盡收眼底。網絡
普通人天天進入「主屏幕」的次數超過90次,可是在主屏幕僅停留幾分鐘,就切換到其餘App了。架構
因此Widget必定要充分利用狹小的屏幕展現最核心的信息,而且要簡潔明瞭。設計新穎,便於快速瀏覽,高效是一個優秀Widget的核心。
用戶不用思考這個Widget怎麼使用,不須要點擊任何按鈕就能夠得到最關心的信息。
蘋果但願Widget能夠和用戶緊密結合,與用戶的行爲所關聯,好比早上起牀,用戶但願看一下天氣;中午恰飯,用戶但願有人推薦下附近的美食;晚高峯的時候,用戶但願瞭解一下行車路線;晚安的時候,但願記錄下第二天的行程。
爲此,蘋果系統提供了一個叫Smart Stacks(智能疊放)的功能,Smart Stacks是一個Widgets的集合。系統會根據每一個人的習慣,自動顯示用戶當前時間點最須要的Widget。
Widget要能爲用戶提供個性化的服務,好比天氣Widget,須要能爲不一樣的用戶提供不一樣細節的天氣狀況。
爲此Apple提供三種不一樣大小的小部件
systemSmall
systemMedium
systemLarge
其中systemSmall大小爲2*2 Icon
,systemMedium大小爲4*2 Icon
,systemLarge大小爲4*4 Icon
,具體的顯示效果以下
另外一方面Widget須要能爲不一樣城市的用戶提供當地的天氣狀況。
爲此Apple在建立Widget時爲開發者提供了兩種類型:
StaticConfiguration
:對於沒有用戶可配置屬性的窗口小部件,也就是用戶無需配置,展現的內容只和用戶信息有關係。例如,顯示通常市場信息的股市窗口小部件,或顯示趨勢頭條的新聞窗口小部件。
IntentConfiguration
:對於具備用戶可配置屬性的窗口小部件,也就是支持用戶配置及用戶意圖的推測。您使用SiriKit自定義意圖來定義屬性。例如,須要一個城市的郵政編碼的天氣小部件,或者須要一個跟蹤號的包裹跟蹤小部件。
須要說明的是,IntentConfiguration並不須要編寫代碼,只須要簡單的配置,Xcode 會自動幫你生成對應的代碼和類型。
此外Widget還支持系統的黑暗模式
Widget的本質是一系列靜態視圖堆疊而成的集合,不一樣的時間點展現不一樣的視圖
這裏要引入Widget的核心Timeline
顧名思義,Timeline就是一條時間線,在對應的時間點發生對應的事件
許多Widget具備可預測的時間點,在這些時間點更新其內容是有意義的。例如,顯示天氣信息的小部件可能會在一成天內每小時更新一次溫度。股市窗口小部件能夠在公開市場時間頻繁更新其內容,但週末則不用徹底更新。經過提早計劃這些時間,生成不一樣的視圖放入時間線中,WidgetKit會在適當的時間到來時自動刷新您的窗口小部件。
這也決定了Widget基本上不能實時更新
另外值得一提的是,WidgetKit會把 Timelines 所定義的Views 結構信息緩存到磁盤,而後在刷新的時候才經過 JIT 的方式來渲染。這使得系統能夠在極低電量開銷下爲衆多 Widgets 處理 Timelines 信息。
很差意思,沒有交互!!!
爲了實現以上的特色,Apple也移除限制了Widget的一些功能
惟一支持的只有用戶點擊Widget喚起主App
其中點擊喚起主App有兩種方案,分別是:
widgetURL喚起App的點擊區域是Widget的全部區域,這種方案適合簡單元素,單一邏輯的小部件
對於systemSmall類型的小部件,只支持widgetURL喚起方式
針對systemMedium
和systemLarge
還可使用更細分的Link喚起方式,這種喚起方式能讓小部件經過不一樣元素的點擊喚起App的不一樣頁面,讓開發者有更多的施展空間
舉個簡單的例子,widgetURL可應用於天氣小部件,博客小部件,點擊直達App;
Link可用於備忘錄和日曆小部件,點擊不一樣的備忘錄和日期直接跳轉到對應的備忘錄詳情和待辦詳情頁面
Widget的出現猶如在一潭死水的iOS桌面上泛起了一片漣漪,必定會有不少App來爭奪這塊肥肉通常的流量入口。
可是仔細研究一下會發現,Apple此次推出的Widget很是剋制,並無很是激進,
俗話說:喜歡是放肆,但愛就是剋制。這裏不得再也不次引用Apple在Widget介紹中出現頻率最高的話widgets are not mini-apps
,由於Widget在設計之初就是爲了能使用最少的成本,向用戶提供最核心的信息。爲了儘量的減小用戶成本(電量,網絡等)和提升用戶體驗,Apple在技術層面上作了不少限制,限制了很是多的功能,大大削弱了Widget的地位和重要程度,也下降了開發者實現的熱情和積極性
其實每一年Apple更新的新技術只有不多的一部分能應用到App上,但願此次的Widget能有動力讓你們結合本身的App,給本身的App帶來更多的流量,也能給用戶帶來更好的體驗。
重頭戲來啦,接下來讓咱們一步一步設計編寫出優秀的小部件吧
開始以前,首先咱們要介紹下Widget的開發語言,Apple特別指定了小部件只能使用SwiftUI來開發
如今iOS主流的開發語言仍是Objective-C,那Apple爲何要選擇2019 WWDC發佈迄今爲止只有一年的SwiftUI呢?
首先,從一開始就將小部件實現多平臺化是Apple的一個目標,SwiftUI在跨設備展現的能力上是一把大殺器;
其次SwiftUI還使自動佈局和暗模式等功能變得很是容易,下降了適配等開發成本,對不須要太多元素的小部件來講,SwiftUI重點關注佈局的特色無疑是最合適的;
從另外一方面來說,只有使用 SwiftUI 才能達到咱們上邊說的對於 Widget 的限制。若是可使用 Objective-C UIKit 的話,咱們強大的開發者可能會想出無數的黑科技來忽略Apple真的小部件的限制。好比開發沒法使用 UIViewRepresentable 來橋接 UIKit;
最後Apple也夾帶了本身的私心,Apple今年已經將 Swift 語言和 SwiftUI 的重要程度提高到了一個新的高度,Swift已經能夠獨立於Foundtion框架,那麼對應的SwiftUI也應該不依賴於UIKit框架了,強行使用SwiftUI可使開發人員儘量容易地將其學習其內容並應用於iOS,iPadOS和macOS,
畢竟5月份卡位第20位Objective-C在6月份已經跌出了前20
這裏要重點說明一下,Widget只要使用任何 UIKit 的元素就會直接 Crash
窗口小部件擴展模板提供了建立窗口小部件的起點。單個小部件擴展能夠包含多種小部件。例如,一個體育應用程序可能有一個顯示團隊信息的小部件,另外一個顯示遊戲時間表的小部件。一個小部件擴展能夠包含兩個小部件。儘管建議將全部窗口小部件包含在一個窗口小部件擴展中,但若有必要,能夠添加多個擴展。
在Xcode中打開您的應用程序項目,而後選擇「文件」>「新建」>「目標」。
從「應用程序擴展」組中,選擇「窗口小部件擴展」,而後單擊「下一步」。
輸入您的包名。
若是窗口小部件提供了用戶可配置的屬性,請選中Include Configuration Intent
複選框。
單擊完成。
重點:小部件不只支持Swift項目,一樣也支持Objective-C項目,OC小夥伴不用擔憂啦
建立完小部件以後,咱們會多出一個SmileEverydayWidget.swift文件,這已是一個能夠run起來的小部件了,由於咱們接下來要逐個方法來分析,因此先將文件全文展現以下
//
// 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@*/
}
}
複製代碼
這裏的概念和代碼比較多,接下來咱們一個一個來解釋
首先咱們從帶有main
字段的方法來講起,
@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
標識的方法都是程序的入口
這段代碼使用SwiftUI聲明瞭一個名爲SmileEverydayWidget的小部件,其中StaticConfiguration
是小部件的初始化方法,它有幾個參數:
kind
provider
placeholder
其中
kind
是標識小部件的字符串,而且應描述小部件所表明的內容。即小部件的包名
provider
爲時間線提供者
PlaceholderView
爲佔位視圖
同時也提供了一些方法,例如
configurationDisplayName()
設置小部件顯示的名稱description()
設置小部件的描述supportedFamilies()
設置小部件支持的尺寸這裏有一個重點,爲了使某個應用程序的窗口小部件出如今窗口小部件庫中,用戶必須在安裝該應用程序後至少啓動一次包含該窗口小部件的應用程序。
WidgetEntryView就是使用SwiftUI佈局的小部件視圖
struct SmileEverydayWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.date, style: .time)
}
}
複製代碼
例如這個小部件視圖就簡單的展現了當前的時間
接下來咱們能夠將默認的佈局更改成咱們本身想要的佈局,例如我在設置了顯示文本的字體和小部件的背景圖
struct SmileEverydaWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
return Text(entry.message)
.background(Image(entry.backgroundImageStr))
.font(.callout)
}
}
複製代碼
每種小部件都須要提供佔位符UI。
佔位符UI是窗口小部件的默認內容。
它應該表明您的小部件類型,但僅此而已。
此用戶界面中不該有任何用戶數據。
想象一下
若是在用戶的主屏幕上出現以下的場景,那麼你的小部件離被移除可能已經不遠了
咱們知道小部件是按照時間線來展現的,TimelineEntry
時間線上的一個個條目
struct SimpleEntry: TimelineEntry {
public let date: Date
}
複製代碼
TimelineEntry有一個必須有的屬性就是date
,也就是這個條目在時間線上的具體時間
另外開發者能夠在TimelineEntry裏自定義各類屬性,用來給小部件視圖提供數據
例如我在TimelineEntry裏自定義了message
和backgroundImageStr
屬性,用來顯示小部件上的文字和背景圖片
struct SimpleEntry: TimelineEntry {
public let date: Date
public let message: String
public let backgroundImageStr : String
}
複製代碼
TimelineProvider
是一個提供了上述咱們所說的TimelineEntry
集合的對象
咱們來看下具體的代碼:
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)
}
}
複製代碼
其中包含兩個方法
爲了在小部件庫中顯示小部件,WidgetKit 要求提供者提供預覽快照, 即snapshot()
,這個方法裏主要提供了一些示例數據,最好是真實數據
,用於時間線不能展現
的時候展現給用戶
須要說明的是,快照是系統須要快速顯示單個條目的位置。
所以,您的擴展程序必須儘快返回視圖,由於這樣作時,用戶會在iOS上漂亮的Widget Gallery中看到真正的Widget。
這不是咱們在設計時必須提供的屏幕截圖或圖像。這是用戶在iOS,iPadOS和macOS上真正的小部件體驗。
在大多數狀況下,時間軸的第一個條目和快照能夠做爲同一條目返回,所以,在「小工具庫」中看到的就是用戶將其添加到設備中時獲得的內容。
例如咱們在Widget Gallery中添加電池小部件時,小部件此時在Widget Gallery中展現的就是當前設備電池信息的實時數據的快照,而不是一些虛假的數據,這個時候小部件的數據是什麼樣子,用戶添加到主屏幕上以後小部件的數據就是什麼樣子,從而提升用戶的體驗。
對比
小部件裏有兩個比較相似的概念,PlaceholderView
和snapshot
,都是一種佔位解決方案,不一樣的是PlaceholderView
是在主屏幕上沒法快速獲取數據時的一種佔位視圖,不至於顯示loading或者白屏給用戶看;而snapshot
主要用於Widget Gallery
中,用來提升用戶體驗的,通常來講,snapshot
就是時間線的第一幀
在請求初始快照後,WidgetKit調用timeline
以請求提供者的常規時間軸。時間軸由一個或多個時間軸條目TimelineEntry
以及一個重載策略ReloadPolicy
組成,該重載策略通知WidgetKit什麼時候請求後續時間軸。
關於重載策略,提供瞭如下幾種策略
atEnd
: 是指 Timeline 執行到最後一個時間片的時候再刷新。atAfter
: 是指在某個時間之後有規律的刷新never
:是指之後不須要刷新了。何時須要從新刷新須要 App 從新告知 Widget根據上邊的分析,咱們能夠將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溯洄從之,道阻且長;溯游從之,宛在水中央。\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時間軸ID " + uuid);
msg.append("\n時間軸第" + String(i+1) + "個視圖")
msg.append("\n本次視圖開始時間 " + currentDateStr)
msg.append("\n下次視圖開始時間 " + 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)
}
}
複製代碼
這段代碼裏咱們提供了一個古詩詞的快照
並生成了一個時間線,其中時間線裏包含了10個entry,且每一個entry 間隔10s
entry文本拼接顯示了
下邊咱們來看一下真實run出來的效果
到此爲止咱們就有了一個能夠在主屏幕上展現的小部件了
而且能根據時間線展現不一樣的視圖了。
爲了使咱們的小部件能隨時提供最新的,而不是過時的信息,咱們須要不時的對小部件進行更新。
咱們已經知道了小部件的本質是一系列的視圖堆疊,那麼更新小部件就是更新這些視圖。
好比一個有三個視圖的小部件,預測瞭如今和將來3小時的天氣預報,這個小部件顯示步驟以下:
可是有一個很重要的問題就是時間線是咱們預測出來的,是預測就會有誤差。好比天氣預報,預報2小時後有雨,可是隨着天氣的變化,2小時後變成晴天了,這個時候咱們若是不更新小部件上的時間線,就會在2小時後給用戶提供錯誤的信息。
爲此咱們須要在有信息變化的時候從新顯示新的視圖,以下圖:
爲了是咱們的小部件信息準確無誤,首先咱們須要瞭解下小部件是如何刷新的
很不幸,Widget 的刷新徹底由 WidgetCenter控制
。開發者沒法經過任何 API 去主動刷新 Widget 的頁面,只能告知 WidgetCenter,Timeline 須要刷新了
因此咱們不能直接刷新小部件的視圖,而是要經過生成一個新的時間線來替換舊的時間線,Reload Timeline 並非直接刷新 Widget,而是 WidgetCenter 從新向 Widget 請求下一階段的數據。
其中Reload Timeline
分爲兩種方式
這個行爲由系統主動發起,會調用一次 Reload Timeline 向 Widget 請求下一階段刷新的數據。系統除了會按時發起 System Reloads 以外,還會動態決策每一個不一樣的 TimeLine 的 System Reloads 的頻次。好比被點擊次數很大程度上直接決定了 System Reloads 的頻率,點擊率越高,更新頻次越快,固然還有一些因爲設備環境變化觸發的行爲也會觸發 System Reloads,好比設備時間進行了變動。
很顯然這種方案不能很好的解決咱們上邊的問題
這種行爲指的是App主動通知小部件,你須要更新信息了。這裏邊根據App的當前的先後臺狀態又分爲兩種方式
當應用在前臺運行的時候,App 能夠直接使用WidgetCenter的 API 來 Reload Timeline;而當應用處於後臺時,可使用後臺推送(Background Notification)來 Reload Timeline。
除了這些,給Timeline設定合適的刷新策略也是很重要的手段
合理的組合使用這些刷新機制,可以極大的提升Widget信息的準確性
前邊咱們說過,widget和app交互有兩種方式SwiftUI widgetURL API
和SwiftUI Link API
這兩種方式的本質都是URL Schemes
,只要監聽SceneDelegate
的scene:openURLContexts:
就能夠了
因爲Schemes你們都太熟悉了,關於如何高效快速準確的傳遞參數,這裏就不展開講了。
若是你已經看到了這裏,而且已經理解了上述的講解,你已經具有了開發小部件的能力。
那麼有哪些關鍵點能給本身的小部件錦上添花呢?
去除額外的App信息
:系統會在小部件下方自動顯示你的應用名稱,所以你無需在內容中重複App的名稱,Icon,而是要經過顏色,佈局和圖像來聯繫您的App
簡潔的描述
。小部件庫中顯示的描述能夠幫助人們理解每一個小部件的功能。從動做動詞開始描述一般效果很好;例如,「查看當前天氣情況和位置預測」或「跟蹤即將舉行的活動和會議」。避免包含沒必要要的短語來引用窗口小部件自己,例如「此窗口小部件顯示…」,「使用此窗口小部件…」或「添加此窗口小部件」。
溫馨的信息密度
:盡收眼底。當內容顯得稀疏時,小部件可能看起來是多餘的;當內容太密集時,小部件將沒法瀏覽。若是要包含不少信息,請避免讓小部件成爲難以解析的項的拼貼。尋求整理內容的方法,以便人們能夠當即掌握關鍵部分,並以更長的時間查看相關細節。您可能還考慮建立一個較大的小部件,並尋找能夠用圖形替換文本而又不會失去清晰度的位置。
明智地使用顏色
:豐富,美麗的色彩吸引眼球,但它們毫不能阻止人們一眼就吸取小部件的信息。使用顏色能夠加強小部件的外觀,而不會與小部件的內容競爭。
使用系統字體,支持系統功能
:例如 支持黑暗模式;使用SF Pro和使用系統字體;文本可縮放。
設計一個真實的預覽以顯示在小部件庫中
:突出顯示小部件的外觀和功能可幫助人們作出明智的決定,並鼓勵他們添加小部件。您能夠在小部件預覽中顯示真實數據,可是若是數據生成或加載所需的時間太長,請顯示真實的模擬數據。
設計佔位符內容,以幫助人們識別您的小部件
。小部件在加載數據時顯示佔位符內容。經過將UI的靜態部分與表明實際內容的半透明形狀結合起來,能夠建立有效的預覽。例如,您可使用不一樣寬度的矩形來建議文本行,並使用圓環或正方形代替字形和圖像。
圖片適配屏幕尺寸
:確保圖片在大部件和小部件下都不會壓縮
簡單的總結一下
一個優秀的小部件是徹底能夠提升用戶體驗,成爲很好的流量入口,給App帶來巨大的商業價值。
可是要設計一個優秀的小部件也並不是易事。
本文拋磚引玉,但願你們能設計出更多優秀的小部件。
本次的Widget指北到這裏就結束了,萬字不易,多多傳播。
喜歡我你就關注我,
有話說你就評論我,
都不干你就點個贊
Demo
參考