今是昨非

今是昨非

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

iOSウィジェット

iOS ウィジェット#

背景#

最初は支付宝のウィジェットがとても美しいことに気づき、模倣しようと思いましたが、作成する過程でウィジェットにはこんなに楽しいところがあることを発見しました。そこで、ここに記録して共有します:

QQ 同期アシスタントのようなウィジェットを作成する方法を知っていますか?

image

東方財富のような異なるグループのウィジェット効果がどのように実現されているか知っていますか?

image image image

下の図の支付宝ウィジェット機能がどのように実現されているか知っていますか?

image image image image

あるいは、こう尋ねることもできます。このいくつかの概念:supportedFamiliesWidgetBundle、およびConfigurable Widgetについて知っていますか?もしすべて知っているなら、この文章を読む必要はありません。

  • QQ 同期アシスタントのウィジェットは、ウィジェットのsupportedFamiliesが systemMedium スタイルのみを設定しているため、1 つのウィジェットの効果だけを表示します;
  • 東方財富の複数のウィジェットグループは、WidgetBundleを使用して実現されており、複数のウィジェットを設定でき、各ウィジェットは独自の大中小を設定できます;WidgetBundleを使用しているかどうかを区別するには、スワイプ時にウィジェットプレビュー画面の同期テキストが一緒にスワイプするかどうかで区別できます:同じウィジェットの大中小の異なるスタイルでは、スワイプ時に上部のタイトルと説明は動きません;異なるグループのウィジェットは、各ウィジェットが独自のタイトルと説明を持ち、スワイプ時にテキストが一緒にスワイプします。
  • 支付宝のウィジェットは、Configurable Widgetを使用しており、Enum型とカスタムデータ型を定義し、Intent のDynamic OptionsSupports multiple valuesを設定しています。

開発#

開始前の説明:
APP とウィジェット間で値を通信する必要がある場合、例えば支付宝の天気表示のように、APP から位置情報を取得して都市をローカルに保存し、ウィジェットからローカルに保存された都市を取得して天気を取得します。この間の値の伝達には APPGroup が必要です。APP とウィジェット間で値を伝達する必要がない場合は、APPGroup を設定する必要はありません;しかし、APPGroup を設定する場合は、ウィジェットと主 APP の APPGroup が一致する必要があります。APPGroup の詳細な使用法はアプリ間のデータ共有 ——App Group の設定を参照してください。ここでは詳しく説明しません。

ウィジェットの作成#

ウィジェットを作成するには、File -> New -> Target -> Widget Extension を選択します。

image

次へ進むをクリックし、ウィジェットの名前を入力し、Include Configuration Intent のチェックを外します。

image

次へ進むをクリックすると、ターゲットを切り替えるかどうかのメッセージが表示されます。以下のように、Activate をクリックしてウィジェットターゲットに切り替えます;

image

上記の手順で Activate をクリックするかキャンセルしてもかまいません;Activate をクリックすると Xcode が自動的にターゲットをウィジェットに切り替え、キャンセルをクリックすると現在のターゲットを保持します。いつでも手動で切り替えることができます。

image

これでウィジェットが作成されました。現在のプロジェクトの構造を見てみましょう。

image

次に、ウィジェット内の.swift ファイルのコードを見てみましょう。エントリとデリゲートメソッドはこのクラスにあります:

いくつかの部分に分かれています。

  • TimeLineProvider、プロトコルタイプはウィジェットのデフォルト表示といつ更新するかのために 3 つの必須メソッドを定義しています
    • func placeholder(in context: Context) -> SimpleEntry
    • func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ())このメソッドはウィジェットプレビューでどのように表示するかを定義するため、ここでデフォルト値を提供する必要があります
    • func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ())このメソッドでは、ウィジェットがいつ更新されるかを決定します
  • TimelineEntry、このクラスも必須で、内部の Date は更新のタイミングを判断するために使用されます。カスタム Intent がある場合も、ここから値を View に渡します
  • View、ウィジェットの View
  • Widget、ウィジェットのタイトルと説明、および supportedFamilies はここで設定されます
  • PreviewProvider、これは SwiftUI のプレビューで、変更しながら効果を確認できます。削除可能です

上記を見てもまだ混乱しているかもしれませんが、心配しないでください。次に進んで、1 つか 2 つのウィジェットを作成すれば、各部分の役割が明らかになります。

QQ 同期アシスタントのウィジェット#

WidgetUI の作成#

最初に最も簡単な QQ 同期アシスタントのウィジェットを作成します。Tutorial1フォルダ内のプロジェクトをダウンロードし、開いて新しいSwiftUIViewを作成します。以下のように:

image

Next をクリックし、ファイル名にQQSyncWidgetViewを入力します。ここで注意が必要なのは、選択したターゲットがウィジェットのターゲットであり、主プロジェクトではないことです。以下のように:

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@*/"こんにちは、世界!"/*@END_MENU_TOKEN@*/)
    }
}

struct QQSyncWidgetView_Previews: PreviewProvider {
    static var previews: some View {
        QQSyncWidgetView()
    }
}

ここで、QQSyncWidgetViewSwiftUI View のコードであり、レイアウトを変更する場所です;QQSyncWidgetView_Previewsはプレビュー View を制御するもので、削除可能です。次に実現する QQ 同期アシスタントのウィジェットに含まれる内容を見てみましょう:

image

上記は 3 つの部分に分けられます。背景画像、左側のテキスト View、右側のテキスト View。背景画像と 2 つの View の前後関係は ZStack を使用して実現し、2 つの View の左右関係は HStack を使用し、View 内のテキストの上下レイアウトは VStack を使用します。テスト用のリソースファイルはQQSyncImagesフォルダに保存します。

SwiftUIの内容はスタンフォード大学の教授のチュートリアルを参考にできます。リンクは以下です:

内容を埋めると、次のようになります:


struct QQSyncWidgetView: View {
        ZStack {
            // 背景画像
            Image("widget_background_test")
                .resizable()
            
            // 左右の2つの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 としてカプセル化することで、再利用が容易になります。左半分の View 表示用にQQSyncQuoteTextViewという名前の SwiftUIView を作成し、右半分の 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()
            
            // 左右の2つのView
            HStack {
                // 左のView
                QQSyncQuoteTextView()
                                
                // 右のView
                QQSyncDateShareView()
            }
            .padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
        }
    }
}

次に実行すると、効果は以前と同じで、成功です。

異なるウィジェットサイズの設定#

次に【ウィジェットサイズの設定】を見てみましょう。現在開発中のウィジェットは Medium サイズでちょうど良い表示ですが、Small と Large のサイズでは表示が正常ではありません。これをどう設定するのでしょうか?異なるサイズに対して異なる内容を表示するウィジェットを設定するには、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:
                // 左右の2つの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の 2 つのクラスをカプセル化して最適化します。以下のように:


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

            // 左右の2つの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クラスを定義し、上記の場所で使用するだけで簡単に追加できます。

次に、筆者が作成したプロジェクトを見てみると、ウィジェットを追加する際にSmallMediumLargeのすべてが表示されます。上記のSwitch familySmallLargeをコメントアウトしても、プレビュー時にこれらの 2 つのサイズが表示され続けます。一方、QQ 同期アシスタントのウィジェットを追加すると、ウィジェットは 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("私のウィジェット")
        .description("これは例のウィジェットです。")
        .supportedFamilies([WidgetFamily.systemMedium]) // プレビューウィジェットでサポートされるサイズの配列を設定
    }
}

ウィジェットの日付更新#

上記の表示部分が完成したので、次に【日付の設定】を見てみましょう。現在の日付は固定されていますが、どうすれば日付をスマートフォンの時間から取得できるのでしょうか?

考慮すべき点は:

  • 日付はどこから来るのか?—— Extension 内で直接 Date () を使用して現在の日付を取得できます。
  • 日付が更新された場合、どのように更新を通知するのか?cs193p-Developing Apps for iOSを参考に、ObservableObjectを使用して@Published修飾のプロパティを定義し、使用する View 内で@ObservedObject修飾のプロパティを使用します。こうすることで、@Published修飾のプロパティに変化があった場合、@ObservedObject修飾のプロパティも変化し、画面が更新されます。

コードの実装は以下の通りです:

まず、swift ファイルを作成します。注意:モデルクラスの作成には 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を作成し、年、月、日、曜日、時、分、秒の String を取得します。


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

次にQQSyncWidgetSmallQQSyncWidgetMediumQQSyncDateShareViewを呼び出す場所を変更し、プロパティ宣言コードを追加し、引数を変更します。次にQQSyncWidgetViewでこれらの 2 つのクラスを呼び出す場所も変更し、プロパティ宣言を追加して引数を変更します。最後にDemoWidgetクラスでDemoWidgetEntryViewを使用している場所を変更し、以下のようにします:


struct DemoWidgetEntryView : View {
    var entry: Provider.Entry

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

次に更新のタイミングを変更します。ウィジェットデータを更新するタイミングはTimeLineProvider内のgetTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ())メソッドで制御されており、これを 2 時間ごとに更新するように変更します。


    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let entry = SimpleEntry(date: Date())
        // 2時間ごとにデータを更新
        let expireDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) ?? Date()
        let timeline = Timeline(entries: [entry], policy: .after(expireDate))
        completion(timeline)
    }

次に実行してデバッグすると、日付が変更されることがわかります。ウィジェットに表示される日付データがスマートフォンの日付の変更に伴って変わります。完了です。

ウィジェットのネットワークデータロジック#

QQ 同期アシスタントのウィジェットと比較すると、一定の時間ごとに画像とテキストが自動的に変わることがわかります。次に、この効果をどのように実現するかを見てみましょう。背景画像の変化とテキストの変化は、どちらもネットワークリクエストによって行われ、データが更新されます。ここでは、テキストの更新を例として示します。

まず、ランダムな名言の API を見つけます。https://github.com/vv314/quotes を参考にし、ここでは一言の API を選択します。API は:https://v1.hitokoto.cn/ です。API が決まったら、ウィジェットのネットワークリクエストをどのように実装するかを見てみましょう。

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

次に、返されるデータ形式を参照して、返されるモデルクラスを作成します。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を生成し、引数に使用します。

コードは以下の通りです:


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)
            // 2時間ごとにデータを更新
            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)
    }
}

デバッグして効果を確認すると、表示される文言が変更されていることがわかります。ネットワークから返されたデータを使用していることが確認でき、ウィジェットの更新タイミングをテストすることもできます。上記のコードでは、2 時間ごとに更新するように設定されているため、スマートフォンの時間を 2 時間後に調整し、ウィジェットの効果を確認すると、テキストが変更されていることがわかります。データが更新されたことを示しており、完了です。

最終的な完全な効果#

最終的な効果は以下の通りです:

最終効果

完全なコードはGithubにアップロードされており、Tutorial2-QQ 同期アシスタントウィジェットのリンクは:https://github.com/mokong/WidgetAllInOne です。

次回は、ウィジェットバンドルの使用方法を説明し、支付宝ウィジェットの効果を実現する方法を説明します。

参考#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。