iOS ウィジェット#
背景#
最初は支付宝のウィジェットがとても美しいことに気づき、模倣しようと思いましたが、作成する過程でウィジェットにはこんなに楽しいところがあることを発見しました。そこで、ここに記録して共有します:
QQ 同期アシスタントのようなウィジェットを作成する方法を知っていますか?
東方財富のような異なるグループのウィジェット効果がどのように実現されているか知っていますか?
下の図の支付宝ウィジェット機能がどのように実現されているか知っていますか?
あるいは、こう尋ねることもできます。このいくつかの概念:supportedFamilies
、WidgetBundle
、およびConfigurable Widget
について知っていますか?もしすべて知っているなら、この文章を読む必要はありません。
- QQ 同期アシスタントのウィジェットは、ウィジェットの
supportedFamilies
が systemMedium スタイルのみを設定しているため、1 つのウィジェットの効果だけを表示します; - 東方財富の複数のウィジェットグループは、
WidgetBundle
を使用して実現されており、複数のウィジェットを設定でき、各ウィジェットは独自の大中小を設定できます;WidgetBundle
を使用しているかどうかを区別するには、スワイプ時にウィジェットプレビュー画面の同期テキストが一緒にスワイプするかどうかで区別できます:同じウィジェットの大中小の異なるスタイルでは、スワイプ時に上部のタイトルと説明は動きません;異なるグループのウィジェットは、各ウィジェットが独自のタイトルと説明を持ち、スワイプ時にテキストが一緒にスワイプします。 - 支付宝のウィジェットは、
Configurable Widget
を使用しており、Enum
型とカスタムデータ型を定義し、Intent のDynamic Options
とSupports multiple values
を設定しています。
開発#
開始前の説明:
APP とウィジェット間で値を通信する必要がある場合、例えば支付宝の天気表示のように、APP から位置情報を取得して都市をローカルに保存し、ウィジェットからローカルに保存された都市を取得して天気を取得します。この間の値の伝達には APPGroup が必要です。APP とウィジェット間で値を伝達する必要がない場合は、APPGroup を設定する必要はありません;しかし、APPGroup を設定する場合は、ウィジェットと主 APP の APPGroup が一致する必要があります。APPGroup の詳細な使用法はアプリ間のデータ共有 ——App Group の設定を参照してください。ここでは詳しく説明しません。
ウィジェットの作成#
ウィジェットを作成するには、File -> New -> Target -> Widget Extension を選択します。
次へ進むをクリックし、ウィジェットの名前を入力し、Include Configuration Intent のチェックを外します。
次へ進むをクリックすると、ターゲットを切り替えるかどうかのメッセージが表示されます。以下のように、Activate をクリックしてウィジェットターゲットに切り替えます;
上記の手順で Activate をクリックするかキャンセルしてもかまいません;Activate をクリックすると Xcode が自動的にターゲットをウィジェットに切り替え、キャンセルをクリックすると現在のターゲットを保持します。いつでも手動で切り替えることができます。
これでウィジェットが作成されました。現在のプロジェクトの構造を見てみましょう。
次に、ウィジェット内の.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
を作成します。以下のように:
Next をクリックし、ファイル名にQQSyncWidgetView
を入力します。ここで注意が必要なのは、選択したターゲットがウィジェットのターゲットであり、主プロジェクトではないことです。以下のように:
次に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()
}
}
ここで、QQSyncWidgetView
はSwiftUI
View のコードであり、レイアウトを変更する場所です;QQSyncWidgetView_Previews
はプレビュー View を制御するもので、削除可能です。次に実現する QQ 同期アシスタントのウィジェットに含まれる内容を見てみましょう:
上記は 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()
}
}
効果は以下の通りです:
効果はすでに 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 サイズの時に表示を変えないようにするには、次の手順を行います。
- 設定するクラスに
WidgetKit
をインポートします。 - プロパティ
@Environment(\.widgetFamily) var family: WidgetFamily
を宣言します。 Switch
を使用してfamily
を列挙します。
注:
@Environment
は SwiftUI 自体の事前定義されたキーを使用しています。@Environment
に関する詳細は以下の 2 つのリンクを参照してください:
具体的なコードは以下の通りです:
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()
}
}
}
}
実行して効果を確認すると、以下のようになります:
効果は期待通りですが、コードが少し見栄えが悪いので、QQSyncWidgetMedium
とQQSyncWidgetSmall
の 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
クラスを定義し、上記の場所で使用するだけで簡単に追加できます。
次に、筆者が作成したプロジェクトを見てみると、ウィジェットを追加する際にSmall
、Medium
、Large
のすべてが表示されます。上記のSwitch family
でSmall
とLarge
をコメントアウトしても、プレビュー時にこれらの 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()
}
}
}
次にQQSyncWidgetSmall
、QQSyncWidgetMedium
でQQSyncDateShareView
を呼び出す場所を変更し、プロパティ宣言コードを追加し、引数を変更します。次に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.swift
とQQSyncWidgetView.swift
のエラーを変更し、上記と同様に@ObservedObject var quoteShareItem: QQSyncWidgetQuoteShareItem
を追加し、引数を変更します。
最後にDemoWidget.swift
を変更します。
SimpleEntry
を変更し、定義したQQSyncWidgetQuoteShareItem
プロパティを追加します。DemoWidgetEntryView
を変更し、引数entry.quoteShareItem
を追加します。Provider
を変更します。placeholder(in context: Context) -> SimpleEntry
に引数を追加し、デフォルト値を使用します。getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ())
に引数を追加し、デフォルト値を使用します。getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ())
メソッド —— ネットワークリクエストの呼び出しを追加し、ネットワークから返されたオブジェクトを使用して対応するQQSyncWidgetQuoteShareItem
を生成し、引数に使用します。
コードは以下の通りです:
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 です。
次回は、ウィジェットバンドルの使用方法を説明し、支付宝ウィジェットの効果を実現する方法を説明します。