SwiftUI / Property Wrappers

Minsang Kang·2023년 10월 23일
0

SwiftUI

목록 보기
9/12
post-thumbnail

이전 글에서 SwiftUI 에서 Data를 다루는 방법으로 @State, @Binding 등 프로퍼티래퍼를 사용한다고 했습니다.
따라서 프로퍼티 래퍼가 무엇인지 한번 살펴보겠습니다!

Apple Developer 문서에 표현된 Property Wrappers 정의 탐방
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Property-Wrappers

UPDATE : 2023-10-24 12:30

Property Wrappers

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Property-Wrappers

프로퍼티래퍼(property wrapper)는 property가 저장되는 방식을 관리하는 코드(set)property를 정의하는 코드(get) 사이에 분리 계층(wrappedValue)을 추가합니다.
예를 들어 thread-safe 안전성 검사 또는 database 내 저장과 같이 property를 관리하는 코드가 필요한 경우에 property wrapper를 사용하여 관리 관련 코드를 wrapper 내 한번만 작성하여 다양한 property에 적용하여 사용이 가능합니다.

property wrapper를 정의하려면 wrappedValue property를 정의하는 struct, enum, class 중 하나의 형태로 정의해야 합니다.
아래 예시코드의 경우 wrappedValue 값이 항상 12보다 작거나 같은 값이 되도록 보장합니다.

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

setter(set)은 새 값이 12보다 작거나 같은지를 확인하고, getter(get)은 저장된 값을 반환합니다.

property wrapper 내에서 사용되는 property는 private으로 표시하여 property wrapper 내에서만 사용되도록 합니다.
다른 곳에서는 getter, setter만으로 값을 접근할 수 있도록 합니다.

여기까지 정의를 살펴보면 wrapper, 말 그대로 property를 get, set으로 감싸 원하는 형태로 get, set 되도록 감싼 property 라고 볼 수 있겠습니다!
그리고 get, set 내에서 원하는 형태가 되도록 작성한다음 이 property wrapper를 property 앞에 붙이기만 하면 자동으로 wrapping 되어 재사용되는 구조로 사용가능하다는 것이죠!

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를 항상 12 이하값으로 보장하는 @TwelveOrLess 프로퍼티래퍼로 선언하면 프로퍼티래퍼 내 정의된 기본값인 0으로 설정됩니다.
  • 10으로 값을 설정시 setter를 통해 10이 그대로 저장됩니다.
  • 24로 값을 설정시 setter를 통해 12보다 큰 값이므로 12로 저장됩니다.

프로퍼티래퍼를 적용하면 컴파일러래퍼에 대한 저장소를 제공하는 코드와 래퍼를 통해 속성에 대한 액세스를 제공하는 코드를 합성합니다.
(프로퍼티래퍼는 래핑된 값을 저장하는 역할을 담당하므로 이에 대한 합성 코드는 없습니다.)

@을 사용한 특수구문 없이도 property wrapper의 동작을 사용하는 방법도 있습니다.
예를 들어 @TwelveOrLess 속성 대신 TwelveOrLess()를 private 값으로 사용하고, 이를 getter, setter로 래핑하여 반환하는 코드는 다음과 같습니다.

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 }
    }
}

여기까지 사용하는 방법들도 살펴보면 @를 사용하여 Property wrapper를 직접 하용하든, @없이 private 변수로 값을 받아 getter, setter를 추가로 정의하든

결국 중요한 포인트는 getter, setter 라는 추가 layer를 통해 property를 관리하는 관련 코드를 넣어 사용이 가능하다는 것이 핵심입니다!
그리고 한번만 property wrapper로 정의하고 propery 앞에 붙이기만 하면 사용가능하므로 재사용이 되는 좋은 구조이기도 하죠!

Setting Initial Values for Wrapped Properties

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Setting-Initial-Values-for-Wrapped-Properties

예시코드의 경우 property 정의시점에 초기값을 설정합니다.
이러한 경우 다른 초기값을 지정할 수 없습니다.

private var number = 0

initializer를 통해 초기값 설정을 지원합니다. 이를 통해 사용자 정의를 지원할 수 있습니다.

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

예시코드의 경우 세 가지 형태의 initializer를 통해 초기값 및 maximum 값을 설정합니다.

프로퍼티래퍼를 사용하는 initializer 형태에 따라서 초기값이 설정됩니다.

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

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

위 예시코드의 경우 init() 메소드를 사용한 형태입니다.
따라서 maxinum = 12, number = 0으로 설정됩니다.

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

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

위 예시코드의 경우 init(wrappedValue: Int) 메소드를 사용한 형태입니다.
프로퍼티래퍼를 선언시 초기값을 지정해주면 wrappedValue 값이 지정됩니다.
따라서 maxinum = 12, number = 1로 설정됩니다.

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"

위 예시코드의 경우 init(wrappedValue: Int, maximum: Int) 메소드를 사용한 형태입니다.
프로퍼티래퍼를 선언시 괄호 안에 인수를 작성하여 초기값을 설정합니다.
따라서 height 값의 경우 maximum = 5, value: 2로 설정됩니다.

struct MixedRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber(maximum: 9) var width: Int = 2
}


var mixedRectangle = MixedRectangle()
print(mixedRectangle.height)
// Prints "1"


mixedRectangle.height = 20
print(mixedRectangle.height)
// Prints "12"

위 예시코드의 경우 init(wrappedValue: Int) 메소드와 init(wrappedValue: Int, maximum: Int) 메소드를 사용한 형태입니다.
프로퍼티래퍼를 선언시 초기값을 할당하면 Swift는 자동으로 wrappedValue 인수처럼 처리하고 포함된 인수를 받아들이는 initializer를 사용합니다.
따라서 height의 경우 maximum = 12, value: 1로 설정되며 width의 경우 maximum = 9, value: 2로 설정됩니다.

여기까지 Property Wrapper를 사용할시 초기값을 설정하고자 하는 경우 initializer를 통해 구현하고, 상황에 따라 알맞은 initializer를 사용하여 초기값을 설정할 수 있습니다!
그리고 재밌는점은 initializer 중에 wrappedValue를 인자로 받는 경우 Property Wrapper 변수를 선언시 초기값을 할당하면 자동으로 wrappedValue 인자를 지닌 initializer를 사용한다는 점이 특징이였습니다!

Projecting a Value From a Property Wrapper

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Projecting-a-Value-From-a-Property-Wrapper

Property Wrapper는 wrappedValue 외에도 projectedValue를 정의하여 추가기능을 노출시킬 수 있습니다.
예를 들어, 데이터베이스 접근을 관리하는 property wrapper는 projectedValue 값에 flashDatabaseConnection() 메소드를 노출할 수 있습니다.
projectedValue는 property명 앞에 $가 붙는다는 것을 제외하면 wrappedValue와 동일합니다.
우리는 $로 시작하는 property를 정의할 수 없기 때문에 projectedValue는 정의된 property 값들을 간섭할 수 없습니다.

위의 SmallNumber 예시에서 너무 큰 숫자로 설정하려할때 property wrapper가 숫자를 조정하여 저장합니다.
아래 예시는 SmallNumber 구조에 projectedValue를 추가하여 property wrapper가 새 값을 저장할 때 값이 조정되었는지 여부를 추척합니다.

@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"

위 예시코드에서 property wrapper 값을 someNumber 변수명으로 선언한 경우 someNumber.$someNumber 처럼 property명 앞에 $를 붙여 projectedValue 값을 접근할 수 있습니다.
wrappedValue를 4로 저장하는 경우 12보다 작기에 projectedValue 값을 false, 55를 저장하는 경우 12보다 크기에 true로 설정됩니다.

projectedValue는 모든 type이 가능합니다.
위 예제처럼 Bool type과 같이 단 하나의 정보가 아닌 하나 이상의 정보를 담고자 하는 경우 다른 type의 instance를 반환하거나, property wrapper instance 자체인 self를 반환할 수 있습니다.

property wrapper의 getter 또는 instance 메소드와 같은 type의 코드에서는 projectedValue 값을 접근시 self를 생략할 수 있습니다. 그저 property명 앞에 $만 붙여 접근하죠.
아래 예시코드의 경우 $height, $width를 통해 projectedValue 값을 얻습니다.

enum Size {
    case small, large
}

struct SizedRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int

    mutating func resize(to size: Size) -> Bool {
        switch size {
        case .small:
            height = 10
            width = 20
        case .large:
            height = 100
            width = 100
        }
        return $height || $width
    }
}

@property wrapper명을 붙여서 아요하는 구문은 그저 getter, setter가 있는 property에 대한 추가구문일 뿐, height와 width와 같이 property wrapper 변수를 접근하는 것은 다른 property 값을 접근하는 것과 동일하게 동작합니다.

예를 들어, 위 코드에서 resize 메소드 내에서 property wrapper 변수인 width와 height다른 property 처럼 사용합니다.
.large로 resize를 하는 경우 property wrapper의 setter를 통해 12보다 큰 값이므로 12로 설정되며, projectedValue 값을 true로 설정합니다. 따라서 반환값은 $height, $width 모두 ture 이므로 true가 반환됩니다.

여기까지가 새로운 projectedValue 내용이였습니다.
property wrapper가 property 에 getter, setter 레이어가 wrapped 되어 반영된 wrappedValue를 지닌 property 였지만
여기에 projectedValue 값이 추가되어 $를 통해 접근하여 사용할 수 있다는 것이였습니다!

저에게 좀 헷갈린 포인트는 대표적인 프로퍼티래퍼인 @State의 경우 $text 와 같이 projectedValue 값을 SwiftUI 내 View에서 접근하여 값을 바꿀 수 있는.. 그런게 있었잖아요?
근데 Documentation 내용에서는 projectedValue는 wrappedValue를 간섭할 수 없고, 오히려 wrappedValue에 의해서 projectedValue 값이 만들어지는 그런 느낌으로 소개되어있었어요..!(projected 라는 명이 붙은 것도 이런 뜻이지 않을까요?)

추후에 @State 프로퍼티래퍼를 좀 더 알아보면 궁금증이 해소될꺼라 생각하며, 마지막으로 다음 글을 살펴볼께요!

Global and Local Variables

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Global-and-Local-Variables

이부분은 Property Wrapper와 관련된 내용이 많지 않아서 요약해서 남기겠습니다!

computed property는 값을 저장하는 stored property 대신 계산하여 값을 반환합니다.
전역, 또는 지역변수로 stored property와 computed property를 지닐 수 있습니다.
전역 변수와 상수의 경우 lazy stored property와 유사한 방식으로 지연 저장되지만 lazy를 표시할 필요가 없습니다.
지역 변수와 상수의 경우는 절때 lazy하게 계산되지 않습니다.

property wrapper의 경우 오로지 지역 변수(local stored variable)로 사용이 가능합니다. 전역 변수와 computed variable로 사용이 불가능합니다.

여기서 몇가지 사실을 알 수 있었는데요, 바로 property wrapper의 경우는 오로지 지역 변수로만, 상수도 불가하고 computed property로도 사용이 불가능하다는 점 이였습니다.
따라서 특정 struct나 class 내에서 static 키워드 없이 사용이 가능한 형태라는 것이죠!

그리고 추가로 전역변수, 상수는 lazy 하게 저장된다는 사실도 알게되었답니다~!

정리

이렇게 SwiftUI에서 Model data를 다루기 위한 기초인 Property Wrapper 내용을 살펴봤습니다.

  • Property Wrapper는 getter, setter 레이어가 추가되어 wrappedValue를 감싼 property 형태이다.
  • thread-safe 하기 위한 구문이나 database에 접근하기 위한 구문을 getter, setter 내 정의하여 property wrapper로 만들어 사용하면 재사용이 가능하다.
  • property wrapper로 사용하고자 하는 변수 앞에 @property wrapper명을 붙여 사용이 가능하다.
  • property wrapper의 wrappedValue의 기본값을 설정하고자 하는 경우 다양한 init 메소드를 구현하여 사용이 가능하다.
  • projectedValue를 추가로 사용이 가능하다. projectedValue는 wrappedValue에 영향을 끼칠 수 없다.
  • projectedValue의 type 제한이 없어 다양한 형태로 사용이 가능하다.
  • projectedValue는 $property명 형식으로 사용이 가능하다.
  • property wrapper는 지역 변수로만 사용이 가능하다.

그러면 다음글에서 SwiftUI에서 자주 사용되는 프로퍼티래퍼인 @State, @Binding, @Environment 에 대해 알아보겠습니다!

다음글: SwiftUI / @State, @Binding, @Environment

profile
 iOS Developer

0개의 댓글