はじめに
iOS17からウィジェットがインタラクティブに進化しました。
公式ドキュメント
ウィジェットからボタンで操作したり、画面にアニメーションをつけたりすることができます。
そこで、今回はボタンでTodoリストのようなものを作って紹介します。
実装手順
ButtonとToggleにintentという新しいイニシャライザが追加されました。
AppIntentsのプロトコルに準拠した構造体を作成し、Button(intent: )を使って実装していきます。
AppIntent
import SwiftUI
import AppIntents
struct TaskIntent: AppIntent {
static var title: LocalizedStringResource = "taskIntent"
@Parameter(title: "id")
var id: String
init() {
}
init(id: String) {
self.id = id
}
func perform() async throws -> some IntentResult {
if let index = TaskDataModel.shared.tasks.firstIndex(where: {
$0.id == id
}) {
TaskDataModel.shared.tasks[index].isCompleted.toggle()
}
return .result()
}
}
AppIntentに準拠したプロトコルでは、titleと@Parameter、id、performメソッドが必要になります。
performのメソッド内に、ボタンをタップしたときに実行したい処理を書きます。
TaskDataModelにisCompletedというフラグ持たせ、toggle()でボタンタップしたことを認識させています。
ここで注意が必要ですが、performが呼ばれると、必ずgetTimelineも呼ばれます。
なので、もしボタンをタップしたときに、データベースにデータを保存するときは、perform内にコードを記述し、新しくデータを取得するときは、getTimelineに記述し、新しいデータを読み込ませて、Viewを更新するようにします。
View
struct TaskWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
ZStack {
VStack(alignment: .leading, spacing: 6, content: {
ForEach(entry.taskList) { task in
HStack {
Button(intent: TaskIntent(id: task.id)) {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(.blue)
}
.buttonStyle(.plain)
VStack {
Text(task.taskTitle)
.strikethrough(task.isCompleted)
}
}
}
})
}
.containerBackground(.fill.tertiary, for: .widget)
}
}
Button(intent: )を書き、先ほど作ったIntentプロトコルを準拠した構造体を引数に持たせます。
iOS17からは、.containerBackground()という新たにviewを囲む背景を指定する必要が出てきました。
EntryModel
struct TaskEntry: TimelineEntry {
let date: Date = .now
let taskList: [TaskModel]
}
TaskModel
struct TaskModel: Identifiable {
var id = UUID().uuidString
var taskTitle: String
var isCompleted = false
}
class TaskDataModel {
static let shared = TaskDataModel()
var tasks: [TaskModel] = [
.init(taskTitle: "プロテイン飲む"),
.init(taskTitle: "腹筋30回する"),
.init(taskTitle: "サプリ飲む")
]
}
Widget
struct TaskWidget: Widget {
let kind: String = "TaskWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
TaskWidgetEntryView(entry: entry)
}
.configurationDisplayName("Task Widget")
.description("interactive widget.")
}
}
Provider
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> TaskEntry {
TaskEntry(taskList: TaskDataModel.shared.tasks)
}
func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> ()) {
let entry = TaskEntry(taskList: TaskDataModel.shared.tasks)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<TaskEntry>) -> ()) {
let taskList = TaskDataModel.shared.tasks
let latestEntries = [TaskEntry(taskList: taskList)]
let timeline = Timeline(entries: latestEntries, policy: .atEnd)
completion(timeline)
}
}