# @State

모던 웹 프론트엔드 앱은 상태값 처리가 개발자에게 필요한 기본 소양이다. 선언형 UI 기반으로 동작하는 스위프트 UI의 경우에도 속성값에 대한 바인딩 로직을 직접 구현하기 보다 상태값에 대한 키워드 선언을 통해 속성과 UI 바인딩의 책임을 내부에 적절히 넘기게 된다.

struct ContentView: View {
    @State private var count = 0

    var body: some View {
        Button("\(count)") {
            count += 1
        }
    }
}

스위프트 UI에서의 뷰는 구조체 기반으로 동작한다. 구조체 인스턴스는 스택 프레임 내에서 관리된다.

구조체 인스턴스에 대해 메서드를 통한 속성값 변경은 원칙적으로 불가능하다. mutating 키워드를 통해 수정은 가능하지만 값 타입에 대한 일시적 복사일 뿐 함수 호출의 종료와 함께 스택프레임에 담긴 데이터는 모두 소멸된다.

@State 키워드는 뷰가 소멸되는 시점까지 상태값을 메모리에서 관리하도록 해준다. 상태값 변화에 따라 스위프트 UI가 이를 인지하여 뷰를 리로드해준다.

@State키워드 기반의 속성은 현재 뷰 안에 제한해두어 사용하는 것이 중요하다. 다른 뷰에서 해당 속성값을 참조하지 못하도록 private으로 선언하는 것이 중요하다.

@State 키워드 속성은 값 타입만 선언하자

Use state as the single source of truth for a given value type that you store in a view hierarchy. (Apple Document)

A State instance isn’t the value itself; it’s a means of reading and mutating the value. To access a state’s underlying value, use its value property.

@State 인스턴스는 값의 변화를 감지해야 하는데 참조 타입으로 선언된 속성은 상태값으로 관리하게 되면 의미가 없을 것이, 주소값 자체의 변화가 이루어지지 않는다는 것이다.

예컨대 참조 타입 변수를 상태값으로 관리하면 16진수 주소 0x.... 자체를 상태값으로 관리할텐데 참조하는 메모리 주소 객체의 값을 구조체에서 알 수 없다는 것이다.

따라서 @State 키워드를 사용할 때에는 밸류 타입의 속성을 사용하는 것이 중요하다.

# @ObservedObject

@State가 하나의 뷰에 제한되어 관리되는 상태값이었다면 @ObservedObject는 여러 뷰에 걸쳐 사용되는 상태값이다. @State 키워드로 선언된 속성 사이에 나타나는 주요한 차이점은 바로 메모리 관리의 주체가 개발자에게 있느냐 SwiftUI에게 넘기느냐이다. @State는 상태값에 대한 메모리 관리를 SwiftUI에게 넘겨준다.

스택프레임에서 소멸되지 않고 메모리 내에 영속적 관리를 위한 데이터는 클래스로 정의한다. 클래스는 반드시 ObservableObject 프로토콜을 채택해야 하며 @ObservedObject 키워드로 속성에 추가된다.

@ObservedObject도 동작 자체는 @State와 동일하다. 뷰를 리로드시킬 트리거 조건될 속성을 반드시 결정해줘야 하는데, 이때 사용하는 키워드가 바로 @Published이다.

class MemoStore: ObservableObject {
    @Published var memoList: [Memo]

    // ...
}

위와 같이 메모 리스트를 코어데이터에서 관리한다고 가정하고 정의한 DAO 클래스이다. 메모 객체에 대해 코어데이터로의 CRUD를 처리해준다.

이때 메모 데이터의 업데이트를 기준으로 UI를 업데이트해줘야 하므로 memoList라는 속성에 @Published 키워드를 붙여주어 데이터 업데이트가 자연스럽게 UI에 대한 업데이트로 이어진다.

@ObservedObject@Published 속성은 값 변화가 감지될 경우 그 즉시 이를 참조하는 모든 뷰에 알람이 전송된다.

# @StateObject

@StateObject@ObservedObject와 거의 유사하다. 클래스에 대해 ObservableObject 프로토콜을 채택하여 구현하고, 뷰 리로드를 트리거할 속성에 @Published 프로퍼티 래퍼를 등록하는 것까지 동일하다.

속성값 변경이 감지되면 body를 리프레시하는 것 역시 동일하다.

두 프로퍼티 래퍼의 주요한 차이점은 객체에 대한 소유권이다. @StateObject는 객체의 생성이 현재 뷰에서 이루어졌음을 나타내며 이에 따라 뷰에서 직접 뷰를 소유한다. 반면 @ObservedObject는 참조하는 객체가 다른 뷰에서 생성되었으므로 속성에 대한 관찰만 이루어지며 객체에 대한 직접적 소유는 하지 않는다.

# @EnvironmentObject

@EnvironmentObject 프로퍼티 래퍼로 선언된 속성은 어플리케이션 실행 환경 안에서 생성되는 모든 뷰에서 접근 가능하다. 앱 내의 뷰가 하나의 모델을 참조하는 형태를 띨때 모델 변화에 따라 뷰의 동기화가 한꺼번에 이루어져야 하는데, 이때 사용하는 프로퍼티 래퍼이다.

# @Environment

@Environment@EnvironmentObject는 미묘하게 다른 부분이 존재한다. @EnvironmentObject는 앱 실행환경에서 접근 가능한 오브젝트를 정의하는 것이고 @Environment는 swiftUI에서 사전 정의된 키패스 오브젝트의 키로 접근할때 사용하게 된다.

예를 들어 실행 환경에서 시스템으로부터 얻어오고 싶은 설정값들이 있을때 쉽게 접근 가능하도록 스위프트UI에서 제공한다. 다크모드 상태값, 렌더링된 뷰 사이즈 등 여러가지가 존재한다.

@Environment(\.colorScheme) var colorScheme

앱 실행환경 내에서 속성값을 추출하여 연결해준다는 방식 자체는 두 프로퍼티 래퍼가 동일하다.

다만 @EnvironmentObject의 경우 오브젝트의 타입을 기준으로 실행환경 내부를 살펴 속성값을 바인딩하기 때문에 문제가 없는 것이고 위의 다크모드 여부를 확인하는 컬러 스킴의 경우 그 타입이 Bool이기에 실행환경 모든 곳에서 통용되는 원시 타입을 속성값에 바인딩하는 것은 불가능하다.

따라서 정확한 swiftUI의 pre-defined 키값을 기준으로 속성값을 바인딩해야 한다.

@MainActor

@MainActor는 백그라운드 스레드에서 처리된 작업을 메인 스레드로 옮겨 UI 업데이트를 할때 사용한다.

@MainActor
private func process(location: CLLocation) {
    guard !isPreviewService else  { return }

    Task {
        currentLocation = try await updateAddress(from: location)
        await fetchWeather(location: location)
        updating = false
    }
}

# Reference

  1. Hacking with Swift - What’s the difference between @ObservedObject, @State, and @EnvironmentObject? (opens new window)
  2. Medium - Understanding the Mutating Keyword in Swift (opens new window)
  3. What does the SwiftUI @State keyword do? (opens new window)
  4. Apple Document - State (opens new window)
  5. What is the @Environment property wrapper? (opens new window)