SwiftUI - ForEach

Marble·2025년 2월 13일

저는 최근 프로젝트를 진행하고 있는데 아래 이미지처럼 일정을 추가하면 달력 뷰가 업데이트 되야하는데 0개에서 1개일 땐 업데이트 되지만 1개에서 2개, 또는 2개에서 3개등 이미 점이 찍힌 후에는 달력 뷰가 업데이트가 되지 않아서 문제였습니다.

몇 시간동안 이유를 찾다가 ForEach를 제대로 이해하지 못하고 사용한 것이 문제였다는 것을 알 수 있었습니다. 그래서 이번 글에서는 ForEach문에 대해서 알아보겠습니다.

ForEach

공식문서에서는 ForEach를 주어진 Collection의 데이터를 기반으로 View들을 계산하는 Structure라고 돼있습니다.

ForEach의 사용 예시를 보겠습니다.

import SwiftUI

struct ContentView: View {
    var body: some View {
        ForEach(0..<5) { index in
            Text("\(index)")
        }
    }
}

0부터 4까지 있는 Int Range를 만들어서 그 값을 계산해서 보여줬는데 여기서 0부터 4까지의 Range가 Collection에 해당합니다. 이때 0...5 같은 closedRange는 사용할 수 없는데 ForEach는 Range만 지원하기 때문입니다.

공식문서에서는 ForEach 문이 요구하는 Data는 반드시 RandomAccessCollection이어야 한다고 합니다. 주로 Array를 사용하는데 대부분의 경우는 RandomAccessCollection을 만족합니다. 하지만 아무렇게나 만들면 다음과 같이 에러가 발생합니다.

이유는 ForEach에 전달된 array가 Identifiable 프로토콜을 준수하지 않기 때문입니다. Identifiable 프로토콜은 Swift에서 객체를 고유하게 식별할 수 있도록 하는 프로토콜입니다.

protocol Identifiable {
    associatedtype ID: Hashable
    var id: ID { get }
}

이 프로토콜을 준수하는 타입은 각 인스턴스가 고유한 식별자(id)를 제공해야 하는데 배열을 사용할 때마다 extension을 사용해서 id를 추가하는 일은 번거로울 것입니다. 그래서 ForEach는 ForEach(data, id:.self)를 제공해줍니다. 이는 자기 자신을 id로 쓴다는 의미인데 이때 hashable을 만족해야 합니다. 중복된 값이 있는 경우 아래와 같이 에러가 발생합니다.


let pointCount: Int = min(filteredEvents.count, maxPointCount)

ForEach(0..<pointCount) { index in
	Circle()
		.frame(width: 5, height: 5)                  
}

위 코드는 처음 보여준 에러 상황에서의 코드입니다. 아까 ForEach는 각 요소를 고유하게 식별할 수 있는 식별자가 필요하다고 말했습니다. 이 식별자는 요소가 변경될 때 SwiftUI가 어떤 뷰를 업데이트해야 하는지를 결정하는 데 사용되기 때문입니다. 하지만 저는 고유 id를 안 줬기 때문에 뷰가 업데이트 되지 않았던 것입니다.

이벤트가 0개에서 1개로 늘었을 때는 업데이트가 됐는데 이는 ForEach가 비어 있다가 하나의 요소가 생기기 때문에 SwiftUI는 새로 추가된 요소를 인식했기 때문에 뷰를 업데이트 된 것이었습니다.

profile
개발자가 되고 싶은 공돌이

0개의 댓글