teratail header banner
teratail header banner
質問するログイン新規登録

回答編集履歴

2

回答追記

2020/04/06 14:57

投稿

TakeOne
TakeOne

スコア6299

answer CHANGED
@@ -90,4 +90,55 @@
90
90
  }
91
91
  }
92
92
  }
93
- ```
93
+ ```
94
+
95
+ ---
96
+ (4/6 23:55追記)
97
+
98
+ コメントを受けて、追加確認しました。
99
+ 確かに`@EnvironmentObject var test: Test`を追加するとAppDelegateでIndex out of rangeのエラーが発生します。
100
+
101
+ それで紹介していただいた記事等を読んだ上でよく考え直してみたのですが、前回提示したコードでRowViewに受け渡している `self.$test.datas[i]` は、一見、配列の要素であるDataだけを受け渡しているように見えますが、実は内部では`test`全体を共有した状態で受け渡しているのではないかと考えるようになりました。DataはstructなのでDataの要素を書き換えるにはDataインスタンスの差し替えが必要で、Dataインスタンスの差し替えをするには、共有されたData配列全体の何番目の要素を差し替えるか知っていないといけないからです。
102
+ つまり、前回示したコードは、RowViewの中で配列全体や配列番号にアクセスするようなコードは自分で明示的に書いていませんが、実は`$data`の内部で配列全体へのアクセスが発生しており、削除された配列番号にアクセスしてしまうという問題は結局解決されていないのではないかと思います。
103
+
104
+ EnvironmentObjectの有無でエラーが発生するかしないかは、ListからViewが削除される前にViewを再構築するか否かという微妙な動きの違いで、たまたまエラーが発生したりしなかったりするというだけなのではないかと思います。
105
+
106
+ そのため、受け渡し先で配列要素を更新するには、配列全体の中から、これから更新するData位置を検索して、存在していればそれを更新し、削除されて既に存在していなければ更新しないという形で処理するしかない(それが最も安全なやり方である)と思うようになりました。これはつまり、Appleのチュートリアルでやっているやり方(RowView2のやり方)と同じ考え方です。ただ、Appleのチュートリアルは、データの削除がないという前提のコードなのでfirstIndexメソッドの結果を `!`でアンラップしていて、そこでエラーが発生しています。データの削除があることを前提にするなら、それがnilだった場合は何もしないようにしたので良さそうに思います。それを踏まえて、配列要素内の値をStepperで変更したい場合、自分ならこうするというのをもう一度書き直してみました。
107
+ ``` swift
108
+ struct RowView: View {
109
+ @EnvironmentObject var test: Test
110
+ let data: Data
111
+
112
+ @State var count: Int = 0
113
+
114
+ var body: some View {
115
+ Stepper(value: $count, onEditingChanged: {touchDown in
116
+ if !touchDown {
117
+ if let index = self.test.datas.firstIndex(where: {$0.id == self.data.id}) {
118
+ self.test.datas[index].count = self.count
119
+ }
120
+ }
121
+ }) {
122
+ Text(data.name + "(data.count)")
123
+ }.onAppear {
124
+ self.count = self.data.count
125
+ }
126
+ }
127
+ }
128
+
129
+ struct ContentView: View {
130
+ @EnvironmentObject var test: Test
131
+
132
+ var body: some View {
133
+ List {
134
+ ForEach(test.datas) { data in
135
+ RowView(data: data)
136
+ }.onDelete { offsets in
137
+ self.test.datas.remove(atOffsets: offsets)
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ ```
144
+ 今回の件は、とても勉強になりました。ありがとうございます。

1

回答追記

2020/04/06 14:57

投稿

TakeOne
TakeOne

スコア6299

answer CHANGED
@@ -57,4 +57,37 @@
57
57
  次のようにdataを渡してRowViewを初期化するだけで正常に動作します。
58
58
  ```
59
59
  RowView(data: data)
60
+ ```
61
+
62
+ ---
63
+ (4/1 23:40追記)
64
+
65
+ 配列要素内の変数値をStepper等で変更したい場合、
66
+ 私だったら次のよう@Bindingで配列要素内のDataを受け渡しして処理します。
67
+ 配列全体(`test.datas`)をRowView1に参照させるべきではないと思います。
68
+
69
+ ``` swift
70
+ struct RowView1: View {
71
+ @Binding var data: Data
72
+
73
+ var body: some View {
74
+ Stepper(value: $data.count) {
75
+ Text(data.name + "(data.count)")
76
+ }
77
+ }
78
+ }
79
+
80
+ struct ContentView: View {
81
+ @EnvironmentObject var test: Test
82
+
83
+ var body: some View {
84
+ List {
85
+ ForEach(0..<test.datas.count, id: .self) { i in
86
+ RowView1(data: self.$test.datas[i])
87
+ }.onDelete { offsets in
88
+ self.test.datas.remove(atOffsets: offsets)
89
+ }
90
+ }
91
+ }
92
+ }
60
93
  ```