SwiftUI

SwiftUI - ContainerValueKey

GREEN.1229 2024. 10. 24. 18:52

안녕하세요. 그린입니다 🍏

이번 포스팅에서는 SwiftUI의 ContainerValueKey에 대해 알아보겠습니다 🙋🏻

 


ContainerValueKey

iOS 18에서 나온 개념으로 컨테이너 값에 접근하기 위한 키입니다.

 

protocol ContainerValueKey

 

익숙한 PreferenceKey처럼 프로토콜이며 비슷하지만, 조금 다른 차이가 있어요.

 

조금 더 차이를 짚어볼까요?

 

PreferenceKey는 특정 뷰에서 계산된 값을 뷰에 전달하는 역할로 많이 쓰이죠.

즉 데이터는 자식 뷰에서 상위 뷰로 전파됩니다.

레이아웃의 정보를 전파하거나 여러 하위 뷰에서 데이터들을 모아서 상위 뷰에서 처리할 때 많이 사용하죠.

 

struct SizePreferenceKey: PreferenceKey {
  static var defaultValue: CGSize = .zero

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

struct ContentView: View {
  @State private var childSize: CGSize = .zero

  var body: some View {
    VStack {
      Text("Child size: \(childSize.width) x \(childSize.height)")
      
      ChildView()
        .background(
          GeometryReader { geometry in
            Color.clear
              .preference(key: SizePreferenceKey.self, value: geometry.size)
          }
       )
    }
    .onPreferenceChange(SizePreferenceKey.self) { newSize in
      self.childSize = newSize
    }
  }
}

 

이런식으로 뷰 크기를 측정하여 전파할때 저는 자주 사용합니다 🙋🏻

 

ContainerValueKey는 뷰 간 사용자 정의 데이터를 전달해줍니다.

각 뷰에 커스텀한 값을 부여하고 전달해줄 수 있죠.

 

어떻게 사용하고 어떨때 사용되는지 공식문서를 가지고 한번 알아보겠습니다 🙋🏻

 

우선, 해당 프로토콜을 채택해 커스텀한 키를 만들어볼까요?

 

private struct MyContainerValueKey: ContainerValueKey {
  static let defaultValue: String = "Default value"
}

extension ContainerValues {
  var myCustomValue: String {
    get { self[MyContainerValueKey.self] }
    set { self[MyContainerValueKey.self] = newValue }
  }
}

 

이렇게 커스텀한 키를 만들고 디폴트 값도 선언해줍니다.

그리고, ContainerValues 타입을 확장해 해당 키를 사용할 수 있도록 새 컨테이너 값 속성을 정의해줍니다.

ContainerValues는 또 아래서 조금 자세히 알아볼께요!

 

MyView()
  .containerValue(\.myCustomValue, "Another string")

 

그리고 이렇게 원하는 뷰에서 containerValue 모디파이어를 이용해 위 정의한 키 경로를 사용하고 정의할 값을 넣어줍니다.

 

이걸 View를 확장해서 이렇게도 쓸 수 있어요.

 

extension View {
  func myCustomValue(_ myCustomValue: String) -> some View {
    containerValue(\.myCustomValue, myCustomValue)
  }
}

MyView()
  .myCustomValue("Another string")

 

그럼 이렇게 조금 더 편리하게 사용할 수 있고 명확해집니다.

 

이렇게 해당 뷰 컨테이너의 값을 설정할 수 있고 이제 받아와볼까요?

 

@ViewBuilder var content: some View {
  Text("A").myCustomValue("Hello")
  Text("B").myCustomValue("World")
}


Group(subviews: content) { subviews in
  ForEach(subviews) { subview in
    Text(subview.containerValues.myCustomValue)
  }
}

 

각 뷰에 Hello와 world로 컨테이너 값을 주었습니다.

그리고 필요 시 해당 값에 접근하여 이 속성 값들을 가져올 수 있죠.

 

큰 범위로 보자면 이렇게도 사용 됩니다.

 

struct MyContainer<Content: View>: View {
  var content: Content

  init(@ViewBuilder content: () -> Content) {
    self.content = content()
  }


  var body: some View {
    Group(subviews: content) { subviews in
      ForEach(subviews) { subview in
        // Display each view side-by-side with its custom value.
        HStack {
          subview
          Text(subview.containerValues.myCustomValue)
        }
    }
  }
}

 

즉, 여러 다른 뷰를 포함하고 있는 상위 뷰에서 하위 뷰의 정보를 추출해올 수 있는것이죠.

 

여기서 우리가 다루지 않은 두 개념 ContainerValues와 containerValue 모디파이어를 보겠습니다.

 


ContainerValues

해당 뷰와 관련된 컨테이너 값의 컬렉션 타입입니다.

 

struct ContainerValues

 

구조체이며, SwiftUI는 ContainerValues를 이용해 구조 내 각 뷰와 연관된 값의 컬렉션을 노출할 수 있어요.

주어진 뷰에 설정한 컨테이너 값은 해당 뷰에만 영향을 미칩니다.

preference와 마찬가지로 컨테이너 값은 설정된 뷰 위의 뷰에서, 즉 상위 뷰에서 값을 읽을 수 있습니다.

그러나 preference와 달리 해당 컨테이너 값이 설정된 뷰 계층 구조에서 그 값을 설정한 뷰의 경계를 넘지 않아요.

즉, 해당 값을 설정한 뷰 내부에서만 유효하고 외부로 퍼지지 않죠.

 

VStack {
  Text("A").containerValue(\.myCustomValue, 1) // myCustomValue = 1
  Text("B").containerValue(\.myCustomValue, 2) // myCustomValue = 2
  // Container values are unaffected by views that aren't containers:
  Text("C")
    .containerValue(\.myCustomValue, 3)
    .padding() // myCustomValue = 3
} // myCustomValue = it's default value, values do not escape the container

 

이 예시에서는 Text 뷰들이 존재하고 각각 컨테이너 값을 설정했어요.

여기선 해당 값은 해당 뷰에서만 영향을 미치고 가까운 텍스트 뷰에 전파되거나 외부로 전달되지는 않는다는 겁니다.

environment나 preference 같은 값들은 하위 뷰에서 설정된 값을 상위 뷰로 전달하거나 병합해서 사용할 수 있는것과 차이가 있죠.

 

그래서 우리는 뷰에 고유한 컨테이너 값을 주기 위해 이 ContainerValues 타입을 확장해 모디파이어를 붙여주는것이죠.

 

앞서 프로토콜을 채택해 커스텀 키를 만들고 ContainerValues를 확장해 연산 프로퍼티로 값을 만드는것도 있는데요.

 

매크로를 활용해 이렇게 간단하게도 사용할 수 있습니다.

 

extension ContainerValues {
  @Entry var myCustomValue: String = "Default value"
}

 

간결하게 끝낼 수 있죠.

다만 ContainerValueKey를 사용하면 확장성이 높고 여러 타입의 값을 처리할 때 좀 더 유용할 수 있어요.

 

마지막으로, containerValue 모디파이어를 살펴볼까요?

 


containerValue

뷰의 특정 컨테이너 값을 설정해주는 모디파이어입니다.

 

nonisolated
func containerValue<V>(
  _ keyPath: WritableKeyPath<ContainerValues, V>,
  _ value: V
) -> some View

 

이렇게 키패스와 값을 받죠.

 

키패스는 업데이트할 ContainerValues의 속성입니다.

그리고 value는 keyPath로 지정된 항목에 설정할 값이죠.

 

Text("A").containerValue(\.myCustomValue, 1)

 

위에서 살펴봤던것처럼 이렇게 모디파이어를 사용할 수 있습니다.

 

그럼 이걸 어떤식으로 쓸 수 있냐?

이건 제가 생각했을때입니다.

 

import SwiftUI

struct ContentView: View {
  @ViewBuilder var content: some View {
    Text("A View")
      .containerValue(\.myCustomValue, "A")
    Text("B View")
      .containerValue(\.myCustomValue, "B")
  }
  
  var body: some View {
    Group(subviews: content) { subviews in
      ForEach(subviews) { subview in
        if subview.containerValues.myCustomValue == "A" {
          VStack {
            subview
            Text(subview.containerValues.myCustomValue)
          }
        }
      }
    }
  }
}

extension ContainerValues {
  @Entry var myCustomValue: String = "Default value"
}

 

이런식으로 해당 컨테이너 값으로 분기해 보여줄 서브뷰만 보여줄 수도 있고, 제외할 서브뷰 빼고 다 보여줄 수도 있을것 같아요.

좀 더 분기 로직이 간단할 수 있을것 같습니다.

 

 

이거 말고도 유용하게 쓰일 수 있을것 같긴한데, 이제 나와서 아직 활발히 적용해볼 수 없었네요 🥲

좀 더 필요를 찾아보려 합니다!

 


레퍼런스

 

ContainerValueKey | Apple Developer Documentation

A key for accessing container values.

developer.apple.com

 

ContainerValues | Apple Developer Documentation

A collection of container values associated with a given view.

developer.apple.com

 

containerValue(_:_:) | Apple Developer Documentation

Sets a particular container value of a view.

developer.apple.com