iOS/번역

SwiftUI의 주요 프로퍼티 래퍼 탐색: @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, 그리고 @Environment

이기회 2024. 2. 23. 17:45

https://fatbobman.com/en/posts/exploring-key-property-wrappers-in-swiftui/

 

Exploring Key Property Wrappers in SwiftUI: @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, and @Environmen

In this article, we will explore several property wrappers that are frequently used and crucial in SwiftUI development. This article aims to provide an overview of the main functions and usage considerations of these property wrappers, rather than a detail

fatbobman.com

해당 글의 한국어 번역글입니다 :)

 


해당 글에서는 SwiftUI 개발에서 빈번히 사용되는 중요한 프로퍼티 래퍼들을 탐색할 것입니다. 이 글은 이러한 프로퍼티 래퍼들의 주요 기능 및 사용 고려 사항에 대한 개요를 제공하는 것이 목적이며, 상세한 사용 가이드가 아닙니다.

 

해당 글은 일반 프로그래밍에는 익숙하지만 SwiftUI에는 비교적 새로운 개발자들이 이러한 프로퍼티 래퍼들의 핵심 기능 및 적용 가능한 시나리오를 빠르게 이해할 수 있도록 돕기 위해 작성되었습니다.

 

1. @State

@State는 SwiftUI에서 가장 일반적으로 사용되는 프로퍼티 래퍼 중 하나로, 주로 뷰 내에서 private 데이터를 관리하는 데 사용됩니다. 문자열, 정수, 열거형 또는 구조체 인스턴스와 같은 값 유형 데이터를 저장하는 데 특히 적합합니다.

  • @State는 뷰의 private 상태를 관리하는 데 사용됩니다.
  • 주로 뷰와 수명이 일치하는 값 유형 데이터를 저장하는 데 사용됩니다.

1.1 Typical Use Cases

  • @State는 뷰 내 데이터의 변경으로 인해 뷰 업데이트가 트리거되는 경우에 이상적인 선택입니다.
  • 이는 주로 스위치 상태, 텍스트 입력 등과 같은 간단한 UI 구성 요소 상태 관리에 일반적으로 사용됩니다.
  • 만약 데이터가 복잡한 교차 뷰 공유를 요구하지 않는다면, @State를 사용하여 상태 관리를 단순화할 수 있습니다. 

1.2 Considerations

  • @State는 뷰 내에서만 사용하고, 명시적으로 private로 표시되지 않더라도 뷰의 private 속성으로 간주하는 것이 좋습니다.
  • @State는 래핑된 데이터에 대한 양방향 데이터 바인딩 파이프라인을 제공하며, 이를 $ 접두사를 사용하여 액세스할 수 있습니다.
  • @State는 대량의 데이터나 복잡한 데이터 모델을 저장하는 데 적합하지 않으며, 이러한 경우에는 @StateObject나 다른 상태 관리 솔루션이 더 적절합니다.
  • 프로퍼티 래퍼는 본질적으로 구조체입니다. @ 접두사는 다른 데이터를 래핑하는 데 사용되며, @가없는 경우에는 자체 유형을 나타냅니다. 자세한 내용은 John SundellAntoine van der Lee를 참조하거나 @State Research in SwiftUI를 참조하십시오.
  • 생성자에서 값 할당 시, 할당에는 밑줄(_)을 사용하여 @State의 원시 값을 액세스합니다.
@State var name: String
init(text: String) {
    // 밑줄 버전에 할당할 때는 해당 값을 State 유형 자체로 래핑하십시오.
    _name = State(wrappedValue: text)
}
  • @State 변수는 뷰의 생성자에서 한 번만 할당할 수 있으며, 이후의 변경은 뷰의 body 내에서 이루어져야 합니다. How to Avoid Repeating SwiftUI View Updates[한국어]를 참조하십시오.
  • 현재 뷰나 하위 뷰에서 값 수정이 필요하지 않은 경우(@Binding을 통해), @State를 사용할 필요가 없습니다.
  • 일부 경우에는 참조 유형과 같은 non-value 유형을 고유성(uniqueness)과 수명을 보장하기 위해 @State에 저장하기도 합니다.
@State var textField: UITextField?
TextField("", text: $text)
    .introspect(.textField, on: .iOS(.v17)) {
        // Holding the UITextField instance
        self.textField = $0
    }
  • Observation 프레임워크의 @State는 @Observable 인스턴스가 뷰 자체보다 짧은 수명을 갖지 않도록 보장합니다. 자세한 내용은 A Deep Dive Into Observation: A New Way to Boost SwiftUI Performance를 참조하십시오.
  • @State는 스레드 안전하며 메인 스레드가 아닌 스레드에서 수정할 수 있습니다.
@State var text: String = ""
Button("Change") {
    // 메인 스레드로 다시 전환할 필요가 없습니다.
    Task.detached {
        text = "hi"
    }
}

 

2. @Binding

@Binding은 SwiftUI에서 두 방향 데이터 바인딩을 구현하는 데 사용되는 프로퍼티 래퍼입니다. 이는 값(예: Bool)과 이러한 값을 표시하고 수정하는 UI 요소 간의 양방향 연결을 생성합니다.

  • @Binding은 데이터를 직접 보유하지 않지만 다른 데이터 소스에 대한 읽기 및 쓰기 액세스에 대한 래퍼를 제공합니다.
  • @Binding을 사용하면 UI 요소가 데이터를 직접 수정하고 이러한 변경 사항을 반영할 수 있습니다.

2.1 Typical Use Cases

  • @Binding은 주로 TextField, Stepper, Sheet 및 Slider와 같이 두 방향 데이터 바인딩을 지원하는 UI 구성 요소와 함께 사용됩니다.
  • @Binding은 자식 뷰에서 부모 뷰의 데이터를 직접 수정해야 하는 상황에 적합합니다.

2.2 Considerations

  • @Binding을 사용할 때 주의하세요. 자식 뷰가 데이터를 수정하지 않고 데이터 변경에만 응답해야하는 경우에는 필요하지 않습니다.
  • 복잡한 뷰 계층 구조에서 @Binding을 여러 수준으로 전달하면 데이터 흐름을 추적하기 어려울 수 있으므로, 이러한 경우 다른 상태 관리 방법을 고려해야 합니다.
  • @Binding의 데이터 소스가 신뢰할 수 있는지 확인하십시오. 잘못된 데이터 소스는 일관성 부족이나 애플리케이션 충돌로 이어질 수 있습니다. @Binding은 단순히 conduit(수도관, 도랑, 파이프라인)일 뿐이므로 호출될 때 해당 데이터 소스가 존재함을 보장하지 않습니다.
  • 개발자는 get 및 set 메서드를 제공하여 Binding을 사용자 정의할 수 있습니다.
let binding = Binding<String>(
    get: { text },
    // 문자열의 길이를 제한
    set: { text = String($0.prefix(10)) }
)
  • ⭐️ Binding 유형에 대한 확장을 생성하면 개발 효율성과 유연성을 크게 향상시킬 수 있습니다. 자세한 내용은 SwiftUI Binding Extensions를 읽어보세요.
// Binding<V?>를 Binding<Bool>으로 변환
extension Binding {
    static func isPresented<V>(_ value: Binding<V?>) -> Binding<Bool> {
        Binding<Bool>(
            get: { value.wrappedValue != nil },
            set: {
                if !$0 { value.wrappedValue = nil }
            }
        )
    }
}
  • Observation 프레임워크에서 @Bindable은 @Observable 인스턴스에 대한 해당 Binding 인터페이스를 생성하는 데 사용될 수 있습니다. 자세한 내용은 A Deep Dive Into Observation: A New Way to Boost SwiftUI Performance를 참조하십시오.
  • 생성자 매개변수를 선언할 때, Binding의 래핑된 값 유형(get 메서드의 반환 유형)을 명시적으로 지정해야 합니다. 예를 들어, Binding<String>과 같이 말이죠.
  • @Binding은 독립적인 데이터 소스가 아닙니다. 그것은 이미 존재하는 데이터에 대한 참조일 뿐입니다. 뷰는 get 메서드가 뷰 업데이트를 트리거 할 수 있는 값을 읽을 때만 업데이트됩니다(@State, @StateObject와 같은). 이는 사용자 정의 Binding에 중요합니다.
struct Test: View {
    let a = A()
    var body: some View {
        let binding = Binding<String>(
            get: { a.name },
            set: { a.name = $0 }
        )
        // A가 ObservableObject 프로토콜을 준수하더라도, 
        // StateObject를 사용하여 뷰와 관련되지 않은 경우에는 
        // 해당 속성에 대해 생성된 Binding도 뷰 업데이트를 트리거하지 않습니다.
        Text(binding.wrappedValue)
        TextField("input:", text: binding)
    }

    class A: ObservableObject {
        @Published var name: String = ""
    }
}

 

3. @StateObject

@StateObject는 ObservableObject 프로토콜을 준수하는 객체 인스턴스를 관리하기 위한 SwiftUI의 프로퍼티 래퍼입니다. 이를 통해 이러한 인스턴스가 현재 뷰의 수명과 적어도 같은 수명을 가지도록 보장합니다.

  • @StateObject는 ObservableObject 프로토콜을 준수하는 인스턴스를 관리하기 위해 특별히 사용됩니다.
  • ⭐️ @StateObject가 달린 객체 인스턴스는 뷰의 전체 수명 주기 동안 고유합니다. 이는 뷰가 업데이트되더라도 해당 인스턴스가 다시 생성되지 않음을 의미합니다.

3.1 Typical Use Cases

  • @StateObject는 일반적으로 뷰 계층 구조의 맨 위에서 ObservableObject 인스턴스를 생성하고 유지하는 데 사용됩니다.
  • 이는 뷰의 전체 수명 주기 동안 유지되어야하는 데이터 모델이나 비즈니스 로직에 일반적으로 사용됩니다.
  • @State와 비교하여, @StateObject는 복잡한 데이터 모델과 해당 로직을 관리하기에 더 적합합니다.

3.2 Considerations

  • ⭐️ @StateObject에서 뷰 업데이트를 트리거하는 조건에는 @Published로 표시된 속성에 값 할당하기(새 값이 이전 값과 다를지 여부와 관계 없이)와 objectWillChange 게시자(publisher)를 호출하는 것이 포함됩니다.
  • @StateObject는 인스턴스 속성의 변경에 반응해야하는 뷰에서만 사용하세요. 데이터를 읽기만 하고 변경 사항을 관찰하지 않아도 되는 경우 다른 옵션을 고려하세요.
  • ⭐️ @StateObject를 도입하면 SwiftUI가 암시적으로 뷰에 @MainActor를 추가하므로 모든 관련 작업이 주 스레드에서 발생한다는 것을 의미합니다. 이에는 비동기 작업도 포함됩니다. non-메인 스레드에서 실행되어야 하는 코드는 뷰 코드와 분리되어야 합니다.
struct B: View {
    // StateObject를 사용하는 것은 현재 뷰에 @MainActor를 추가하는 것과 같습니다.
    @StateObject var store = Store()
    var body: some View {
        Button("Main Thread") {
            Task.detached {
                await printThreadName()
                // output <_NSMainThread: 0x60000170c000>{number = 1, name = main}
            }
        }
    }
    
    func printThreadName() async {
        print(Thread.current)
    }
}
  • 만약 인스턴스가 현재 수명이 보장된 뷰 컨텍스트에서 생성되며(예: 앱 레벨에서), 그 인스턴스의 속성 변경에 대응할 필요가 현재 레벨에서 없다면, @StateObject는 필요하지 않을 수 있습니다.
struct DemoApp: App {
    // 이 레벨의 뷰의 수명이 애플리케이션과 일치하며 'store'의 변경에 대응할 필요가 없다면 
    // StateObject는 필요하지 않습니다.
    let store = Store()

    var body: some Scene {
        WindowGroup {
            Test()
                .environmentObject(store)
        }
    }
}

 

4. @ObservedObject

@ObservedObject는 SwiftUI에서 사용되는 프로퍼티 래퍼로, 주로 뷰의 수명 동안 외부 ObservableObject 인스턴스를 사용하기 위해 사용됩니다.

  • @ObservedObject는 관찰되는 인스턴스를 소유하지 않으며, 인스턴스의 수명을 보장하지 않습니다.
  • @ObservedObject는 뷰의 수명 동안 연결된 인스턴스를 변경할 수 있습니다.

4.1 Typical Use Cases

  • ⭐️ @ObservedObject은 종종 @StateObject와 함께 사용됩니다. 여기서 부모 뷰가 @StateObject를 사용하여 인스턴스를 생성하고, 자식 뷰가 이 인스턴스를 @ObservedObject를 통해 사용하여 인스턴스의 변경 사항에 응답합니다.
  • 인스턴스의 동적 전환이 필요한 시나리오에 적합합니다. 예를 들어, NavigationSplitView에서 사이드바에서 다른 인스턴스를 선택하면 detail view에서 데이터 소스가 동적으로 변경됩니다. 자세한 내용은 StateObject and ObservedObject를 참조하십시오.
// Define a data model conforming to the ObservableObject protocol
class DataModel: ObservableObject, Identifiable {
    let id = UUID()
}

struct MyView: View {
    @State private var items = [DataModel(), DataModel()]

    var body: some View {
        VStack {
            // MySubView와 관련된 DataModel 인스턴스를 변경
            Button("Replace Model") {
                items.reverse()
            }
            MySubView(model: items.first!)
        }
    }
}

// Subview
struct MySubView: View {
    // @ObservedObject를 사용하여 외부 ObservableObject 인스턴스를 사용
    @ObservedObject var model: DataModel 

    var body: some View {
        VStack {
            // 'items' 배열이 변경될 때 MyView에서 현재 DataModel 인스턴스의 UUID를 표시하면 
            // 여기에 표시된 UUID가 업데이트되어 @ObservedObject가 동적으로 변경됨을 보여줍니다.
            Text(model.id.uuidString)
        }
    }
}
  • 뷰에서 사용하여 ObservableObject 인스턴스를 사용하는데, 이들 인스턴스의 수명이 외부 프레임워크나 코드에 의해 보장되는 경우에 사용됩니다. 예를 들어, Core Data에서 NSManagedObject 인스턴스를 사용하는 경우입니다.

4.2 Considerations

  • iOS 13에서는 @StateObject가 없었기 때문에 @ObservedObject가 유일한 선택이었으며, 인스턴스의 수명을 보장할 수 없어 예상치 못한 결과를 초래할 수 있었습니다. 이러한 문제를 피하기 위해 안정성이 걱정되지 않는 더 높은 수준의 뷰에서 @State를 사용하여 인스턴스를 보유하고, 그런 다음 사용하는 뷰에서 @ObservedObject를 통해 사용할 수 있습니다.
  • third-party에서 제공한 ObservableObject 인스턴스를 사용할 때, @ObservedObject로 참조된 객체가 뷰의 전체 수명 동안 사용 가능한지 확인하는 것이 중요합니다. 그렇지 않으면 런타임 오류가 발생할 수 있습니다.

 

5. @EnvironmentObject

@EnvironmentObject은 SwiftUI에서 사용되는 프로퍼티 래퍼로, 현재 뷰와 환경(environment)을 통해 상위 뷰에서 전달된 ObservableObject 인스턴스 간의 연결을 생성합니다. 각 뷰의 생성자를 명시적으로 통과시키지 않고도 서로 다른 뷰 계층 구조에 공유 데이터를 도입하는 편리한 방법을 제공합니다.

5.1 Typical Use Cases

  • 여러 뷰 간에 동일한 데이터 모델을 공유하는 데 이상적입니다. 사용자 설정, 테마 또는 애플리케이션 상태와 같은 것들을 예로 들 수 있습니다.
  • 동일한 ObservableObject 인스턴스에 여러 뷰가 액세스해야하는 복잡한 뷰 계층 구조를 구축하는 데 적합합니다.

5.2 Considerations

  • @EnvironmentObject를 사용하기 전에 해당 인스턴스가 뷰 계층 구조 상위에서 제공되었는지 확인하십시오(.environmentObject modifier를 사용하여). 그렇지 않으면 런타임 오류가 발생합니다.
  • @EnvironmentObject의 뷰 업데이트를 트리거하는 조건은 @StateObject 및 @ObservedObject의 경우와 동일합니다.
  • @ObservedObject와 마찬가지로 @EnvironmentObject는 연결된 인스턴스를 동적으로 변경하는 것을 지원합니다.
struct MyView: View {
    @State private var items = [DataModel(), DataModel()]
    var body: some View {
        VStack {
            Button("Replace Model") {
                // 자식 뷰 MySubView와 관련된 인스턴스를 변경
                items.reverse()
            }
            MySubView()
                .environmentObject(items.first!)
        }
    }
}

struct MySubView: View {
    @EnvironmentObject var model: DataModel // 연결된 인스턴스를 동적으로 변경
    var body: some View {
        VStack {
            Text(model.id.uuidString)
        }
    }
}
  • ⭐️ @EnvironmentObject는 필요한 경우에만 도입하세요. 그렇지 않으면 불필요한 뷰 업데이트가 발생할 수 있습니다. 종종 여러 레벨의 다양한 뷰가 동일한 인스턴스를 관찰하고 응답하기 때문에, 응용 프로그램의 성능 저하를 피하기 위해 적절한 최적화가 필요합니다. 이것이 많은 개발자들이 @EnvironmentObject를 경계하는 이유입니다.
  • 뷰 계층 구조에서 동일한 유형의 환경 객체의 경우 유효한 인스턴스는 하나뿐입니다.
@StateObject var a = DataModel()
@StateObject var b = DataModel()

MySubView()
    .environmentObject(a) // 해당 뷰에 가까운 것만 유효
    .environmentObject(b)

 

6. @Environment

@Environment은 뷰가 환경에서 특정 값을 읽고, 응답하고, 호출하는 데 사용되는 프로퍼티 래퍼입니다. 이를 통해 뷰는 SwiftUI 또는 앱 환경에서 제공되는 데이터, 인스턴스 또는 메서드에 액세스할 수 있습니다.

6.1 Typical Use Cases

  • 시스템이나 상위 레벨 뷰에서 제공하는 환경 값에 액세스하고 응답해야 할 때, 예를 들어 인터페이스 스타일(다크/라이트 모드), 장치 방향 또는 글꼴 크기(일반적으로 값 유형과 대응).
  • SwiftUI의 ModelContext에 액세스하고 호출해야 할 때 (참조 유형에 해당).
  • 시스템에서 제공하는 메서드를 사용할 때, 예를 들어 dismiss 또는 openURL(구조체의 callAsFunction 메서드를 통해 캡슐화).

6.2 Considerations

  • @EnvironmentObject에서 제공하는 인스턴스가 처리하는 복잡한 로직과 비교하여, @Environment에서 제공하는 데이터는 일반적으로 더 구체적인 기능을 가지고 있습니다.
  • 개발자는 사용자 정의 EnvironmentKey를 정의함으로써 사용자 정의 환경 값을 생성할 수 있습니다. 시스템에서 제공하는 환경 값과 마찬가지로, 다양한 유형(value types, Bindings, reference types, methods)을 정의할 수 있습니다. 자세한 내용은 Custom SwiftUI Environment Values Cheatsheet를 참조하세요.
public struct ContainerEnvironmentKey: EnvironmentKey {
    // 예제 환경 키의 기본값
    public static var defaultValue = ContainerEnvironment(containerName: "Default")
}

public extension EnvironmentValues {
    var overlayContainer: ContainerEnvironment {
        get { self[ContainerEnvironmentKey.self] }
        set { self[ContainerEnvironmentKey.self] = newValue }
    }
}
  • SwiftUI에서 EnvironmentKey와 유사한 정의 스타일은 많은 방법으로 사용됩니다. 한 번 마스터하면, PreferenceKey(자식-부모 뷰 통신용), FocusedValueKey(포커스 기반 값용) 및 LayoutValueKey(자식-레이아웃 컨테이너 통신용)와 같은 다른 개념을 이해하는 것이 쉽습니다.
  • 기본값이 있기 때문에 @Environment은 값이 누락되어도 앱 충돌을 유발하지 않지만, 이는 개발자가 값을 주입하는 것을 잊을 수도 있습니다.
  • @EnvironmentObject와 달리 하위 레벨 뷰는 조상 뷰에서 전달된 EnvironmentValue 값을 수정할 수 없습니다.
  • 다른 EnvironmentKeys를 정의하여 동일한 유형의 여러 속성을 가진 EnvironmentValue를 만들 수 있습니다.

 

요약

  • @StateObject, @ObservedObject 및 @EnvironmentObject는 특별히 ObservableObject 프로토콜을 준수하는 인스턴스를 연관시키기 위해 사용됩니다.
  • @StateObject는 때때로 @ObservedObject를 대체할 수 있고 유사한 기능을 제공할 수 있지만, 각각 고유한 사용 사례가 있습니다. @StateObject는 일반적으로 인스턴스를 생성하고 유지하는 데 사용되는 반면, @ObservedObject는 이미 존재하는 인스턴스를 도입하고 응답하는 데 사용됩니다.
  • iOS 17+ 환경에서는 응용 프로그램이 주로 Observation 및 SwiftData 프레임워크에 의존하는 경우, 이 세 가지 프로퍼티 래퍼의 사용빈도가 비교적 적을 수 있습니다.
  • @State 및 @Environment는 값 유형뿐만 아니라 다른 유형에도 사용할 수 있습니다.
  • @Environment은 EnvironmentValue를 통해 기본값을 제공할 수 있기 때문에 환경 데이터를 도입하는 상대적으로 안전한 방법을 제공합니다. 이는 데이터 주입이 누락되어 애플리케이션 충돌이 발생하는 위험을 줄입니다.
  • Observation 프레임워크의 맥락에서 @State와 @Environment는 주요 프로퍼티 래퍼가 됩니다. 값 유형이든 @Observable 인스턴스이든, 이러한 래퍼를 통해 뷰에 사용될 수 있습니다.
  • 사용자 정의 Binding은 간결한 코드로 바인딩에 의존하는 데이터 소스와 UI 구성 요소 간의 복잡한 로직을 구현할 수 있도록 큰 유연성을 제공합니다.

각 프로퍼티 래퍼에는 고유한 사용 사례와 장점이 있습니다. 올바른 도구를 선택하는 것은 효율적이고 유지 관리 가능한 SwiftUI 애플리케이션을 구축하는 데 중요합니다. 소프트웨어 개발에서 자주 언급되는 대로, 어떤 도구도 만병 통치약은 아니지만 적절하게 사용하면 개발 효율성과 애플리케이션 품질을 크게 향상시킬 수 있습니다.