일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- WWDC
- 최적화
- Animation
- ios
- stateobject
- GCD
- dataflow
- Network
- 달력
- combine
- SwiftUI
- arkit
- swift
- avsession
- firebase
- UIKit
- withAnimation
- Performance
- 접근성제어
- toolbarvisibility
- iphone
- gesture
- CS
- Concurrency
- authentication
- state
- 네트워크
- view
- RxSwift
- auth
- Today
- Total
XLOG
[최적화] View 의 Update 를 최소화 하는 방법 본문
1. SwiftUI 는 언제 View를 Update 하는가?

우리가 SwiftUI 를 사용하며 View 를 정의할 때 @State, @StateObject 와 같은 변수를 정의하게 되면 View 는 Source of truth 를 만들어 관리를 한다. 이때 UI 를 통해 Action 을 발생시켜 Source of truth 의 값의 변화를 준다면 그 값의 변화를 View 에 알리고 View에서는 body 를 다시 compute 를 한다. body를 다시 compute 한다는 것은 body 내부에 코드를 한줄한줄 실행을 해준다는 의미이다.
2. 그렇다면 어떻게 Update 를 최소화 하는가?
2-1. SubView를 사용하여 View가 의존하는 변수를 최적화한다.
위에서 설명을 했지만, View 가 추적하고 있는 변수의 변화가 발생하게 되면 body 를 재실행하게 된다. 즉 모든 View Component 를 하나의 View 에 모두 정의하게 되면 모든 View의 코드들을 실행하게 된다. 하지만 Subview 를 정의하여 적용하게 되면 부모View 의 body 가 재실행되면서 Subview 가 init 이 되고 내가 정의한 View 는 기존 View 와의 변화를 확인하여 변화가 없으면 subview 의 body 를 재실행하지 않는다. 즉 UI 를 담당하는 body 를 update 하는 것은 본인 자신에게 있다고 할 수 있다. 코드로 비교를 해보자.
import Foundation
struct TestModel: Identifiable, Hashable {
let id: String
var name: String
}
class PracticeViewModel: ObservableObject {
@Published var count: Int
@Published var items: [TestModel]
init() {
self.count = 0
self.items = [
.init(id: UUID().uuidString, name: "number 1"),
.init(id: UUID().uuidString, name: "number 2"),
.init(id: UUID().uuidString, name: "number 3"),
.init(id: UUID().uuidString, name: "number 4"),
.init(id: UUID().uuidString, name: "number 5"),
]
}
func update(_ item: TestModel) {
if let index = items.firstIndex(of: item) {
var temp = self.items
temp[index].name = "Change"
self.items = temp
}
}
}
struct DataFlowOptimizeView: View {
@StateObject private var vm: PracticeViewModel = .init()
var body: some View {
VStack {
let _ = Self._printChanges()
ForEach(vm.items) { item in
HStack {
let _ = Self._printChanges()
Image(systemName: "person")
Text(item.name)
.padding()
Spacer()
}
.background( Color.randomColor )
.padding()
}
Spacer()
Button(action: {
let item = TestModel(id: UUID().uuidString, name: "name \(vm.items.count)")
vm.items.append(item)
}) {
Text("Add Item")
.padding()
.background( Color.randomColor )
}
}
.padding()
}
}
ViewModel.items 에 아이템을 추가하게 되면 StateObject 의 변화는 body를 재실행하게 되고 그로 인하여 For문에 모든 코드를 다시 실행하게 된다. 하지만 For 구문 내부를 Subview 로 적용해주게 된다면 단지 Subview 초기화만 진행을 하고 그 초기화로 인하여 변화된 부분을 확인하여 View update 를 진행하지 않게 된다.
struct DataFlowOptimizeView: View {
@StateObject private var vm: PracticeViewModel = .init()
var body: some View {
VStack {
let _ = Self._printChanges()
ForEach(vm.items) { item in
DataFlowOptimizeRowView(item: value)
}
Spacer()
Button(action: {
let item = TestModel(id: UUID().uuidString, name: "name \(vm.items.count)")
vm.items.append(item)
}) {
Text("Add Item")
.padding()
.background( Color.randomColor )
}
}
.padding()
}
}
struct DataFlowOptimizeRowView: View {
let item: TestModel
init(item: TestModel) {
self.item = item
print("init ChildView")
}
var body: some View {
HStack {
let _ = Self._printChanges()
Image(systemName: "person")
Text(item.name)
.padding()
Spacer()
}
.background( Color.randomColor )
.padding()
}
}
기존 코드에서 ForEach 내부의 UI 를 DataFlowOptimizeRowView 로 따로 정의하여 사용을 해보자.
결과를 확인해보면 상위View 에 body가 재실행되면서 아이템의 갯수만큼 DataFlowOptimizeRowView 의 init 이 호출이 되지만 body 부분은 딱 한번 호출이 되는 것을 볼 수 있다. 이는 새로운 아이템이 추가가 되어 그 아이템에 대한 UI 를 그릴때 발생하는 것 이다.
여기서 약간의 트릭으로 ViewModifier 와 EnvironmentObject 를 활용한다면 View를 Subview로 바꾸어 사용할 수 있다. 이론상 StateObject 의 변화를 주게 되면 해당 ViewModel 을 EnvironmentObject 로 가지고 있는 모든 View는 body가 재실행된다. 이는 엄청난 낭비가 될 수 있다. 하지만 ViewModifer 를 이용하여 view에 content에 해당하는 body 의 실행을 최소화 할 수 있다.
struct DataFlowOptimizeRowView: View {
let item: TestModel
init(item: TestModel) {
self.item = item
print("init ChildView")
}
var body: some View {
HStack {
let _ = Self._printChanges()
Image(systemName: "person")
Text(item.name)
.padding()
Spacer()
}
.background( Color.randomColor )
.padding()
.modifier(MyModifier(item: item))
}
}
struct MyModifier: ViewModifier {
let item: TestModel
@EnvironmentObject var vm: PracticeViewModel
func body(content: Content) -> some View {
Button(action: {
// vm 의 기능 동작 정의
vm.update(item)
}) {
content
}
}
}
기존의 Subview 를 MyModifier 로 감싸줌으로 인해서 subview body 에 해당하는 것은 environmentObject의 직접적으로 받지 않게 된다.
2-2. Combine 활용
이전 StateObject 에 대한 포스팅을 작성했을 때도 얘기를 했지만, StateObject Published 프로퍼티의 변화가 있으면 body 를 재실행하게 된다. 그렇다면 Combine을 어떻게 활용을 하라는 것인가?
class PracticeViewModel: ObservableObject {
@Published var publicshedOffset: CGFloat = .zero
var combineOffset: CurrentValueSubject<CGFloat, Never> = .init(.zero)
}
struct DataFlowOptimizeView: View {
@StateObject private var vm: PracticeViewModel = .init()
var body: some View {
let _ = Self._printChanges()
ZStack {
ScrollView {
GeometryReader { reader in
Color.clear.preference(key: ViewOffsetKey.self, value: reader.frame(in: .named("scroll")).origin.y)
}
ForEach(0..<100, id: \.self) { index in
Text("index \(index)")
.frame(maxWidth: .infinity)
.padding()
.background(Color.randomColor)
}
}
.padding()
OffsetView(offset: vm.publicshedOffset)
}
.onPreferenceChange(ViewOffsetKey.self, perform: {
vm.publicshedOffset = $0
})
.onReceive(vm.combineOffset, perform: {
print($0)
})
.environmentObject(vm)
}
}
struct OffsetView: View {
let offset: CGFloat
var body: some View {
Text("\(offset)")
.frame(maxWidth: .infinity)
.padding()
.background(Color.randomColor)
}
}
극단적인 예로 Scroll 값에 따라 Published 의 변화를 준다면 해당 View에서는 StateObject 변화에 따라 body 내부를 모두 재실행하게 된다. 하지만 Combine 을 사용하게 되면 원하는 View에서만 onReceive를 해서 이와 같은 비효율을 막을 수 있다.
import SwiftUI
struct DataFlowOptimizeView: View {
@StateObject private var vm: PracticeViewModel = .init()
var body: some View {
let _ = Self._printChanges()
ZStack {
ScrollView {
GeometryReader { reader in
Color.clear.preference(key: ViewOffsetKey.self, value: reader.frame(in: .named("scroll")).origin.y)
}
ForEach(0..<100, id: \.self) { index in
Text("index \(index)")
.frame(maxWidth: .infinity)
.padding()
.background(Color.randomColor)
}
}
.padding()
OffsetView()
}
.onPreferenceChange(ViewOffsetKey.self, perform: {
vm.combineOffset.send($0)
})
.environmentObject(vm)
}
}
struct OffsetView: View {
@State private var offset: CGFloat = .zero
@EnvironmentObject var vm: PracticeViewModel
var body: some View {
Text("\(offset)")
.frame(maxWidth: .infinity)
.padding()
.background(Color.randomColor)
.onReceive(vm.combineOffset, perform: {
self.offset = $0
})
}
}
3. 느낀점
회사 프로젝트를 진행하면서 subview 는 많아지고, 어느 순간 각자 작업한 내용에서 View가 다시 렌더링이 되는데 왜 되는지, 정확히 모르고 있다는 생각을 해서 시작한 공부였다. Published 가 있고 Concurrency 가 있는데 SwiftUI 에서 Combine을 사용할 필요가 있을까에 대한 고민을 한 적도 있다. 하지만 이렇게 DataFlow 를 어떻게 설계하는냐에 따라 View의 Update 횟수를 엄청나게 줄일 수 있다는 사실을 알 수 있었다.
참고
https://developer.apple.com/wwdc20/10040
https://developer.apple.com/wwdc19/226
https://youtu.be/yvfv6N60-vY?si=ZJ7fIb7nL7qY4LpG
https://youtu.be/TOmxDvCz7e4?si=ZaORk7zZkbYDNsn4
https://youtu.be/GlLyPYWgHEo?si=hbzVWdKNlKlNgpRq
'Swift > SwiftUI' 카테고리의 다른 글
[SwiftUI] VoiceOver 사용자를 위한 Accessibility 요소 적용기 (0) | 2024.12.22 |
---|---|
[SwiftUI] Observation (feat iOS 17, 써야하는 이유) (2) | 2024.08.17 |
[DataFlow] State, Binding, StateObject, ObservedObject, EnvironmentObject 는 무엇이고 차이는? (0) | 2024.07.01 |
[SwiftUI] Environment 활용하기 (0) | 2024.06.25 |
[SwiftUI] 밀리의 서재 책정보 Sheet 애니메이션 아이디어 및 구현 (0) | 2024.06.24 |