SwiftUI를 사용하다 보면 @State
, @Binding
, @ObservedObject
같은 Property Wrapper를 자주 사용합니다. 하지만 커스텀 이니셜라이저를 만들 때 "Cannot assign to property: 'self' is immutable" 같은 에러를 만나본 적이 있으신가요? 이때 필요한 것이 바로 Underlying Value입니다.
SwiftUI의 Property Wrapper는 실제로 3가지 다른 값을 제공합니다:
@State private var count: Int = 0
// count로 접근 - Int 타입
Text("\(count)") // 0
count += 1 // 직접 값 수정
@State private var count: Int = 0
// _count로 접근 - State<Int> 타입
let stateItself = _count // State 구조체 자체
@State private var count: Int = 0
// $count로 접근 - Binding<Int> 타입
TextField("Number", text: $count) // 양방향 바인딩
가장 흔한 사용 사례입니다. 외부에서 받은 값으로 @State
를 초기화해야 할 때:
struct CounterView: View {
@State private var count: Int
let minValue: Int
init(startValue: Int, minValue: Int) {
self.minValue = minValue
// ❌ 컴파일 에러!
// self.count = startValue
// ✅ 올바른 방법
self._count = State(initialValue: startValue)
}
var body: some View {
Text("Count: \(count)")
}
}
저장된 값이 있으면 사용하고, 없으면 기본값을 사용하는 패턴:
struct PersistentCounter: View {
@State private var count: Int
let key: String
init(key: String, defaultValue: Int = 0) {
self.key = key
// UserDefaults에서 값 확인
if let savedValue = UserDefaults.standard.object(forKey: key) as? Int {
self._count = State(initialValue: savedValue)
} else {
self._count = State(initialValue: defaultValue)
}
}
var body: some View {
// ...
}
}
Core Data 엔티티의 값으로 State를 초기화:
struct TaskView: View {
@State private var isCompleted: Bool
@ObservedObject var task: Task // Core Data Entity
init(task: Task) {
self.task = task
// Core Data의 현재 값으로 State 초기화
self._isCompleted = State(initialValue: task.isCompleted)
}
var body: some View {
Toggle("Completed", isOn: $isCompleted)
.onChange(of: isCompleted) { newValue in
task.isCompleted = newValue
// Core Data 저장
}
}
}
복잡한 로직으로 초기값을 결정해야 할 때:
struct SmartCounter: View {
@State private var count: Int
@State private var step: Int
init(range: ClosedRange<Int>) {
let midPoint = (range.lowerBound + range.upperBound) / 2
let calculatedStep = max(1, (range.upperBound - range.lowerBound) / 10)
self._count = State(initialValue: midPoint)
self._step = State(initialValue: calculatedStep)
}
var body: some View {
Stepper("Value: \(count)", value: $count, step: step)
}
}
struct MyView: View {
@State private var text: String
init(initialText: String) {
_text = State(initialValue: initialText)
}
}
class ViewModel: ObservableObject {
@Published var data: String
init(data: String) {
self.data = data
}
}
struct MyView: View {
@StateObject private var viewModel: ViewModel
init(initialData: String) {
_viewModel = StateObject(wrappedValue: ViewModel(data: initialData))
}
}
struct ChildView: View {
@Binding var value: Int
init(value: Binding<Int>) {
self._value = value
}
// 또는 더 간단하게
init(value: Binding<Int>) {
self._value = value
}
}
struct SettingsView: View {
@AppStorage private var username: String
init(userKey: String, defaultName: String = "Guest") {
_username = AppStorage(wrappedValue: defaultName, userKey)
}
}
Underlying value는 주로 이니셜라이저에서만 사용합니다. View의 body나 다른 메서드에서는 일반적으로 wrapped value를 사용하세요.
count
: Int
타입_count
: State<Int>
타입$count
: Binding<Int>
타입각각 다른 타입이므로 용도에 맞게 사용해야 합니다.
@StateObject
는 wrappedValue
파라미터를 사용합니다:
// @State와 다른 형식
_viewModel = StateObject(wrappedValue: ViewModel())
Underlying Value는 SwiftUI에서 Property Wrapper의 초기값을 동적으로 설정할 때 필수적인 개념입니다. 특히 다음과 같은 경우에 유용합니다:
_variableName
형식으로 접근하며, 주로 이니셜라이저에서 State(initialValue:)
와 함께 사용됩니다.