티스토리 뷰
[ 인트로 ]
SwiftUI는 state-driven, data-driven 방식으로 state 가 변하면 view 를 다시 그립니다 (body를 호출하여)
하지만 body를 재호출한다고 모든 것을 다시 그리는 것이 아니라 변경이 필요한 부분만 다시 렌더링 합니다.
그렇다면.. re-rendering 또는 re-draw를 결정하는 SwiftUI의 diff 판단 로직은 어떻게 될까요?
(= body가 호출될 때 무엇을 다시 그리고, 무엇을 다시 안그릴 지 어떻게 결정할까요?)
WWDC21 - Demystify SwiftUI 를 보면 identity, life time, dependencies 이 세가지 요인이 메인요소인 것 같습니다.
저는 이 중에서 identity 위주로 저의 re-rendering 추측을 전개 (?) 해보려고 합니다.
이 글 의 실험이 너무 좋아서 저도 따라해보았어요! 🤍
랜덤 백그라운드를 줘서 다시 그려짐을 확인하는 방법 (gist) 인데, SwiftUI Instrument 사용하는 것보다 더 간단하고 명확한 것 같아요!
[ identity ]
SwiftUI의 모든 view는 identity 를 가집니다.
view가 처음 만들어질 때 SwiftUI는 identity 를 assign 합니다.
WWDC21 - Demystify SwiftUI 에서는
- explicit identity
- structual identity
를 나눠서 말하는데 저는 구분안하고 사용하겠습니다.
[ 추측 ]
1) SwiftUI는 extract Subview 여부 / dynamic property 의 종류 를 기준으로 삼아 뷰에 identity 를 부여한다.
- body 안의 서브뷰들에게 같은 identity 를 부여한다.
하지만 Extract Subview를 한 경우, 다른 identity를 부여한다. // Part 1 실험
- State의 경우, dependency 와 직접 관련있는 뷰에 별도의 identity 를 부여한다.
하지만 ObservableObject의 경우, 구체적인 dependency가 아니라 이 객체 자체와 관련있는 뷰라면 같은 identity를 부여한다 // Part 2 실험
2) state의 변화가 생기면 관련 identity를 갱신해서 뷰를 업데이트 한다.
Part 1. Dependency 가 하나일 때
[ 예제 1 ]
아래 예제의 경우, body 안에서 isOn을 사용하는데요,
body의 서브뷰들에 모두 같은 identity를 부여하는 것 같습니다.
그래서 state가 변하면 state와 상관없는 Text도 다시 그려집니다.
struct ContentView: View {
@State private var isOn = true
private var lightStack: some View {
VStack(spacing: 10) {
Text(isOn ? "ON" : "OFF")
.background(.random)
Button {
isOn.toggle()
} label: {
Text("🔦")
}.background(.random)
}
}
var body: some View {
lightStack
Spacer()
.frame(height: 50)
Text("NO MATTER!")
.background(.random)
}
}
[ 예제 2 ]
NoMatterText 를 extract subview 해보겠습니다.
이때 SwiftUI는 body안의 서브뷰들과 달리 NoMatterText에 다른 identity 를 부여합니다.
그래서 state가 변화해서 ContentView의 body를 재호출해도 NoMatterText는 다시 그리지 않습니다.
(명확한 실험을 위해 Spacer에도 컬러를 줘보겠습니다)
struct ContentView: View {
@State private var isOn = true
private var lightStack: some View {
VStack(spacing: 10) {
Text(isOn ? "ON" : "OFF")
.background(.random)
Button {
isOn.toggle()
} label: {
Text("🔦")
}.background(.random)
}
}
var body: some View {
lightStack
Spacer()
.frame(width: 100, height: 50)
.background(.random)
NoMatterText()
}
}
struct NoMatterText: View {
var body: some View {
Text("NO MATTER!")
.background(.random)
}
}
[ 예제 2.1 ]
반면 body가 재호출될 때마다 NoMatterText에 다른 id를 부여하면
텍스트도 같이 다시 그려지는 것을 확인할 수 있습니다.
저는 이것을 보고 identity가 갱신되면 뷰를 다시 그린다고 생각했어요
(id modifier 문서에 id 값을 주면 identity가 reset 된다고 나와있음)
struct ContentView: View {
@State private var isOn = true
private var lightStack: some View {
VStack(spacing: 10) {
Text(isOn ? "ON" : "OFF")
.background(.random)
Button {
isOn.toggle()
} label: {
Text("🔦")
}.background(.random)
}
}
var body: some View {
lightStack
Spacer()
.frame(height: 50)
✅ NoMatterText().id(UUID())
}
}
[ 예제 3 ]
반대로 lightStack 을 extract subview 해봅시다.
이때 버튼을 누르면, ContentView의 body는 재호출되지 않고 LightStack의 body만 재호출됩니다.
(State의 경우, dependency 와 직접 관련있는 뷰에만 별도의 identity 를 부여하기 때문)
그래서 당연히 state가 변해도 text 는 다시 그려지지 않습니다
struct ContentView: View {
@State private var isOn = true
var body: some View {
LightStack(isOn: $isOn)
Spacer()
.frame(height: 50)
Text("NO MATTER!")
.background(.random)
}
}
struct LightStack: View {
@Binding var isOn: Bool
var body: some View {
VStack(spacing: 10) {
Text(isOn ? "ON" : "OFF")
.background(.random)
Button {
isOn.toggle()
} label: {
Text("🔦")
}.background(.random)
}
}
}
[ 예제 3.1 ]
그럼 더 나아가..
LightStack의 Button Label도 extract subview 해보겠습니다.
이 경우도 ButtonText에는 다른 identity를 부여합니다.
그래서 Button과 달리 ButtonText는 다시 그려지지 않습니다.
struct LightStack: View {
@Binding var isOn: Bool
struct ButtonText: View {
var body: some View {
Text("🔦").background(.random)
}
}
var body: some View {
VStack(spacing: 10) {
Text(isOn ? "ON" : "OFF")
.background(.random)
Button {
isOn.toggle()
} label: {
ButtonText()
}.background(.random)
}
}
}
[ 예제 4 ]
state 의 경우, 전등 버튼을 누르면 ContentView의 body는 안불리고
LightStack의 body만 불렸습니다. (예제 3)
반면 StateObject, ObservedObject 의 경우, 버튼을 누르면 ContentView의 body가 불린 후, LightStack의 body가 불립니다.
(ObservableObject의 경우, 구체적인 dependency가 아니라 이 객체 자체와 관련있는 뷰라면 같은 identity를 부여하기 때문.)
하지만 위에서 계속 봤듯이 ContentView의 body가 다시 불려도
extract subview를 한 NoMatterText의 identity는 갱신되지 않기 때문에 텍스트는 다시 그려지지 않습니다.
(반면 extract subview를 안한 경우 텍스트를 다시 그림)
class LightState: ObservableObject {
@Published var isOn = true
}
struct ContentView: View {
@StateObject private var state = LightState()
var body: some View {
LightStack(isOn: $state.isOn)
Spacer()
.frame(height: 50)
NoMatterText()
}
}
Part 2. dependency가 여러 개인 경우
[ 예제 5 ]
State의 경우, 구체적인 dependency 를 보고 identity를 부여합니다.
그래서 dependency와 직접 관련이 있는 뷰들만 다시 그려집니다.
손전등을 토글하면 LightStack의 body만 재호출.
텍스트를 클릭하면 NoMatterText 의 body만 재호출.
struct ContentView: View {
@State var isOn = true
@State var noMatterText = "NO MATTER!"
var body: some View {
LightStack(isOn: $isOn)
Spacer()
.frame(height: 50)
NoMatterText(text: $noMatterText)
}
}
struct LightStack: View {
@Binding var isOn: Bool
var body: some View {
VStack(spacing: 10) {
Text(isOn ? "ON" : "OFF")
.background(.random)
Button {
isOn.toggle()
} label: {
Text("🔦")
}.background(.random)
}
}
}
struct NoMatterText: View {
@Binding var text: String
var body: some View {
Text(text)
.background(.random)
.onTapGesture {
text += "!"
}
}
}
[ 예제 6 ]
ObservableObject 의 경우,
구체적인 dependency가 아니라 이 객체 자체와 관련있는 뷰 에 동일한 identity 를 할당하고 갱신하는 것 같습니다.
ContentView의 body가 재호출되고
참조하고 있는 @Published 는 다르지만 LightStack, NoMatterView가 같이 다시 그려지고 있기 때문입니다.
class SomeState: ObservableObject {
@Published var isOn = true
@Published var noMatterText = "NO MATTER!"
}
struct ContentView: View {
@StateObject var state = SomeState()
var body: some View {
LightStack(isOn: $state.isOn)
Spacer()
.frame(height: 50)
NoMatterText(text: $state.noMatterText)
}
}
[ 예제 7 ]
그럼 NoMatterText에 dependency를 제거하면 어떻게 될까요?
NoMatterText는 ObservableObject 와 관련이 없는 서브뷰이기 때문에 다른 identity를 부여받고
state 변화에도 다시 그려지지 않습니다.
class SomeState: ObservableObject {
@Published var isOn = true
@Published var noMatterText = "NO MATTER!"
}
struct ContentView: View {
@StateObject var state = SomeState()
var body: some View {
LightStack(isOn: $state.isOn)
Spacer()
.frame(height: 50)
NoMatterText()
}
}
[ 예제 8 ]
하지만 Part 1의 교훈을 잊으면 안됩니다.
extract subview를 안한 경우, 별도의 identity를 부여받지 않으므로 text는 다시 그려집니다.
class SomeState: ObservableObject {
@Published var isOn = true
@Published var noMatterText = "NO MATTER!"
}
struct ContentView: View {
@StateObject var state = SomeState()
var body: some View {
LightStack(isOn: $state.isOn)
Spacer()
.frame(height: 50)
Text("NO MATTER!")
.background(.random)
}
}
[ 참고 ]
다시 그려지는 방식을 내가 원하는대로 바꿀 수도 있습니다.
[SwiftUI] re-rendering, re-draw 커스터마이징 (?) 을 참고해주세요!
'🍏 > SwiftUI + Combine' 카테고리의 다른 글
[SwiftUI] rotationEffect, scaleEffect (0) | 2023.01.06 |
---|---|
[SwiftUI] re-rendering, re-draw 커스터마이징 (?) (1) | 2022.11.16 |
[SwiftUI] accessibilityRepresentation 과 accessibilityChildren (0) | 2021.10.27 |
[SwiftUI] iOS13에서 onChange(of:perform:) 을 사용하고 싶을 때 (2) | 2021.09.03 |
[SwiftUI] @StateObject (0) | 2021.08.31 |
- Total
- Today
- Yesterday
- Flutter getter setter
- Flutter 로딩
- 장고 Custom Management Command
- Python Type Hint
- Django Firebase Cloud Messaging
- 구글 Geocoding API
- flutter 앱 출시
- Sketch 누끼
- drf custom error
- Django FCM
- 플러터 얼럿
- DRF APIException
- ipad multitasking
- 플러터 싱글톤
- flutter dynamic link
- ribs
- 장고 URL querystring
- Flutter Text Gradient
- flutter build mode
- METAL
- Flutter Clipboard
- github actions
- Django Heroku Scheduler
- flutter deep link
- Flutter Spacer
- Watch App for iOS App vs Watch App
- Dart Factory
- PencilKit
- cocoapod
- SerializerMethodField
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |