版本記錄
| 版本號(hào) | 時(shí)間 |
|---|---|
| V1.0 | 2020.11.20 星期五 |
前言
WidgetKit是iOS14的新的SDK,接下來幾篇我們就一起看一下這個(gè)專題。感興趣的可以看下面幾篇文章。
1. WidgetKit框架詳細(xì)解析(一) —— 基本概覽(一)
開始
首先看下主要內(nèi)容:
在本教程中,您將向一個(gè)大型
SwiftUI應(yīng)用添加小部件(widget),重用其視圖以顯示該應(yīng)用存儲(chǔ)庫中的條目。內(nèi)容來自翻譯。
下面就看下寫作環(huán)境
Swift 5, iOS 14, Xcode 12
接著就是正文了
在今年的WWDC Platforms State of the Union中看到新的主屏幕小部件(widgets)后,我就知道必須為自己喜歡的應(yīng)用制作一個(gè)!在主屏幕上看到它真是太好了。我并不孤單。每個(gè)人都在做!蘋果公司知道自己是贏家,并提供了一個(gè)由三部分組成的代碼,以使所有人開始使用。
已經(jīng)發(fā)布了幾本指導(dǎo)手冊(cè),那么本教程有什么不同?好吧,我決定將一個(gè)小部件添加到由一組開發(fā)人員編寫的相當(dāng)大的SwiftUI應(yīng)用程序中,但沒人是我。有大量的代碼可供篩選,以查找構(gòu)建窗口小部件所需的內(nèi)容。而且所有這些都沒有考慮到小部件的編寫。因此,請(qǐng)跟隨我,向我展示如何做到。
注意:您將需要
Xcode 12 beta。您還需要運(yùn)行iOS 14的iOS設(shè)備。Catalina可以。如果您有運(yùn)行Big Sur Beta的Mac [partition],則可以嘗試在其中運(yùn)行代碼,以防它無法在Catalina上運(yùn)行。最重要的是,目前這是一個(gè)真正的
bleeding-edge API。WWDC演示中出現(xiàn)的內(nèi)容不是Xcode 12 beta 1的一部分。您可能會(huì)遇到一些不穩(wěn)定的情況。就是說,Widgets很酷,很有趣!
在打開啟動(dòng)程序項(xiàng)目之前,請(qǐng)打開Terminal,cd到starter / emitron-iOS-development文件夾,然后運(yùn)行以下命令:
scripts/generate_secrets.sh
您正在生成運(yùn)行項(xiàng)目所需的一些機(jī)密文件。
現(xiàn)在,在starter / emitron-iOS-development文件夾中打開Emitron項(xiàng)目。這需要一些時(shí)間才能獲取一些軟件包,因此,這里有一些有關(guān)項(xiàng)目的信息,您可以在等待的同時(shí)進(jìn)行。
Emitron是raywenderlich.com應(yīng)用程序。如果您是訂閱者(subscriber),則一定已將其安裝在iPhone和iPad上。它可以讓您流式傳輸視頻,并且,如果您具有專業(yè)訂閱,則可以下載視頻以進(jìn)行離線播放。
該項(xiàng)目是開源的。您可以在其GitHub存儲(chǔ)庫GitHub repository,中閱讀有關(guān)它的信息,當(dāng)然,歡迎您為它的改進(jìn)做出貢獻(xiàn)。
您下載的starter版進(jìn)行了一些修改:
- 設(shè)置是最新的,
iOS Deployment Target是14.0。 - 在
Downloads / DownloadService.swift中,注釋了兩個(gè)在Xcode beta 1中引起錯(cuò)誤的promise語句。下載服務(wù)是針對(duì)專業(yè)訂閱的,本教程不需要它。 - 在
Guardpost / Guardpost.swift中,將authSession?.prefersEphemeralWebBrowserSession設(shè)置為false,從而避免每次構(gòu)建和運(yùn)行應(yīng)用程序時(shí)都需要輸入登錄詳細(xì)信息。您仍然必須點(diǎn)擊Sign in,提示您使用raywenderlich.com登錄。點(diǎn)擊Continue。首次構(gòu)建和運(yùn)行時(shí),您可能仍必須輸入電子郵件和密碼,但是在隨后的構(gòu)建和運(yùn)行中,點(diǎn)擊Continue會(huì)跳過登錄表單。
到目前為止,Xcode已經(jīng)安裝了所有軟件包。在模擬器中構(gòu)建并運(yùn)行。

忽略有關(guān)Hashable和SwiftyJSON的警告。 滾動(dòng)和播放在Xcode beta 1中效果不佳,但是您不會(huì)在本教程中對(duì)此進(jìn)行修復(fù)。 如果滾動(dòng)“太多”,則該應(yīng)用程序?qū)⒈罎ⅰ?也不是你的問題。
WidgetKit
本教程全部關(guān)于向Emitron添加閃亮的新小部件。
Adding a Widget Extension
首先添加帶有File ? New ? Target…的widget extension。

搜索widget,選擇Widget Extension并點(diǎn)擊Next:

將其命名為EmitronWidget,并確保未選中Include Configuration Intent:

有兩種窗口小部件配置(widget configurations):Static 和 Intent。 具有IntentConfiguration的小部件使用Siri Intents來使用戶自定義小部件參數(shù)。
單擊Finish并同意激活方案(activate-scheme)對(duì)話框:

1. Running Your Widget
小部件模板(widget template)提供了許多您只需自定義的樣板代碼。它可以直接使用,因此,您可以立即設(shè)置所有內(nèi)容,以確保在準(zhǔn)備測(cè)試代碼時(shí)一切都能順利運(yùn)行。
注意:
Xcode 12 beta 1模擬器不會(huì)在widget gallery中顯示您的widget。因此,在將來的某個(gè)Beta版本之前,您必須在iOS 14設(shè)備上構(gòu)建并運(yùn)行。如果您沒有可用的設(shè)備,則可以運(yùn)行Widget scheme而不是主Emitron scheme,該Widget將出現(xiàn)在模擬器的主屏幕上。
在項(xiàng)目導(dǎo)航器中,選擇頂級(jí)Emitron文件夾對(duì)targets進(jìn)行簽名。更改bundle identifier,并為每個(gè)target的每個(gè)版本設(shè)置team。
注意:對(duì)于
widget,您可能會(huì)遇到一個(gè)明顯的Xcode bug,該bug將三個(gè)版本中的兩個(gè)的簽名標(biāo)識(shí)設(shè)置為Distribution。如果看到此錯(cuò)誤,請(qǐng)打開Build Settings,搜索distribution并將簽名標(biāo)識(shí)更改為Apple Development。
最后一個(gè)陷阱:確保小部件的bundle ID前綴與應(yīng)用程序的ID匹配。這意味著您將需要在“ ios”和“ EmitronWidget”之間插入dev以獲取your.prefix.emitron.ios.dev.EmitronWidget。
OK,現(xiàn)在連接您的iOS設(shè)備,選擇Emitron scheme和您的設(shè)備,然后構(gòu)建并運(yùn)行。登錄,然后關(guān)閉應(yīng)用程序,然后在主窗口的空白區(qū)域上按,直到圖標(biāo)開始抖動(dòng)。
點(diǎn)擊右上角的+按鈕,然后向下滾動(dòng)以找到raywenderlich:

選擇它可以查看三種尺寸的快照:

點(diǎn)擊Add Widget以在屏幕上查看您的小部件:

點(diǎn)擊widget以重新打開Emitron。
您的小部件widget起作用了! 現(xiàn)在,您只需要使其顯示來自Emitron的信息即可。
Defining Your Widget
使您的小部件顯示應(yīng)用程序?yàn)槊總€(gè)教程顯示的一些信息是很有意義的。

此視圖在UI / Shared / Content List / CardView.swift中定義。 我的第一個(gè)想法是將窗口widget target添加到此文件中。 但這需要添加越來越多的文件,以容納Emitron中所有復(fù)雜的連接。
您真正需要的只是Text視圖。 這些圖片很可愛,但是您需要包括持久性基礎(chǔ)結(jié)構(gòu)以防止它們消失。
您將復(fù)制相關(guān)Text視圖的布局。 它們使用幾個(gè)實(shí)用程序擴(kuò)展,因此找到這些文件并將EmitronWidgetExtension target添加到其中:

注意:確保注意到圖像頂部
Assets。
CardView顯示ContentListDisplayable對(duì)象的屬性。 這是Displayable / ContentDisplayable.swift中定義的協(xié)議:
protocol ContentListDisplayable: Ownable {
var id: Int { get }
var name: String { get }
var cardViewSubtitle: String { get }
var descriptionPlainText: String { get }
var releasedAt: Date { get }
var duration: Int { get }
var releasedAtDateTimeString: String { get }
var parentName: String? { get }
var contentType: ContentType { get }
var cardArtworkUrl: URL? { get }
var ordinal: Int? { get }
var technologyTripleString: String { get }
var contentSummaryMetadataString: String { get }
var contributorString: String { get }
// Probably only populated for screencasts
var videoIdentifier: Int? { get }
}
您的widget僅需要name,cardViewSubtitle,descriptionPlainText和releasedAtDateTimeString。 因此,您將為這些屬性創(chuàng)建一個(gè)結(jié)構(gòu)。
1. Creating a TimelineEntry
創(chuàng)建一個(gè)新的名為WidgetContent.swift的Swift文件,并確保其targets是emitron和EmitronWidgetExtension:

它應(yīng)該在EmitronWidget組中。
現(xiàn)在,將此代碼添加到新文件中:
import WidgetKit
struct WidgetContent: TimelineEntry {
var date = Date()
let name: String
let cardViewSubtitle: String
let descriptionPlainText: String
let releasedAtDateTimeString: String
}
要在窗口小部件中使用WidgetContent,它必須符合TimelineEntry。 唯一必需的屬性是date,您可以將其初始化為當(dāng)前日期。
2. Creating an Entry View
接下來,創(chuàng)建一個(gè)視圖以顯示四個(gè)String屬性。 創(chuàng)建一個(gè)新的SwiftUI View文件,并將其命名為EntryView.swift。 確保其target僅是EmitronWidgetExtension,并且也應(yīng)位于EmitronWidget組中:

現(xiàn)在,用以下代碼替換struct EntryView的內(nèi)容:
let model: WidgetContent
var body: some View {
VStack(alignment: .leading) {
Text(model.name)
.font(.uiTitle4)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
.padding([.trailing], 15)
.foregroundColor(.titleText)
Text(model.cardViewSubtitle)
.font(.uiCaption)
.lineLimit(nil)
.foregroundColor(.contentText)
Text(model.descriptionPlainText)
.font(.uiCaption)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
.lineSpacing(3)
.foregroundColor(.contentText)
Text(model.releasedAtDateTimeString)
.font(.uiCaption)
.lineLimit(1)
.foregroundColor(.contentText)
}
.background(Color.cardBackground)
.padding()
.cornerRadius(6)
}
您實(shí)質(zhì)上是從CardView復(fù)制Text視圖并添加填充間距。
完全刪除EntryView_Previews。
3. Creating Your Widget
現(xiàn)在開始定義窗口widget。 打開EmitronWidget.swift并在該行中雙擊SimpleEntry:
public typealias Entry = SimpleEntry
選擇Editor ? Edit All in Scope,并將名稱更改為WidgetContent。 這將導(dǎo)致一些錯(cuò)誤,您將在接下來的幾個(gè)步驟中進(jìn)行修復(fù)。 首先刪除聲明:
struct WidgetContent: TimelineEntry {
public let date: Date
}
現(xiàn)在,此聲明是多余的,并且與WidgetContent.swift中的聲明沖突。
4. Creating a Snapshot Entry
provider的一種方法提供了一個(gè)快照條目,以顯示在widget gallery中。 為此,您將使用特定的WidgetContent對(duì)象。
在import語句的下面,添加此全局對(duì)象:
let snapshotEntry = WidgetContent(
name: "iOS Concurrency with GCD and Operations",
cardViewSubtitle: "iOS & Swift",
descriptionPlainText: """
Learn how to add concurrency to your apps! \
Keep your app's UI responsive to give your \
users a great user experience.
""",
releasedAtDateTimeString: "Jun 23 2020 ? Video Course (3 hrs, 21 mins)")
這是我們的并發(fā)視頻課程的更新,該課程在WWDC第2天發(fā)布。
現(xiàn)在,將snapshot(with:completion:)的第一行替換為:
let entry = snapshotEntry
當(dāng)您在gallery中查看此小部件時(shí),它將顯示此條目。
5. Creating a Temporary Timeline
小部件需要一個(gè)TimelineProvider才能為其提供TimelineEntry類型的條目。 它會(huì)在條目的date屬性指定的時(shí)間顯示每個(gè)條目。
最重要的provider方法是timeline(with:completion:)。 它已經(jīng)有一些代碼來構(gòu)造時(shí)間軸,但是您沒有足夠的條目。 因此,注釋掉最后兩行以外的所有內(nèi)容,并添加以下行:
let entries = [snapshotEntry]
您正在創(chuàng)建一個(gè)僅包含snapshotEntry的entries數(shù)組。
6. Creating a Placeholder View
小部件在等待實(shí)際時(shí)間軸條目時(shí)顯示其PlaceholderView。 您還將為此使用snapshotEntry。
以此替換Text視圖:
EntryView(model: snapshotEntry)
WWDC代碼中還顯示了一個(gè)特殊的修飾符,該修飾符使視圖的內(nèi)容模糊不清,以表明這是一個(gè)占位符,而不是真實(shí)的東西。 是這樣的:
.isPlaceholder(true)
在WWDC視頻中看起來很酷,但是在Xcode 12 beta 1中沒有編譯。有關(guān)更多信息,請(qǐng)參閱Apple開發(fā)者論壇中的此項(xiàng) this entry。
7. Defining Your Widget
最后,您可以將所有這些部分放在一起。
首先,刪除EmitronWidgetEntryView。 您將改用EntryView。
現(xiàn)在,將EmitronWidget的內(nèi)部替換為以下內(nèi)容:
private let kind: String = "EmitronWidget"
public var body: some WidgetConfiguration {
StaticConfiguration(
kind: kind,
provider: Provider(),
placeholder: PlaceholderView()
) { entry in
EntryView(model: entry)
}
.configurationDisplayName("RW Tutorials")
.description("See the latest video tutorials.")
}
這三個(gè)字符串是您想要的:kind描述您的窗口小部件,最后兩個(gè)字符串顯示在庫中每個(gè)窗口小部件上方的尺寸。
在您的設(shè)備上構(gòu)建并運(yùn)行,登錄,然后關(guān)閉該應(yīng)用以查看您的小部件。
如果仍然顯示時(shí)間,請(qǐng)將其刪除并重新添加。

這是中等大小的小部件現(xiàn)在的樣子:

只有中等大小的小部件看起來不錯(cuò),因此請(qǐng)修改您的小部件以僅提供該大小。 在.description下面添加此修飾符:
.supportedFamilies([.systemMedium])
接下來,您將直接從應(yīng)用程序的存儲(chǔ)庫中為時(shí)間線提供真實(shí)的條目!
Providing Timeline Entries
該應(yīng)用程序?qū)⒃?code>Data / ContentRepositories / ContentRepository.swift中創(chuàng)建的contents中顯示ContentListDisplayable對(duì)象的數(shù)組。 要與您的小部件widget共享此信息,您將創(chuàng)建一個(gè)應(yīng)用程序組。 然后,在ContentRepository.swift中,將文件寫入此應(yīng)用程序組,并在EmitronWidget.swift中讀取該文件。
1. Creating an App Group
在項(xiàng)目頁面上,選擇emitron target。 在Signing & Capabilities選項(xiàng)卡中,單擊+ Capability,然后將App Group拖到窗口中。 將其命名為group.your.prefix.emitron.contents;確保適當(dāng)替換your.prefix。
現(xiàn)在,選擇EmitronWidgetExtension target并添加App Group功能。 滾動(dòng)瀏覽App Group以查找并選擇group.your.prefix.emitron.contents。
2. Writing the Contents File
在ContentRepository.swift的頂部,在import Combine語句的下面,添加以下代碼:
import Foundation
extension FileManager {
static func sharedContainerURL() -> URL {
return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.your.prefix.emitron.contents"
)!
}
}
這只是獲取應(yīng)用程序組容器的URL的一些標(biāo)準(zhǔn)代碼。 確保替換您的應(yīng)用標(biāo)識(shí)符前綴。
現(xiàn)在,在var contents下面,添加此輔助方法:
func writeContents() {
let widgetContents = contents.map {
WidgetContent(name: $0.name, cardViewSubtitle: $0.cardViewSubtitle,
descriptionPlainText: $0.descriptionPlainText,
releasedAtDateTimeString: $0.releasedAtDateTimeString)
}
let archiveURL = FileManager.sharedContainerURL()
.appendingPathComponent("contents.json")
print(">>> \(archiveURL)")
let encoder = JSONEncoder()
if let dataToSave = try? encoder.encode(widgetContents) {
do {
try dataToSave.write(to: archiveURL)
} catch {
print("Error: Can't write contents")
return
}
}
}
在這里,您將創(chuàng)建一個(gè)WidgetContent對(duì)象的數(shù)組,每個(gè)對(duì)象用于存儲(chǔ)庫中的每個(gè)項(xiàng)目。 您將它們分別轉(zhuǎn)換為JSON并將其保存到app group的容器中。
在let archiveURL行設(shè)置一個(gè)斷點(diǎn)。
設(shè)置contents后,您將調(diào)用此方法。 將此didSet閉包添加到contents中:
didSet {
writeContents()
}
如果Xcode在警告WidgetContent。 跳轉(zhuǎn)到WidgetContent的定義,使其符合Codable:
struct WidgetContent: Codable, TimelineEntry {
現(xiàn)在,在模擬器中構(gòu)建并運(yùn)行該應(yīng)用程序。 在斷點(diǎn)處,widgetContents具有20個(gè)值。
繼續(xù)執(zhí)行程序并在應(yīng)用程序中向下滾動(dòng)。 在斷點(diǎn)處,widgetContents現(xiàn)在具有40個(gè)值。 因此,您可以控制與小部件共享多少項(xiàng)。
停止應(yīng)用程序,禁用斷點(diǎn),然后從調(diào)試控制臺(tái)復(fù)制URL文件夾路徑并在Finder中定位。 看一下contents.json。
接下來,轉(zhuǎn)到并設(shè)置widget以讀取此文件。
3. Reading the Contents File
在EmitronWidget.swift中,添加相同的FileManager代碼:
extension FileManager {
static func sharedContainerURL() -> URL {
return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.your.prefix.emitron.contents"
)!
}
}
確保更新您的前綴。
將此幫助程序方法添加到Provider:
func readContents() -> [Entry] {
var contents: [WidgetContent] = []
let archiveURL =
FileManager.sharedContainerURL()
.appendingPathComponent("contents.json")
print(">>> \(archiveURL)")
let decoder = JSONDecoder()
if let codeData = try? Data(contentsOf: archiveURL) {
do {
contents = try decoder.decode([WidgetContent].self, from: codeData)
} catch {
print("Error: Can't decode contents")
}
}
return contents
}
這將讀取您保存到應(yīng)用程序組容器中的文件。
取消注釋timeline(with:completion:)中的代碼,然后替換此行:
var entries: [WidgetContent] = []
使用下面
var entries = readContents()
接下來,修改注釋和for循環(huán)以將日期添加到條目中:
// Generate a timeline by setting entry dates interval seconds apart,
// starting from the current date.
let currentDate = Date()
let interval = 5
for index in 0 ..< entries.count {
entries[index].date = Calendar.current.date(byAdding: .second,
value: index * interval, to: currentDate)!
}
刪除for循環(huán)下面的let entry行。
之后的那一行設(shè)置時(shí)間軸運(yùn)行并指定刷新策略。 在這種情況下,時(shí)間軸將在用完所有當(dāng)前條目后刷新。
在您的設(shè)備上構(gòu)建并運(yùn)行,登錄并加載列表。 然后關(guān)閉該應(yīng)用程序,添加您的小部件并觀看它每5秒更新一次。

我可以整天看這個(gè)。
如果您沒有滾動(dòng)列表,則該widget將在20個(gè)項(xiàng)目后用完所有條目。如果等待那么長(zhǎng)時(shí)間,您會(huì)在刷新時(shí)看到它暫停。
注意:這是
Beta版軟件。如果未獲得預(yù)期的結(jié)果,請(qǐng)嘗試從設(shè)備中刪除該應(yīng)用,然后重新啟動(dòng)設(shè)備。另外,請(qǐng)記住,小部件并不是要以秒為單位測(cè)量時(shí)間間隔。在教程設(shè)置中,非常短的間隔只是更加方便。但是結(jié)果是,時(shí)間軸刷新的等待時(shí)間感覺很長(zhǎng)!最后一條警告:不要讓5秒小部件在設(shè)備上運(yùn)行,因?yàn)樗鼤?huì)耗盡電池電量。
Enabling User Customization
我為時(shí)間軸間隔選擇了5秒,因此無需等待很長(zhǎng)時(shí)間即可看到更新。如果您想要更短或更長(zhǎng)的間隔,只需更改代碼中的值即可?;蛘?code>...創(chuàng)建一個(gè)intent,讓您可以通過在主屏幕上直接編輯小部件來設(shè)置時(shí)間間隔!
注意:使用
intent更改時(shí)間間隔時(shí),直到widget刷新其時(shí)間軸,您才會(huì)看到效果。
1. Adding an Intent
首先,添加您的intent:創(chuàng)建一個(gè)新文件(Command-N),搜索intent,選擇SiriKit Intent Definition File并將其命名為TimelineInterval。確保其target同時(shí)是emitron和EmitronWidgetExtension。
在intent側(cè)邊欄的左下角,單擊+,然后選擇New Intent。

將intent命名為TimelineInterval。 如圖所示,使用Category View設(shè)置Custom Intent:

并添加一個(gè)名為Integer類型的interval的參數(shù),其默認(rèn)值,最小值和最大值(如所示)和Type Field。 或設(shè)置您自己的值和/或使用步進(jìn)器。

2. Reconfiguring Your Widget
在EmitronWidget.swift中,將小部件重新配置為IntentConfiguration。
將Provider協(xié)議更改為IntentTimelineProvider。
struct Provider: IntentTimelineProvider {
將snapshot(with:completion:)定義為:
public func snapshot(
for configuration: TimelineIntervalIntent,
with context: Context,
completion: @escaping (Entry) -> Void
) {
現(xiàn)在,將timeline(with:completion:)的定義更改為:
public func timeline(
for configuration: TimelineIntervalIntent,
with context: Context,
completion: @escaping (Timeline<Entry>) -> Void
) {
在timeline(for:with:completion)中,更改interval以使用配置參數(shù):
let interval = configuration.interval as! Int
最后,在EmitronWidget中,將StaticConfiguration(kind:provider:placeholder :)更改為此:
IntentConfiguration(
kind: kind,
intent: TimelineIntervalIntent.self,
provider: Provider(),
placeholder: PlaceholderView()
) { entry in
在您的設(shè)備上構(gòu)建并運(yùn)行,登錄并加載列表。 關(guān)閉應(yīng)用程序,添加小部件,然后長(zhǎng)按該小部件。 它會(huì)翻轉(zhuǎn)以顯示Edit Widget按鈕。

點(diǎn)擊此按鈕更改間隔值


本教程向您展示了如何利用大型應(yīng)用程序中的代碼來創(chuàng)建一個(gè)小部件,以顯示該應(yīng)用程序自己的存儲(chǔ)庫中的項(xiàng)目。 以下是一些您可以添加到Emitron小部件中的想法:
- 為大型窗口小部件設(shè)計(jì)一個(gè)視圖,該視圖顯示兩個(gè)或更多條目。 查看
Apple的EmojiRangers示例應(yīng)用程序,以了解如何為小部件系列修改EntryView。 - 將一個(gè)
widgetURL添加到EntryView,以便點(diǎn)按該小部件可在該項(xiàng)目的詳細(xì)信息視圖中打開Emitron。 - 添加
intent以使用戶可以從小部件設(shè)置應(yīng)用程序的過濾器。
后記
本篇主要講述了基于
WidgetKit和SwiftUI的簡(jiǎn)單示例,感興趣的給個(gè)贊或者關(guān)注~~~
