ForEach

요구에 따라 Identified Data의 기본 Colletion로부터 뷰를 계산하는 Structure

struct ForEach<Data, ID, Content> where Data : [RandomAccessCollection](https://developer.apple.com/documentation/Swift/RandomAccessCollection), ID : [Hashable](https://developer.apple.com/documentation/Swift/Hashable)

  • Data는 RandomAccessCollection을 준수해야한다.
  • ID는 Hashable을 준수해야한다.

 

ForEach에서 id를 쓰는 이유

init(
    _ data: Data,
    @ViewBuilder content: @escaping (Data.Element) -> Content
)

 

기본 Initailizer에서 data는 Identifiable 프로토콜을 준수해야한다. (각 View들을 식별하기 위해)

하지만, [Int]와 같은 Identifiable을 준수하지 않는 데이터 유형은 data에 넣을 수 없다.

그래서 넣어보면 컴파일 에러가 생긴다. (Cannot convert value of type '[Int]' to expected argument type 'Range’)

이를 해결하기 위해 id가 존재하는 initalizer를 사용한다.

 

init(
    _ data: Data,
    id: KeyPath<Data.Element, ID>,
    @ViewBuilder content: @escaping (Data.Element) -> Content
)

 

해당 initializer에서는 data가 꼭 Identifieable을 준수할 필요가 없다.

id가 그 역할을 대신하기 때문이다.

ForEach View를 사용할 때 ID로 \.self를 넣어주고는 한다.

 

struct ContentView: View {
    let colors: [Color] = [.red, .green, .blue]

    var body: some View {
        VStack {
            ForEach(colors, id: \.self) { color in
                Text(color.description.capitalized)
                    .padding()
                    .background(color)
            }
        }
    }
}

 

ForEach로 생성되는 각 뷰들이 생성되거나 삭제될 때 그 부분만 업데이트하기위해 ID값을 통해서 뷰를 구별한다.

 

public init(_ data: Data,
                        id: KeyPath<Data.Element, ID>, 
                        @ViewBuilder content: @escaping (Data.Element) -> Content)

 

ForEach 뷰에 전달하는 id인자는 keyPath로 전달하게 되는데, 참조한 keyPath를 통해

data에서 가져온 값을 unique identifier 로 결정지어 각 뷰가 구별되도록한다.

때문에 위 예시에서 \Color.self 를 생략한 \.self를 전달했으므로 각 뷰의 unique identifier는 차례대로.red, .green, .blue가 된다.

그렇다면, data의 각 요소가 같다면 어떻게 해야할까?

 

예를들어 colors 배열이 다음과 같은 경우를 말한다.

let colors: [Color] = [.red, .red, .blue]

한 스택 오버플로우 답변 에서는 struct 구조체로 감싸서 Identifiable 프로토콜을 준수하도록 함을 권장한다.

 

How to allow ForEach layout item to show duplicate item from array in SwiftUI?

I am working with on multiple choose answer quiz app. var itemsTemp = ["ant","bat", "bear", "bee", "bird", "butterfly", "camel", &

stackoverflow.com

 

다음과 같은 구조체를 생성해서 UUID() 함수를 통해 unique identifier을 생성한다.

 

struct MyColor: Identifiable {
    var id = UUID()
    var color: Color
}

 

다음과 같이 keyPath를 .id로 전달해서 각 view가 unique해지도록 한다.

 

struct ContentView: View {
    let colors: [MyColor] = [.init(color: .red), .init(color: .red), .init(color: .blue)]

    var body: some View {
        VStack {
            ForEach(colors, id: \.id) { myColor in
                Text(myColor.color.description.capitalized)
                    .padding()
                    .background(myColor.color)
            }
        }
    }
}

 

문제상황

struct BlueRectangle: View {
    @State var blueRectSize = CGSize(width: 20, height: 40)

    var body: some View {
        VStack {
            Rectangle()
                .frame(width: blueRectSize.width, height: blueRectSize.height)
                .foregroundStyle(.blue)

            BlackRectangle()
                .frame(height: 60)
        }
    }
}
struct BlackRectangle: View {
    var body: some View {
        HStack {
            Rectangle()
            Rectangle()
            Rectangle()
            Rectangle()
        }
    }
}

BlueRectangle 1개와 BlackRectangle 3개로 구성되어있다.

여기서 BlackRectangle의 사이즈는 동적으로 결정되었다.

이 상황에서 BlueRectangle 객체의 사이즈를 BlackRectangle 한 개의 사이즈과 똑같이 만들고 싶다.

따라서 이 장에서는 다음을 해결해야한다.

  • BlackRectangle 한 개의 사이즈를 구한다.
  • 구한 사이즈를 상위뷰로 전달한다.

BlackRectangle Size 구하기

GeometryReader를 이용해서 구할 수 있다.

뷰를 GeometryReader로 감싸면 GeometryProxy 타입의 변수를 통해 Size를 알 수 있다.

ForEach structure에 감싸져 있던 Rectangle을 GeometryReader로 감싸서,

overlay로 width값을 확인해 보자.

GeometryReader { proxy in
    Rectangle()
        .overlay {
            Text("\(proxy.size.width)")
                .foregroundStyle(.white)
        }  
}

 

이제 오른쪽 화면을 보면 BlackRectangle이 101.5의 width값을 가짐을 확인했다.

즉, GeometryReader를 통해 뷰의 Size를 구할 수 있음을 알았다.

이제 사이즈를 값을 분리된 상위뷰인 BlueRectangle에 전달해보자.

상위뷰로 데이터 전달하기

PreferenceKey 프로토콜를 통해 데이터를 상위뷰로 전달할 수 있다.

먼저, PreferenceKey를 준수하는 struct를 하나 정의한다.

struct GeometryPrefereneKey: PreferenceKey {
    typealias Value = CGSize

    static var defaultValue: CGSize = .zero

    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

preference modifier를 통해 데이터를 송신시키자.

GeometryReader { proxy in
      Rectangle()
          .overlay {
              Text("\(proxy.size.width)")
                   .foregroundStyle(.white)
          }
          .preference(key: GeometryPrefereneKey.self, value: proxy.size)
}

이제 onPreferenceChange modifier를 통해 데이터를 수신할 수 있다.

onPreferenceChange는 blueRectangle 변수에 값을 넣을 수 있는 뷰에 적당히 넣자. 나는 VStack을 선택했다.

그리고 수신된 데이터를 blueRectSize에 저장하면 문제가 해결된다.

(추가로 blueRectangle의 width값의 확인을 위해 overlay로 화면에 width를 렌더링했다)

VStack {
       Rectangle()
            .frame(width: blueRectSize.width, height: blueRectSize.height)
            .foregroundStyle(.blue)
            .overlay {
                Text("\(blueRectSize.width)")
            }

       BlackRectangle()
           .frame(height: 60)
}
.onPreferenceChange(GeometryPrefereneKey.self) { blackRectangleSize in
    blueRectSize = blackRectangleSize
}

대충 순서를 보면 다음과 같다 :

GeometryReader를 통해 Size 찾기

-> BlackRectangle에서 데이터 송신

-> BlueRectangle에서 데이터 수신

-> 수신된 데이터를 blueRectSize에 저장

-> @State에 의해 뷰 reRendering

-> blueRectangle의 사이즈 변경됨

'SwiftUI' 카테고리의 다른 글

[SwiftUI] ForEach and ID  (0) 2023.11.06

+ Recent posts