본문 바로가기

학습 노트/Swift (2021)

109 ~ 117. Property (속성)

Stored Property (저장속성)

형식 내부에 변수와 상수를 선언하면 속성이 된다.
이러한 속성을 저장속성(stored property)라고 한다.

Syntax

Variable Stored Property
var name: Type DefaultValue

Contant Stored Property
let nameType = DefaultValue

 

클래스와 구조체에서 생성할 수 있으며, 저장속성은 인스턴스에 해당한다.
따라서 저장속성이 생길 때마다 새로운 메모리 공간을 요구한다.

문법에서 보듯 일반적인 변수와 상수를 선언하는 방식과 동일하다.
또한, 선언과 함께 초기화하는 경우 형식추론을 통해 형식을 생략하는 것도 가능하다.
다른 요소에 의존적인 값이라면 생성자를 통해 초기화 하는 것도 가능하다.

class Person {
	let name: String = "Jhon"
	var age: Int = 30
}

Person.name은 let으로 정의된 상수 저장 속성으로 앞으로는 값을 변경할 수 없다.
Person.age는 var로 정의된 변수 저장 속성으로 값을 변경할 수 있다.

Explicit Member Expression

 

Syntax

instanceName.propertyName
instanceName.propertyName = NewValue

 

class Person {
	let name: String = "Jhon"
	var age: Int = 30
}

//-----

let p = Person()
p.name
p.age
결과

"Jhon"
33

선언된 속성에는 익숙한 문법으로 접근한다.
해당 문법을 Dot Syntax(점문법) 혹은 Explicit Member Expression(명시적 멤버 표현식)이라고 한다.
swift에선 후자를 사용한다.

struct Person {
	let name: String = "Jhon"
	var age: Int = 30
}

var p = Person()

//-----

p.age = 60
p.age
결과

60

값을 바꿀 때에는 변수 저장 속성에 한해 점문법으로 접근해 변경한다.
또한 구조체와 클래스의 속성 문법이 동일하기 때문에 단순히 키워드만 바꿔줘도 호환이 가능하다.
다만 구조체의 가변성은 속성의 가변성에 영향을 주기 때문에 p를 상수로 선언하면 age도 변경할 수 없게 됨을 주의하자.

Lazy Stored Properties (지연 저장 속성)

지연 저장 속성은 초기화 시점은 지연시킨다는데 의미가 있다.

 

Syntax

lazy var name: Type = DefaultValue

 

일반 저장 속성은 속성이 생성될 때 초기화되지만
지연 저장 속성은 속성에 처음 접근할 때 초기화된다.
이런 특징 때문에 몇 가지 제약사항이 존재한다.

  1. 지연 저장 속성은 인스턴스가 생성된 이후 개별적으로 초기화된다.
    따라서 항상 변수 저장 속성으로 선언해야 한다.
  2. 생성자에서 초기화하지 않기 때문에 선언할 때 기본값을 저장해야 한다.
struct Image {
	init() {
		print("image")
	}
}

struct Post {
	let title: String = "title"
	let content: String = "content"
	let attach: Image = Image()
}

let post = Post()
결과

image

위와 같이 Post.attch를 저장 속성으로 선언하면 인스턴스 초기화와 함께 속성이 초기화되어 생성자에 선언해 둔 로그가 출력된다.

struct Image {
	init() {
		print("image")
	}
}

struct Post {
	let title: String = "title"
	let content: String = "content"
	lazy var attach: Image = Image()
}

let post = Post()
결과

Post.attch를 지연 저장 속성으로 바꿔보면 콘솔에는 아무것도 출력되지 않는 것을 확인할 수 있다.

struct Image {
	init() {
		print("image")
	}
}

struct Post {
	let title: String = "title"
	let content: String = "content"
	lazy var attach: Image = Image()
}

let post = Post()
post.attach
결과

//error

이때 속성에 접근을 시도하면 에러가 발생한다.
위에서 언급했든 구조체의 가변성은 속성의 가변성에도 영향을 준다.
인스턴스인 post가 let으로 선언되어 있으므로, 내부 속성인 attach 또한 영향을 받는다.
따라서 코드를 다음과 같이 수정한다.

struct Image {
	init() {
		print("image")
	}
}

struct Post {
	let title: String = "title"
	let content: String = "content"
	lazy var attach: Image = Image()
}

var post = Post()
post.attach
결과

image

post의 선언을 var로 바꾸면 속성의 변경 또한 가능해지고, 때문에
attach에 접근 함과 동시에 속성이 초기화되어 'image'를 콘솔에 출력한다.

struct Image {
	init() {
		print("image")
	}
}

struct Post {
	let title: String = "title"
	let content: String = "content"
	lazy var attach: Image = Image()
	
	let date: Date = Date()
	
	lazy var formattedDate: String = {
		let f = DateFormatter()
		f.dateStyle = .long
		f.timeStyle = .medium
		return f.string(from: date)
	} ()
}

var post = Post()
post.formattedDate
결과

"June 24, 2021 at 6:30:52 PM"

Post.formattedDate는 먼저 선언된 Post.date에 접근해 해당 데이터를 날짜 형식으로 바꿔 저장하는 지연 저장 속성이다.
속성에서 데이터를 저장하는 것 외에 클로저로 전달하거나 수식을 전달하는 등의 작업도 가능하지만 반환 값이 항상 미리 선언해 둔 자료형과 일치해야 함을 기억해 두자.
또한, 위와 같이 속성에서 다른 속성에 접근하는 경우 문법적으로 지연 저장 속성으로 선언해야 함을 기억해 두자.

 

Computed Property (계산 속성)

 

Syntax

var nameType {
    get {
        statements
        return expr
    }
    set(name) {
        statements
    }
}

 

Computed는 수식계산의 의미가 아닌 다른 속성에 의해 본인의 속성이 결정된다는 것을 의미한다.

계산 속성은 메모리 공간을 가지지 않는다.
대신 다른 속성의 값을 읽어서 가공 후 반환하거나 다른 속성에 저장한다.
때문에 접근할 때마다 값이 달라지는 경우가 생기므로, 즉시 호출하는 것이 좋다.
항상 변하는 값이므로 let으로 선언할 수 없다.

get을 사용하는 블록은 get block 혹은 getter
getter는 속성 값을 읽을 때 실행되며,
반드시 선언된 자료형과 같은 형태의 값을 반환하거나 다른 속성에 저장해야 한다.

set을 사용하는 블록은 set block 혹은 setter라고 부른다.
setter는 값을 저장할 때 실행된다.

계산 속성은 클래스와 구조체, 열거형에서 사용할 수 있다.

class Person {
	var name: String
	var yearOfBirth: Int
	
	init(name: String, year: Int) {
		self.name = name
		self.yearOfBirth = year
	}
}

Person 클래스는 문자열의 name, 정수의 yearOfBirth가 저장되고,
전달되는 값으로 초기화하는 생성자가 선언되어 있다.

class Person {
	var name: String
	var yearOfBirth: Int
	
	init(name: String, year: Int) {
		self.name = name
		self.yearOfBirth = year
	}
	
	var age: Int {
		get {
			let calendar  = Calendar.current
			let now = Date()
			let year = calendar.component(.year, from: now)
			return year - yearOfBirth
		}
		set {
			let calendar = Calendar.current
			let now = Date()
			let year = calendar.component(.year, from: now)
			yearOfBirth = year - newValue
		}
	}
}

age라는 새로운 계산 속성을 선언했다.
getter는 생년을 입력받으면 현재 년도에서 빼 나이를 유추해 반환한다.
setter는 생년을 입력 받으면 현재 년도에서 빼 나이를 유추해 가변 저장 속성인 yearOfBirth에 저장한다.
이때 setter에서 parameter 이름을 생략했다. 이런 경우 newValue를 parameter 이름의 기본값으로 사용하게 된다.

class Person {
	var name: String
	var yearOfBirth: Int
	
	init(name: String, year: Int) {
		self.name = name
		self.yearOfBirth = year
	}
	
	var age: Int {
		get {
			let calendar  = Calendar.current
			let now = Date()
			let year = calendar.component(.year, from: now)
			return year - yearOfBirth
		}
		set {
			let calendar = Calendar.current
			let now = Date()
			let year = calendar.component(.year, from: now)
			yearOfBirth = year - newValue
		}
	}
}

//-----

var p = Person(name: "Kim", year: 1966)
p.age
결과

55

새로운 인스턴스 p를 생성하면서 이름 "Kim"과 출생 연도 "1966"을 전달했다.
이후 p.age에 접근하면 getter에 의한 결과인 유추된 나이를 반환한다.

class Person {
	var name: String
	var yearOfBirth: Int
	
	init(name: String, year: Int) {
		self.name = name
		self.yearOfBirth = year
	}
	
	var age: Int {
		get {
			let calendar  = Calendar.current
			let now = Date()
			let year = calendar.component(.year, from: now)
			return year - yearOfBirth
		}
		set {
			let calendar = Calendar.current
			let now = Date()
			let year = calendar.component(.year, from: now)
			yearOfBirth = year - newValue
		}
	}
}

//-----

var p = Person(name: "Kim", year: 1966)
p.age = 70
p.yearOfBirth
결과

1951

이번엔 인스턴스를 생성한 다음, p.age 속성에 70을 저장 후 결과를 확인했다.
이렇게 되면 p.yearOdBirth의 값이 함께 바뀌게 된다.

Read-only Computed Properties

마지막 결과가 조금 이상하다.
나이를 바꿨더니 출생 연도가 함께 바뀌다니.
setter와 p.age를 바꾸는 부분을 삭제하면 읽기 전용 계산 속성이 된다.

 

Syntax

var nameType {
    get {
        statements
        return expr
    }
}

var nameType {
    statements
    return expr
}

 

setter가 없다는 걸 빼면 기존의 계산 속성과 동일하다.
또한 아래와 같이 get 키워드와 brace를 삭제해도 무방하다.
보통은 아래의 간소화된 문법을 사용한다.

class Person {
	var name: String
	var yearOfBirth: Int
	
	init(name: String, year: Int) {
		self.name = name
		self.yearOfBirth = year
	}
	
	var age: Int {
		let calendar  = Calendar.current
		let now = Date()
		let year = calendar.component(.year, from: now)
		return year - yearOfBirth
	}
}

//-----

var p = Person(name: "Kim", year: 1966)
p.yearOfBirth
결과

1966

이 경우엔 해당 속성이 초기화된 이후엔 값을 바꿀 수 없다.

 

Property Observer (속성 감시자)

 

Syntax

var nameType = DefaultValue {
    willSet(name) {
        statements
    }
    didSet(name) {
        statements
    }
}

 

속성 감시자 안에는 willSet과 didSet 블록이 만들어진다.
willSet은 이름 그대로 속성의 값이 저장되기 직전에 호출된다.
저장되는 값은 parameter로 전달되는데 이 parameter의 이름이 괄호 안에 들어간다.
이 또한 동일하게 parameter 이름을 생략할 수 있고, 이 경우 newValue라는 기본값이 사용되게 된다.

didSet은 willSet의 반대로, 값이 저장된 직후 호출된다.
따라서 이전 값이 parameter로 전달 되게 되며, parameter 이름을 생략할 수 있고, 이 경우 oldValue라는 기본값이 사용된다.

Property Observer는 변수 저장 속성에서만 사용할 수 있다.
또한 willSet, didSet 중 하나는 존재해야 한다.

class Size {
	var width = 0.0
}

width를 0.0으로 초기화하는 Size 클래스다.
width의 변화에 반응하도록 Property Oberver를 추가해 보자.

class Size {
	var width = 0.0 {
		willSet {
			print(width, "->", newValue)
		}
		didSet {
			print(oldValue, "->", width)
		}
	}
}

willSet은 저장되기 직전 호출되어 저장될 값을 출력한다.
didSet은 저장된 직후 호출되어 이전의 값을 출력한다.

class Size {
	var width = 0.0 {
		willSet {
			print(width, "->", newValue)
		}
		didSet {
			print(oldValue, "->", width)
		}
	}
}

//-----

var s = Size()
s.width = 100
결과

0.0 -> 100.0
0.0 -> 100.0

새로운 인스턴스 s를 생성한 후 s.width 속성을 변경했다.
새로운 값이 저장되기 직전 willSet이 호출된다.
Size.width의 willSet은 기존의 값과 새로 저장될 값을 출력하는데 이때 각각의 값은 0.0과 100.0이다.
새로운 값인 100이 저장된 이후 didSet이 호출된다.
Size.width의 didSet은 이전의 값과 저장된 값을 출력하는데 이때 각각의 값은 0.0과 100.0이다.

 

Type Property (형식 속성)

저장 속성은 인스턴스마다 다른 값이 저장된다.
따라서 각각의 계산 속성이 반환하는 값들도 인스턴스마다 달라질 수 있다.
이는 각각의 인스턴스가 각자의 메모리 공간을 가지기 때문이다.
이를 모두 포함해서 Instance Property(인스턴스 속성)이라고 부른다.

따라서 형식 속성은 형식 전체에 관한 속성이다.
이 형식 속성은 인스턴스마다 개별 생성되지 않고, 같은 형식으로 만들어진 인스턴스들이 모두 공유하는 속성이다.
형식 속성은 클래스, 구조체, 열거형에 사용할 수 있다.
이전에 언급된 저장 속성과 계산 속성으로 선언할 수 있다.
이들을 저장 형식 속성과 계산 형식 속성이라고 부른다.

Stored Type Property

 

Syntax

Variable Stored Type Property
static var nameType = DefaultValue

 

가장 앞에 붙는 static 키워드를 제외하면 동일하다.
하지만 기본값을 초기화할 수 없어 반드시 초기화해 주어야 한다.
저장 형식 속성은 최초로 형식에 접근할 때 초기화된다.
형식 속성은 인스턴스 이름으로는 접근이 불가능하다.

 

TypeName.propertyName

 

반드시 위와 같이 형식의 이름을 통해 접근해야 한다.

class Math {
	static let pi = 3.14
}

let m = Math()
m.pi
결과

//error

위와 같이 형식 속성은 인스턴스의 이름으로는 접근할 수 없다.

class Math {
	static let pi = 3.14
}

let m = Math()
Math.pi
결과

3.14

이렇게 형식의 이름으로 접근해야 한다.

형식 속성은 지연 속성이다.
위에서 언급했듯 처음 접근함과 동시에 초기화된다.

Computed Type Property

 

Syntax

static var name: Type {
    get {
        statements
        return expr
    }
    set(name) {
        statements
    }
}

class var name: Type {
    get {
        statements
        return expr
    }
    set(name) {
        statements
    }
}

 

계산 형식 속성도 static 키워드를 제외하면 동일하다.
다만 class에 한해 제한적으로 아래와 같이 사용하기도 한다.
둘의 차이는 오버라이딩의 가능 여부인데 static으로 선언한 경우엔 불가하지만 class로 선언한 경우엔 가능하다.
오버라이딩에 관해선 이후에 언급한다.

enum Weekday: Int {
	case sunday = 1, monday, tuesday, wednesday, thursday, friday, saturday
	
	static var today: Weekday {
		let cal = Calendar.current
		let today = Date()
		let weekday = cal.component(.weekday, from: today)
		return Weekday(rawValue: weekday)!
	}
}

Weekday.today
결과

thursday

Weekday는 원시 값으로 요일을 갖는 열거형 자료형이다.
해당 자료형 안에 today란 오늘의 날짜를 반환하는 계산 형식 속성을 선언한다.

 

self & super

self와 super는 형식에 추가되는 특별한 '속성'이다.

self

self는 인스턴스에 자동으로 추가되는 속성이다.
인스턴스 멤버 내부에서 접근하면 해당 인스턴스에 접근한다.
타입 멤버 내부에서 접근하면 형식 자체에 접근한다.

 

Syntax

self
인스턴스 자체에 접근

self.propertyName
인스턴스 속성에 접근

self.method()
인스턴스 method에 접근

self[index]
서브스크립트 호출

self.init(parameters)
동일한 형식에 있는 다른 생성자 호출

 

method와 propertyName은 앞의 self를 생략할 수 있지만
서브스크립트 호출과 생성자 호출을 self를 생략할 수 없다.
인스턴트 자체에 접근할

class Size {
	var width = 0.0
	var height = 0.0
	
	func calcArea() -> Double {
		return self.width * self.height
	}
}

정석대로라면 위와 같이 'self.'을 포함해서 작성해야 한다.
하지만 우리 눈에 익숙한 코드가 다음과 같듯이 'self.'을 생략 할 수 있다.

class Size {
	var width = 0.0
	var height = 0.0
	
	func calcArea() -> Double {
		return width * height
	}
}

이번엔 계산 속성을 추가하고 받은 값을 그대로 반환한다.

class Size {
	var width = 0.0
	var height = 0.0
	
	func calcArea() -> Double {
		return width * height
	}
	
	var area: Double {
		return self.calcArea()
	}
}

이 경우에도 'self.'을 생략한다.

class Size {
	var width = 0.0
	var height = 0.0
	
	func calcArea() -> Double {
		return width * height
	}
	
	var area: Double {
		return calcArea()
	}
}

이번엔 속성들을 업데이트한다.

class Size {
	var width = 0.0
	var height = 0.0
	
	func calcArea() -> Double {
		return width * height
	}
	
	var area: Double {
		return self.calcArea()
	}
	
	func update(width: Double, height: Double) {
		self.width = width
		self.height = height
	}
}

이런 경우 method의 변수 이름과 형식의 속성 이름이 같아 컴파일러가 구분하지 못한다.
이 때는 'self.'를 생략하지 못한다.

class Size {
	var width = 0.0
	var height = 0.0
	
	func calcArea() -> Double {
		return width * height
	}
	
	var area: Double {
		return self.calcArea()
	}
	
	func update(width: Double, height: Double) {
		self.width = width
		self.height = height
	}
	
	func something() {
		let c - { self.width * self.height }
	}
}

이번엔 클로저에서 속성에 접근하고 있다.
이 경우엔 'self.'를 반드시 캡처해야 한다.

이번엔 형식 속성과 형식 method를 추가한다.

class Size {
	var width = 0.0
	var height = 0.0
	
	func calcArea() -> Double {
		return width * height
	}
	
	var area: Double {
		return self.calcArea()
	}
	
	func update(width: Double, height: Double) {
		self.width = width
		self.height = height
	}
	
	func something() {
		let c - { self.width * self.height }
	}
	
	static let unit = ""
	
	static func doSomething() {
		self.width
	}
}
결과

//error

위에서 언급했듯 self를 타입 멤버에서 사용하면 형식을 의미한다.
타입 멤버에선 인스턴스 속성에 접근할 수 없다.

'self.'는 현재 인스턴스에 접근하기 위한 속성이다.
'self.'를 타입 멤버에서 사용하면 인스턴스가 아닌 형식을 나타낸다.

struct Size {
	var width = 0.0
	var height = 0.0
	
	mutating func reset(value: Double) {
		width = value
		height = value
	}
}

구조체로 바꾼 뒤 지정하는 값으로 초기화하는 method를 선언했다.

struct Size {
	var width = 0.0
	var height = 0.0
	
	mutating func reset(value: Double) {
		self = Size(width: value, height: value)
	}
}

이는 위와 같이 스스로를 불러와 생성자로 다시 생성하는 것으로도 구현 가능하다.
하지만 class에서 사용할 수 없어 범용성이 낮다.

super

super는 상속과 관련 있는 속성이기 때문에 상속을 지원하는 클래스에서만 사용할 수 있다.

 

Syntax

super.propertyName
super.method()
super[index]
super.init(parameters)

 

super는 상속과 오버라이딩 파트에서 다시 언급한다.

 

Self Type

중복되는 내용인 것 같지만 아니다.
위에서 언급된 self는 속성이고, 이번에 언급할 Self는 타입이다.
이름이 같지만 대상이 다르므로 엄격히 구분해서 사용해야 한다.

extension Int {
	static let zero: Int = 0
	
	var zero: Int {
		return 0
	}
	
	func makeZero() -> Int {
		return Int()
	}
}

extension의 첫 번째 형식 속성은 Int로, extension의 형식과 같다.
이때 Int eotls Self를 사용할 수 있다.

extension Int {
	static let zero: Self = 0

	var zero: Int {
		return 0
	}
	
	func makeZero() -> Int {
		return Int()
	}
}

다른 말로는 현재 타입이라고 할 수 있다.
이는 형식 속성뿐만이 아닌 저장 속성과 계산 속성에도 동일하게 적용할 수 있다.

extension Int {
	static let zero: Self = 0

	var zero: Self {
		return 0
	}
	
	func makeZero() -> Self {
		return Self()
	}
}

보기엔 별 의미 없는 것 같지만 코드의 형식에서 자유로워져 범용성이 높아진다는 장점이 있다.
다음을 보자

extension Double {
	static let zero: Int = 0
	
	var zero: Int {
		return 0
	}
	
	func makeZero() -> Int {
		return Int()
	}
}

extension의 형식이 Double로 바뀌었다.
따라서 안의 모든 속성들의 형식도 Double로 바꿔줘야 하는 번거로움이 생긴다.
이때 Self였다면 따로 수정하지 않아도 extension의 형식에 대응할 수 있게 된다.

extension Int {
	static let zero: Self = 0
	
	var zero: Self {
		return 0
	}
	
	func makeZero() -> Self {
		Self.zero
		return Self()
	}
}

인스턴스 멤버에서 타입 멤버에 접근할 때는 타입 이름을 통해 명시적으로 접근해야 한다.
이 때도 Self를 대신해서 사용할 수 있다.
생성자도 마찬가지이다.

 

Property Wrapper

보통은 저장 속성을 사용하지만
직접 속성을 조작하고 싶은 경우 계산 속성의 사용이 불가피하다.
계산 속성을 다시 생각해 보면
getter와 setter가 존재하고, 이 둘은 코드가 거의 동일했던 것을 떠올릴 수 있다.

var age: Int {
	get {
		let calendar  = Calendar.current
		let now = Date()
		let year = calendar.component(.year, from: now)
		return year - yearOfBirth
	}
	set {
		let calendar = Calendar.current
		let now = Date()
		let year = calendar.component(.year, from: now)
		yearOfBirth = year - newValue
	}
}

이전에 작성했던 계산 속성이다.
getter와 setter의 마지막 return문을 제외하면 정말 동일한 코드이다.

이렇게 반복되는 부분을 재사용할 수 있도록 도와주는 것이 property wrapper이다.

struct PlayerSetting {
	var initialSpeed: Double {
		get {
			return UserDefaults.standard.double(forKey: "initialSpeed")
		}
		set {
			UserDefaults.standard.set(newValue, forKey: "initialSpeed")
		}
	}

	var supportGesture: Bool {
		get {
			return UserDefaults.standard.bool(forKey: "supportGesture")
		}
		set {
			UserDefaults.standard.set(newValue, forKey: "supportGesture")
		}
	}
}

PlayerSetting은 설정 데이터를 저장하기 위해 UserDefaults를 사용하고 있다.
Player.initialSpeed의 getter는 UserDefaults에 저장된 값을 반환하고,
setter는 속성에 입력된 값을 UserDefault에 저장한다.

suppeortGesture도 동일하다.

이런 것들이 반복되면 생산성도 떨어지고, key로 문자열을 사용하기 때문에 오타로 인한 오류의 가능성도 높다.
Property Wrapper를 사용하면 이를 해결할 수 있다.

struct PlayerSetting {
	@UserDefaultHelper(key: "initialSpeed", defaultValue: 1.0)
	var initialSpeed: Double
	
	@UserDefaultHelper(key: "supportGesture", defaultValue: true)
	var supportGesture: Bool
}
	
@propertyWrapper
struct UserDefaultHelper<Value> {
	let key: String
	let defaultValue: Value
	
	var wrappedValue: Value {
		get {
			UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
		}
		set {
			UserDefaults.standard.setValue(newValue, forKey: key)
		}
	}
}

Property Wrapper는 @propertyWrapper 키워드로 작성한다.
이후엔 클래스, 구조체, 열거형을 선언하면 되고, 속성 추가도 같다.
다만 반드시 wrappedValue 속성에 계산 속성을 작성해야 함에 유의하자.

구조체 안에서 Property Wrapper를 사용하는 경우에도 Property Wrapper의 이름을 '@'와 함께 사용한다.

struct PlayerSetting {
	@UserDefaultHelper(key: "initialSpeed", defaultValue: 1.0)
	var initialSpeed: Double
	
	@UserDefaultHelper(key: "supportGesture", defaultValue: true)
	var supportGesture: Bool
}

@propertyWrapper
struct UserDefaultHelper<Value> {
	let key: String
	let defaultValue: Value
	
	var wrappedValue: Value {
		get {
			UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
		}
		set {
			UserDefaults.standard.setValue(newValue, forKey: key)
		}
	}
}

//-----

var currentSetting = PlayerSetting()

또한 값을 전달하는 역할을 Property Wrapper에서 위임했으므로 인스턴스 생성 시에 데이터를 전달할 필요가 없다.

struct PlayerSetting {
	@UserDefaultHelper(key: "initialSpeed", defaultValue: 1.0)
	var initialSpeed: Double
	
	@UserDefaultHelper(key: "supportGesture", defaultValue: true)
	var supportGesture: Bool
}

@propertyWrapper
struct UserDefaultHelper<Value> {
	let key: String
	let defaultValue: Value
	
	var wrappedValue: Value {
		get {
			UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
		}
		set {
			UserDefaults.standard.setValue(newValue, forKey: key)
		}
	}
}

var currentSetting = PlayerSetting()

//-----

currentSetting.initialSpeed
currentSetting.initialSpeed = 1.5
currentSetting.initialSpeed

currentSetting.supportGesture
currentSetting.supportGesture = false
currentSetting.supportGesture
결과

1
1.5
true
false

설정을 변경하면 알맞게 반응하는 것을 확인할 수 있다.

샘플은 단순해서 크게 차이가 안 나지만, 옵션이 늘어날수록 코드의 가독성 측면에서 유리하고,
각각이 모듈화 되어 있어, 유지보수 측면에서도 뛰어나다.

구조체, 클래스, 열거형에서 같은 방식으로 사용할 수 있다.

Projected Value

struct PlayerSetting {
	@UserDefaultHelper(key: "initialSpeed", defaultValue: 1.0)
	var initialSpeed: Double
	
	@UserDefaultHelper(key: "supportGesture", defaultValue: true)
	var supportGesture: Bool
}

@propertyWrapper
struct UserDefaultHelper<Value> {
	let key: String
	let defaultValue: Value
	
	var wrappedValue: Value {
		get {
			UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
		}
		set {
			UserDefaults.standard.setValue(newValue, forKey: key)
		}
	}
}

위에서 사용했던 예제 코드이다.
단순히 값의 입출력을 구현했을 뿐이지 Property Wrapper를 이렇게만 쓰라는 법은 없다.

struct PlayerSetting {
	@UserDefaultHelper(key: "initialSpeed", defaultValue: 1.0)
	var initialSpeed: Double
	
	@UserDefaultHelper(key: "supportGesture", defaultValue: true)
	var supportGesture: Bool
	
	func resetAll() {
		initialSpeed.reset()
	}
}

@propertyWrapper
struct UserDefaultHelper<Value> {
	let key: String
	let defaultValue: Value
	
	var wrappedValue: Value {
		get {
			UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
		}
		set {
			UserDefaults.standard.setValue(newValue, forKey: key)
		}
	}
	func reset() {
		UserDefaults.standard.setValue(defaultValue, forKey: key)
	}
}
결과

//error

Property Wrapper에서 method를 만들고 이를 구조체에서 사용하려 하면 error가 발생한다.
이런 이유는 호출의 방식 때문이다.

reset method는 Property Wrapper에서 선언했지만 현재 코드는 Double에서 reset method를 호출하고 있다.
따라서 찾을 수 없으므로 error가 발생하는 게 당연하다.
이를 다음과 같이 수정한다.

struct PlayerSetting {
	@UserDefaultHelper(key: "initialSpeed", defaultValue: 1.0)
	var initialSpeed: Double
	
	@UserDefaultHelper(key: "supportGesture", defaultValue: true)
	var supportGesture: Bool
	
	func resetAll() {
		_initialSpeed.reset()
	}
}

@propertyWrapper
struct UserDefaultHelper<Value> {
	let key: String
	let defaultValue: Value
	
	var wrappedValue: Value {
		get {
			UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
		}
		set {
			UserDefaults.standard.setValue(newValue, forKey: key)
		}
	}

	func reset() {
		UserDefaults.standard.setValue(defaultValue, forKey: key)
	}
}

호출문 앞에 '_'가 추가되었다.

initialSpeed 속성은 이미 Property Wrapper가 적용된 속성이다.
따라서 그냥 이름으로 접근하면 wrappedValue로 접근하게 되는데,
우리가 원하는 reset method에 접근하기 위해선 더 상위에 접근해야 한다.
'_'는 이를 가능하게 한다.

struct PlayerSetting {
	@UserDefaultHelper(key: "initialSpeed", defaultValue: 1.0)
	var initialSpeed: Double
	
	@UserDefaultHelper(key: "supportGesture", defaultValue: true)
	var supportGesture: Bool
	
	func resetAll() {
		_initialSpeed.reset()
		_supportGesture.reset()
	}
}

@propertyWrapper
struct UserDefaultHelper<Value> {
	let key: String
	let defaultValue: Value
	
	var wrappedValue: Value {
		get {
			UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
		}
		set {
			UserDefaults.standard.setValue(newValue, forKey: key)
		}
	}

	func reset() {
		UserDefaults.standard.setValue(defaultValue, forKey: key)
	}
}

이렇게 '_'를 통해 설청 초기화를 구현했다.

Property Wrapper는 접근 권한을 provate를 가지기 때문에 기본적으로 외부에서는 접근할 수 없다.
하지만 Projected Value를 사용하면 간접적으로 접근 할 수 있다.

@propertyWrapper
struct UserDefaultHelper<Value> {
	let key: String
	let defaultValue: Value
	
	var wrappedValue: Value {
		get {
			UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
		}
		set {
			UserDefaults.standard.setValue(newValue, forKey: key)
		}
	}
	
	func reset() {
		UserDefaults.standard.setValue(defaultValue, forKey: key)
	}

	var projectedValue: Self { return self }
}

projectedValue의 타입은 property의 타입과 동일하고, 반환도 self를 하는 것이 기본형이다.
어디까지나 기본이기 때문에 다르게 설정할 수도 있다.
하지만 이름은 항상 projectedValue로 고정해야 한다.

이렇게 projectedValue를 추가한 다음애는 간접적으로 접근할 수 있다.

struct PlayerSetting {
	@UserDefaultHelper(key: "initialSpeed", defaultValue: 1.0)
	var initialSpeed: Double
	
	@UserDefaultHelper(key: "supportGesture", defaultValue: true)
	var supportGesture: Bool
	
	func resetAll() {
		_initialSpeed.reset()
		_supportGesture.reset()
	}
}

@propertyWrapper
struct UserDefaultHelper<Value> {
	let key: String
	let defaultValue: Value
	
	var wrappedValue: Value {
		get {
			UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
		}
		set {
			UserDefaults.standard.setValue(newValue, forKey: key)
		}
	}
	
	func reset() {
		UserDefaults.standard.setValue(defaultValue, forKey: key)
	}
	
	var projectedValue: Self { return self }
}

var currentSetting = PlayerSetting()

currentSetting.initialSpeed = 7.0
currentSetting.initialSpeed
currentSetting.$initialSpeed.reset()
currentSetting.initialSpeed
결과

7
1

이때 접근하기 위해 이름 앞에 '$'를 추가한다.

속성에 Property Wrapper 속성을 추가하면 컴파일러가 필요한 코드를 자동으로 추가해 준다.
과 '$'로 시작하는 속성이 존재한다.
속성에 이름 만으로 접근하면 Wrapping 된 값에 접근한다.
'_'로 시작하는 속성은 타입 내부에서 Property Wrapper에 접근할 때 사용한다.
'$'로 시작하는 속성은 타입 외부에서 Property Wrapper에 접근 할 때 사용한다. 단, 사용하려면 Property Wrapper 내부에 projectedValue가 선언돼 있을 때만 사용 가능하다.

초기화 하기

@propertyWrapper
class SimpleWrapper {
	var wrappedValue: Int
	
	init() {
		print(#function)
		wrappedValue = 0
	}
}

이번엔 클래스로 구현된 Property Wrapper이다.
Property Wrapper를 추가하면 wrappedValue 속성을 반드시 추가해야 한다.
이때 보통은 계산 속성을 추가하지만 위처럼 일반적인 저장 속성을 추가하는 것도 가능하다.

@propertyWrapper
class SimpleWrapper {
	var wrappedValue: Int
	
	init() {
		print(#function)
		wrappedValue = 0
	}
}

//-----

struct MyType {
	@SimpleWrapper
	var a: Int
}

let t = MyType()

Property Wrapper를 구조체에 적용하면 결과를 확인할 수 있다.
의도한 대로 생성자가 정상적으로 작동한다.

Property Wrapper를 적용하면 구조체의 생성자가 아닌 Wrapper의 생성자가 함수를 초기화한다.
따라서 MyType에 변수를 전달하면

@propertyWrapper
class SimpleWrapper {
	var wrappedValue: Int
	
	init() {
		print(#function)
		wrappedValue = 0
	}
}

//-----

struct MyType {
	@SimpleWrapper
	var a: Int
}

let t = MyType(a: 12)
결과

//error

에러가 발생한다.

이번엔 원하는 값으로 초기화하도록 코드를 개선해 본다.

@propertyWrapper
class SimpleWrapper {
	var wrappedValue: Int
	
	init() {
		print(#function)
		wrappedValue = 0
	}

	init(value: Int) {
		print(#function)
		wrappedValue = value
	}
}


struct MyType {
	@SimpleWrapper
	var a: Int = 124
}
결과

//error

기본값을 전달하면 parameter가 하나인 생성자를 자동으로 호출하는데,
argument label이 wrappedValue인 생성자를 호출한다.
따라서 코드를 다음과 같이 수정한다.

@propertyWrapper
class SimpleWrapper {
	var wrappedValue: Int
	
	init() {
		print(#function)
		wrappedValue = 0
	}
	
	init(wrappedValue value: Int) {
		print(#function)
		wrappedValue = value
	}
}


struct MyType {
	@SimpleWrapper
	var a: Int = 124
}
결과

init(wrappedValue:)

정상적으로 실행이 된 것을 확인할 수 있다.

초기화하는 데는 두 가지 방법이 있다.

@propertyWrapper
class SimpleWrapper {
	var wrappedValue: Int
	
	init() {
		print(#function)
		wrappedValue = 0
	}
	
	init(wrappedValue value: Int) {
		print(#function)
		wrappedValue = value
	}
}


struct MyType {
	@SimpleWrapper
	var a: Int = 124
	
	@SimpleWrapper(wrappedValue: 45)
	var b: Int
}

let t = MyType()
결과

init(wrappedValue:)
init(wrappedValue:)
struct MyType {
	@SimpleWrapper
	var a: Int = 124
	
	@SimpleWrapper(wrappedValue: 45)
	var b: Int
}

두 가지 모두 같은 코드이지만 두 번째 코드를 더 자주 사용한다.

다만 지금과 같이 초기화하는 값이 하나라면 간단한 편이지만 값이 늘어난다면 조금 주의가 필요하다.

@propertyWrapper
class SimpleWrapper {
	var wrappedValue: Int
	var metadata: String?
	
	init() {
		print(#function)
		wrappedValue = 0
		metadata = nil
	}
	
	init(wrappedValue value: Int) {
		print(#function)
		wrappedValue = value
		metadata = nil
	}
	
	init(wrappedValue: Int, metadata: String?) {
		print(#function)
		self.wrappedValue = wrappedValue
		self.metadata = metadata
	}
}


struct MyType {
	@SimpleWrapper
	var a: Int = 124
	
	@SimpleWrapper(wrappedValue: 45)
	var b: Int
	
	@SimpleWrapper(wrappedValue: 123, metadata: "number")
	var c: Int
}

let t = MyType()
결과

init(wrappedValue:)
init(wrappedValue:)
init(wrappedValue:metadata:)

두 번째 방법처럼 특성을 전부 전달하는 방법이 있다.
또한, 데이터를 직접 전달하는 경우 wrappedValue에 접근한다는 것을 이용하면

struct MyType {
	@SimpleWrapper
	var a: Int = 124
	
	@SimpleWrapper(wrappedValue: 45)
	var b: Int
	
	@SimpleWrapper(wrappedValue: 123, metadata: "number")
	var c: Int
	
	@SimpleWrapper(metadata: "number")
	var d: Int = 123
}
결과

init(wrappedValue:)
init(wrappedValue:)
init(wrappedValue:metadata:)
init(wrappedValue:metadata:)

이렇게 접근하는 방법도 있다.

제약

  • Property Wrapper는 global scope에서 사용할 수 없다.
@propertyWrapper
class SimpleWrapper {
	var wrappedValue: Int
	var metadata: String?

	init() {
		print(#function)
		wrappedValue = 0
		metadata = nil
	}
	
	init(wrappedValue value: Int) {
		print(#function)
		wrappedValue = value
		metadata = nil
	}

	init(wrappedValue: Int, metadata: String?) {
		print(#function)
		self.wrappedValue = wrappedValue
		self.metadata = metadata
	}
}


struct MyType {
	@SimpleWrapper
	var a: Int = 124
	
	@SimpleWrapper(wrappedValue: 45)
	var b: Int
	
	@SimpleWrapper(wrappedValue: 123, metadata: "number")
	var c: Int
	
	@SimpleWrapper(metadata: "number")
	var d: Int = 123
}

let t = MyType()

@SimpleWrapper
var a: Int

 

결과

//error
  • 속성을 추가할 때는 다양한 키워드를 추가할 수 있다.
    단, lazy, @NSCopying, NSManaged, weak, unowned는 사용할 수 없다.
  • 계산 속성에는 Property Wrapper를 적용할 수 없다.
  • 서브 클래스에서 Property Wrapper가 적용된 속성을 오버라이딩 할 수 없다.
  • 프로토콜이나 익스텐션에 선언되어 있는 속성에는 Property Wrapper를 적용할 수 없다.

 


Log

2021.09.09.
블로그 이전으로 인한 글 옮김 및 수정