SwiftUI Property Wrappers

2021년 5월 8일 수정

SwiftUI에서는 Property Wrapper를 이용해서 Swift 언어로 양방향 Model 혹은 View Model 구현을 할 수 있도록 지원한다. 이렇게 SwiftUI에서 유용하게 사용되는 Property Wrapper 들을 정리해보자.

@State

@State 는 특정 프로퍼티를 뷰의 상태(state)로 만들어준다. 즉 이 프로퍼티가 변경되면 자동으로 뷰의 데이터도 변경되고, 뷰의 데이터를 바꿔도 이 프로퍼티의 데이터도 자동으로 변경된다.

struct ContentView: View {
    @State private var name = "World"

    var body: some View {
        VStack {
            Text("Hello, \(name)!")
                .padding()
            Button(
                action: { self.switchName() },
                label: { Text("Switch") }
            )
        }
    }

    func switchName() {
        if name == "World" {
            name = "Universe"
        } else {
            name = "World"
        }
    }
}

위의 예제는 버튼을 누르면 프로퍼티의 내용이 바뀌는데 이때 텍스트 뷰의 내용도 자동으로 바뀌는 것을 볼 수 있다.

참고로 예제에서도 볼 수 있다시피 @Stateprivate 프로퍼티에만 사용할 수 있다.

@Binding

@Binding 은 다른 인스턴스 소유의 @State 프로퍼티를 빌려올 때 사용한다.

struct MyToggleButton: View {
    @Binding var value: Bool

    var body: some View {
        Button(action: {
            self.value.toggle()
        }, label: {
            Text(self.value ? "Hello" : "World")
        })
    }
}

struct ContentView: View {
    @State private var value = false

    var body: some View {
        VStack {
            MyToggleButton(value: $value)
        }
    }
}

위의 예제에서 MyToggleButtonvalue 프로퍼티가 @Binding 으로 선언되어 있다. 그리고 이 프로퍼티는 나중에 ContentView 에서 뷰를 생성할 때 value 프로퍼티와 연결된다.

따라서 이 두 값은 연결되기 때문에 어느 한 쪽의 값이 바뀌면 다른 한 쪽도 값이 동일하게 바뀐다. 또한 뷰도 이 데이터의 변경을 알아채고 역시 알아서 업데이트된다.

@ObservedObject

@State 의 대표적인 단점은 Value 타입에서만 사용이 가능하다는 점이 다. 즉 클래스 오브젝트의 경우는 @State@Binding 이 불가능하다. 대신 이 경우 @ObservableObject 를 상속받은 클래스의 프로퍼티에 @ObservedObject 라는 Property Wrapper 를 적용해 비슷하게 뷰와 프로퍼티를 연결할 수 있다.

class MyData: ObservableObject {
    @Published var name = "World"
    @Published var buttonTitle = "Switch to Universe"

    func switchName() {
        if name == "World" {
            name = "Universe"
            buttonTitle = "Switch to World"
        } else {
            name = "World"
            buttonTitle = "Switch to Universe"
        }
    }
}

struct ContentView: View {
    @ObservedObject var data = MyData()

    var body: some View {
        VStack {
            Text("Hello, \(data.name)!")
                .padding()
            Button(
                action: { self.data.switchName() },
                label: { Text(self.data.buttonTitle) }
            )
        }
    }
}

다만 클래스의 모든 프로퍼티의 변화를 추적하지는 않는다. 위의 예에서 볼 수 있다시피 추적을 원하는 프로퍼티는 @Published 라는 Property Wrapper를 적용해야 한다.

@EnvironmentObject

@EnvironmentObject 의 경우 오브젝트라는 이름이 붙은 것처럼 클래스 오브젝트를 추적하기 위한 용도의 Property Wrapper다. 다만 차이가 있다면 공유 인스턴스 형태에 적합하게 사용할 수 있다는 점이 있다.

class SharedData: ObservableObject {
    @Published var configName = "default"
    ...
}

struct ContentView: View {
    @EnvironmentObject var sharedData: SharedData
    ...
}

struct FooView: View {
    @EnvironmentObject var sharedData: SharedData
    ...
}

위의 경우 ObservableObject 를 상속받은 클래스를 여러 뷰에서 @EnvironmentObject 형식으로 참조하는 것을 볼 수 있다. 따라서 이름처럼 환경설정 등 여러 곳에서 공유될 만한 데이터를 관리하는 모델로 사용하기 좋다.

다만 최초 생성을 참조가 시작되기 전에 되어야만 할 것이다. 보통은 해당 뷰를 만들기 전에 오브젝트를 생성하고 이걸 environmentObject() 로 알려주어야 한다.

var sharedData = SharedData()
...
window.rootViewController =
    UIHostingController(rootView: ContentView().environmentObject(sharedData))

위 코드가 SharedData 오브젝트를 생성해서 공유를 시작하는 시점이다. 이 코드를 어디에 만들어야 하나 궁금할 수 있는데, SceneDelegate.swift 라는 파일이 보인다면 이 파일 안에서 찾아보자. 아마도 비슷한 곳을 찾을 수 있을 것이다.

@StateObject

@StateObject 는 새로 추가된 Property Wrapper로 iOS 14, macOS 11 Big Sur 혹은 그 이후 버전에서만 사용이 가능하다. 이 글을 쓰는 이가 아직 테스트용 맥이 없어서 이 부분을 확인하지 못 하여서 일단 제목만 남겨 놓는다.

추측으로 @ObservedObject 와 비슷하면서도 @State 처럼 private 로만 액세스 가능한 프로퍼티를 구현하는 것이 목적일 것 같다.