iOS Widget#
背景#
一開始是發現支付寶的 Widget 做得很好看,打算仿作一個,做的過程中才發現,原來 Widget 有這麼多好玩的地方。所以,在這裡記錄分享一下:
你知道如何創建類似於 QQ 同步助手的 Widget 嗎?
你知道類似於東方財富的不同組 Widget 效果是怎麼實現的嗎?
你知道下圖中支付寶 Widget 功能是怎麼實現的嗎?
或者這麼問,對於這幾個的概念:supportedFamilies
、WidgetBundle
以及Configurable Widget
是否知曉,如果都知道,那就不需要看本篇文章了。
- QQ 同步助手的 Widget 只顯示一個 Widget 的效果,是設置了 Widget 的
supportedFamilies
只有 systemMedium 樣式; - 東方財富的多組 Widget,是通過
WidgetBundle
實現的,可以設置多個 Widget,每個 Widget 都可以設置自己的大中小;區分是否使用了WidgetBundle
,可以通過滑動時,Widget 預覽界面同步的文字是否跟著滑動來區分:同一個 Widget 的大中小不同樣式,滑動時頂部的標題和描述是不會動的;不同組的 Widget,每個 Widget 都有自己的標題和描述,滑動時,文字是跟著一起滑動的。 - 支付寶的 Widget,使用了
Configurable Widget
,定義了Enum
類型和自定義數據類型,設置 Intent 的Dynamic Options
和Supports multiple values
。
開發#
開始前的說明:
如果要用到 APP 和 Widget 傳值通信,比如支付寶中天氣的顯示,從 APP 定位獲取的城市,保存到本地,從 Widget 中獲取到本地保存的城市,再去獲取天氣。這中間的傳值需要 APPGroup,如果不需要 APP 和 Widget 傳值的,則不需要設置 APPGroup;但是如果設置 APPGroup 的話,需要注意 Widget 和主 APP 的 APPGroup 要一致。APPGroup 的詳細使用可參考 App 之間的數據共享 ——App Group 的配置,這裡就不展開說明了。
創建 Widget#
創建 Widget,選擇 File -> New -> Target -> Widget Extension。
點擊下一步,輸入 Widget 的名字,取消勾選 Include Configuration Intent。
點擊下一步,會出現是否切換 Target 的提示,如下,點擊 Activate,切換到 Widget Target;
上面的步驟點擊 Activate 或者取消都可以;點擊 Activate 是 Xcode 主動把 Target 切換到 Widget,點擊取消是保持當前 Target。隨時可以手動切換。
這樣 Widget 就創建好了,來看下目前項目的結構,如下
再來看下,Widget 中 .swift 文件的代碼,入口和代理方法都在這個類中:
其中分為幾個部分,如下
- TimeLineProvider,protocol 類型定義了三個必須實現的方法,用於 Widget 的默認展示和何時刷新
func placeholder(in context: Context) -> SimpleEntry
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ())
這個方法定義 Widget 預覽中如何展示,所以提供默認值要在這裡func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ())
這個方法裡,決定 Widget 何時刷新
- TimelineEntry,這個類也是必須實現的,裡面的 Date 用於判斷刷新時機,如果有自定義 Intent 的話,也是從這裡傳值到 View
- View,Widget 的 View
- Widget,Widget 的 title 和 description,以及 supportedFamilies 都在這裡設置
- PreviewProvider,這個是 SwiftUI 的預覽,即邊修改邊看效果,可刪除
看完上面可能還是一頭霧水,不要緊,接著往下看,跟著做一兩個 Widget 就明白每個部分的作用了。
QQ 同步助手的 Widget#
WidgetUI 創建#
先來做一個最簡單的 QQ 同步助手的 Widget,下載 Tutorial1
文件夾中的項目,打開,新建 SwiftUIView
,如下:
點擊 Next,文件名字輸入 QQSyncWidgetView
,這裡需要注意選中的 Target 是 Widget 的 Target,而不是主工程的,如下:
然後打開 QQSyncWidgetView
,文件內容如下:
//
// QQSyncWidgetView.swift
// DemoWidgetExtension
//
// Created by Horizon on 01/06/2022.
//
import SwiftUI
struct QQSyncWidgetView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
struct QQSyncWidgetView_Previews: PreviewProvider {
static var previews: some View {
QQSyncWidgetView()
}
}
其中,QQSyncWidgetView
中是 SwiftUI
View 的代碼,即要修改佈局的地方;QQSyncWidgetView_Previews
是控制預覽 View 的,可以刪除。然後來看要實現的 QQ 同步助手的 Widget 包含有哪些內容:
如上可以分為三個部分,背景圖片、左側文字 View、右側文字 View,背景圖片和兩個 View 之間前後關係用 ZStack 的實現,兩個 View 之間左右關係用 HStack,View 裡面的文字的上下佈局是 VStack,測試用的資源文件放在 QQSyncImages
文件夾下。
SwiftUI
的內容可以參考斯坦福老爺子的教程,鏈接如下:
填充內容後大致如下:
struct QQSyncWidgetView: View {
ZStack {
// 背景圖片
Image("widget_background_test")
.resizable()
// 左右兩個 View
HStack {
Spacer()
// 左 View
VStack(alignment: .leading) {
Spacer()
Text("所有快樂都向你靠攏,所有好運都在路上。")
.font(.system(size: 19.0))
.fontWeight(.semibold)
.minimumScaleFactor(0.5)
.foregroundColor(.white)
Spacer()
Text("加油,打工人!😄")
.font(.system(size: 16.0))
.minimumScaleFactor(0.5)
.foregroundColor(.white)
Spacer()
}
Spacer()
// 右 View
VStack {
Spacer()
Text("06")
.font(.system(size: 50.0))
.fontWeight(.semibold)
.foregroundColor(.white)
.padding(EdgeInsets(top: -10.0, leading: 0.0, bottom: -10.0, trailing: 0.0))
Text("06月 周一")
.lineLimit(1)
.minimumScaleFactor(0.5)
.font(.system(size: 14.0))
.foregroundColor(.white)
Spacer()
Text("去分享")
.fixedSize()
.font(.system(size: 14.0))
.padding(EdgeInsets(top: 5.0, leading: 20.0, bottom: 5.0, trailing: 20.0))
.background(.white)
.foregroundColor(.black)
.cornerRadius(12.0)
Spacer()
}
Spacer()
}
.padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
}
}
然後修改入口,打開 DemoWidget.swift
,其中 DemoWidgetEntryView
是組件顯示的 View,所以這裡修改為剛剛創建的 QQSyncWidgetView
,修改如下:
struct DemoWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
QQSyncWidgetView()
}
}
效果如下:
效果已經和 QQ 同步助手的類似,但是上面的代碼還需要再優化一下,類太臃腫;可以把每個 VStack
單獨封裝成一個 view,也能方便復用。創建 SwiftUIView 命名為 QQSyncQuoteTextView
用於 Widget 左半邊的 view 展示;創建右半邊的 view 命名為 QQSyncDateShareView
,最終代碼為:
QQSyncQuoteTextView
類:
import SwiftUI
struct QQSyncQuoteTextView: View {
var body: some View {
VStack(alignment: .leading) {
Spacer()
Text("所有快樂都向你靠攏,所有好運都在路上。")
.font(.system(size: 19.0))
.fontWeight(.semibold)
.minimumScaleFactor(0.5)
.foregroundColor(.white)
Spacer()
Text("加油,打工人!😄")
.font(.system(size: 16.0))
.minimumScaleFactor(0.5)
.foregroundColor(.white)
Spacer()
}
}
}
QQSyncDateShareView
類:
import SwiftUI
struct QQSyncDateShareView: View {
var body: some View {
VStack {
Spacer()
Text("06")
.font(.system(size: 50.0))
.fontWeight(.semibold)
.foregroundColor(.white)
.padding(EdgeInsets(top: -10.0, leading: 0.0, bottom: -10.0, trailing: 0.0))
Text("06月 周一")
.lineLimit(1)
.minimumScaleFactor(0.5)
.font(.system(size: 14.0))
.foregroundColor(.white)
Spacer()
Text("去分享")
.fixedSize()
.font(.system(size: 14.0))
.padding(EdgeInsets(top: 5.0, leading: 20.0, bottom: 5.0, trailing: 20.0))
.background(.white)
.foregroundColor(.black)
.cornerRadius(12.0)
Spacer()
}
}
}
最後修改 QQSyncWidgetView
為:
import SwiftUI
struct QQSyncWidgetView: View {
var body: some View {
ZStack {
// 背景圖片
Image("widget_background_test")
.resizable()
// 左右兩個 View
HStack {
// 左 View
QQSyncQuoteTextView()
// 右 View
QQSyncDateShareView()
}
.padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
}
}
}
然後再運行,發現效果和之前相同,bingo。
不同 widget 尺寸設置#
再來看【widget 大小的設置】,目前開發的 Widget 在 Medium 大小時,顯示是正好的,但是還有 Small 和 Large 的大小,顯示都是不正常的,那這個是如何設置的呢?怎麼針對不同大小,設置顯示不同的內容?
設置不同大小不同內容的 Widget,需要使用 WidgetFamily
,使用需要導入 WidgetKit
,比如設置 Small 時 Medium 的右半部分,Medium 時顯示不變,要怎麼做呢?
- 在要設置的類中導入
WidgetKit
- 聲明屬性
@Environment(\.widgetFamily) var family: WidgetFamily
- 使用
Switch
列舉family
注:
@Environment
是使用SwiftUI
本身預定義的 key,更多關於@Environment
的內容可以參考下面兩個鏈接:
具體代碼如下:
import SwiftUI
import WidgetKit
struct QQSyncWidgetView: View {
@Environment(\.widgetFamily) var family: WidgetFamily
var body: some View {
ZStack {
// 背景圖片
Image("widget_background_test")
.resizable()
switch family {
case .systemSmall:
QQSyncDateShareView()
case .systemMedium:
// 左右兩個 View
HStack {
// 左 View
QQSyncQuoteTextView()
// 右 View
QQSyncDateShareView()
}
.padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
// case .systemLarge:
// break
// case .systemExtraLarge:
// break
default:
QQSyncQuoteTextView()
}
}
}
}
運行查看效果,如下:
發現效果和預期一樣,但是代碼看起來真的有點醜,同樣再優化一下,封裝 QQSyncWidgetMedium
和 QQSyncWidgetSmall
兩個類,如下:
import SwiftUI
struct QQSyncWidgetSmall: View {
var body: some View {
ZStack {
// 背景圖片
Image("widget_background_test")
.resizable()
QQSyncDateShareView()
}
}
}
import SwiftUI
struct QQSyncWidgetMedium: View {
var body: some View {
ZStack {
// 背景圖片
Image("widget_background_test")
.resizable()
// 左右兩個 View
HStack {
// 左 View
QQSyncQuoteTextView()
Spacer()
// 右 View
QQSyncDateShareView()
}
.padding(EdgeInsets(top: 0.0, leading: 20.0, bottom: 0.0, trailing: 20.0))
}
}
}
然後修改 QQSyncWidgetView
,如下:
import SwiftUI
import WidgetKit
struct QQSyncWidgetView: View {
@Environment(\.widgetFamily) var family: WidgetFamily
var body: some View {
switch family {
case .systemSmall:
QQSyncWidgetSmall()
case .systemMedium:
QQSyncWidgetMedium()
// case .systemLarge:
// break
// case .systemExtraLarge:
// break
default:
QQSyncWidgetMedium()
}
}
}
再次運行,查看效果,還是預期的效果,但是代碼看起來簡潔明瞭了,如果想要添加 Large 的 View,只需要再定義 QQSyncWidgetLarge
類,然後在上面這個地方使用即可,方便快捷。
接著再來看筆者創建的項目,添加 Widget 時,Small
、Medium
、Large
都有,即使在上面的 Switch family
中註釋掉了 Small
和 Large
,預覽時仍舊這兩個尺寸仍舊在;而當添加 QQ 同步助手的 Widget 時,可以看到它的 Widget 只有一個 Medium 尺寸的,這又是如何做到的呢?
這是通過 @main 入口處設置 supportedFamilies
屬性實現的,supportedFamilies
傳入一個尺寸數組,傳入幾個尺寸則支持幾個尺寸,而參照 QQ 同步助手的效果,只傳入了 .systemMedium
尺寸,代碼如下:
@main
struct DemoWidget: Widget {
let kind: String = "DemoWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
DemoWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
.supportedFamilies([WidgetFamily.systemMedium]) // 設置預覽 widget 中支持的尺寸數組
}
}
widget 日期更新#
上面顯示的部分已經完成,接下來,再來看【日期的設置】,目前的日期是固定的,如何讓日期取用手機時間,要怎麼做?
需要考慮的是:
- 日期從哪裡來?—— 可以在 Extension 中,直接使用 Date () 來獲取到當前的日期
- 日期更新了怎麼通知刷新?參考 cs193p-Developing Apps for iOS,使用
ObservableObject
定義一個@Published
修飾的屬性,然後在使用的 View 中使用@ObservedObject
修飾的屬性,這樣當@Published
修飾的屬性有變化時,@ObservedObject
修飾的屬性就會變化,從而刷新界面。
代碼實現如下:
首先創建 swift 文件,注意,model 類創建使用的 swift,而 UI 創建的類是 SwiftUI。
新建 String_Extensions.swift
,定義獲取指定日期類型的字符串,代碼如下:
import Foundation
enum DisplayDateType {
case Year
case Month
case Day
case hour
case minute
case second
}
extension String {
func getFormatDateStr(_ type: DisplayDateType) -> String {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
guard let formatDate = dateFormatter.date(from: self) else { return "" }
let calendar = Calendar.current
_ = calendar.component(.era, from: formatDate)
let year = calendar.component(.year, from: formatDate)
let month = calendar.component(.month, from: formatDate)
let day = calendar.component(.day, from: formatDate)
let hour = calendar.component(.hour, from: formatDate)
let minute = calendar.component(.minute, from: formatDate)
let second = calendar.component(.second, from: formatDate)
switch type {
case .Year:
return String(format: "%.2zd", year)
case .Month:
return String(format: "%.2zd", month)
case .Day:
return String(format: "%.2zd", day)
case .hour:
return String(format: "%.2zd", hour)
case .minute:
return String(format: "%.2zd", minute)
case .second:
return String(format: "%.2zd", second)
}
}
func getWeekday() -> String {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
guard let formatDate = dateFormatter.date(from: self) else { return "" }
let calendar = Calendar.current
let weekDay = calendar.component(.weekday, from: formatDate)
switch weekDay {
case 1:
return "周日"
case 2:
return "周一"
case 3:
return "周二"
case 4:
return "周三"
case 5:
return "周四"
case 6:
return "周五"
case 7:
return "周六"
default:
return ""
}
}
}
新建 QQSyncWidgetDateItem.swift
類,用於獲取年、月、日、周、時、分、秒的 String
import Foundation
struct QQSyncWidgetDateItem {
var year: String
var month: String
var day: String
var week: String
var hour: String
var minute: String
var second: String
static func generateItem() -> QQSyncWidgetDateItem {
let dateStr = date2String(date: Date())
let year = dateStr.getFormatDateStr(DisplayDateType.Year)
let month = dateStr.getFormatDateStr(DisplayDateType.Month)
let day = dateStr.getFormatDateStr(DisplayDateType.Day)
let week = dateStr.getWeekday()
let hour = dateStr.getFormatDateStr(DisplayDateType.hour)
let minute = dateStr.getFormatDateStr(DisplayDateType.minute)
let second = dateStr.getFormatDateStr(DisplayDateType.second)
let item = QQSyncWidgetDateItem(year: year,
month: month,
day: day,
week: week,
hour: hour,
minute: minute,
second: second)
return item
}
static func date2String(date:Date, dateFormat:String = "yyyy-MM-dd HH:mm:ss") -> String {
let formatter = DateFormatter()
formatter.locale = Locale.init(identifier: "zh_CN")
formatter.dateFormat = dateFormat
let date = formatter.string(from: date)
return date
}
}
新建 QQSyncWidgetDateShareItem.swift
,類似於 Util,把 model 轉為 view 直接能顯示的邏輯和響應點擊的邏輯都可以放入這個類
import Foundation
import SwiftUI
class QQSyncWidgetDateShareItem: ObservableObject {
@Published private var dateItem = QQSyncWidgetDateItem.generateItem()
func dateShareStr() -> String {
let resultStr = dateItem.month + "月 " + dateItem.week
return resultStr
}
func dayStr() -> String {
return dateItem.day
}
// MARK: action
}
然後修改 QQSyncDateShareView
類,添加 QQSyncWidgetDateShareItem
屬性,固定的日期改為從 QQSyncWidgetDateShareItem
獲取
import SwiftUI
struct QQSyncDateShareView: View {
@ObservedObject var dateShareItem: QQSyncWidgetDateShareItem
var body: some View {
VStack {
Spacer()
Text(dateShareItem.dayStr())
.font(.system(size: 50.0))
.fontWeight(.semibold)
.foregroundColor(.white)
.padding(EdgeInsets(top: -10.0, leading: 0.0, bottom: -10.0, trailing: 0.0))
Text(dateShareItem.dateShareStr())
.lineLimit(1)
.minimumScaleFactor(0.5)
.font(.system(size: 14.0))
.foregroundColor(.white)
Spacer()
Text("去分享")
.fixedSize()
.font(.system(size: 14.0))
.padding(EdgeInsets(top: 5.0, leading: 20.0, bottom: 5.0, trailing: 20.0))
.background(.white)
.foregroundColor(.black)
.cornerRadius(12.0)
Spacer()
}
}
}
然後修改有調用 QQSyncDateShareView
的地方,QQSyncWidgetSmall
、QQSyncWidgetMedium
中都添加屬性聲明代碼,並且修改傳入參數;然後修改有引用這兩個類的地方即 QQSyncWidgetView
,也添加屬性聲明修改傳入參數;最後再修改 DemoWidget
類中使用了 DemoWidgetEntryView
的地方,修改為如下:
struct DemoWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
QQSyncWidgetView(dateShareItem: QQSyncWidgetDateShareItem())
}
}
最後修改刷新時機,即何時刷新 widget 數據,是 TimeLineProvider
中的 getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ())
方法控制的,修改成每隔 2 個小時刷新。
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let entry = SimpleEntry(date: Date())
// refresh the data every two hours
let expireDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) ?? Date()
let timeline = Timeline(entries: [entry], policy: .after(expireDate))
completion(timeline)
}
然後運行調試,修改日期,就會發現,widget 展示的日期數據隨著手機日期的修改也跟著變了,done。
widget 網絡數據邏輯#
對比 QQ 同步助手的 Widget,可以發現,每隔一段時間,圖片和文字就自動變化了一次。接下來一起看下這個效果怎麼做,背景圖片的變化和文字的變化類似都是網絡請求,然後更新數據,這裡就僅以文字的更新作為示例。
首先找一個隨機名言的接口,可以參考 https://github.com/vv314/quotes,這裡選擇裡面一言的接口,接口為:https://v1.hitokoto.cn/。接口找好之後,來看下 widget 網絡請求怎麼實現。
新建 Network 文件夾,在 Network 文件夾下新建 NetworkClient.swift
,用於封裝 URLSession
網絡請求,代碼如下:
import Foundation
public final class NetworkClient {
private let session: URLSession = .shared
enum NetworkError: Error {
case noData
}
func executeRequest(request: URLRequest, completion: @escaping (Result<Data, Error>) -> Void) {
session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NetworkError.noData))
return
}
completion(.success(data))
}.resume()
}
}
在 Network 文件夾下新建 URLRequest+Quote.swift
,用於生成 Quote 的 URLRequest,代碼如下:
import Foundation
extension URLRequest {
private static var baseURLStr: String { return "https://v1.hitokoto.cn/" }
static func quoteFromNet() -> URLRequest {
.init(url: URL(string: baseURLStr)!)
}
}
然後參照返回數據格式,創建返回的 model 類,創建 QuoteResItem.swift
,返回數據中只用到了 hitokoto 字段,所以只需要定義這個字段即可,代碼如下:
import Foundation
struct QuoteResItem: Codable {
/**
"id": 6325,
"uuid": "2017e206-f81b-48c1-93e3-53a63a9de199",
"hitokoto": "自責要短暫,不過要長久銘記。",
"type": "h",
"from": "當你沉睡時",
"from_who": null,
"creator": "沈時筠",
"creator_uid": 6568,
"reviewer": 1,
"commit_from": "web",
"created_at": "1593237879",
"length": 14
*/
var hitokoto: String
// 默認生成對象
static func generateItem() -> QuoteResItem {
let item = QuoteResItem(hitokoto: "所有快樂都向你靠攏,所有好運都在路上")
return item
}
}
再在 Network 文件夾下新建 QuoteService.swift
,定義外部調用的接口,內部封裝請求邏輯,代碼如下:
import Foundation
public struct QuoteService {
static func getQuote(client: NetworkClient, completion: ((QuoteResItem) -> Void)?) {
quoteRequest(.quoteFromNet(),
on: client,
completion: completion)
}
private static func quoteRequest(_ request: URLRequest,
on client: NetworkClient,
completion: ((QuoteResItem) -> Void)?) {
client.executeRequest(request: request) { result in
switch result {
case .success(let data):
let decoder = JSONDecoder()
do {
let quoteItem = try decoder.decode(QuoteResItem.self, from: data)
completion?(quoteItem)
} catch {
print(error.localizedDescription)
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
然後添加調用的入口,在添加調用之前,需要考慮下使用的場景,和日期相同,定義一個 Published 修飾的屬性,然後在使用的地方,使用定義 @ObservedObject 修飾的屬性來監聽變化。
創建 QQSyncWidgetQuoteShareItem.swift
,用於處理 Quote 的數據,代碼如下:
import Foundation
class QQSyncWidgetQuoteShareItem: ObservableObject {
@Published private var quoteItem = QuoteResItem.generateItem()
func quoteStr() -> String {
return quoteItem.hitokoto
}
func updateQuoteItem(_ item: QuoteResItem) {
self.quoteItem = item
}
}
在 QQSyncQuoteTextView.swift
中添加屬性,並修改使用,代碼如下
import SwiftUI
struct QQSyncQuoteTextView: View {
@ObservedObject var quoteShareItem: QQSyncWidgetQuoteShareItem
var body: some View {
VStack(alignment: .leading) {
Spacer()
Text(quoteShareItem.quoteStr())
.font(.system(size: 19.0))
.fontWeight(.semibold)
.minimumScaleFactor(0.5)
.foregroundColor(.white)
Spacer()
Text("加油,打工人!😄")
.font(.system(size: 16.0))
.minimumScaleFactor(0.5)
.foregroundColor(.white)
Spacer()
}
}
}
然後修改 QQSyncWidgetMedium.swift
和 QQSyncWidgetView.swift
中的報錯,和上面類似,添加 @ObservedObject var quoteShareItem: QQSyncWidgetQuoteShareItem
,修改傳入參數。
最後再修改 DemoWidget.swift
- 修改
SimpleEntry
,添加定義的QQSyncWidgetQuoteShareItem
屬性 - 修改
DemoWidgetEntryView
,添加傳入參數entry.quoteShareItem
- 修改
Provider
placeholder(in context: Context) -> SimpleEntry
添加傳入參數,使用默認值getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ())
添加傳入參數,使用默認值getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ())
方法 —— 添加網絡請求的調用,用網絡返回對象生成對應的QQSyncWidgetQuoteShareItem
,傳入參數使用生成的 item
代碼如下:
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), quoteShareItem: QQSyncWidgetQuoteShareItem())
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), quoteShareItem: QQSyncWidgetQuoteShareItem())
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
QuoteService.getQuote(client: NetworkClient()) { quoteResItem in
let quoteShareItem = QQSyncWidgetQuoteShareItem()
quoteShareItem.updateQuoteItem(quoteResItem)
let entry = SimpleEntry(date: Date(), quoteShareItem: quoteShareItem)
// refresh the data every two hours
let expireDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) ?? Date()
let timeline = Timeline(entries: [entry], policy: .after(expireDate))
completion(timeline)
}
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
var quoteShareItem: QQSyncWidgetQuoteShareItem
}
struct DemoWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
QQSyncWidgetView(dateShareItem: QQSyncWidgetDateShareItem(), quoteShareItem: entry.quoteShareItem)
}
}
調試查看效果,可以看到顯示的文案已經改變,說明已經使用了網絡返回的數據;還可以測試一下 widget 刷新的時機,上面的代碼中設置了每隔兩個小時刷新一下,所以可以把手機時間調後兩個小時,然後再來查看下 widget 效果,可以發現文字發生了改變,說明刷新了數據,讚,完成。
最終完整效果如下:
完整代碼已放在:Github 中 Tutorial2-QQ 同步助手 widget
,鏈接:https://github.com/mokong/WidgetAllInOne
下一篇,會先講 WidgetBundle 的使用,然後再講怎麼實現一個支付寶 Widget 效果。