今是昨非

今是昨非

日出江花红胜火,春来江水绿如蓝

iOS Widget

iOS Widget#

背景#

一开始是发现支付宝的 Widget 做的很好看,打算仿作一个,做的过程中才发现,原来 Widget 有这么多好玩的地方。所以,在这里记录分享一下:

你知道如何创建类似于 QQ 同步助手的 Widget 吗?

image

你知道类似于东方财富的不同组 Widget 效果是怎么实现的吗?

image image image

你知道下图中支付宝 Widget 功能是怎么实现的吗?

image image image image

或者这么问,对于这几个的概念:supportedFamiliesWidgetBundle以及Configurable Widget是否知晓,如果都知道,那就不需要看本篇文章了。

  • QQ 同步助手的 Widget 只显示一个 Widget 的效果,是设置了 Widget 的supportedFamilies只有 systemMedium 样式;
  • 东方财富的多组 Widget,是通过WidgetBundle实现的,可以设置多个 Widget,每个 Widget 都可以设置自己的大中小;区分是否使用了 WidgetBundle,可以通过滑动时,Widget 预览界面同步的文字是否跟着滑动来区分:同一个 Widget 的大中小不同样式,滑动时顶部的标题和描述是不会动的;不同组的 Widget,每个 Widget 都有自己的标题和描述,滑动时,文字是跟着一起滑动的。
  • 支付宝的 Widget,使用了Configurable Widget,定义了Enum类型和自定义数据类型,设置 Intent 的Dynamic OptionsSupports 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。

image

点击下一步,输入 Widget 的名字,取消勾选 Include Configuration Intent。

image

点击下一步,会出现是否切换 Target 的提示,如下,点击 Activate,切换到 Widget Target;

image

上面的步骤点击 Activate 或者取消都可以;点击 Activate 是 Xcode 主动把 Target 切换到 Widget,点击取消是保持当前 Target。随时可以手动切换。

image

这样 Widget 就创建好了,来看下目前项目的结构,如下

image

再来看下,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,如下:

image

点击 Next,文件名字输入QQSyncWidgetView,这里需要注意选中的 Target 是 Widget 的 Target,而不是主工程的,如下:

image

然后打开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 包含有哪些内容:

image

如上可以分为三个部分,背景图片、左侧文字 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()
    }
}

效果如下:

image

效果已经和 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 时显示不变,要怎么做呢?

  1. 在要设置的类中导入WidgetKit
  2. 声明属性@Environment(\.widgetFamily) var family: WidgetFamily
  3. 使用Switch枚举 family

注:

具体代码如下:



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()
            }
        }
    }
}

运行查看效果,如下:

image

发现效果和预期一样,但是代码看起来真的有点丑,同样再优化一下,封装QQSyncWidgetMediumQQSyncWidgetSmall两个类,如下:


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 时,SmallMeidumLarge都有,即使在上面的Switch family中注释掉了SmallLarget,预览时仍旧这两个尺寸仍旧在;而当添加 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的地方,QQSyncWidgetSmallQQSyncWidgetMedium中都添加属性声明代码,并且修改传入参数;然后修改有引用这两个类的地方即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.swiftQQSyncWidgetView.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 效果,可以发现文字发生了改变,说明刷新了数据,赞,完成。

最终完整效果如下:

最终效果

完整代码已放在:GithubTutorial2-QQ 同步助手 widget,链接:https://github.com/mokong/WidgetAllInOne

下一篇, 会先讲 WidgetBundle 的使用,然后再讲怎么实现一个支付宝 Widget 效果。

参考#

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。