Swift 5.1버젼부터 Property Wrapper라는 기능이 추가 되었습니다. Property Behaviors, Property Delegate라고도 하는 이 기능이 무엇이고 왜 추가 되었는지, 어떻게 사용하는 지를 알아보도록 하겠습니다.
이 포스트는 Swift 공식 refernce와 Swift-evolution의 proposal을 기준으로 작성되었습니다.
proposal
reference
-
Property Wrapper의 목적
프로퍼티를 구현할 때 반복적으로 사용되는 패턴이 있습니다. lazy를 가장 대표적인 예시로 들 수 있습니다. Swift는 이를 언어 차원에서 제공하지만, 이를 언어 지원 없이 구현해야 한다면, 필요할 때마다 다음과 같은 코드를 작성해야 할 것입니다. (이를 Boilerplate Code라 합니다.)
struct Lazy { // lazy var foo = 1738 private var _foo: Int? var foo: Int { get { if let value = _foo { return value } let initialValue = 1738 _foo = initialValue return initialValue } set { _foo = newValue } } }
지금은 언어 차원에서 이를 지원하지만, 이러면 컴파일러 구현과 언어가 복잡해지고 여러가지 구현을 유연하게 제공해주기 어렵다는 단점이 있습니다. 컴파일러에 하드코딩을 할 수도 있겠지만 하드 코딩이 좋은 선택이라고 보기는 어렵죠.
따라서 이러한 코드를 라이브러리로 만들어 사용할 수 있게 함으로 컴파일러의 변경을 최소화하면서 더 많은 매커니즘을 재사용할 수 있도록 만들려는 시도의 결과가 Property Wrapper입니다.
-
Property Wrapper의 사용
proposal에서 제시하는 Lazy의 Property Wrapper 버전 구현은 다음과 같습니다.@propertyWrapper enum Lazy<Value> { case uninitialized(() -> Value) case initialized(Value) init(wrappedValue: @autoclosure @escaping () -> Value) { self = .uninitialized(wrappedValue) } var wrappedValue: Value { mutating get { switch self { case .uninitialized(let initializer): let value = initializer() self = .initialized(value) return value case .initialized(let value): return value } } set { self = .initialized(newValue) } } } @Lazy var foo: Int = 1738 // (Property Wrapper) [var/let] (name)
이렇게 하면 foo 프로퍼티는 Int 타입처럼 사용이 가능하면서, Lazy라는 Wrapper를 통해 값에 접근하게 됩니다. 즉, 다음과 같이 바뀝니다.
private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738) // 프로퍼티명에 _(언더바)가 추가된 형태로 존재한다. 접근 제한자는 private다. var foo: Int { get { return _foo.wrappedValue } set { _foo.wrappedValue = newValue } }
이렇게 자동으로 getter와 setter가 제공되기 때문에 프로그래머가 get,set 블록을 제공해 줄 수는 없습니다. 다만, willSet, didSet 블록은 제공할 수 있습니다.
Property Wrapper는 반드시 wrappedValue라는 인스턴스 프로퍼티를 제공해야 합니다. 이 프로퍼티는 계산 프로퍼티, 저장 프로퍼티 모두 가능합니다.
또한 projectedValue라는 프로퍼티를 선택적으로 제공해 줄 수 있는데, Property Wrapper의 값을 다른 타입으로 바꿔서 반환하는 역할을 합니다. 이 projectedValue는 $접두사를 앞에 붙여서 사용할 수 있습니다.
@propertyWrapper struct WrapperWithProjection { var wrappedValue: Int var projectedValue: SomeProjection { return SomeProjection(wrapper: self) } } struct SomeProjection { var wrapper: WrapperWithProjection } struct SomeStruct { @WrapperWithProjection var x = 123 } let s = SomeStruct() s.x // Int value s.$x // SomeProjection value s.$x.wrapper // WrapperWithProjection value
또한 이름부터가 ‘Property’ Wrapper이기 때문에, Property가 아닌 지역 변수나 전역 변수에는 사용할 수 없습니다.
-
Property Wrapper초기화
Property Wrapper는 여러가지 초기화 방법을 제공합니다. 예제 코드로 한꺼번에 살펴보도록 하겠습니다.@propertyWrapper struct SomeWrapper { var wrappedValue: Int var someValue: Double init() { self.wrappedValue = 100 self.someValue = 12.3 } init(wrappedValue: Int) { self.wrappedValue = wrappedValue self.someValue = 45.6 } init(wrappedValue value: Int, custom: Double) { self.wrappedValue = value self.someValue = custom } } struct SomeStruct { // init() 호출 @SomeWrapper var a: Int // init(wrappedValue:) 호출 @SomeWrapper var b = 10 // init(wrappedValue:custom:) 호출, 둘 다 유효한 호출 @SomeWrapper(custom: 98.7) var c = 30 // 할당문은 wrapped value를 초기화 하도록 자동으로 사용된다. @SomeWrapper(wrappedValue: 30, custom: 98.7) var d }
-
Property Wrapper합성
Property Wrapper는 두개 이상 적용할 수 있습니다. 이 때 교환법칙은 성립하지 않습니다. 또한 합성시에는 각 Property Wrapper의 제한 조건을 만족하는 방향으로 설정이 되어야 합니다.// DelayedMutable<Copying<UIBezierPath>> @DelayedMutable @Copying var path1: UIBezierPath // Copying<DelayedMutable<UIBezierPath>> @Copying @DelayedMutable var path2: UIBezierPath