티스토리 뷰

🍏/Swift

[Swift] Property Wrapper

eungding 2022. 1. 6. 00:19
반응형

 

[DI] DI Container, IOC Container 개념과 예제  에서 간단하게 Property Wrapper 를 사용했었는데요, 

Swift Docs > Properties > Property Wrappers 에서 더 자세한 내용들을 알게 되어서 정리합니다 ✏️

 

# Property Wrapper  전제조건

Property Wrapper 는 Swift 5.1 에 추가되었고 

local stored variable 에만 사용가능합니다 (global variable 또는 computed variable 에서 사용불가) 

 

 

# Property Wrapper 정의하기 

Property Wrapper 를 정의하려면 wrappedValue property 를 정의한 structure, enumeration, class 를 만들면 됩니다.

아래 코드에서 TwelveOrLess structure 는 래핑되는 값이 항상 12 이하 이도록 보장합니다. 

(number가 TwelveOrLess 구현내에서만 사용되도록 private로 선언되어있습니다)

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

 

# Property Wrapper 사용하기 

property name 앞에 wrapper name을 적어줌으로써 wrapper를 적용할 수 있습니다. 

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0"

rectangle.height = 10
print(rectangle.height)
// Prints "10"

rectangle.height = 24
print(rectangle.height)
// Prints "12"

height와 width 프로퍼티는 TwelveOrLess에서 초기값을 가져옵니다.  (number가 0)

 

 

# Property Wrapper 필요성

Property Wrapper를 안쓰면 위의 코드는 이렇게 구현되어야합니다. 

struct SmallRectangle {
    private var _height = TwelveOrLess()
    private var _width = TwelveOrLess()
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
}

 

이렇게 여러 프로퍼티들에 동일한 관리 코드를 작성해줘야하는 경우, 프로퍼티 래퍼를 유용하게 사용할 수 있습니다. 

프로퍼티 래퍼를 정의해서 관리 코드를 한번만 작성해준 후,  여러 프로퍼티들에 프로퍼티 래퍼를 적용하여

관리 코드를 재사용할 수 있기 때문입니다. 

 

영어가 더 명확한 것 같아서 첨부!

 When you use a property wrapper, you write the management code once when you define the wrapper, and
 then reuse that management code by applying it to multiple properties.

 

 

# Wrapped Property  초기값 설정

위에서 살펴본 코드는 wrapped property 에 대한  initial value 를 TweetwOrLess 에 정의된 값으로 세팅하고 있습니다.

다른 initial value 를 사용할 수 없는 상황이죠! 

 

초기값 설정을 지원하고 싶으면 property wrapper 에 이니셜라이저를 추가해줘야합니다. 

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
    
    init(wrappedValue: Int) {
        number = min(wrappedValue, 12)
    }
}

struct SmallRectangle {
    @TwelveOrLess var height: Int = 2
    @TwelveOrLess var width: Int = 200
}

var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "2"

print(rectangle.width)
// Prints "12"

 

 

이렇게 바꾸면 아래와 같이 initial value 를 명시하지 않을 때 컴파일 에러가 납니다.

 @TwelveOrLess var width: Int

 

이런 경우도 대응해주기 위해, init() 이니셜라이저를 추가해줍니다. 

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
    
    init() {
        
    }
    
    init(wrappedValue: Int) {
        number = min(wrappedValue, 12)
    }
}

struct SmallRectangle {
    @TwelveOrLess var height: Int = 2
    @TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "2"

print(rectangle.width)
// Prints "0"

 

 

여기서 더 나아가서  maximum 값도 설정할 수 있도록 해봅시다. 

네이밍도 TwelveOrLess 에서 SmallNumber로 바꿔주겠습니다. 

SmallNumber 는 세개의 이니셜라이저를 가지고 있기 때문에 다양하게 사용될 수 있습니다. 

@propertyWrapper
struct SmallNumber {
    private var maximum: Int
    private var number: Int

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, maximum) }
    }

    init() {
        maximum = 12
        number = 0
    }
    
    init(wrappedValue: Int) {
        maximum = 12
        number = min(wrappedValue, maximum)
    }
    
    init(wrappedValue: Int, maximum: Int) {
        self.maximum = maximum
        number = min(wrappedValue, maximum)
    }

 

1) 

// Swift uses the init() initializer

struct ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}

var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Prints "0 0"

 

2) 

// Swift uses the init(wrappedValue:) initializer to set up the wrapper. 

struct UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Prints "1 1"

 

3) 

// Swift uses the init(wrappedValue:maximum:) initializer
 
struct NarrowRectangle {
    @SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
    @SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
}

var narrowRectangle = NarrowRectangle()
print(narrowRectangle.height, narrowRectangle.width)
// Prints "2 3"

narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// Prints "5 4"

3번처럼 property wrapper에 argument를 포함하는 방식이 가장 일반적인 방식이라고 합니다!

 

그리고 argument를 포함하는 경우에도 assignment를 통해 initial value를 할당해줄 수 있는데,

Swift는 이를 wrappedValue argument 로 여긴다고 합니다. 

예를들어 아래와 같은 경우에 SmallNumber(wrappedValue: 3, maximum: 4) 를 콜합니다. 

@SmallNumber(maximum: 4) var width: Int = 3

 

 

# Projected Value

property wrapper는 wrapped value 뿐만아니라 projected value를 추가하여 기능을 확장할 수 있습니다. 

예를들어 projected value 를 통해 property wrapper가 저장하기 전에 값을 조정(adjust) 했는 지 알려줄 수 있습니다. 

@propertyWrapper
struct SmallNumber {
    private var number: Int
    private(set) var projectedValue: Bool

    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }

    init() {
        self.number = 0
        self.projectedValue = false
    }
}

 

struct SomeStructure {
    @SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()

someStructure.someNumber = 4
print(someStructure.$someNumber)
// Prints "false"

someStructure.someNumber = 55
print(someStructure.$someNumber)
// Prints "true"

 

 

projectedValue 프로퍼티를 property wrapper에 추가하면 (꼭 이 네이밍 이여야합니다!!)

private(set) var projectedValue: Bool

 

property wrapper를 사용하는 쪽에서 dollar sign ($) 으로 접근할 수 있는 프로퍼티가 자동으로 생깁니다.

 

$someNumber의 타입은 Bool, someNumber의 타입은 Int

$someNumber 를 통해 wrapper의 projected value 에 접근하여 

내가 세팅해준 값이 property wrapper에 의해  조정되었는 지 여부를 알 수 있습니다.

 

projected value 는 어떤 타입이던지 가능합니다.

위의 예제에서는 하나의 정보(whether the number was adjusted)만 필요해서 bool 값으로만 정의했지만, 

더 많은 정보를 노출해야하는 wrapper인 경우,  다른 데이터 타입의 instance를 리턴하거나 self를 리턴할 수 도 있습니다. 

 

 

반응형
댓글