ForEach에서 id는 어떤 역할을 할까요?

SteadySlower·2022년 8월 16일
0

SwiftUI

목록 보기
26/64

🧪 실험 개요

이번 실험은 ForEach에서 id가 어떤 역할을 하는지 알아보겠습니다. 지금 구현해볼 View는 각각의 Cell에 하트를 눌러서 하트를 눌렀는지 표시해주는 View입니다. 하트를 누른 Cell을 제거해보면서 id가 어떤 역할을 하는지 실험을 해보도록 하겠습니다.

id가 index인 경우

ForEach 안에서 Cell에 데이터를 전달할 때 range로 전달하도록 합시다. id는 index값 (Int 타입) 자체를 사용합니다.

이렇게 하면 SwiftUI는 각각의 Cell을 구분하기 위해서 index값을 사용합니다.

참고로 삭제를 할 때 index를 전달해서 삭제를 하면 index out of range 에러가 발생합니다. 이 포스팅을 참고해주세요.

import SwiftUI

// Table의 View
struct Table: View {
    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        VStack {
            ForEach(0..<viewModel.names.count, id: \.self) { index in
                Cell(name: viewModel.names[index], viewModel: viewModel)
            }
        }
    }
}

// Table의 ViewModel
class ViewModel: ObservableObject {
    @Published var names = ["Kim", "Lee", "Park", "Choi"]
    
    func delete(_ name: String) {
        names = names.filter { $0 != name }
    }
}

// Cell의 View
struct Cell: View {
    let name: String
    @ObservedObject var viewModel: ViewModel
    @State var isHeart = false

    init(name: String, viewModel: ViewModel) {
        self.name = name
        self.viewModel = viewModel
    }

    var body: some View {
        HStack {
            Text(name)
            Image(systemName: isHeart ? "heart.fill" : "heart")
                .font(.system(.body))
                .foregroundColor(isHeart ? .red : .black)
                .onTapGesture { isHeart.toggle() }
            Button("Delete") {
                viewModel.delete(name)
            }
        }
    }
}

위 코드를 실행해보겠습니다. Kim에 하트를 체크하고 Kim을 지우면 Kim이 삭제되어야 하는데 삭제를 해도 하트가 맨 위에 위치한 Cell에 계속 있는 상황입니다.

이유는 우리가 index 값을 id로 전달했기 때문입니다. SwiftUI는 각각의 Cell을 id로 구분합니다. 따라서 isHeart가 true인 Cell을 id가 0, 즉 index가 0인 Cell로 저장하고 있습니다. 따라서 우리가 현재 index가 0인 Kim을 지우더라도 SwiftUI 입장에서는 0번 Cell이 지워진 것이 아닙니다. (새로운 배열에서는 Lee가 0번 인덱스가 됩니다.) 0번 index는 계속 존재하기 때문에 하트가 사라지지 않고 0번 Cell에 계속 유지되고 있습니다.

Model에 id가 있는 경우 (Identifiable)

이번의 경우 Cell에 이름을 전달할 때 일반 String을 사용하는 것이 아니라 Identifiable을 준수하는 데이터 Model을 하나 만들어서 전달하도록 합시다. id는 간단하게 UUID를 사용합시다.

이렇게 하면 SwiftUI는 Name의 id를 각각의 Cell을 구분하기 위해서 사용합니다.

import SwiftUI

// Model
struct Name: Identifiable {
    let id = UUID()
    let name: String
}

// Table의 View
struct Table: View {
    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        VStack {
            ForEach(viewModel.names) { name in
                Cell(name: name, viewModel: viewModel)
            }
        }
    }
}

// Table의 ViewModel
class ViewModel: ObservableObject {
    @Published var names = [
        Name(name: "Kim"),
        Name(name: "Lee"),
        Name(name: "Park"),
        Name(name: "Choi"),
    ]
    
    func delete(_ name: Name) {
        names = names.filter { $0.id != name.id }
    }
}

// Cell의 View
struct Cell: View {
    let name: Name
    @ObservedObject var viewModel: ViewModel
    @State var isHeart = false

    init(name: Name, viewModel: ViewModel) {
        self.name = name
        self.viewModel = viewModel
    }

    var body: some View {
        HStack {
            Text(name.name)
            Image(systemName: isHeart ? "heart.fill" : "heart")
                .font(.system(.body))
                .foregroundColor(isHeart ? .red : .black)
                .onTapGesture { isHeart.toggle() }
            Button("Delete") {
                viewModel.delete(name)
            }
        }
    }
}

이번에는 우리가 하트를 표시한 Cell에 정확하게 지워지는 것을 볼 수 있습니다. 이번에는 SwiftUI가 Cell을 구분하기 위해서 index가 아니라 고유한 id 값을 사용합니다. 따라서 Cell의 isHeart가 true인 Cell이 어떤 Cell인지 각각의 id 값을 사용해서 구분합니다. 이 때 하트가 체크된 Cell을 삭제하면 SwiftUI는 하트가 체크된 Cell이 사라졌음을 인지하고 정확하게 남은 Cell들을 표시합니다.

결론

이 포스팅에서 삭제를 할 때 index를 사용하면 안되는 이유를 알아보았습니다. 따라서 ForEach를 쓸 때, 특히 동적인 배열로 ForEach를 쓸 때는 index를 사용해서 구현하는 것을 지양해야 합니다.

profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.

0개의 댓글