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 时 Meidum 的右半部分,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
、Meidum
、Large
都有,即使在上面的Switch family
中注释掉了Small
和Larget
,预览时仍旧这两个尺寸仍旧在;而当添加 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 效果。