Widgets小部件在iOS中存在很長一段時間,但iOS 14對其進行了徹底的改進,WWDC2020明確要求使用SwiftUI編寫。 iOS 14的小部件有多種類型,范圍從簡單的信息方塊到可以從其父應用程序的Siri Intents檢索和顯示信息的小部件。
但是,其中最具吸引力的就是可以在主頁放置小部件,這意味著從技術上講,您現在可以制作可視化的“微型應用程序”。如果您發現自己一遍又一遍地執行相同的任務,例如檢查某個應用程序支持的版本或最新版本中的崩潰次數,則可以在iOS 14中創建一個小部件。
盡管除了觸摸小部件(它會觸發應用程序啟動)之外,您無法與其進行交互,但是在它們中顯示的內容并沒有很多限制,因此您可以使用它們來開發只讀的可視化應用程序。
開發“快速提交跟蹤器”小部件
我發現自己偶爾去Swift倉庫,看看社區在做什么。為了讓我的生活更輕松,如何在主屏幕上直接顯示此信息?為此,我們可以向GitHub的公共API發出請求,解析信息并將其呈現給我們的小部件。
我們可以從創建一個新的iOS項目開始-該項目的細節無關緊要,因為在本教程中,所有代碼都將在Widget的模塊內。
創建項目后,通過轉到File-> New-> Target并選擇Widget Extension target添加一個Widget模塊。
確保取消選中“Include Configuration Intent”復選框,因為這涉及一項僅在本文稍后介紹的功能!生成目標后,請確保刪除示例代碼,以便我們可以逐步檢查它。
要定義Widget,您要做的就是創建一個從Widget繼承并配置其功能的結構:
@main
struct CommitCheckerWidget: Widget {
private let kind: String = "CommitCheckerWidget"
public var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: CommitTimeline(), placeholder: PlaceholderView()) { entry in
CommitCheckerWidgetView(entry: entry)
}
.configurationDisplayName("Swift's Latest Commit")
.description("Shows the last commit at the Swift repo.")
}
}
在定義此類功能之前,不會編譯此代碼,但這對于第一步很重要,因為這是Widget本身。 WidgetConfiguration返回值描述此窗口小部件是什么以及如何構建,但最重要的是,它如何獲取其內容。
小部件類型
StaticConfiguration WidgetConfiguration定義了無需用戶任何輸入即可自行解決的Widget。您可以在Widget的父應用中獲取任何相關數據,并將結果作為“用戶輸入”發送到Widget模塊,但是由于在配置Widget時可以進行API調用,因此如果沒有上下文,則無需這樣做請求中涉及的信息。
另一方面,您可以使用IntentConfiguration WidgetConfiguration定義一個依賴于父應用程序中Siri Intent的Widget,它允許您構建可配置的動態Widget。例如,當使用意圖時,食品配送應用程序可以創建一個小部件,以顯示用戶最新訂單的配送狀態。這是通過讓應用程序分派一個Siri Intent(就像開發Siri快捷方式時一樣)來完成的,這些IntentConfiguration會自動將它們拾取并用于更新Widget。您可以通過在創建Widget擴展時選中intent框來創建基本的IntentConfiguration Widget,但是由于我們要做的只是解析GitHub的公共API,因此我們可以使用StaticConfiguration Widget并避免與應用程序本身進行交互。
時間線提供者
iOS 14的小部件顯示的內容類似于watchOS的復雜性,在某種意義上,您無需提供一直在運行的擴展程序,而是一次提供了操作系統應在整個小時,幾天內顯示的事件的“時間表”甚至幾個星期這對于“天氣”和“日歷”等應用程序很有用,您可以在其中“預測”將來將要顯示的內容,因為它們已經具有該信息。
在我們的案例中,由于我們無法預測Swift的提交,因此我們將提供一個僅包含單個事件的時間軸-使iOS更定期地刷新Widget。
要創建時間線,我們首先需要定義一個TimelineEntry。當預期在小部件中呈現此條目時,TimelineEntry僅需要日期,但是它也可以包含您需要的任何其他信息。在我們的例子中,我們的條目將包含我們要在小部件中顯示的提交。
struct Commit {
let message: String
let author: String
let date: String
}
struct LastCommitEntry: TimelineEntry {
public let date: Date
public let commit: Commit
}
但是在創建時間軸之前,我們需要能夠獲取此類提交。讓我們創建一個CommitLoader類,該類獲取并解析Swift的最新提交:
struct Commit {
let message: String
let author: String
let date: String
}
struct CommitLoader {
static func fetch(completion: @escaping (Result<Commit, Error>) -> Void) {
let branchContentsURL = URL(string: "https://api.github.com/repos/apple/swift/branches/master")!
let task = URLSession.shared.dataTask(with: branchContentsURL) { (data, response, error) in
guard error == nil else {
completion(.failure(error!))
return
}
let commit = getCommitInfo(fromData: data!)
completion(.success(commit))
}
task.resume()
}
static func getCommitInfo(fromData data: Foundation.Data) -> Commit {
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
let commitParentJson = json["commit"] as! [String: Any]
let commitJson = commitParentJson["commit"] as! [String: Any]
let authorJson = commitJson["author"] as! [String: Any]
let message = commitJson["message"] as! String
let author = authorJson["name"] as! String
let date = authorJson["date"] as! String
return Commit(message: message, author: author, date: date)
}
}
調用fetch時,此加載程序類型將請求發送到GitHub的公共API并解析最新的提交-為我們提供消息,作者及其時間戳。現在,我們可以創建一個時間軸,以獲取最新的提交,將其添加為條目,并計劃在一段時間后進行更新。
struct CommitTimeline: TimelineProvider {
typealias Entry = LastCommitEntry
/* protocol methods implemented below! */
}
TimelineProvider協議有兩種我們需要實現的方法:
snapshot()-小部件的偽造信息
TimelineProvider協議所需的snapshot()方法定義了當Widget在瞬態情況下(例如Widget選擇屏幕)出現時,應該如何配置Widget。當顯示正確的信息無關緊要時,將使用此配置:
要創建快照配置,所有要做的就是創建并返回TimelineEntry對象的假條目。
public func snapshot(with context: Context, completion: @escaping (LastCommitEntry) -> ()) {
let fakeCommit = Commit(message: "Fixed stuff", author: "John Appleseed", date: "2020-06-23")
let entry = LastCommitEntry(date: Date(), commit: fakeCommit)
completion(entry)
}
timeline()-小部件的真實信息
但是,timeline()方法定義了窗口小部件應使用的真實信息。目的是讓您返回一個時間軸實例,其中包含要顯示的所有條目,預期顯示的條目(條目的日期)以及時間軸“到期”的時間。
由于我們的應用程序無法像Weather應用程序那樣“預測”其未來狀態,因此我們只需創建一個具有立即顯示的單個條目的時間軸即可,這可以通過將條目的日期設置為當前Date()來完成。 :
public func timeline(with context: Context, completion: @escaping (Timeline<LastCommitEntry>) -> ()) {
let currentDate = Date()
let refreshDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
CommitLoader.fetch { result in
let commit: Commit
if case .success(let fetchedCommit) = result {
commit = fetchedCommit
} else {
commit = Commit(message: "Failed to load commits", author: "", date: "")
}
let entry = LastCommitEntry(date: currentDate, commit: commit)
let timeline = Timeline(entries: [entry], policy: .after(refreshDate))
completion(timeline)
}
}
時間軸的policy屬性定義了iOS何時應嘗試丟棄此時間軸并獲取新的時間軸。當前,它們可以是.never(顯示靜態內容的小部件,這些內容永遠不變)、. atEnd(顯示時間線中的最后一個條目時)或.after(Date),即顯示完后的特定時間后時間表。由于我們的時間軸只有一個條目,因此我決定使用.after告訴iOS該小部件應每5分鐘重新加載一次。
但是請注意,Widget API的文檔指出您無法預測何時更新Widget。即使確實會在5分鐘后再次獲取時間軸本身,也無法保證iOS會同時更新視圖。從我撰寫本文的個人經驗來看,視圖實際上需要大約20分鐘的時間來更新。更新時間基于幾個因素,其中包括用戶看到窗口小部件的頻率。如果需要強制更新小部件,則可以使用主應用程序中的WidgetCenter API重新加載所有時間軸(或特定時間軸):
WidgetCenter.shared.reloadAllTimelines()
盡管我們的Widget不需要這樣做,但重要的是要提到時間表和條目還有其他有趣的功能。例如,可以為條目設置相關性值,這將使iOS可以確定小部件的重要性。例如,這用于確定堆棧內小部件的順序:
struct LastCommit: TimelineEntry {
public let date: Date
public let commit: Commit
var relevance: TimelineEntryRelevance? {
return TimelineEntryRelevance(score: 10) // 0 - not important | 100 - very important
}
}
創建小部件視圖
現在我們已經配置了時間線,我們可以創建小部件的可視組件。我們需要創建兩個視圖:在加載時間線時顯示的占位符,以及能夠呈現時間線條目的實際Widget視圖。
struct PlaceholderView : View {
var body: some View {
Text("Loading...")
}
}
struct CommitCheckerWidgetView : View {
let entry: LastCommitEntry
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text("apple/swift's Latest Commit")
.font(.system(.title3))
.foregroundColor(.black)
Text(entry.commit.message)
.font(.system(.callout))
.foregroundColor(.black)
.bold()
Text("by \(entry.commit.author) at \(entry.commit.date)")
.font(.system(.caption))
.foregroundColor(.black)
Text("Updated at \(Self.format(date:entry.date))")
.font(.system(.caption2))
.foregroundColor(.black)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .leading)
.padding()
.background(LinearGradient(gradient: Gradient(colors: [.orange, .yellow]), startPoint: .top, endPoint: .bottom))
}
static func format(date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "MM-dd-yyyy HH:mm"
return formatter.string(from: date)
}
}
您應該能夠編譯您的代碼,并且現在已經提供了所有組件,我們可以再次查看我們的配置方法,以了解如何將它們全部包裝在一起:
@main
struct CommitCheckerWidget: Widget {
private let kind: String = "CommitCheckerWidget"
public var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: CommitTimeline(), placeholder: PlaceholderView()) { entry in
CommitCheckerWidgetView(entry: entry)
}
.configurationDisplayName("Swift's Latest Commit")
.description("Shows the last commit at the Swift repo.")
}
}
我們創建了一個靜態小部件,該小部件可從CommitTimeline獲取其內容,具有PlaceholderView作為占位符,并在準備顯示條目時生成CommitCheckerWidgetView。
在運行我們的應用程序并將小部件添加到我們的家中之后,我們現在可以看到一個自動更新的Swift提交顯示器!
允許用戶配置要可視化的倉庫/分支
如前所述,iOS 14的新API還支持與Siri Intent綁定的Widget,從而使您可以創建可由用戶配置的動態Widget。我們可以有一個基于意圖的Widget,允許用戶直接從Widget本身配置要觀看的倉庫。
要創建基于意圖的Widget,我們首先需要一個Siri意圖。在您的窗口小部件擴展中,添加一個SiriKit Intent Definition File。
為了允許用戶查看任何回購或分支的提交,讓我們創建一個支持account,回購和分支屬性的LastCommitIntent。確保也選中“小部件的意圖”框。
可以將Widgets與任何捐贈的Siri Intent的數據一起使用,但是魔術之處在于不需要這樣做。如果該意圖具有窗口小部件功能(如我們創建的那樣),則您可以直接在窗口小部件上設置參數,稍后我們將看到。
升級Widget之前,請確保我們的代碼支持從其他存儲庫中獲取提交。讓我們升級時間線條目以支持回購配置:
struct RepoBranch {
let account: String
let repo: String
let branch: String
}
struct LastCommit: TimelineEntry {
public let date: Date
public let commit: Commit
public let branch: RepoBranch
}
從這里,我們可以升級fetcher的fetch()方法,以從任何存儲庫下載任何分支:
static func fetch(account: String, repo: String, branch: String, completion: @escaping (Result) -> Void) {
let branchContentsURL = URL(string: "https://api.github.com/repos/\(account)/\(repo)/branches/\(branch)")!
// ...
}
如前所述,基于意圖的窗口小部件需要使用IntentConfiguration,它與我們之前的靜態方法的唯一主要區別在于,我們必須提供此窗口小部件鏈接到的意圖。讓我們更新小部件以使用IntentConfiguration和LastCommitIntent:
@main
struct CommitCheckerWidget: Widget {
private let kind: String = "CommitCheckerWidget"
public var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: LastCommitIntent.self, provider: CommitTimeline(), placeholder: PlaceholderView()) { entry in
CommitCheckerWidgetView(entry: entry)
}
.configurationDisplayName("A Repo's Latest Commit")
.description("Shows the last commit at the a repo/branch combination.")
}
}
我們必須進行的另一項修改是更新時間軸以從IntentTimelineProvider繼承,而不是從TimelineProvider繼承。它們的工作方式大致相同,不同之處在于intents變體提供對我們intent實例的訪問,從而使我們能夠掌握用戶所做的任何自定義。在這種情況下,我們將更新snapshot()以另外返回偽造的倉庫和我們的時間軸方法以獲取用戶的倉庫配置并使用這些參數來獲取提交。
struct CommitTimeline: IntentTimelineProvider {
typealias Entry = LastCommit
typealias Intent = LastCommitIntent
public func snapshot(for configuration: LastCommitIntent, with context: Context, completion: @escaping (LastCommit) -> ()) {
let fakeCommit = Commit(message: "Fixed stuff", author: "John Appleseed", date: "2020-06-23")
let entry = LastCommit(
date: Date(),
commit: fakeCommit,
branch: RepoBranch(
account: "apple",
repo: "swift",
branch: "master"
)
)
completion(entry)
}
public func timeline(for configuration: LastCommitIntent, with context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let currentDate = Date()
let refreshDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
guard let account = configuration.account,
let repo = configuration.repo,
let branch = configuration.branch
else {
let commit = Commit(message: "Failed to load commits", author: "", date: "")
let entry = LastCommit(date: currentDate, commit: commit, branch: RepoBranch(
account: "???",
repo: "???",
branch: "???"
))
let timeline = Timeline(entries: [entry], policy: .after(refreshDate))
completion(timeline)
return
}
CommitLoader.fetch(account: account, repo: repo, branch: branch) { result in
let commit: Commit
if case .success(let fetchedCommit) = result {
commit = fetchedCommit
} else {
commit = Commit(message: "Failed to load commits", author: "", date: "")
}
let entry = LastCommit(date: currentDate, commit: commit, branch: RepoBranch(
account: account,
repo: repo,
branch: branch
))
let timeline = Timeline(entries: [entry], policy: .after(refreshDate))
completion(timeline)
}
}
}
盡管此代碼有效,但我們的視圖仍將“ apple / swift”硬編碼到其中。讓我們對其進行更新以使用條目現在擁有的新參數:
struct RepoBranchCheckerEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text("\(entry.branch.account)/\(entry.branch.repo)'s \(entry.branch.branch) Latest Commit")
.font(.system(.title3))
.foregroundColor(.black)
Text("\(entry.commit.message)")
.font(.system(.callout))
.foregroundColor(.black)
.bold()
Text("by \(entry.commit.author) at \(entry.commit.date)")
.font(.system(.caption))
.foregroundColor(.black)
Text("Updated at \(Self.format(date:entry.date))")
.font(.system(.caption2))
.foregroundColor(.black)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .leading)
.padding()
.background(LinearGradient(gradient: Gradient(colors: [.orange, .yellow]), startPoint: .top, endPoint: .bottom))
}
static func format(date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "MM-dd-yyyy HH:mm"
return formatter.string(from: date)
}
}
現在,運行您的應用程序并檢查Widget。您看到的回購配置將由您在intents文件中添加的默認值確定,但是如果您長按Widget并單擊Edit(編輯)按鈕,現在將能夠自定義Intent的參數并更改獲取的回購!