이전 글에서 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 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 앞에 붙이기만 하면 사용가능하므로 재사용
이 되는 좋은 구조이기도 하죠!
예시코드의 경우 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를 사용
한다는 점이 특징이였습니다!
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 프로퍼티래퍼를 좀 더 알아보면 궁금증이 해소될꺼라 생각하며, 마지막으로 다음 글을 살펴볼께요!
이부분은 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 내용을 살펴봤습니다.
그러면 다음글에서 SwiftUI에서 자주 사용되는 프로퍼티래퍼인 @State, @Binding, @Environment 에 대해 알아보겠습니다!