만들고자 하는 기능은 아래 캡쳐와 같습니다. 수정하기 모드일 때 단어 Cell을 클릭하면 해당 단어를 수정할 수 있는 모달을 띄우는 것입니다.
모달을 띄운다는 점에서 기존의 모달과 크게 다른 점은 없어보이지만 하나 크게 다른 점이 있습니다. 바로 Modal에 어떤 단어를 수정할 지에 대한 데이터를 전달해야 한다는 것입니다. 따라서 Modal의 initializer에 word를 인자로 받도록 구현했습니다.
extension WordInputView {
final class ViewModel: ObservableObject {
let word: Word?
@Published var meaningText: String
@Published var kanjiText: String
@Published var ganaText: String
init(word: Word?) {
self.word = word
self.meaningText = word?.meaningText ?? ""
self.kanjiText = word?.kanjiText ?? ""
self.ganaText = word?.ganaText ?? ""
}
}
최초에 구현한 방식은 아래와 같습니다. 처음에는 간단하게 다른 View와 마찬가지로 ForEach의 전달하면 될 것 같았습니다.
ForEach(viewModel.words, id: \.id) { word in
ZStack {
WordCell(word: word, frontType: viewModel.frontType, eventPublisher: viewModel.eventPublisher)
EditableCell()
.onTapGesture { showEditModal = true }
}
.frame(width: deviceWidth * 0.9, height: word.hasImage ? 200 : 100)
.sheet(isPresented: $showEditModal,
content: { WordInputView(word) })
}
하지만 결과는 아래와 같았습니다. 어떤 단어를 터치해서 모달을 띄워도 현재 init된 View의 첫번째 단어를 수정하는 화면만 나왔습니다.
ForEach 안에서 .sheet를 통해서 모달을 만들 경우 현재 init된 모든 View의 단어의 Modal이 init이 됩니다. (Cell이 init될 때 비록 showEditModal이 false일 때도 init이 됩니다.) 수정을 원하는 단어 뿐만 아니라 다른 단어들의 수정 모달이 전부 init되는 것입니다. 따라서 Modal을 띄웠을 때 존재하는 Modal 중에 최상위에 있는 Modal인 가장 첫 단어의 Modal이 보여지게 되는 것이죠.
따라서 Modal을 만들 때는 ForEach문 밖에서 만들어야 합니다.
84%86%E1%85%A1%E1%86%AB_%E1%84%82%E1%85%A1%E1%84%8B%E1%85%A9%E1%86%B7.gif)
결국 ForEach 밖에서 만드려면 .sheet를 ForEach 밖에서 사용하는 방법 밖에는 없습니다. 방법을 찾던 와중에 .sheet(item:)을 발견했습니다. 기존의 .sheet(isPresented)가 modal을 보여주기 위해서 Bool 값을 사용한다면 .sheet(item:)은 Identifiable을 준수하는 모든 자료형이 사용할 수 있습니다. item에 Binding으로 연결된 변수가 nil이 아닌 값이 할당되면 Modal이 띄워지게 됩니다.
그리고 content 부분에서는 아래의 코드처럼 인자를 받아서 Modal을 init할 수 있습니다. 아래처럼 작성하면 손쉽게 Modal에 수정할 단어를 전달할 수 있습니다.
ForEach(viewModel.words, id: \.id) { word in
ZStack {
WordCell(word: word, frontType: viewModel.frontType, eventPublisher: viewModel.eventPublisher)
EditableCell()
.onTapGesture { viewModel.toEditWord = word }
}
}
.sheet(item: $viewModel.toEditWord) { word in
WordInputView(word)
}
// 뷰모델
class ViewModel {
@published var toEditWord: Word?
}
하지만 Word라는 프로토콜에 Identifiable을 준수하게 하는 순간 위와 같은 에러 메시지가 나타났습니다. 저의 경우는 어디서 한번 본 메시지인데요. 자세한 설명은 이 포스팅을 참고해주시기 바랍니다. 결론적으로 이 문제 때문에 결국 .sheet(item:)을 사용할 수 없었습니다.
결국 해결한 방법은 .sheet(isPresented:)와 .sheet(item:)의 방식을 짬뽕(?)하는 방식입니다. 일단 코드를 보시죠.
ForEach(viewModel.words, id: \.id) { word in
ZStack {
WordCell(word: word, frontType: viewModel.frontType, eventPublisher: viewModel.eventPublisher)
EditableCell()
.onTapGesture {
viewModel.toEditWord = word
showEditModal = true
}
}
}
.sheet(isPresented: $showEditModal,
onDismiss: { viewModel.toEditWord = nil; viewModel.studyViewMode = .normal },
content: { WordInputView(viewModel.toEditWord, dependency: dependency, eventPublisher: viewModel.eventPublisher) })
코드는 .sheet(isPresented:)를 활용하고 있지만 .sheet(item:)의 동작원리를 벤치마킹한 방식입니다. EditableCell을 탭하면 viewModel에 있는 toEditWord에 word를 할당하고 showEditModal에 true를 할당하는 방식입니다.그리고 onDismiss에 toEditWord에 nil을 할당하도록 하면 .sheet(item:)과 거의 유사하게 동작하는 모달을 만들 수 있습니다.