본문 바로가기
Ios/Swift UI

@State, @Binding, @ObservedObject

by 잘먹는 개발자 에단 2024. 7. 26.

Property Wrapper는 감싸고 있는 속성에 변화가 생기면 해당 값을 읽거나, 아님 읽고 어떤 동작을 하거나, 아니면 새로 쓸 수 있는 녀석들이다.

 

@State는 뷰 내부에서 상태를 관리하기 위해 사용된다.

- 상태가 변경되면 뷰가 다시 렌더링 된다.

- State 변수값이 변경되면 뷰를 다시 렌더링한다. 그렇기 때문에 항상 UI에서 보여주는 state는 최신 값이다.

- @State 속성으로 어떤 프로퍼티의 초기값을 지정했다면, 다른 값으로 재할당 할 수 없다. @Binding 변수를 통해서만 가능하다.

    ㄴ 개인적으로 이 말이 잘 이해가 안가서 하단에서 좀 자세히 다뤄볼게요.

    ㄴ 이건, 약간 설명이 애매모호해요.

    ㄴ 결론적으로 변경이 가능합니다. 근데, 다른 @State로 값을 재할당할 수 없다는 것 같습니다. 

 

- @State 프로퍼티는 뷰가 생성될 때 초기화되고, 이후에는 값만 변경할 수 있다. 

    ㄴ 중간에 State를 만들어낼 수 없다는 소리이다. 

import SwiftUI

struct ContentView: View {
    // @State를 사용하여 상태 변수를 선언합니다.
    @State private var counter = 0

    var body: some View {
        VStack {
            // 상태 변수를 사용하여 텍스트를 표시합니다.
            Text("Counter: \(counter)")
            
            // 버튼을 눌렀을 때 상태 변수를 변경합니다.
            Button(action: {
                counter += 1
            }) {
                Text("Increment")
            }
        }
    }
}

 

근데 위와 같은 설명이라면 다음과 같은 코드는 안될거에요.

import SwiftUI

struct ContentView: View {
    // @State를 사용하여 상태 변수를 선언합니다.
    @State private var counter = 0

    var body: some View {
        VStack {
            // 상태 변수를 사용하여 텍스트를 표시합니다.
            Text("Counter: \(counter)")
            
            HStack{
             	// 버튼을 눌렀을 때 상태 변수를 변경합니다.
            	Button(action: {
                	counter += 1
            	}) {
                	Text("Increment")
            	}
                
                Button(action :{
                	_counter = State(InitialValue : 10)
                    counter = _counter
                }){
                	Text("이건 안되죠")
                }
            }
            
        }
    }
}

 

값을 변경할 수는 있지만, 아예 State를 다른 State로 만들어서 바꾸려고 하는 건 안됩니다.

 

@Binding은 부모 뷰로부터 전달된 상태를 자식 뷰에서 읽고 쓸 수 있도록 한다. 

상태를 직접 소유하지 않지만, 부모 뷰와 상태를 공유한다.

import SwiftUI

struct ParentView: View {
    // 부모 뷰에서 상태 변수를 선언합니다.
    @State private var counter = 0

    var body: some View {
        VStack {
            Text("Counter: \(counter)")
            
            // 자식 뷰에 상태 변수를 바인딩으로 전달합니다.
            ChildView(counter: $counter)
        }
    }
}

struct ChildView: View {
    // @Binding을 사용하여 바인딩 변수를 선언합니다.
    @Binding var counter: Int

    var body: some View {
        Button(action: {
            counter += 1
        }) {
            Text("Increment from Child")
        }
    }
}

 

이건 쓸일이 많고, 리액트의 Props 과 같은 느낌이에요!

리액트도 렌더링의 조건이 몇가지 있잖아요!

1. 부모가 준 prop이 변경될 때

2. 상태가 변경될 때

3. 강제로 리렌더링 시킬때

4. 구독하고 있는 전역 상태가 변경되었을 때 

(이게 다 맞나?) 결국에 UI를 프레임워크들은 상태를 중요하게 볼 수 밖에 없는 것 같아요. 

 

@ObservedObject는 ObservableObject 프로토콜을 준수하는 객체를 관찰한다. 

- @Published로 선언된 객체의 속성이 변경되면 뷰가 다시 !렌더링!된다. 

- 주로 뷰 외부에서 생성된 데이터 모델을 뷰가 관찰하도록 할 때 사용된다. 

 

@Published : ObservableObject 프로토콜을 준수하는 클래스 내에서 사용할 수 있는 프로퍼티 래퍼이다. 

 

import SwiftUI
import Combine

// ObservableObject 프로토콜을 준수하는 클래스입니다.
class CounterModel: ObservableObject {
    // @Published를 사용하여 상태 변수를 선언합니다.
    @Published var counter = 0
}

struct ContentView: View {
    // @ObservedObject를 사용하여 관찰 객체를 선언합니다.
    @ObservedObject var counterModel = CounterModel()

    var body: some View {
        VStack {
            // 관찰 객체의 상태 변수를 사용하여 텍스트를 표시합니다.
            Text("Counter: \(counterModel.counter)")
            
            // 버튼을 눌렀을 때 관찰 객체의 상태 변수를 변경합니다.
            Button(action: {
                counterModel.counter += 1
            }) {
                Text("Increment")
            }
        }
    }
}

 

 

 

 

상태와 관련된 프로퍼티 래퍼를 몇가지 더 살펴보자!

 

@StateObject

- @State와 유사하지만 ObservableObject 프로토콜을 준수하는 객체를 관리한다.

- 주로 뷰의 생명주기동안 객체를 소유하고 관리하는데 사용되고

- @ObservedObject와 다르게, @StateObject는 뷰가 처음 생성될 때 한번만 초기화되며, 뷰가 다시 렌더링 될 때 초기화되지 않는다.

- 이것은 마치 리액트의 ref와 비슷하다.

import SwiftUI
import Combine

class UserData: ObservableObject {
    @Published var name: String = "John Doe"
}

struct ContentView: View {
    // @StateObject로 데이터 모델을 관리
    @StateObject private var userData = UserData()

    var body: some View {
        VStack {
            Text("User name: \(userData.name)")
            Button(action: {
                userData.name = "Jane Doe"
            }) {
                Text("Change Name")
            }
        }
    }
}

 

 

 

@EnvironmentObject

- 어플리케이션의 여러 뷰 계층 구조에서 공유할 수 있는 객체( ObservableObject 프로토콜을 준수하는 )를 전달하는데에 사용된다. 

- 주로 앱 전역 상태나 설정을 관리할 때 유용하다.

 

1. 먼저, ObservableObject 프로토콜을 준수하는 전역 객체를 정의한다.

import SwiftUI
import Combine

class Settings: ObservableObject {
    @Published var isDarkMode: Bool = false
}

 

다음으로, 이 객체를 환경에 추가하고 여러 뷰에서 사용할 수 있도록 한다.

import SwiftUI

struct ParentView: View {
    // @EnvironmentObject로 전역 객체를 선언
    @EnvironmentObject var settings: Settings

    var body: some View {
        VStack {
            Text("Dark Mode: \(settings.isDarkMode ? "On" : "Off")")
            ChildView()
        }
    }
}

struct ChildView: View {
    // @EnvironmentObject로 전역 객체를 선언
    @EnvironmentObject var settings: Settings

    var body: some View {
        Button(action: {
            settings.isDarkMode.toggle()
        }) {
            Text("Toggle Dark Mode")
        }
    }
}



// 여기는 앱의 루트뷰 파일이라고 하자. 

@main
struct MyApp: App {
    // 환경 객체를 앱의 루트 뷰에 추가
    //var settings = Settings()
    // 근데 이렇게 하기 보다는
    

    var body: some Scene {
        WindowGroup {
            ParentView()
                .environmentObject(Settings())
                // 그냥 여기서 바로 인스턴스 생성하면서 넘기자
        }
    }
}