今是昨非

今是昨非

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

iOS 小工具

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 時 Medium 的右半部分,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 時,SmallMediumLarge 都有,即使在上面的 Switch family 中註釋掉了 SmallLarge,預覽時仍舊這兩個尺寸仍舊在;而當添加 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 效果。

參考#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。