정의
View에 의해 생성된 이름이 있는 값
PreferenceKey는 View에서 어떤 값을 관찰하고 싶을 때 관찰할 값의 타입을 정의하는 프로토콜입니다.
조금 더 자세히 말해보자면, 뷰 계층 구조에서 전달할 수 있는 값을 생성하는데 사용하는 프로토콜이에요.
얘를 통해서 자식뷰가 부모 뷰와 통신을 할 수 있습니다.
💡 뭔가 SwiftUI 에 존재하는 Environment Value와 비슷해보이는데, 조금만 생각해보면 이 친구와는 반대로 움직이는 걸 알 수 있을 겁니다.
통상 EnvironmentValues, EnvironmentObject 와 같은 친구들은 데이터를 뷰 계층 아래로 뿌려주는데 사용하죠. 그런데 우리가 이번에 살펴볼 PreferenceKey로 생성한 값은 그 반대인 아래에서 위로 데이터를 공유할 수 있는 방식이라고 보시면 될 것 같아요.
이러한 아이라는 것을 우리는 알게 되었네요.
그럼 얘를 어디다 쓰는게 좋을까요?
사용하기 좋은 예시 중 하나로 PreferenceKey를 사용하여 navigationTitle를 직접 수정하지 않고도 변경되는 값을 전달할 수가 있습니다.
만일 뷰 하나에 여러개의 자식 뷰가 존재할 경우, PreferenceKey에 필수 메서드인 reduce를 이용하여 모든 값을 단일 값으로 합칠 수 있는 방법을 제공합니다.
무슨말인지 저도 사실 이해가 잘 안가는데;
예시 코드를 보면서 같이 이해해봅시다.
먼저 PreferenceKey 프로토콜을 준수하는 구조체를 하나 만들어보겠습니다.
struct TPreferenceKey: PreferenceKey {}
PreferenceKey 프로토콜은 2가지의 필수 세팅값이 존재합니다.
첫번째
첫번째로는 defaultValue
로, 얘는 명시적으로 세팅되어있지 않은 경우에 사용할 기본적인 값을 나타냅니다. defaultValue
는 Equatable
프로토콜을 준수하므로 기본 제공 타입이든 커스텀 타입이든 상관이 없죠.
실제 코드로 선언해보면 다음과 같이 보여집니다.
근데 IDE에서 컴파일 에러를 내뿜고 있네요. defaultValue
의 타입을 지정해달라는 에러인 것 같습니다.
이를 해결하기 위해서는 typealias 로 생성되어있는 Value의 타입을 지정해주면 됩니다. 지정된 타입은 PreferenceKey 내부에서 값을 공유 혹은 전달할때 공통적으로 사용하게 되는 타입입니다.
그럼 다음과 에러를 해결해봅시다.
초기값이 없다고 에러를 내뿜는 컴파일러입니다. 초기값을 넣어줍시다.
두번째
두번째로는 reduce
함수 입니다. reduce 함수는 뷰의 하위에 존재하는 모든 값을 결합(Combine) 하는 데 사용됩니다. 이 함수는 이전에 누적된 값을 클로저가 제공하는 다음 값과 결합하여 값의 시퀀스를 수정하는 방식으로 모든 값을 하나로 결합하게 됩니다.
값을 다 긁어모아서 합치는 역할을 한다는 것을 대충 알게 되었습니다. 그럼 받고 있는 두 개의 매개변수를 봅시다.
value
: 이 값은 이전에 이 메서드를 호출하면서 누적된 값을 나타냅니다.nextValue
: 이 클로자는 시퀀스에서 다음 값을 반환하게 됩니다.이렇게 얘기 해봤자 잘 이해가 안가니 코드로 봅시다.
저는 음식 메뉴 두개를 받아서 합치는 코드를 만들었습니다. (맛있는거 옆에 맛있는거)
Preference Modifier
PreferenceKey 를 만들고 난 다음 중요한 것을 잠깐 보겠습니다.
SwiftUI에는 다양한 modifier가 존재하는데, PreferenceKey를 적절히 세팅하고 사용할 수 있는 modifier 들이 있습니다.
먼저 preference(key:value:)
은 지정된 PreferenceKey에 대한 값을 설정할 수 있는 modifier 입니다.
그리고 onPreferenceChange(_:perform:)
은 지정된 PreferenceKey의 값이 변경될 때 수행할 작업을 추가할 수 있는 modifier 입니다.
예제에서는 VStack 안에 3개의 Text를 세팅하였고, 1-2 번째 Text는 맛있다고 생각하는 음식을, 3번째는 합쳐진 값을 출력하도록 구성하였죠.
그럼 이렇게 reduce 에 정의한 액션대로 값이 합쳐져 결과값이 보여지는 것을 확인할 수 있습니다.
그럼 이제 적절히 사용하면 좋을 곳들을 총 3개정도 살펴봅시다!
PreferenceKey의 예시 중 하나는 장바구니의 합계를 계산하는 로직입니다. 이 경우 모든 값을 가져오므로 reduce
의 강력한 기능을 활용할 수가 있죠. 단순히 모든 자식의 값을 더하고 구하는 로직입니다.
먼저 PreferenceKey 구조체를 하나 만듭시다.
그 다음, 무작위로 생성된 가격 값을 가진 List를 만들었고, 각 값을 아이템 가격 표시부분에 전달할 수 있도록 세팅하였습니다. 마지막으로 변경된 값의 합을 total에 넣어줄 수 있게 세팅하였죠.
그럼 다음과 같이 합계를 볼 수 있는 것으로 활용할 수 있습니다.
PreferenceKey의 또 다른 실용적인 예시는 리스트의 항목들을 정렬하는 것입니다.
먼저 문제 상황을 이해해보도록 하죠. 두개의 항목을 표시하는 리스트가 있다고 가정해봅시다.
이렇게 보니 리스트 텍스트들이 정렬되지 않은게 눈에 보입니다.
둘의 시작 위치가 다르죠? 아무래도 정렬되어 있으면 보기에 좋아보이는데 말이죠..
이 문제를 해결하기 위해 HStack에 첫번째 Text 뷰에 고정된 너비를 주어봅시다.
음~..이건 좋은 방법은 아닌 것 같네요.
또한 숫자가 20000 이라면? 2000000000000 이라면?
다 보여지지도 않거나 레이아웃 자체가 깨질게 뻔하죠.
이런 상황에서 PreferenceKey가 도움이 될 수 있습니다.
우선 PreferenceKey 프로토콜을 준수하는 구조체를 만듭시다.
이 프로토콜의 주요 목표는 모든 자식 뷰로부터 너비를 수집하고, 이를 배열에 캐시해두는 것입니다.
다음으로 ViewModifier를 하나 만들어서 GeometryReader
의 오버레이를 컨텐츠에 추가할겁니다.
이렇게 하게 되면 GeometryProxy
를 통해서 너비를 수집할 수 있습니다.
자, 이제 메인뷰에는 적용할 너비를 저장할 State 변수 하나를 추가할거에요.
모든 자식 뷰로부터 너비를 수집하고,
그 중 가장 넓은 너비를 찾아서 모든 자식뷰에 동일한 너비로 설정하는 것이죠!
훨씬 나아졌네요 :)
우리는 지금까지 PreferenceKey 프로토콜을 사용하여 기본 제공 타입을 사용하는 예제를 살펴보았습니다.
그럼 커스텀 타입은 어떨까요? 커스텀 타입도 Equatable
프로토콜을 준수하는 것이라면 PreferenceKey 프로토콜에서 지원합니다.
이 개념을 실제 예시로 한번 살펴보도록 하죠.
먼저, 서울의 기상 패턴을 담기 위한 데이터 구조체를 정의 해보겠습니다.
또한 정의하는 이 구조체는 Identifiable
, Equatable
프로토콜을 모두 준수하도록 만들겠습니다.
위에서 우리는 WeatherData 라는 구조체를 정의했고,
이는 Identifiable
, Equatable
프로토콜을 모두 준수합니다.
이제 이 데이터를 활용해서 최대 온도를 찾는 로직을 추가할 수 있습니다.
PreferenceKey 구조체를 만들어보죠.
이 MaxTemperatureKey는 온도값을 모아 최대 온도를 추출하는 역할을 합니다.
이제 화면을 구현해봅시다.
보여지는 화면은 임의로 서울의 평일 날씨를 리스트로 보여주고, 온도가 표시되는 각 Text에 온도를 전달하여 onPreferenceChange 를 통해 각 온도들 중 가장 큰 온도를 세팅할 수 있게 구성된 코드입니다.이렇게 모든 값을 가져와 비교하여 보여줄 수 있기도 합니다.
이렇게 PreferenceKey를 한번 훑어봤는데요. 사실 PreferenceKey를 자주 사용해봤다고 생각했지만 이게 정확히 어떤 역할을 하고, 어떻게 width, height를 구할 수 있는가에 대해 제대로 인지하지 않고 사용했었는데, 이번기회로 PreferenceKey가 뭐고 어떻게 정의하냐에 따라 각기 다양한 곳에서 적절히 사용할 수 있겠다라는 것을 알게 됐네용
https://developer.apple.com/documentation/swiftui/preferencekey
https://www.devtechie.com/community/public/posts/231536-preferencekey-preference-modifier-in-swiftui