스스챌 에러를 해결하던 중.. @State와 @Binding을 사용하는 경우도 많았고,
해당 개념이 있었다면 더 빠르게 해결 할 수 있었을 문제도 있어서 간단하게 정리해보려 한다.
Property wrapper
우선 프로퍼티 래퍼(@붙은 녀석들)가 무엇인지 알아보자.
'랩으로 감싼다' 프로퍼티를 특정 타입으로 감싸서 추가적인 동작이나 기능을 가진 프로퍼티를 선언할 때 쓰인다.
이 타입은 PropertyWrapper 프로토콜을 준수하고, 프로퍼티 읽기/쓰기 (get, set) 에서의 동작을 정의한다.
SwiftUI에서 Property wrapper는 데이터 상태 관리를 심플하게 하는데 자주 쓰인다.
대표적인 프로퍼티 래퍼가 @State, @Binding 이다.
@State
프로퍼티의 값이 변경될 때 뷰를 업데이트 할 수 있는 상태 변수를 선언하는데 사용됨.
https://developer.apple.com/documentation/swiftui/state
State | Apple Developer Documentation
A property wrapper type that can read and write a value managed by SwiftUI.
developer.apple.com
UIKit에서는 Property Observer를 통해 변화가 일어나면 뷰를 업데이트 했지만
SwiftUI에서는 @State 프로퍼티 래퍼를 통해 더 간단하게 뷰를 업데이트 할 수 있다.
@State로 선언된 변수의 값이 변할 때마다 View를 다시 그림!
@State변수는 '해당 뷰 내에서만' 사용되는 상태 변수이다.
만약 다른 뷰에서 직접 접근해 버리면 사이드이펙트가 발생할 수 있다.
그래서 보통 private 접근제어를 걸어준다. @State private var a: Int = 7 이런 식으로
다른 뷰에서 해당 @State 변수를 업데이트 하지 말고, @Binding을 사용해서
다른 뷰에서 '간접적으로 업데이트' 하는 방식으로 처리한다.
그런데 SwiftUI에서 View는 구조체이다..
그리고 화면에 보이는 뷰인 body가 get-only 계산속성이다..
View가 참조타입이 아닌 값타입인데 어떤 방식으로 변경사항을 반영해서 뷰를 새로 그리는 것일까?
@State 변수는 Heap에 할당된다!
View에는 @State 변수의 데이터가 할당되어 있는 메모리 주소를 가리키는 포인터만 있다.
그래서 새로운 뷰가 만들어져도(값타입_복사되어도) 해당 포인터는 여전히 Heap의 메모리 주소를 가리키고 있기 때문에
데이터의 변경 사항이 새로운 뷰에 적용 되는 것이다.
@Binding
https://developer.apple.com/documentation/swiftui/binding
Binding | Apple Developer Documentation
A property wrapper type that can read and write a value owned by a source of truth.
developer.apple.com
바인딩은 데이터를 저장하는 프로퍼티와 데이터를 표시하고 변경하는 뷰 사이의 다리 역할을 한다.
음.. 설명만 보면 애매해서 공식문서의 예시 코드를 보자.
struct PlayButton: View {
@Binding var isPlaying: Bool
var body: some View {
Button(isPlaying ? "Pause" : "Play") {
isPlaying.toggle()
}
}
}
바인딩 변수는 타입선언만 해두었다. 직접 값을 할당해 초기화 하지 않았다.
즉 PlayButton 구조체에서 직접 바인딩 변수에 값을 할당하거나 변경하는 것이 아니라는 뜻.
버튼뷰를 다른 뷰에서 초기화 한다.
struct PlayerView: View {
var episode: Episode
@State private var isPlaying: Bool = false
var body: some View {
VStack {
Text(episode.title)
.foregroundStyle(isPlaying ? .primary : .secondary)
PlayButton(isPlaying: $isPlaying) // Pass a binding.
}
}
}
body 부분에서 버튼뷰를 만들었는데 isPlaying의 값으로 $isPlaying을 전달하고 있다.
여기서 $isPlaying은
@State private var isPlaying: Bool = false 요걸 바인딩으로 변환해 전달한다는 뜻
자 이제 버튼을 누르면 힙 영역에 저장되어 있는 PlayView의 @State 변수의 값이 변경될 것이고,
변경되니까 뷰도 다시 그려질 것이다.
"값을 뷰의 부모 뷰에서 전달받아 자식뷰에서 부모 뷰의 값을 업데이트 하는 것"
struct ParentView: View {
@State private var count = 0
var body: some View {
ChildView(count: $count)
}
}
struct ChildView: View {
@Binding var count: Int
var body: some View {
Button(action: {
count += 1
}) {
Text("Increment count")
}
}
}
이 코드를 보면
ParentView가 count 변수를 가지고 있고
ChildView가 count 변수를 업데이트하는 버튼을 가지고 있다.
즉 부모 뷰에서 선언한 변수를 자식 뷰에서 업데이트하는 것이다.
부모 - 자식 간의 연결고리 정도로 이해해 볼 수 있겠다.
사실 생각보다 바로 와닿지는 않는데.. 해당 프로퍼티 래퍼들에 대해 알아본 이유는
초반에 언급했던 것처럼 스스챌 코딩 중에 바인딩을 쓰지 않고 온통 @State변수만 사용하니까
부모 뷰와 자식 뷰가 상태변화에 따라 전부 업데이트를 하는 바람에 의도와는 다른 결과가 많이 나왔다.
(배경뷰의 애니메이션은 그대로 유지되어야 하는데, 부모 뷰에서 어떤 액션을 취하면
부모 뷰의 @State변수에 따라 뷰가 업데이트되면서 배경에도 영향이 간다든지..)
요약하면 부모 뷰에서 직접 값을 세팅하고 변경해야 하는 코드가 있다면,
자식 뷰에서는 해당 변수를 @State가 아닌 @Binding으로 선언해서 해결된 문제가 많았다는 것이다.
사실 중요한 건 부모 뷰의 업데이트인데 자식뷰들까지 계속 상태변화에 따라 업데이트를 했던 것...
좀 더 많이 써보고 설계도 해보고 해야 직접적인 개념이 더 잘 와닿고 활용도도 높아질 것 같다.
스스챌 에러를 수정하면서 부모-자식 뷰에 대해 다시 한번 고민할 수 있게 되었고,
@State, @Binding 프로퍼티 래퍼에 대한 개념과 예시를 알아볼 수 있어서 좋았다.
우선 에러 해결해서 만족 ㅎ...
프로퍼티 래퍼는 공부 더 깊게 해야 할 듯 ㅎ...
'SwiftUI > SwiftUI(Basic)' 카테고리의 다른 글
15. [SwiftUI] .ignoresSafeArea() & .edgesIgnoringSafeArea() (0) | 2023.04.20 |
---|---|
14. [SwiftUI] LazyVGrid, LazyHGrid, GridItems (0) | 2023.04.19 |
13. [SwiftUI] ScrollView / LazyVStack & LazyHStack (0) | 2023.04.13 |
12. [SwiftUI] ForEach / .indices / Hashable (0) | 2023.04.13 |
11. [SwiftUI] init() (0) | 2023.04.12 |