【SwiftUI】iOS17 インタラクティブウィジェット

# はじめに
iOS17からウィジェットがインタラクティブに進化しました。

### 公式ドキュメント
https://developer.apple.com/documentation/widgetkit/adding-interactivity-to-widgets-and-live-activities

https://developer.apple.com/videos/play/wwdc2023/10028/

ウィジェットからボタンで操作したり、画面にアニメーションをつけたりすることができます。
そこで、今回はボタンでTodoリストのようなものを作って紹介します。

https://youtube.com/shorts/xENrkg4y7sk?feature=share

# 実装手順
ButtonとToggleにintentという新しいイニシャライザが追加されました。

AppIntentsのプロトコルに準拠した構造体を作成し、Button(intent: )を使って実装していきます。

## AppIntent

```swift: TaskIntent.swift

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

 ```swift: EntryView.swift
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

```swift: TaskEntry.swift
struct TaskEntry: TimelineEntry {
    let date: Date = .now
    let taskList: [TaskModel]
}
```

### TaskModel
```swift: TaskModel.swift
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
``` swift: TaskWidget.swift
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
``` swift: Provier.swift
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)
    }
}
```