【SwiftUI】ForEachで追加したviewを識別する方法
・やりたいこと
1つの画面にForEachで複数のviewを追加する。この時、複数のviewを識別し、それぞれの状態に対応した処理を実行する。
・具体例
SNSのタイムラインの画面で、複数の投稿をForEachでlistに追加(リスト上で1行となる)することを想定する。
ここで、返信(吹き出し記号)をタップすると、その色が赤に反転するようにしたい。
// TimelineCell.swift
import SwiftUI
struct TimeLineCell: View {
var replies = [TimelinePost]()
var elementCount: Int
init(replies: [TimelinePost]) {
self.replies = replies
elementCount = replies.count
prepFlags()
}
@State var isReplied: [Bool] = [Bool]()
mutating func prepFlags(){
var initIsReplied: [Bool] = [Bool]()
for _ in 0..<elementCount {
initIsReplied.append(false)
}
_isReplied = State(initialValue: initIsReplied)
}
func symbolColor(_ isActive: Bool) -> Color {
if isActive {
return Color.red
} else {
return Color.gray
}
}
var body: some View {
return VStack(alignment: .leading){
ForEach(replies, id: \.self) {reply in
HStack{
if self.replies.firstIndex(of: reply) != nil {
Image(systemName: "bubble.right")
.foregroundColor(self.symbolColor(self.isReplied[self.replies.firstIndex(of: reply)!]))
.onTapGesture {
if let index = self.replies.firstIndex(of: reply) {
self.isReplied[index].toggle()
}
}
}
Text(String(reply.replyCount))
}
}
}
}
}
・課題
(1)返信記号を行ごと(ForEachのインデックス毎)に識別する必要がある(datasourceであるrepliesに固定値としてこの記号を与えると、ForEachの中でletになるため、タップジェスチャーで色などを変更できない)
→ 「ForEach<Data, ID, Content> * Data : コレクション」を採用した場合に、どのようにインデックスを取得するか?
(2)返信記号のタップを受け付けるために、インデックス毎にフラグ(isReplied: [Bool])を用意し、Stateプロトコルに準拠する。
→ Stateプロトコルは、Computed Propertyにできないし、初期化もできないが、初期値として、どのように"false"をappendするか?
(参考)
@Stateを付けたプロパティは、イニシャライザの中で値を変更しても必ず無視される
・解決策
(1)今回は、以下のパターンを採用。前提として、リストの並び替えや削除を実装しないので、リスト表示後にインデックスが変わることがない。
Foreach(array) { item in
let index = array.firstIndex(of: item)
}
(参考)Getting the index in ForEach
(2)上述の参考リンクにあるように、State.init(initialValue:)と言うイニシャライザで初期化する