SwiftUI

property wrapper

ytw_developer 2023. 11. 10. 13:12

@Published, @Binding, @ObservedObject, @State 같은 애들이 Property Wrapper 프로퍼티 래퍼입니다.

Property Wrapper 를 사용하는 이유는 특정 기능을 동작하게 하는데 좀 더 간단하게 코드를 짤 수 있도록 도와주고 코드의 중복을 없애 간결하게 만들어주기 때문입니다.

 

아래 예제 코드로 Property Wrapper 를 사용하기 전과 후의 차이를 봐서 얼마나 간결해졌는지 확인할 수 있습니다. width 와 height 는 값을 가져올 때 width 와 height 값과 10을 비교해서 작은 값을 반환하는 코드인데 로직이 중복되는 것을 확인할 수 있습니다.

struct Rectangle {
    private var _width: Int
    private var _height: Int
    
    init(width: Int, height: Int) {
    	self._width = width
        self._height = height
    }
    
    var width: Int {
    	get { return min(_width, 10) }
        set { _width = newValue }
    }
    var height: Int {
    	get { return min(_height, 10) }
        set { _height = newValue }
    }
}

 

하지만 중복되는 로직을 아래처럼 @propertyWrapper로 선언하여 코드를 간결하게 만들 수 있습니다. 만일 @propertyWrapper로 만들어주면 wrappedValue는 반드시 만들어줘야합니다.

@propertyWrapper struct LimitedValueByTen {
    
    private var value: Int = 0

    var wrappedValue: Int {
    	get { self.value }
        set { value = min(10, newValue) }
    }
}

 

다음은 @propertyWrapper 를 통해서 간결하게 만든 LimitedValueByTen 구조체를 이용하여 Rectangle 구조체를 다시 만듭니다.

struct Rectangle {
    @LimitedValueByTen var width: Int
    @LimitedValueByTen var height: Int
}

 

위에서 만든 Rectangle 구조체를 사용하여 인스턴스를 만들고 인스턴스의 저장 프로퍼티에 값을 넣어 만약 10보다 크면 10이 저장되고 아니면 설정한 값이 저장되게끔 동작합니다.

var rectangle = Rectangle()
rectangle.height = 12
rectangle.width = 8
print(rectangle.height, rectangle.width) // 10, 8

 

만약 LimitedValueByTen 에 프로퍼티 래퍼에 초기값을 설정하는 방법은 2가지가 있다. 첫 번째로는 @propertyWrapper 에 초기화 함수를 구현한 경우, 두 번째로는 @propertyWrapper 내부에 초기값을 지정하는 경우다. 

 

다음 코드는 프로퍼티 래퍼에 초기값을 @propertyWrappper 에 초기화 함수를 구현하는 예제 코드다

@propertyWrapper struct LimitedValueByTen {
    private var value: Int
    
    var wrappedValue: Int {
        get { self.value }
        set { value = min(10, newValue) } 
    }
    
    init(wrappedValue initialValue: Int) {
        self.value = initialValue
    }
}

struct Rectangle {
    @LimitedValueByTen var width: Int
    @LimitedValueByTen var height: Int
}

var rectangle = Rectangle(width: 12, height: 8)
rectangle.height = 12
rectangle.width = 8
print(rectangle.height, rectangle.width) // 12, 8

 

다음 코드는 프로퍼티 래퍼에 초기값을 @propertyWrapper 내부에 미리 지정하는 경우다.

@propertyWrapper struct LimitedValueByTen {
    private var value: Int = 0
    
    var wrappedValue: Int {
        get { self.value }
        set { value = min(10, newValue) } 
    }
}

struct Rectangle {
    @LimitedValueByTen var width: Int
    @LimitedValueByTen var height: Int
}

var rectangle = Rectangle()
print(rectangle.height, rectangle.width) // 0, 0

 

 

조금 더 나아가 UserDefaults 에서도 사용될 수 있습니다. 다음 UserManager 클래스를 보면 getset 부분이 중복되는 것을 확인할 수 있는데 @propertyWrapper 를 사용해서 코드를 간결하게 만들 수 있습니다.

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 부분은 UserDefaults.standard.object(forKey: _) 를 사용하여 명시된 Key로 설정된 값을 반환하며, set 부분은 UserDefaults.standard.set(newValue, forKey: self.key) 를 통해서 위에 코드들을 간결하게 만들었습니다. 만일 UserDefault를 통해서 가져온 value 값이 T 타입이 아니라면 defaultValue 타입으로 대체하는 코드입니다.

@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: Key) }
    }
}

 

@propertyWrapper UserDefault로 개선된 UserManager 클래스 입니다.

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

}