今是昨非

今是昨非

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

iOS Widget

iOS Widget#

Background#

At first, I found that Alipay's Widget looked very nice, so I planned to create a similar one. During the process, I discovered that Widgets have so many interesting aspects. Therefore, I am recording and sharing here:

Do you know how to create a Widget similar to QQ Sync Assistant?

image

Do you know how the different group Widget effects similar to Eastmoney are achieved?

image image image

Do you know how the Alipay Widget functions shown in the image below are implemented?

image image image image

Or to ask differently, are you aware of the concepts of supportedFamilies, WidgetBundle, and Configurable Widget? If you know all of them, then you don't need to read this article.

  • The QQ Sync Assistant's Widget only displays one Widget effect, as it sets the Widget's supportedFamilies to only the systemMedium style;
  • The multiple group Widgets of Eastmoney are implemented through WidgetBundle, allowing multiple Widgets to be set, each with its own sizes; to distinguish whether WidgetBundle is used, you can check if the text in the Widget preview screen scrolls along when swiping: for the same Widget with different sizes, the title and description at the top will not move when swiping; for different groups of Widgets, each Widget has its own title and description, and the text will scroll along when swiping.
  • The Alipay Widget uses Configurable Widget, defining Enum types and custom data types, setting Intent's Dynamic Options and Supports multiple values.

Development#

Before we start, a note:
If you need to communicate between the APP and Widget, such as displaying the weather in Alipay, you need to obtain the city from the APP's location, save it locally, and then retrieve the locally saved city from the Widget to get the weather. This communication requires an APPGroup; if you don't need to pass values between the APP and Widget, you don't need to set an APPGroup. However, if you set an APPGroup, you need to ensure that the Widget and the main APP's APPGroup are consistent. For detailed usage of APPGroup, refer to Data Sharing Between Apps - App Group Configuration, which will not be elaborated here.

Creating a Widget#

To create a Widget, select File -> New -> Target -> Widget Extension.

image

Click Next, enter the name of the Widget, and uncheck Include Configuration Intent.

image

Click Next, a prompt will appear asking whether to switch the Target, as shown below. Click Activate to switch to the Widget Target;

image

You can either click Activate or cancel in the above steps; clicking Activate means Xcode actively switches the Target to Widget, while clicking cancel keeps the current Target. You can switch manually at any time.

image

Now the Widget has been created. Let's take a look at the current project structure, as shown below.

image

Next, let's look at the code in the .swift file of the Widget. The entry and delegate methods are all in this class:

It is divided into several parts, as follows:

  • TimeLineProvider, a protocol that defines three required methods for the default display of the Widget and when to refresh
    • func placeholder(in context: Context) -> SimpleEntry
    • func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) this method defines how the Widget preview is displayed, so default values should be provided here
    • func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) this method determines when the Widget refreshes
  • TimelineEntry, this class is also required, where the Date is used to determine the refresh timing. If there is a custom Intent, it is also passed to the View from here
  • View, the Widget's View
  • Widget, the Widget's title and description, as well as supportedFamilies are set here
  • PreviewProvider, this is SwiftUI's preview, allowing you to see the effect while modifying, which can be deleted

After reading the above, you might still be confused, but don't worry, keep reading, and after creating a couple of Widgets, you will understand the purpose of each part.

QQ Sync Assistant's Widget#

WidgetUI Creation#

Let's start by creating the simplest QQ Sync Assistant's Widget. Download the project from the Tutorial1 folder, open it, and create a new SwiftUIView, as follows:

image

Click Next, enter the file name as QQSyncWidgetView, and make sure the selected Target is the Widget's Target, not the main project, as shown below:

image

Then open QQSyncWidgetView, and the file content is as follows:


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

In this, QQSyncWidgetView contains the code for the SwiftUI View, which is where you modify the layout; QQSyncWidgetView_Previews controls the preview View, which can be deleted. Now let's look at what the QQ Sync Assistant's Widget should contain:

image

As shown above, it can be divided into three parts: background image, left text View, and right text View. The relationship between the background image and the two Views is implemented using ZStack, the left-right relationship between the two Views is implemented using HStack, and the vertical layout of the text inside the View is done using VStack. The resource files for testing are placed in the QQSyncImages folder.

The content of SwiftUI can refer to Stanford's tutorial, linked below:

After filling in the content, it looks something like this:


struct QQSyncWidgetView: View {
        ZStack {
            // Background image
            Image("widget_background_test")
                .resizable()
            
            // Left and right Views
            HStack {
                Spacer()
                // Left View
                VStack(alignment: .leading) {
                    Spacer()
                    Text("All happiness converges towards you, all good luck is on the way.")
                        .font(.system(size: 19.0))
                        .fontWeight(.semibold)
                        .minimumScaleFactor(0.5)
                        .foregroundColor(.white)

                    Spacer()
                    
                    Text("Keep it up, workers! 😄")
                        .font(.system(size: 16.0))
                        .minimumScaleFactor(0.5)
                        .foregroundColor(.white)
                    Spacer()
                }
                
                Spacer()
                
                // Right 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("June 06, Monday")
                        .lineLimit(1)
                        .minimumScaleFactor(0.5)
                        .font(.system(size: 14.0))
                        .foregroundColor(.white)
                    Spacer()
                    Text("Share")
                        .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))
        }
    }

Then modify the entry, open DemoWidget.swift, where DemoWidgetEntryView is the View displayed by the component, so modify it to the QQSyncWidgetView we just created, as follows:


struct DemoWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        QQSyncWidgetView()
    }
}

The effect is as follows:

image

The effect is already similar to QQ Sync Assistant, but the code above still needs to be optimized; the classes are too bulky. Each VStack can be encapsulated into a separate view for easier reuse. Create a SwiftUIView named QQSyncQuoteTextView for displaying the left half of the Widget; create the right half view named QQSyncDateShareView, and the final code will be:

QQSyncQuoteTextView class:


import SwiftUI

struct QQSyncQuoteTextView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Spacer()
            Text("All happiness converges towards you, all good luck is on the way.")
                .font(.system(size: 19.0))
                .fontWeight(.semibold)
                .minimumScaleFactor(0.5)
                .foregroundColor(.white)

            Spacer()
            
            Text("Keep it up, workers! 😄")
                .font(.system(size: 16.0))
                .minimumScaleFactor(0.5)
                .foregroundColor(.white)
            Spacer()
        }
    }
}

QQSyncDateShareView class:


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("June 06, Monday")
                .lineLimit(1)
                .minimumScaleFactor(0.5)
                .font(.system(size: 14.0))
                .foregroundColor(.white)
            Spacer()
            Text("Share")
                .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()
        }
    }
}

Finally, modify QQSyncWidgetView to:


import SwiftUI

struct QQSyncWidgetView: View {
    var body: some View {
        ZStack {
            // Background image
            Image("widget_background_test")
                .resizable()
            
            // Left and right Views
            HStack {
                // Left View
                QQSyncQuoteTextView()
                                
                // Right View
                QQSyncDateShareView()
            }
            .padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
        }
    }
}

Then run it again, and you will find that the effect is the same as before, bingo.

Different Widget Size Settings#

Next, let's look at the [Widget size settings]. Currently, the Widget we developed displays perfectly in Medium size, but the displays for Small and Large sizes are not correct. How can we set this? How can we set different content for different sizes?

To set different content for different sizes of Widgets, you need to use WidgetFamily, and you need to import WidgetKit. For example, when setting Small, if you want to display the right half of Medium, and keep Medium unchanged, how do you do it?

  1. Import WidgetKit in the class you want to set.
  2. Declare the property @Environment(\.widgetFamily) var family: WidgetFamily.
  3. Use a Switch statement to enumerate family.

Note:

The specific code is as follows:



import SwiftUI
import WidgetKit

struct QQSyncWidgetView: View {
    @Environment(\.widgetFamily) var family: WidgetFamily
    
    var body: some View {
        ZStack {
            // Background image
            Image("widget_background_test")
                .resizable()
            switch family {
            case .systemSmall:
                QQSyncDateShareView()
            case .systemMedium:
                // Left and right Views
                HStack {
                    // Left View
                    QQSyncQuoteTextView()
                                        
                    // Right View
                    QQSyncDateShareView()
                }
                .padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
//            case .systemLarge:
//                break
//            case .systemExtraLarge:
//                break
            default:
                QQSyncQuoteTextView()
            }
        }
    }
}

Run it to see the effect, as shown below:

image

The effect is as expected, but the code looks a bit ugly. Let's optimize it again by encapsulating QQSyncWidgetMedium and QQSyncWidgetSmall into two classes, as follows:


import SwiftUI

struct QQSyncWidgetSmall: View {
    var body: some View {
        ZStack {
            // Background image
            Image("widget_background_test")
                .resizable()
            
            QQSyncDateShareView()
        }
    }
}


import SwiftUI

struct QQSyncWidgetMedium: View {
    var body: some View {
        ZStack {
            // Background image
            Image("widget_background_test")
                .resizable()

            // Left and right Views
            HStack {
                // Left View
                QQSyncQuoteTextView()

                Spacer()

                // Right View
                QQSyncDateShareView()
            }
            .padding(EdgeInsets(top: 0.0, leading: 20.0, bottom: 0.0, trailing: 20.0))
        }
    }
}

Then modify QQSyncWidgetView as follows:



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

Run it again, check the effect, and it is still as expected, but the code looks clearer. If you want to add a Large View, you just need to define the QQSyncWidgetLarge class and use it in the above place, which is convenient and quick.

Next, let's look at the project created by the author. When adding a Widget, there are Small, Medium, and Large sizes available. Even if the Small and Large sizes are commented out in the above Switch family, they still appear in the preview. However, when adding the QQ Sync Assistant's Widget, it only has one Medium size. How is this achieved?

This is done by setting the supportedFamilies property in the @main entry, where supportedFamilies takes an array of sizes. If you pass in several sizes, it supports those sizes. To achieve the effect of QQ Sync Assistant, only pass in the .systemMedium size, as shown in the code below:


@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]) // Set the array of supported sizes for the preview widget
    }
}

Widget Date Update#

The above display part has been completed. Next, let's look at [date settings]. Currently, the date is fixed. How can we make the date use the phone's time?

Considerations include:

  • Where does the date come from? — You can directly use Date() in the Extension to get the current date.
  • How to notify the refresh when the date is updated? Refer to cs193p-Developing Apps for iOS, use ObservableObject to define a property decorated with @Published, and then use a property decorated with @ObservedObject in the View. This way, when the property decorated with @Published changes, the property decorated with @ObservedObject will change, thus refreshing the interface.

The code implementation is as follows:

First, create a Swift file. Note that the model class is created using Swift, while the UI creation class is SwiftUI.

Create a new String_Extensions.swift to define a method for getting a specified date type string, as follows:


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 "Sunday"
        case 2:
            return "Monday"
        case 3:
            return "Tuesday"
        case 4:
            return "Wednesday"
        case 5:
            return "Thursday"
        case 6:
            return "Friday"
        case 7:
            return "Saturday"
        default:
            return ""
        }
    }
}

Create a new QQSyncWidgetDateItem.swift class to get the year, month, day, week, hour, minute, and second as strings.


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

Create QQSyncWidgetDateShareItem.swift, similar to a utility, to convert the model into viewable logic and handle click responses.


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

}

Then modify the QQSyncDateShareView class to add a QQSyncWidgetDateShareItem property, and change the fixed date to be retrieved from 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("Share")
                .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()
        }
    }
}

Then modify the places where QQSyncDateShareView is called, adding property declaration code in both QQSyncWidgetSmall and QQSyncWidgetMedium, and modify the parameters passed in; then modify the places where these two classes are referenced, namely QQSyncWidgetView, to also add property declarations and modify the parameters passed in; finally, modify the DemoWidget class where DemoWidgetEntryView is used to look like this:


struct DemoWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        QQSyncWidgetView(dateShareItem: QQSyncWidgetDateShareItem())
    }
}

Finally, modify the refresh timing, i.e., when to refresh the widget data, which is controlled by the getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) method in TimeLineProvider. Modify it to refresh every 2 hours.


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

Then run and debug, modify the date, and you will find that the date data displayed in the widget changes with the phone's date, done.

Widget Network Data Logic#

Comparing the QQ Sync Assistant's Widget, you can find that the background image and text change automatically after a certain period. Next, let's see how to achieve this effect. The background image change and text change are similar, both require network requests to update data. Here, we will only take text updates as an example.

First, find a random quote API, which can refer to https://github.com/vv314/quotes. Here, we choose the Hitokoto API, which is: https://v1.hitokoto.cn/. After finding the API, let's see how to implement network requests for the widget.

Create a Network folder, and in the Network folder, create NetworkClient.swift to encapsulate URLSession network requests, as follows:


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

In the Network folder, create URLRequest+Quote.swift to generate the Quote's URLRequest, as follows:


import Foundation

extension URLRequest {
    private static var baseURLStr: String { return "https://v1.hitokoto.cn/" }
    
    static func quoteFromNet() -> URLRequest {
        .init(url: URL(string: baseURLStr)!)
    }
}

Then, according to the returned data format, create a return model class, create QuoteResItem.swift, where only the hitokoto field is used, so only this field needs to be defined, as follows:


import Foundation

struct QuoteResItem: Codable {
    /**
     "id": 6325,
     "uuid": "2017e206-f81b-48c1-93e3-53a63a9de199",
     "hitokoto": "Self-blame should be short, but long remembered.",
     "type": "h",
     "from": "When You Are Asleep",
     "from_who": null,
     "creator": "Shen Shiyun",
     "creator_uid": 6568,
     "reviewer": 1,
     "commit_from": "web",
     "created_at": "1593237879",
     "length": 14
     */
    var hitokoto: String

    // Default object generation
    static func generateItem() -> QuoteResItem {
        let item = QuoteResItem(hitokoto: "All happiness converges towards you, all good luck is on the way.")
        return item
    }
}

Next, create QuoteService.swift in the Network folder, defining the external calling interface and encapsulating the request logic internally, as follows:


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

Then add the calling entry. Before adding the call, consider the usage scenario, similar to the date, define a property decorated with Published, and then use a property decorated with @ObservedObject to listen for changes.

Create QQSyncWidgetQuoteShareItem.swift to handle the Quote data, as follows:


import Foundation

class QQSyncWidgetQuoteShareItem: ObservableObject {
    @Published private var quoteItem = QuoteResItem.generateItem()
    
    func quoteStr() -> String {
        return quoteItem.hitokoto
    }
    
    func updateQuoteItem(_ item: QuoteResItem) {
        self.quoteItem = item
    }
}

In QQSyncQuoteTextView.swift, add a property and modify the usage, as follows:


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("Keep it up, workers! 😄")
                .font(.system(size: 16.0))
                .minimumScaleFactor(0.5)
                .foregroundColor(.white)
            Spacer()
        }
    }
}

Then modify the errors in QQSyncWidgetMedium.swift and QQSyncWidgetView.swift, similarly adding @ObservedObject var quoteShareItem: QQSyncWidgetQuoteShareItem and modifying the parameters passed in.

Finally, modify DemoWidget.swift

  • Modify SimpleEntry to add the defined QQSyncWidgetQuoteShareItem property.
  • Modify DemoWidgetEntryView to add the parameter entry.quoteShareItem.
  • Modify Provider
    • placeholder(in context: Context) -> SimpleEntry to add the parameter, using the default value.
    • getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) to add the parameter, using the default value.
    • getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) method — add the network request call, generate the corresponding QQSyncWidgetQuoteShareItem from the network return object, and use the generated item as the parameter.

The code is as follows:


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

Debugging to check the effect, you can see that the displayed text has changed, indicating that the network return data has been used. You can also test the widget's refresh timing. The code above sets it to refresh every two hours, so you can adjust the phone's time back two hours and then check the widget effect again. You will find that the text has changed, indicating that the data has been refreshed, great, completed.

The final complete effect is as follows:

Final Effect

The complete code has been placed in: Github in Tutorial2-QQ Sync Assistant widget, link: https://github.com/mokong/WidgetAllInOne

In the next article, I will first talk about the use of WidgetBundle, and then how to achieve an Alipay Widget effect.

References#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.