프로퍼티 옵저버는 말 그대로 프로퍼티의 값이 수정된지 확인할 수 있다. 프로퍼티 값이 설정될 때마다 프로퍼티 옵저버가 호출되는데, 그 값이 동일한 값이더라도 호출된다.
프로퍼티 옵저버는 직접 정의하거나 상속한 저장 프로퍼티, 상속한 연산 프로퍼티 등에 사용할 수 있다.
상속한 프로퍼티에 프로퍼티 옵저버를 사용할 때에는 서브클래스로 프로퍼티를 오버라이딩해야 한다. 연산 프로퍼티를 쓸 때에는 프로퍼티 세터를 활용한다.
값이 저장되기 전에 호출되는 프로퍼티 옵저버 willSet
를 구현하려면 상수 파라미터로 새로운 프로퍼티 값에 전달되어야 한다. 새로운 값이 저장된 직후 호출되는 프로퍼티 옵저버 didSet
를 구현하려면, 이전 값을 상수 파라미터로 전달하면 된다.
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("About to set totalSteps to \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("Added \(totalSteps - oldValue) steps")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps
willSet
과 didSet
을 사용한 저장 프로퍼티 totalSteps
는 정수 타입으로 새로운 값을 할당받을 때마다 호출된다. 프로퍼티 옵저버 newTotalSteps
는 할당받은 값을, didSet
프로퍼티 옵저버는 기존의 값을 저장하고 있다가 새로 할당한 값과의 차를 출력하는 데 사용한다. 옵저버를 가진 프로퍼티를 인-아웃 프로퍼티로 함수에 전달할 때에는 willSet
과 didSet
옵저버가 항상 호출된다.
프로퍼티 래퍼는 프로퍼티 저장 관리 및 프로퍼티 정의를 담당하는 코드를 분리하는 역할을 한다. 스레드 세이프티를 체크하거나 데이터베이스의 데이터를 저장하려면 프로퍼티 래퍼를 통해 안전성을 확보할 수 있다. 래퍼를 정의할 때 관리 코드를 한 번 쓰고, 다른 프로퍼티에 여러 번 적용하자.
wrappedValue
를 정의하는 구조체, 열거형, 클래스를 정의해 래퍼를 사용하자.
@propertyWrapper
struct TwelveOrLess {
private var number = 0
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
세터를 통해 할당 가능한 수의 범위를 12로 지정할 수 있다.
프로퍼티 래퍼를 어트리뷰트로 줄 수도 있다.
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"
직접 프로퍼티 래퍼를 일반 구조체처럼 줄 수도 있다. 이때 private
를 쓴 까닭은 외부에서 세터가 아니라 .
연산자로 직접 값을 설정하는 것을 방지하기 위해서다.
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 }
}
}
이니셜라이저를 통해 초깃값을 설정하자.
@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)
}
}
struct ZeroRectangle {
@SmallNumber var height: Int
@SmallNumber var width: Int
}
var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Prints "0 0"
struct UnitRectangle {
@SmallNumber var height: Int = 1
@SmallNumber var width: Int = 1
}
var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Prints "1 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"
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"
Java의 생성자 선언 시 파라미터로 주는 값에 따라 다른 초깃값을 줄 수 있는 것과 정확히 동일하다.
프로퍼티 래퍼를 통해 프로젝팅한 값을 정의할 수 있다. 예를 들어 데이터베이스 접근을 관리하는 프로퍼티 래퍼는 flushDatabaseConnection()
메소드를 프로젝팅한 값에 드러낼 수 있다. $
를 앞에 붙여서 프로젝팅한 값이라는 것을 알리자.
projectedValue
프로퍼티를 SmallNumber
구조체에 추가해 프로퍼티 래퍼가 새 값을 저장하기 전에 프로퍼티에 대해 새로운 값을 받아들였는지 확인 가능하다.
@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"
$
를 통해 래퍼의 프로젝트된 값에 접근하자. 새로 할당한 수가 12보다 크다면 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
}
}
$height || $width
리턴문에서 프로퍼티 래퍼가 높이 및 너비를 수정했는지 알려준다.