SwiftUI

[SwiftUI] SwiftUI의 View Rendering

yujaehui 2024. 5. 8. 12:42

@State, View Rendering

firstNumber 변경 - 별도의 구조체 (let)

struct ContentView: View {
    @State private var firstNumber = 0
    @State private var secondNumber = 0
    @State private var thirdNumber = 0
    @State private var forthNumber = 0

    var body: some View {
        VStack {
            Text("First: \\(firstNumber)").background(Color.random())
            second
            third()
            FourthView(fourthNumber: forthNumber)
            
            Button("Number Change Button") {
                firstNumber = Int.random(in: 1...100) //firstNumber만 변경
            }
            .buttonStyle(.borderedProminent)
            .padding()
        }
        .font(.title)
    }
    
    // 프로퍼티로
    var second: some View {
        Text("Second: \\(secondNumber)").background(Color.random())
    }
    
    // 메서드로
    func third() -> some View {
        Text("Third \\(thirdNumber)").background(Color.random())
    }
}
// 별도의 구조체로 (let)
struct FourthView: View {
    let fourthNumber: Int
    
    var body: some View {
        Text("Fourth: \\(fourthNumber)").background(Color.random())
    }
}
  • firstNumber만 변경했음에도 secondNumber(프로퍼티)와 thirdNumber(메서드)도 렌더링되면서 백그라운드의 컬러가 변경
  • 별도의 구조체로 설정한 fourthNumber는 렌더링되지 않기에 백그라운드의 컬러가 유지

firstNumber 변경 - 별도의 구조체 (@Binding var)

// 별도의 구조체로 (@Binding var)
struct FourthView: View {
    @Binding var fourthNumber: Int
    
    var body: some View {
        Text("Fourth: \\(fourthNumber)").background(Color.random())
    }
}
  • let으로 설정했을 때와 동일한 결과
  • firstNumber만 변경했음에도 secondNumber(프로퍼티)와 thirdNumber(메서드)도 렌더링되면서 백그라운드의 컬러가 변경
  • 별도의 구조체로 설정한 fourthNumber는 렌더링되지 않기에 백그라운드의 컬러가 유지

fourthNumber 변경 - 별도의 구조체 (let)

struct ContentView: View {
    @State private var firstNumber = 0
    @State private var secondNumber = 0
    @State private var thirdNumber = 0
    @State private var forthNumber = 0

    var body: some View {
        VStack {
            Text("First: \\(firstNumber)").background(Color.random())
            second
            third()
            FourthView(fourthNumber: forthNumber)
            
            Button("Number Change Button") {
                forthNumber = Int.random(in: 1...100) //forthNumber만 변경
            }
            .buttonStyle(.borderedProminent)
            .padding()
        }
        .font(.title)
    }
    
    // 프로퍼티로
    var second: some View {
        Text("Second: \\(secondNumber)").background(Color.random())
    }
    
    // 메서드로
    func third() -> some View {
        Text("Third \\(thirdNumber)").background(Color.random())
    }
}
// 별도의 구조체로 (let)
struct FourthView: View {
    let fourthNumber: Int
    
    var body: some View {
        Text("Fourth: \\(fourthNumber)").background(Color.random())
    }
}
  • fourthNumber만 변경되었음에도 firstNumber, secondNumber, thridNumber가 전부 렌더링되면서 백그라운드의 컬러가 변경

fourthNumber 변경 - 별도의 구조체 (@Binding var)

// 별도의 구조체로 (@Binding var)
struct FourthView: View {
    @Binding var fourthNumber: Int
    
    var body: some View {
        Text("Fourth: \\(fourthNumber)").background(Color.random())
    }
}
  • 별도의 구조체만 렌더링 되고 다른 요소들은 렌더링되지 않아 백그라운드의 컬러가 변경되지 않는 것을 알 수 있음

@Published, View Rendering …with ViewModel

firstNumber 변경 - 별도의 구조체 (let)

class ContentViewModel: ObservableObject {
    @Published var firstNumber: Int = 0
    @Published var secondNumber: Int = 0
    @Published var thirdNumber: Int = 0
    @Published var fourthNumber: Int = 0
}
struct ContentView: View {
    @StateObject var viewModel: ContentViewModel = ContentViewModel()

    var body: some View {
        VStack {
            Text("First: \\(viewModel.firstNumber)")
                .background(Color.random())
            second
            third()
            FourthView(fourthNumber: viewModel.fourthNumber)
            
            Button("Number Change Button") {
                viewModel.firstNumber = Int.random(in: 1...100) //firstNumber만 변경
            }
            .buttonStyle(.borderedProminent)
            .padding()
        }
        .font(.title)
    }
    
    var second: some View {
        Text("Second: \\(viewModel.secondNumber)")
            .background(Color.random())
    }
    
    func third() -> some View {
        Text("Third \\(viewModel.thirdNumber)")
            .background(Color.random())
    }
}
// 별도의 구조체로 (let)
struct FourthView: View {
    let fourthNumber: Int
    
    var body: some View {
        Text("Fourth: \\(fourthNumber)")
            .background(Color.random())
    }
}
  • 뷰모델이 존재하지 않았을 때와 동일
  • firstNumber만 변경했음에도 secondNumber(프로퍼티)와 thirdNumber(메서드)도 렌더링되면서 백그라운드의 컬러가 변경
  • 별도의 구조체로 설정한 fourthNumber는 렌더링되지 않기에 백그라운드의 컬러가 유지

firstNumber 변경 - 별도의 구조체 (@Binding var)

// 별도의 구조체로 (@Binding var)
struct FourthView: View {
    @Binding var fourthNumber: Int
    
    var body: some View {
        Text("Fourth: \\(fourthNumber)").background(Color.random())
    }
}
  • 뷰모델이 존재하지 않았을 때와 동일
  • let으로 설정했을 때와 동일한 결과
  • firstNumber만 변경했음에도 secondNumber(프로퍼티)와 thirdNumber(메서드)도 렌더링되면서 백그라운드의 컬러가 변경
  • 별도의 구조체로 설정한 fourthNumber는 렌더링되지 않기에 백그라운드의 컬러가 유지

fourthNumber 변경 - 별도의 구조체 (let)

struct ContentView: View {
    @StateObject var viewModel: ContentViewModel = ContentViewModel()

    var body: some View {
        VStack {
            Text("First: \\(viewModel.firstNumber)")
                .background(Color.random())
            second
            third()
            FourthView(fourthNumber: viewModel.fourthNumber)
            
            Button("Number Change Button") {
                viewModel.fourthNumber = Int.random(in: 1...100) //forthNumber만 변경
            }
            .buttonStyle(.borderedProminent)
            .padding()
        }
        .font(.title)
    }
    
    var second: some View {
        Text("Second: \\(viewModel.secondNumber)")
            .background(Color.random())
    }
    
    func third() -> some View {
        Text("Third \\(viewModel.thirdNumber)")
            .background(Color.random())
    }
}
// 별도의 구조체로 (let)
struct FourthView: View {
    let fourthNumber: Int
    
    var body: some View {
        Text("Fourth: \\(fourthNumber)")
            .background(Color.random())
    }
}
  • 뷰모델이 존재하지 않았을 때와 동일
  • fourthNumber만 변경되었음에도 firstNumber, secondNumber, thridNumber가 전부 렌더링되면서 백그라운드의 컬러가 변경

fourthNumber 변경 - 별도의 구조체 (@Binding var) ‼️

// 별도의 구조체로 (@Binding var)
struct FourthView: View {
    @Binding var fourthNumber: Int
    
    var body: some View {
        Text("Fourth: \\(fourthNumber)").background(Color.random())
    }
}
  • 뷰모델로 관리할 경우 별도의 구조체의 바인딩을 해도 다른 요소들이 함께 렌더링되어 모든 요소들의 백그라운드 컬러가 변경되게 됨
  • 별도의 구조체로 관리하고 @Binding으로 값을 전달받았음에도 왜 독립적으로 렌더링되지 않는지 (아래 설명)

@State와 @Published의 Binding 동작 차이

@State와 @Published는 SwiftUI에서 상태 관리를 위해 사용되지만, 렌더링 범위와 방식에 차이가 존재.

@State는 뷰 내부의 로컬 상태 관리에 최적화되어 있고, @Published는 뷰 모델과 같은 외부 객체에서 상태를 관찰할 때 사용.


1. @State로 관리할 때 바인딩된 하위 뷰만 렌더링

@State는 SwiftUI 내부에서 뷰의 로컬 상태로 관리되며, SwiftUI가 이 상태 변경을 아주 정교하게 추적.

  • @State는 뷰 계층 구조와 직접 연결.
  • 부모 뷰에서 @State로 관리하는 값이 변경되더라도, 바인딩된 하위 뷰만 해당 상태를 관찰하고 있으므로, 그 하위 뷰만 다시 렌더링.
  • SwiftUI는 상태 변경이 정확히 어느 뷰와 연결되어 있는지를 알고 최적화.

2. @Published로 관리할 때 상위 뷰 전체가 렌더링

@Published는 옵저버블 상태 관리 프로퍼티로, SwiftUI와 결합하여 @StateObject나 @ObservedObject로 사용.

  • @Published는 뷰 모델에서 상태가 변경될 때, SwiftUI에 전체적으로 상태가 변경되었다는 신호를 보냄.
  • SwiftUI는 뷰 모델(@StateObject)과 연결된 최상위 뷰 전체의 body를 다시 계산.
  • SwiftUI는 @Published의 변경 범위를 정확히 추적하지 못하기 때문에, 관련된 모든 뷰를 다시 렌더링.

3. 왜 이런 차이가 있을까?

SwiftUI의 상태 추적 방식

  • @State는 SwiftUI가 직접 관리하기 때문에, 특정 상태와 뷰의 연결을 정밀하게 추적.
    • 변경 범위를 최소화하여, 필요한 뷰만 다시 렌더링.
  • @Published는 Combine 프레임워크의 옵저버블 객체로 동작하며, SwiftUI는 이 객체의 상태 변경 범위를 세밀하게 추적할 수 없음.
    • 대신, 뷰 모델의 objectWillChange 신호를 통해 전체 뷰를 다시 계산.

Binding의 동작 차이

  • @Binding은 부모의 특정 @State를 참조하므로, SwiftUI가 정확히 어느 뷰와 연결되었는지를 알고, 변경된 하위 뷰만 다시 렌더링.
  • 반면, @Binding이 @Published 상태를 참조하는 경우, SwiftUI는 @StateObject와 연결된 부모 뷰 전체를 다시 렌더링.

렌더링과 init의 차이

  • SwiftUI는 상태 변경 시마다 뷰를 다시 렌더링하지만, 별도의 구조체의 init은 항상 호출.
  • 즉, 렌더링이 발생하지 않아도 뷰 구조체의 초기화는 수행.
  • init에서 네트워크 통신이나 복잡한 작업이 있다면 성능 문제로 이어질 수 있음.

'SwiftUI' 카테고리의 다른 글

[SwiftUI] ForEach in SwiftUI  (0) 2024.05.13
[SwiftUI] NavigationView vs NavigationStack  (0) 2024.05.09
[SwiftUI] ViewModifier, modifier(_:)  (0) 2024.05.03
[SwiftUI] Source of Truth  (0) 2024.04.29
[SwiftUI] @State와 @Binding의 projectedValue ($)  (0) 2024.04.29