Environment 와 Transaction
지난번에 포스팅 하지 못했던 UIViewRepresentable 관련 내용을 정리하고자 합니다.
개발 및 테스팅 환경은 지난 포스팅과 동일합니다.
Environment
Context 객채에서 아래와 같이 접근할 수 있는 environment 에 대해서 먼저 알아보도록 하겠습니다.
정의부터 살펴보도록 하겠습니다.
A collection of environment values propagated through a view hierarchy.
뷰 하이어라키를 통해 전달되는 environment value 의 컬렉션 입니다.
여기서 설명하는 environment value 란, SwiftUI (시스템) 가 자동적으로 세팅 및 업데이트 해주는 pixelLength, scenePhase, locale 등이 존재합니다.
위와 같은 value 들을 environment(_:_:)
메소드를 통해 아래와 같이 override 나 set 해줄 수 있습니다.
MyView()
.environment(\.lineLimit, 2)
몇몇 value 에 대해서는 위 코드보다 더 읽기 쉬운 직접적인 메소드를 사용하여 값을 적용해 줄 수 있습니다.
MyView()
.lineLimit(2)
시스템이 자동으로 제공하는 값 이 외에도 EnvironmentKey 프로토콜을 준수하는 타입을 만들어 커스텀한 Environment Value 를 생성 할 수 있습니다.
EnvironmentKey Protocol
Environment key 는 아래와 같이 코드에 정의되어 있습니다.
애플 예제를 참고하여 직접 CustomEnvironment Value 를 만들어 보도록 하겠습니다.
먼저, static 변수인 defaultValue 를 작성해 주어야 합니다.
private struct WeatherEnvironmentValue: EnvironmentKey {
static let defaultValue: String = "Sunny!"
}
defaultValue 을 작성할 때 타입을 명시 한다면 따로 이를 통해 컴파일러가 자동으로 관련값을 유추해내기 때문에
typealias Value = ...
와 같은 코드를 작성하지 않아도 됩니다.
다음으로 생성한 EnvironmentKey 에 대응되는 EnvironmentValues property 를 생성해 주어야 합니다.
extension EnvironmentValues {
var weather: String {
get { self[WeatherEnvironmentValue.self] }
set { self[WeatherEnvironmentValue.self] = newValue }
}
}
EnvironmentValues 는 key - value 형태로 저장되어 있기 때문에, 위와 같이 접근하여 값을 얻거나 세팅해 줄 수 있습니다.
생성한 EnvironmentValue 를 사용하기 위해 위에서 소개했던 environment(_:_:)
메소드를 이용하거나, View 의 Extension 을 통해 새로운 메소드를 생성하여 사용해 줄 수 있습니다.
또 하나의 방법으로 @Environment 를 이용하여 readonly 변수로 사용할 수 있습니다.
extension View {
func weather(_ weatherValue: String) -> some View {
environment(\.weather, weatherValue)
}
}
Text("plain text")
.weather("Cloudy :(") // way 1
Text("plain text")
.environment(\.weather, "Cloudy :(") // way 2
@Environment(\.weather) var weather: String // way 3 -> read only!!
어디에 어떻게 쓰게 될지 아직 정확히 감은 안오지만, 위와 같이 Custom 하게 만든 EnvironmentValues 를 사용한 예제를 만들어 보았습니다.
struct ContentView: View {
@State var weatherString: String = "" // [1]
var body: some View {
VStack {
LottieView()
.frame(width: 200, height: 200, alignment: .center)
.weather(weatherString) // [2]
Text("지금 날씨는 ??")
HStack {
Button("맑음") {
weatherString = "sunny" // [1]
}
Button("흐림") {
weatherString = "cloudy" // [1]
}
}
}
}
}
struct LottieView: UIViewRepresentable {
typealias UIViewType = UIView
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: .zero)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
if context.environment.weather.count > 0 {
let animation = AnimationView(animation: .named(context.environment.weather)) // [3]
uiView.addSubview(animation)
animation.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
animation.widthAnchor.constraint(equalTo: uiView.widthAnchor),
animation.heightAnchor.constraint(equalTo: uiView.heightAnchor)
])
animation.play { _ in
animation.removeFromSuperview()
}
}
}
}
버튼을 통해 weather environment value 를 update 하여 해당 weather environment value 에 맞는 Lottie 를 재생시키는 예제입니다.
[1]
버튼에 따라 weather environment 에 지정할 string value 를 지정합니다.
[2]
아까 작성하였던 weather 메소드로 environment value 를 수정합니다.
[3]
environment update 에 따른 updateUIView
의 호출에서
지정된 environment value 에 접근해 해당 value 에 맞는 Lottie 를 재생합니다.
실행 결과는 아래와 같습니다.
이 예제가 Environment 를 올바르게 사용한 건지 잘은 모르겠으나, 위와 같이 사용할 수 있습니다.
여기서 드는 궁금증으로,
설정한 Environment 값이 다른 View 에서도 같은 값으로 전달 받게 될까?
라는 점이 예제를 만드는 도중 해당 부분이 궁금해서 테스트 해봤는데,
로티 재생 없이 environment value 를 print 해본 결과 아래와 같습니다.
흐림 버튼을 눌렀을 때 weather 메소드로 environment value 를 지정해준 view 1 은 "cloudy" 로 표시되었지만, weather 사용이 없는 view 2 에서는 기본값인 "Sunny!" 가 그대로 사용되고 있는 것을 확인하였습니다.
결론은 writeable 한 environment value 라면 각 View 마다 다를 수 있습니다.
그렇다면 굳이 왜 view 2 의 updateUIView
가 호출되었는지는 궁금증으로 남긴 하네요.
Transaction
우선 정의부터 살펴보겠습니다.
The context of the current state-processing update.
현재 State 변경에 따른 처리에 대한 컨텍스트 단위 입니다.
Transaction 을 사용하여 뷰 하이어라키 내 뷰 간의 애니메이션을 전달할 수 있습니다.
State 변경 시에 생성되며 해당 State 의 Update 시 발생할 수 있는 애니메이션을 조작할 수 있습니다.
아래 두 방법을 통해 Transaction (Animation) 을 구성하여 View 에 전달 해 줄 수 있습니다.
1. withAnimation 사용
2. withTransaction 사용
해당 글에서 참고한 예제를 통해 살펴보도록 하겠습니다.
struct ContentView: View {
@State private var scaleModify: Bool = false
var body: some View {
Circle()
.foregroundColor(.brown)
.frame(width: 200, height: 200, alignment: .center)
.scaleEffect(scaleModify ? 1.0 : 0.5)
Circle()
.foregroundColor(.blue)
.frame(width: 200, height: 200, alignment: .center)
.scaleEffect(scaleModify ? 1.0 : 0.5)
.transaction { transaction in // [2]
transaction.animation = .none
}
Button("크기 조절") {
let transaction = Transaction(animation: .easeInOut)
withTransaction(transaction) { // [1]
scaleModify.toggle()
}
}
}
}
버튼을 터치할 시 Circle 의 scale 을 바꾸는 간단한 예제입니다.
첫 번째 Circle 은 정상적으로 scale 조정 애니메이션이 실행되나, 두 번째 Circle 은 애니메이션이 실행되지 않도록 Transaction 을 수정합니다.
withTransaction
메소드를 통해 애니메이션이 포함 된 State 의 update 를 지정해 줍니다. [1]
withAnimation
을 사용하여도 결과는 동일합니다.
2번째 Circle 의 경우 transaction
메소드를 통해 전달받게 될 Transaction 을 미리 캡쳐하여 지정된 애니메이션을 none 으로 바꿉니다. [2]
실행 결과는 아래와 같습니다.
위 코드에서 Circle View 를 UIViewRepresentable 을 준수하는 Custom View 로 수정하고 updateUIView
에서 Transaction 을 확인해 보았습니다.
코드는 아래와 같습니다.
struct ContentView: View {
@State private var scaleModify: Bool = false
var body: some View {
AnimationView(dummy: $scaleModify)
.background(.brown)
.frame(width: 200, height: 200, alignment: .center)
.scaleEffect(scaleModify ? 1.0 : 0.5)
AnimationView(dummy: $scaleModify)
.background(.blue)
.frame(width: 200, height: 200, alignment: .center)
.scaleEffect(scaleModify ? 1.0 : 0.5)
.transaction { transaction in
transaction.animation = .none
}
Button("크기 조절") {
let transaction = Transaction(animation: .easeInOut)
withTransaction(transaction) {
scaleModify.toggle()
}
}
}
}
struct AnimationView: UIViewRepresentable {
typealias UIViewType = UIView
@Binding var dummy:Bool
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: .zero)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
let transaction = context.transaction
print(transaction.animation) // [1]
}
}
AnimationView 는 다른 추가사항 없이 transaction 만을 확인하기 위해 위와 같이 구성하였습니다.
State 변경을 통해
updateUIView
콜백을 받기 위해 dummy Binding을 추가하였습니다.
[1]
의 결과를 확인해보면 아래와 같습니다.
애니메이션이 적용된 첫 번째 뷰에서는 정상적으로 작동된 animation 의 값을 확인할 수 있었고, 두 번째 뷰는 transaction 메소드를 통해 수정한 대로 애니메이션이 적용되지 않음을 확인할 수 있었습니다.
이와 같이 UIViewRepresentable context 내에서도 Transaction 을 전달 받을 수 있습니다.
아직 이 개념을 어디다 써야 할 지는 잘 감이 오지 않네요.
저도 공부 하면서 정리한 내용이라 잘못된 내용이 존재하리라 생각합니다.
잘못된 내용이 있어도 너그러이 이해해 주시고 피드백 부탁드리겠습니다.
읽어주셔서 감사합니다.
<참고>
EnvironmentValues
EnvironmentKey
로티 이미지 출처 1
로티 이미지 출처 2
Transaction
Transactions in SwiftUI
withAnimation(::)
withTransaction(::)
How to override animations with transactions
Author And Source
이 문제에 관하여(Environment 와 Transaction), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@235/Environment-와-Transaction저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)