일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 자료구조
- ftz
- Stack
- windosw 문자열
- 스택
- 큐
- web
- 미로 탐색 알고리즘
- 재귀
- 시간복잡도
- 암호수학
- OSI
- 파이썬
- HTML
- System
- ftz level13
- level13
- Java
- 정렬 알고리즘
- War Game
- PHP
- c언어
- C
- SWiFT
- LoB
- windosws wbcs
- 백준
- pwnable.kr
- 두근두근 자료구조
- 파일 시스템
- Today
- Total
나의 기록, 현진록
[SwiftUI] @ObservedObject @StateObject 관련 트러블 슈팅 본문
발단
- 상위 ViewModel에서 하위 View를 위해 사용하는 여러 프로퍼티가 존재
- 상위 ViewModel의 책임을 분리하기 위해 하위 View에서만 관련된 상위 ViewModel의 일부를 하위 ViewModel에 분리
- 하위 View에서만 사용하는 연산 프로퍼티는 하위 View에 이동
- 하위 View에도 관련 있는 상위 ViewModel 프로퍼티는 하위 뷰 모델에서 @Binding으로 선언된 프로퍼티에 전달
이전
class ParentViewModel: ObservedObject{
@Published var url: String = ""{
didSet{
self.isLoading = true
}
// URL 관련 비동기 작업이 끝나면 false 처리 됨
}
@Published var isInvalidURL: Bool = false
@Published var isLoading: Bool = false
var URLStateSystemImage: String{
isInvalidURL ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"
}
var URLStateImageForegroundColor: Color{
isInvalidURL ? .red : .green
}
}
리팩토링 후
class ParentViewModel: ObservedObject{
@Published var url: String = ""{
didSet{
self.isLoading = true
}
// URL 관련 비동기 작업이 끝나면 false 처리 됨
}
@Published var isInvalidURL: Bool = false
@Published var isLoading: Bool = false
}
class ChildViewModel: ObservedObject{
@Binding internal var isLoading: Bool
@Binding internal var url: String
@Binding internal var isInvalidURL: Bool
init(isLoading: Binding<Bool>, url: Binding<String>, isInvalidURL: Binding<Bool>) {
self._isLoading = isLoading
self._url = url
self._isInvalidURL = isInvalidURL
}
internal var URLStateSystemImage: String{
isInvalidURL ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"
}
internal var URLStateImageForegroundColor: Color{
isInvalidURL ? .red : .green
}
}
이슈
struct ChildView: View{
@StateObject var viewModel: ChildViewModel
var body: some View {
if viewModel.isLoading {
ProgressView()
.scaleEffect(0.7)
}else{
Image(systemName: viewModel.URLStateSystemImage)
.foregroundStyle(viewModel.URLStateImageForegroundColor)
}
}
}
위 코드 블럭에는 작성하지 않았지만 TextField에 URL이 입력되면 해당 URL의 HTML을 파싱하는 비동기 작업이 진행되는데 작업 전에 isLoading이 true가 되고, 비동기 작업이 마무리 되면 isLoading이 false가 되며 비동기 작업의 결과에 따라 해당 URL이 유효한 값인지에 대한 유무에 따라 관련된 Image를 표시하게 된다.
하위 View에서는 상위 뷰 모델에서 바인딩된 isLoading 값에 따라 true일 때 ProgreeView를 보이고 false일 때 Image를 띄워야 한다.
그러나...
텍스트 필드에 올바른 URL이 입력되어도 ProgressView만 보이게 된다...
디버깅
class ParentViewModel: ObservedObject{
@Published var url: String = ""{
didSet{
self.isLoading = true
}
// URL 관련 비동기 작업이 끝나면 false 처리 됨
}
@Published var isInvalidURL: Bool = false
@Published var isLoading: Bool = false{
didSet{
print("parentViewModel isLoading -> \(isLoading)")
}
}
}
class ChildViewModel: ObservedObject{
@Binding internal var isLoading: Bool{
didSet{
print("childViewModel isLoading -> \(isLoading)")
}
}
@Binding internal var url: String
@Binding internal var isInvalidURL: Bool
init(isLoading: Binding<Bool>, url: Binding<String>, isInvalidURL: Binding<Bool>) {
print("Init ChildViewModel")
self._isLoading = isLoading
self._url = url
self._isInvalidURL = isInvalidURL
}
internal var URLStateSystemImage: String{
isInvalidURL ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"
}
internal var URLStateImageForegroundColor: Color{
isInvalidURL ? .red : .green
}
}
각 ViewModel에 있는 isLoading에 didSet과 하위 ViewModel 이니셜라이저에 출력문을 추가하여 디버깅을 해 보았다.
하위 ViewModel에 있는 isLoading didSet 구문이 출력되지 않는 것으로 보아.
상위 ViewModel의 isLoading 값이 하위 ViewModel의 isLoading에 전달되지 않아 문제가 발생하지 않는 것인가라는 생각을 했다.
해결
@StateObject -> @ObservedObject로 변경하여 해결이 가능했다.
struct ChildView: View{
@ObservedObject var viewModel: ChildViewModel
var body: some View {
if viewModel.isLoading {
ProgressView()
.scaleEffect(0.7)
}else{
Image(systemName: viewModel.URLStateSystemImage)
.foregroundStyle(viewModel.URLStateImageForegroundColor)
}
}
}
사실 문제가 발견하자마자 즉시 해결하긴 했다.
일반적으로 @StateObject는 상위 뷰, @ObservedObject는 하위 뷰에서 쓰인다라는 어렴풋이 떠올랐기 때문이다.
하지만 의문인 것은 아래 사진이다.
하위 ViewModel의 didSet이 실행되지 않고 하위 ViewModel 자체가 초기화 된다.
아직 @ObservedObject, @StateObject, @Binding이나 SwiftUI의 뷰 업데이트 방식을 충분히 이해하지 못 하고 있는 것 같다.
이 글을 작성하는 이유는 그러한 부족한 점을 보충하기 위함이다.
분석
1. @StateObject, @ObservedObject
@StateObject와 @ObservedObject은 @Published 프로퍼티 래퍼를 가진 변수가 변경되었을 때 View을 새롭게 렌더링한다는 공통점이 있지만 가장 큰 차이점은 각각의 생명주기이다.
@StateObject이나 @ObservedObject 모두 내부 프로퍼티의 값이 변경되었을 때 View를 재생성하는 것은 동일하지만, 이 때 @ObservedObject로 선언된 ViewModel도 같이 재생성되지만 @StateObject로 선언된 ViewModel은 해당 인스턴스를 가진 상위 클래스의 생명주기와 동일한 생명주기를 가진다.
결국 위에 이슈가 발생한 이유는 하위 View에 생성한 @StateObject 타입의 ViewModel 때문에 초기의 이니셜라이저 값을 그대로 가지기 때문에 viewModel.isLoading의 값이 변경되지 않았기 때문이다.
@StateObject이 아닌 @ObservedObject 타입으로 변경하여 상위 ViewModel에서 값이 변경될 때 @ObservedObject 타입인 하위 ViewModel도 재생성하여 변경된 값으로 이니셜라이저가 이루어지고 변경된 값에 따라 View를 재생성할 수 있었던 것이다.
2. isLoading 값이 변경되지 않았다는 뜻인데 어떻게 뷰가 업데이트 되었는가
이전에는 상위 ViewModel의 값이 변경되면 하위 ViewModel에 값이 "전달"되어 변경되는 줄 이해하였으나.......
결과적으로는 변경은 변경이지만 값이 전달되는 것이라기보다 변경된 값으로 새롭게 재생성이 더 적절한 표현 같다.
그렇기 때문에 didSet 구문이 실행되지 않았고 재생성된 ViewModel을 기반으로 View가 업데이트 되기 때문에 의도한 UI를 볼 수 있던 것이다.
'iOS' 카테고리의 다른 글
[iOS] 탭 바 이동 시 애니메이션 적용하기 (0) | 2024.09.15 |
---|---|
[iOS] 화면 이동 시 탭바(UITabBar) 숨기기 (0) | 2023.05.28 |
[iOS] 앱 실행 순서 app launch sequence (0) | 2023.04.05 |
[iOS] SceneDelegate에 대해서 (0) | 2023.04.04 |
[iOS] AppDelegate에 대해서 (0) | 2023.03.29 |