LoginSignup
6
4

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

Last updated at Posted at 2023-10-10

はじめに

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

公式ドキュメント

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

実装手順

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

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

AppIntent

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

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

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

TaskModel

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

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

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)
    }
}
6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4