우리가 구현하고자 하는 페이지는 아래와 같습니다. 메인 뷰에서 모든 학생들의 이름과 점수의 Text로 보여주고 해당 학생들 마다 점수수정 페이지로 가는 NavigationLink가 있습니다.
NavigationLink의 목적지는 TextField가 있어서 해당 학생의 점수를 수정하는 Edit 뷰입니다. 여기서 수정한 점수를 Back 버튼을 눌러서 메인 뷰로 이동했을 때 바로 반영되게 할 예정입니다.
학생은 이름과 점수를 property로 가지고 있도록 하겠습니다. ForEach에서 사용하기 위해서 id를 만들어 Identifiable Protocol을 준수하겠습니다.
ViewModel은 학생들의 배열은 @Publish 변수로 가지고 있겠습니다.
struct Student: Identifiable {
let id = UUID()
var name: String
var score: Int
}
class MainView2Model: ObservableObject {
@Published var students = [
Student(name: "김철수", score: 100),
Student(name: "김영희", score: 90),
Student(name: "이영수", score: 80),
Student(name: "이영희", score: 70),
Student(name: "박철수", score: 60),
Student(name: "박영희", score: 50),
Student(name: "최철수", score: 40),
Student(name: "최영희", score: 30),
Student(name: "정철수", score: 20),
Student(name: "정영희", score: 10),
]
}
위 기능을 구현하기 위해서는 하위 뷰에 바인딩 변수를 전달해서 상위 뷰의 변수와 연결해주어야 합니다. 그냥 일반적으로 데이터를 전달하면 Edit 뷰에서 아무리 수정을 하더라도 상위 뷰의 데이터에는 아무런 영향을 끼치지 않을 것입니다.
보통 위와 같은 리스트를 구현할 때는 ForEach문을 활용합니다. ForEach문에 그냥 뷰모델의 데이터를 전달하면 안됩니다. 꼭 $를 붙여서 바인딩으로 전달하도록 합시다.
struct MainView2: View {
@ObservedObject var viewModel = MainView2Model()
var body: some View {
NavigationView {
ScrollView {
LazyVStack (spacing: 20) {
ForEach($viewModel.students) { $student in
HStack {
Text("\(student.name)의 점수는 \(student.score)점입니다.")
Spacer()
NavigationLink {
EditView(student: $student)
} label: {
Text("점수 수정")
}
}
.padding(.horizontal, 10)
}
}
}
}
}
}
$viewModel의 타입을 보면 아래와 같이 나옵니다. ObservedObject인데 뒤에 Wrapper라는 것이 붙어(?)있죠? 이 부분을 공식문서를 참고해보면 아래와 같습니다.
Wrapper는 기존의 observed object를 감싸서 해당 객체의 property들의 binding을 만들어 줄 수 있도록 한다고 합니다. 즉 MainView2Model을 감싸서 그 property인 students를 bindings로 만들 수 있게 해줍니다.
따라서 ForEach문 내부에서 $students로 하위 View인 EditView에 전달할 수 있게 된 것이죠.
이 경우에도 마찬가지입니다. 다만 여기에서는 observed object가 아니라 [students] 자체를 Binding으로 감싸서 ForEach에 전달하면 됩니다.
struct MainView2: View {
@State var students = [
Student(name: "김철수", score: 100),
Student(name: "김영희", score: 90),
Student(name: "이영수", score: 80),
Student(name: "이영희", score: 70),
Student(name: "박철수", score: 60),
Student(name: "박영희", score: 50),
Student(name: "최철수", score: 40),
Student(name: "최영희", score: 30),
Student(name: "정철수", score: 20),
Student(name: "정영희", score: 10),
]
var body: some View {
NavigationView {
ScrollView {
LazyVStack (spacing: 20) {
ForEach($students) { $student in
HStack {
Text("\(student.name)의 점수는 \(student.score)점입니다.")
Spacer()
NavigationLink {
EditView(student: $student)
} label: {
Text("점수 수정")
}
}
.padding(.horizontal, 10)
}
}
}
}
}
}
Text에 전달할 때 우리는 Binding 변수를 사용하지 않고 그냥 student를 사용하는데요. 만약 Binding 변수를 사용해야 하는 경우 (ex. 하위 View에서 전달 받은 타입이 Binding인 경우)에는 wrappedValue에 접근하면 됩니다. 읽기 뿐만 아니라 wrappedValue를 수정할 때도 활용할 수 있습니다.
Text("\($student.wrappedValue.name)의 점수는 \($student.wrappedValue.score)점입니다.")
EditView에서는 TextField에 직접 score를 binding으로 연결해서 수정하도록 하겠습니다.
struct EditView: View {
@Binding var student: Student
var body: some View {
VStack(alignment: .center) {
Spacer()
Text("\(student.name)의 점수 수정하기")
TextField("", value: $student.score, formatter: NumberFormatter())
.frame(width: 50, height: 20)
.border(.gray, width: 1)
Spacer()
}
}
}
MainView와 DetailView가 Binding으로 연결되었기 때문에 Detail에서 수정된 점수가 Main에도 반영된 것을 볼 수 있습니다.