Property Wrapper?

What is Property Wrapper?

Swift 공식문서: A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property.

번역해보자면 Property Wrapper는 프로퍼티가 저장되는 방식을 관리하는 코드와 프로퍼티가 정의되는 코드 사이에 분리된 계층을 추가해준다는 뜻이다

WWDC 19에서 사용한 UserDefaults예제로 활용법을 살펴보자

class UserManager {

    static var usesTouchID: Bool {  
        get { return UserDefaults.standard.bool(forKey: "usesTouchID") }
        set { UserDefaults.standard.set(newValue, forKey: "usesTouchID") }
    }

    static var myEmail: String? {  
        get { return UserDefaults.standard.string(forKey: "myEmail") }
        set { UserDefaults.standard.set(newValue, forKey: "myEmail") }
    }

    static var isLoggedIn: Bool {  
        get { return UserDefaults.standard.bool(forKey: "isLoggedIn") }
        set { UserDefaults.standard.set(newValue, forKey: "isLoggedIn") }
    }
}

get/set 부분의 코드가 중복돼있는걸 볼수있다

Property Wrapper를 활용하여 코드를 개선해보면

@propertyWrapper
struct UserDefault<T> {

    let key: String
    let defaultValue: T

    var wrappedValue: T {
        get { UserDefaults.standard.object(forKey: self.key) as? T ?? self.defaultValue }
        set { UserDefaults.standard.set(newValue, forKey: self.key) }
    }
} // 선언

class UserManager {

    @UserDefault(key: "usesTouchID", defaultValue: false)
    static var usesTouchID: Bool

    @UserDefault(key: "myEmail", defaultValue: nil)
    static var myEmail: String?

    @UserDefault(key: "isLoggedIn", defaultValue: false)
    static var isLoggedIn: Bool    
} // 사용

이렇게 Property Wrapper는 반복되는 로직들을 프로퍼티 자체에 연결 시켜줄 수 있다

그럼 이제 SwiftUI에서 사용하는 Property Wrapper들을 알아보자

What kind?

1. @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"
        }
    }
}

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

2. @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)
        }
    }
}

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

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

3. @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를 적용해야 한다.

4. @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 라는 파일이 보인다면 이 파일 안에서 찾아보자. 아마도 비슷한 곳을 찾을 수 있을 것이다.


References

SwiftUI Property Wrappers

Property Wrapper