Last active
September 2, 2021 10:36
-
-
Save auramagi/3d9a2a979db121b9a26640cf7c494369 to your computer and use it in GitHub Desktop.
ForEachでBindingを渡す書き方のサンプルコード
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
@main | |
struct ToDoApp: App { | |
@StateObject var model: Model = .init() | |
var body: some Scene { | |
WindowGroup { | |
ScrollView { | |
VStack(alignment: .leading) { | |
Button("Shuffle") { | |
withAnimation { | |
model.shuffle() | |
} | |
} | |
// 一つずつ試してね | |
// 1. 公式チュートリアルのような書き方 (Bindingを使わない) | |
// 動くが子ビューにModelを提供しないといけないとか、indexを手動で探さないといけないとかの課題がある | |
OfficialTutorialForEach() | |
// 2. (0..<model.tasks.count)のレンジで、idを提供しない | |
// 間違った書き方。ForEachの都合で明示的に使えないようになっていて、警告が出て実行時に要素の削除でクラッシュする | |
// NaiveRangeForEach() | |
// 3. (0..<model.tasks.count)のレンジで、idを提供する | |
// 動くが、次の問題が発生する: | |
// - idはレンジのindexの値になっているためアニメーションなどがちゃんと動かなくなる | |
// - (非常に稀なケースかと思うが) 元の配列model.tasksのindexは(0, 1, 2...)ではなく | |
// (...2, 1, 0)とか(1, 3, 6...)とかになる場合はレンジのindexと合わなくなるのでクラッシュにつながる | |
// RangeForEach() | |
// 4. enumeratedを使う | |
// 動いて上の3の書き方のidの問題が解消されているが、配列のindexが合わない場合には対応していない | |
// EnumeratedForEach() | |
// 5. indicesを使ってindexをidに指定する | |
// 動いて上の3の書き方の配列のindexが合わない場合の問題が解消されているが、idの問題には対応していない | |
// NaiveIndicesForEach() | |
// 6. zipでindicesと元の配列model.tasksを組み合わせて、元の配列の要素をidに指定する | |
// 問題なく動くが、見た目はあまり良くない | |
// CorrectIndicesForEach() | |
// 7. Indexを使わずBindingをそのもの作る | |
// 最も望ましい書き方。Xcode 13で導入されているが、iOS 13までバックポートされている | |
// Xcode13ForEach() | |
// 以上 | |
Divider() | |
AddTaskField(onSave: model.save) | |
} | |
.padding() | |
} | |
.environmentObject(model) | |
} | |
} | |
} | |
struct Task: Identifiable, Codable, Hashable, Equatable { | |
let id: UUID | |
let name: String | |
var isDone: Bool | |
} | |
class Model: ObservableObject { | |
private let defaults: UserDefaults | |
@Published var tasks: [Task] { | |
didSet { saveTasks() } | |
} | |
var specialTasks: [Task] { tasks.suffix(3) } | |
init(defaults: UserDefaults = .standard) { | |
self.defaults = defaults | |
if let tasksString = defaults.string(forKey: "tasks"), | |
let tasks = try? JSONDecoder().decode([Task].self, from: tasksString.data(using: .utf8)!) { | |
self.tasks = tasks | |
} else { | |
self.tasks = [] | |
} | |
} | |
private func saveTasks() { | |
guard let data = try? JSONEncoder().encode(tasks), | |
let string = String(data: data, encoding: .utf8) else { return } | |
defaults.set(string, forKey: "tasks") | |
} | |
func shuffle() { | |
tasks.shuffle() | |
} | |
func save(text: String) { | |
let task = Task(id: UUID(), name: text, isDone: false) | |
tasks.append(task) | |
} | |
func removeTask(_ task: Task) { | |
tasks.removeAll { $0.id == task.id } | |
} | |
func removeIndex(_ index: Int) { | |
tasks.remove(at: index) | |
} | |
} | |
// MARK: - ForEachの書き方それぞれ | |
struct OfficialTutorialForEach: View { | |
@EnvironmentObject var model: Model | |
var body: some View { | |
ForEach(model.tasks) { task in | |
OfficialTutorialTaskView(task: task) | |
} | |
} | |
} | |
struct NaiveRangeForEach: View { | |
@EnvironmentObject var model: Model | |
var body: some View { | |
ForEach(0..<model.tasks.count) { i in | |
TaskView( | |
task: $model.tasks[i], | |
onDelete: { model.removeIndex(i) } | |
) | |
} | |
} | |
} | |
struct RangeForEach: View { | |
@EnvironmentObject var model: Model | |
var body: some View { | |
ForEach(0..<model.tasks.count, id: \.self) { i in | |
TaskView( | |
task: $model.tasks[i], | |
onDelete: { model.removeIndex(i) } | |
) | |
} | |
} | |
} | |
struct EnumeratedForEach: View { | |
@EnvironmentObject var model: Model | |
var body: some View { | |
ForEach(Array(model.tasks.enumerated()), id: \.1) { i, task in | |
TaskView( | |
task: $model.tasks[i], | |
onDelete: { model.removeIndex(i) } | |
) | |
} | |
} | |
} | |
struct NaiveIndicesForEach: View { | |
@EnvironmentObject var model: Model | |
var body: some View { | |
ForEach(model.tasks.indices, id: \.self) { i in | |
TaskView( | |
task: $model.tasks[i], | |
onDelete: { model.removeIndex(i) } | |
) | |
} | |
} | |
} | |
struct CorrectIndicesForEach: View { | |
@EnvironmentObject var model: Model | |
var body: some View { | |
ForEach(Array(zip(model.tasks.indices, model.tasks)), id: \.1) { i, task in | |
TaskView( | |
task: $model.tasks[i], | |
onDelete: { model.removeTask(task) } | |
) | |
} | |
} | |
} | |
struct Xcode13ForEach: View { | |
@EnvironmentObject var model: Model | |
var body: some View { | |
ForEach($model.tasks) { $task in | |
TaskView( | |
task: $task, | |
onDelete: { model.removeTask($task.wrappedValue) } | |
) | |
} | |
} | |
} | |
// MARK: - TaskView | |
struct OfficialTutorialTaskView: View { | |
let task: Task | |
// 公式チュートリアルのような書き方 | |
@EnvironmentObject var model: Model | |
var taskIndex: Int { | |
model.tasks.firstIndex(where: { $0.id == task.id })! | |
} | |
var body: some View { | |
HStack { | |
if task.isDone { | |
Text(task.name) | |
.strikethrough() | |
} else { | |
Text(task.name) | |
} | |
Spacer() | |
HStack { | |
Button(task.isDone ? "Undo" : "Done", action: toggleTask) | |
Button("Delete", action: delete) | |
} | |
} | |
.padding(.vertical) | |
} | |
func toggleTask() { | |
model.tasks[taskIndex].isDone.toggle() | |
} | |
func delete() { | |
model.removeTask(task) | |
} | |
} | |
struct TaskView: View { | |
@Binding var task: Task | |
let onDelete: () -> Void | |
var body: some View { | |
HStack { | |
if task.isDone { | |
Text(task.name) | |
.strikethrough() | |
} else { | |
Text(task.name) | |
} | |
Spacer() | |
HStack { | |
Button(task.isDone ? "Undo" : "Done", action: toggleTask) | |
Button("Delete", action: onDelete) | |
} | |
} | |
.padding(.vertical) | |
} | |
func toggleTask() { | |
task.isDone.toggle() | |
} | |
} | |
// MARK: - | |
struct AddTaskField: View { | |
@State var text = "" | |
var onSave: (String) -> Void | |
var body: some View { | |
TextField( | |
"Task", | |
text: $text, | |
onEditingChanged: { _ in }, | |
onCommit: save | |
) | |
} | |
private func save() { | |
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } | |
onSave(text) | |
text = "" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment