🚩출처
들어가며
- @StateObject와 @ObservedObject SwiftUI View에게 관찰하는 값의 변화를 알려주는 프로퍼티 래퍼이다. 이 둘은 유사해보이지만 중요한 다른점이 있다.
- 우선, 왜 항상 @ObservedObject만 쓰면 안되는지 궁금할 것이다.
ObservedObject란 무엇인가?
- @StateObject와 @ObservedObject 모두 ObservableObject 프로젝트를 채택한 객체가 필요하다.
- 이 프로토콜은 객체가 변경되기 전에 방출하는 게시자가 있는 객체를 나타내며 SwiftUI가 뷰 다시 그리기를 트리거하도록 알려준다
- This protocol stands for an object with a publisher that emits before the object has changed and allows you to tell SwiftUI to trigger a view redraw.
- ObservableObject를 채택하는 타입은 SwiftUI뷰에 연결되고 SwiftUI로 하여금 변화를 감지한 후 다시 그리게 만들기 위하여 @ObservedObject 프로퍼티 래퍼와 결합될 수 있다.
예시 - 카운터
📌 예제 깔끔 Good! 🙌 🙌 🙌
final class CounterViewModel: ObservableObject {
@Published var count = 0
func incrementCounter() {
count += 1
}
}
struct CounterView: View {
@ObservedObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("Count is: \(viewModel.count)")
Button("Increment Counter") {
viewModel.incrementCounter()
}
}
}
}
- ObservabbleObject인 뷰 모델 클래스를 선언
- 구독하는 구조체에서는 @ObservedObject로 뷰 모델을 구독
- 뷰 모델 내부의 @Published 카운터는 버튼(Increment Counter 버튼)이 눌러질 때마다 증가한다. 그리고 모든 구독자(위에선 CounterView)에게 변경 신호를 준다.
예시 - 카운터
수동적 방법
final class CounterViewModel: ObservableObject {
private(set) var count = 0
func incrementCounter() {
count += 1
objectWillChange.send()
}
}
- 자동으로 변경 신호를 주는 @Published 안 쓰고 objectWillChange.send()같은 수동으로 신호를 보내는(신호를 줘라!할 때만 변경 신호를 보내는) 방법도 쓴다고 한다.
- 만약 게시된 여러 속성(published property)을 한번에 없데이트하여 일반 매개변수로 나타내고 수동 신호를 사용하는 경우 이 방법을 쓰는 게 더 나을 수도 있다
- ?????
- However, it might be a better solution if you’re updating multiple published properties at once to mark those properties as regular arguments and use the manual signal instead.
@StateObject란 무엇인가?
- @StateObject는 @ObservedObject와 비슷하게 작용한다.
- 우리는 앞의 @ObservedObject를 쓴 카운터 예제를 @StateObject를 써서 바꿀 수 있다.
예시 - 카운터
📌 @StateObject 사용 버전
struct CounterView: View {
/// ✅Using @StateObject instead of @ObservedObject
@StateObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("Count is: \(viewModel.count)")
Button("Increment Counter") {
viewModel.incrementCounter()
}
}
}
}
- @ObservedObject 대신에 @StateObject를 쓴 것 말고는 딱히 바뀐게 없다.
- 그러나 @StateObject를 언제 @ObservedObject 대신에 쓸지 명확히 하는 데에는 상당한 차이가 있다.
- @StateObject 프로퍼티 래퍼로 표시한 구독 객체 파괴되지 않으며 그것들이 뷰 스트럭트가 다시 그려질 때에 재-인스턴스화(re-instantiated)되지 않는다.
- 또 다른 뷰가 하위 뷰를 담고 있을 때 이 차이를 이해하는 것은 필수다.
- 이 것이 어떻게 동작하는지 설명하기 위해서 앞선 카운터 예제의 뷰 를 또 다른 뷰로 감싼다.
카운터 예제 변형 - 하위 뷰 포함
struct RandomNumberView: View {
@State var randomNumber = 0
var body: some View {
VStack {
Text("Random number is: \(randomNumber)")
Button("Randomize number") {
randomNumber = (0..<1000).randomElement()!
}
}.padding(.bottom)
//✅ 중요!
//가장 처음에 구현했던 @ObservedObject로
//뷰 모델을 구독하는 CounterView()를
//RandomNumberView의 하위뷰로 넣었다. 이러면 어떤 일이 벌어질까?
CounterView()
}
}
- randomNumberView는 randomize button을 누르면 난수가 생성되도록 한다.
@State 프로퍼티 래퍼로 표시되어 있는 randomNumber 프로퍼티는 CounterView뷰를 다시 그리도록 뷰를 다시 그릴 것이다.
- RandomNumberView가 다시 그려지면 그 안에 포함된 CounterView도 다시 그려진다.
- CounterView 내부에 @ObservedObject를 썼을 때는 난수가 생성될 때마다 카운터가 리셋되는 것을 볼 수 있다.
- 즉, 하위 뷰에서 @ObservedObject를 써서 게시 객체 구독하는 경우에는 상위 뷰가 다시 그려질 때마다 하위 뷰의 구독 객체가 게시 객체의 초기 상태로 되돌아 간다.
- randomize button을 눌렀을 뿐인데 @ObservedObject를 썼던 카운터까지 초기화된다.
- 본 의도대로라면 카운터는 난수 생성과 별개로 실행되어야 하기때문에 randomize button을 누르더라도 카운터의 숫자는 보존되어야 한다. 그러나 @ObservedObject 프로퍼티 래퍼를 써서 뷰 모델을 구독했기 때문에 카운터가 리셋되어 0이 되는 것이다.
- 이를 바로 잡기 위해선, @ObservedObject 대신에 @StateObject를 써야 한다. @StateObject는 뷰 모델 객체가 뷰가 다시 그려지는 동안에도 카운터 수가 보존되도록 한다.
실행 영상
@ObservedObject로 뷰 모델을 구독할 때. randomize 버튼을 누르면 카운터 수가 보존되지 않고 리셋된다.
https://www.avanderlee.com/wp-content/uploads/2022/02/observedobject_counter_demo.mp4
그래서, 언제 @StateObject를 써야 하는가?
- 언제 @StateObject를 @ObservedObject 대신에 쓸지 아는 것이 좋다.
- SwiftUI는 뷰를 언제나 (any time) 재 생성하기 때문에 @ObservedObject 프로퍼티를 뷰 안에 생성하는 것은 안정적이지 않다.
- @ObservedObject를 의존성 때문에 쓰는 게 아니라면, @StateObject 프로퍼티 래퍼를 쓰는 것이 뷰가 새로 그려지는 상황에서도 일관적인 결과를 보장한다.
그렇다면 모든 뷰에 항상 같은 인스턴스를 사용하기 위해 @StateObject만 써야 하는가?
- Siblings observing the same @StateObject instance down the line don’t require marking the object with this property wrapper.
- 그 후에(아마 한번 @StateObject를 써서 구독한 이 후를 말하는 듯???) 같은 @StateObject 인스턴스를 관찰하는 것들은 @StateObject 프로퍼티 래퍼를 써서 표시할 필요가 없다.
- It’s important not to do this since you’ll ask to retain and manage the object’s lifecycle in two places.
- 왜냐하면 두 곳에서 객체의 생명주기를 관리하고 유지하기를 요청하게 되는 것이기 때문에 그렇게 하지 않는 것이 중요하다.
- As described in the previous section: if you inject the observed object, you should use @ObservedObject.
- 전 섹션에서 설명한대로, 관찰된 객체를 주입하는 경우 @ObservedObject를 써야 한다.
결론
- @StateObject와 @ObservedObject는 비슷한 특성들을 가지고 있지만 SwiftUI가 그들의 생명주기를 관리하는 방식이 다르다.
- 현재 뷰에 구독하는 객체를 생성할 때는 결과의 일관성을 확실히 하기 위해 StateObject 프로퍼티 래퍼를 써라.
- 관찰하는 객체의 의존성을 주입할 때마다 @ObservedObject를 쓸 수 있다.
stand for
: 의미하다
emit
: 방출하다
tell
:(지시,충고,명령의) 말을 하다, 시키다
combine with
: ~와 결합되다
comform to(with)
: ~에 따르다
significant
:상당한
down the line
: 일이 발생하고 있는 도중에
- 뒤에 특정 시점이 오면, 특정 시점 이후를 뜻함
retain
:유지하다
describe
:설명하다
정보 감사합니다.