Skip to content

Instantly share code, notes, and snippets.

@auramagi
Last active September 2, 2021 10:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save auramagi/3d9a2a979db121b9a26640cf7c494369 to your computer and use it in GitHub Desktop.
Save auramagi/3d9a2a979db121b9a26640cf7c494369 to your computer and use it in GitHub Desktop.
ForEachでBindingを渡す書き方のサンプルコード
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