안녕하세요🐶
빈지식 채우기의 비니🙋🏻♂️ 입니다.
최근에 프로젝트에서 SwiftUI 를 사용할 일이 있었습니다.
평소에 SwiftUI에 대해 사용할 일이 많기 없었기 때문에 구글링을 통해 배껴서(?) 작업을 했는데,
점점 더 활용도가 높아져 공부할 필요성을 느껴 핵심만 골라 배우는 SwiftUI 기반의 iOS 프로그래밍 이라는 책을 구매했습니다.
공부하다가 정리하면 좋을 내용이 있어 작성하게 되었습니다.
오늘의 주제는 프로퍼티 래퍼 라는 내용입니다.
바로 가시죠~
1. 프로퍼티 래퍼 이해하기
실제로 우리가 작업할때,
여러 클래스나 구조체에 생성한 연산 프로퍼티들이 유사한 패턴을 갖는 경우가 빈번하게 발생한다.
간단하게 로직을 공유하는 방법은 유사한 패턴의 코드를 복사하여 각각의 클래스나 구조체에 포함시키는 것이였다.
이러한 방법은 너무 비효율적일 뿐만 아니라,
계산 방법이 달라지면 해당 코드가 들어간 모든 구간을 찾아 수정해야 한다는 것이다. ( 넘나 비효율적 ㅠ.ㅠ )
이러한 단점을 개선하기 위해 나온 기능이 프로퍼티 래퍼이다!
기본적으로 연산 프로퍼티의 기능을 클래스와 구조체에서 분리시켜,
앱 코드에서 재사용할 수 있게 만들어준다.
2. 간단한 프로퍼티 래퍼 예제
다음과 같이 도시 이름을 저장하는 String 프로퍼티를 가진 구조체가 있다고 하자.
struct Address {
private var cityName: String = ""
var city: String {
get { cityName }
set { cityName = newValue.uppercased() }
}
}
- 도시 이름이 프로퍼티에 할당되면 연산 프로퍼티의 세터(Set)가 private cityname에 저장하기 전에 대문자로 변환한다.
var address = Address()
address.city = "London"
print(address.city)
- 연산 프로퍼티는 도시 이름의 문자열을 대문자로 변환하였다.
- 이와 동일한 작업이 다른 구조체나 클래스에 있을 경우 코드를 복사하여 붙여넣는 방법이 있다.
- 하지만 많은 양의 코드의 경우 적절하지 않고 비효율적이다.
연산 프로퍼티를 사용하는 대신 이 로직을 프로퍼티 래퍼로 구현할 수 있다.
예를 들어,
다음의 선언부는 문자열을 대문자로 변환하도록 설계된 FixCase 라는 프로퍼티 래퍼를 구현한다.
@propertyWrapper
struct FixCase {
private(set) var value: String = ""
var wrappedValue: String { // 프로퍼티 래퍼 구현 시 항상 포함되어야함
get { value }
set { value = newValue.uppercased() }
}
init(wrappedValue initialValue: String) {
self.wrappedValue = initialValue
}
}
- 프로퍼티 래퍼는 @propertyWrapper 지시자를 이용하여 선언한다.
- 모든 프로퍼티 래퍼는 wrappedValue 프로퍼티를 가져야 한다.
- 앞의 코드는 초기의 값을 문자열을 대문자로 변환하고 private 변수에 저장하는 wrappedValue 프로퍼티에 할당한다.
struct Contact {
@FixCase var name: String // FixCase 프로퍼티 래퍼 선언
@FixCase var city: String
@FixCase var country: String
}
var contact = Contact(name: "john Smith", city: "London", country: "United Kingdom")
print("\(contact.name), \(contact.city), \(contact.country)")
프로퍼티 래퍼를 사용하면 위 처럼 FixCase ( 대문자로 변환하는 프로퍼티 래퍼 ) 를 선언하고
필요한 프로퍼티 앞에 @FixCase 지시자를 붙이면 된다.
3. 여러 변수와 타입 지원하기
어떤 작업을 수행할 때 사용될 여러 값을 받도록 좀 더 복잡한 프로퍼티 래퍼를 구현할 수 있다.
@propertyWrapper
struct MinMaxVal {
var value: Int
let max: Int
let min: Int
init(value: Int, max: Int, min: Int) {
self.value = value
self.max = max
self.min = min
}
var wrappedValue: Int {
get { return value }
set {
if newValue > max {
value = max
} else if newValue < min {
value = min
} else {
value = newValue
}
}
}
}
- init() 메서드는 래퍼 값에 추가된 max 와 min을 받아서 구현된다.
- wrappedValue 세터(set)는 값이 특정 범위 안에 있는지를 검사하여 그 값을 할당한다.
struct Demo {
@MinMaxVal(max: 200, min: 100) var value: Int = 100
}
var demo = Demo()
demo.value = 150
print(demo.value)
demo.value = 250
print(demo.value)
- 첫 번째 실행문은 150을 출력한다. 왜냐하면 150은 min 과 max 허용 범위 내에 들어있기 때문이다.
- 두 번째 실행문은 200을 출력한다. 왜냐하면 250은 max 값인 200보다 크기 때문에 max 값인 200을 출력한다.
지금까지 보았던 예제는 단순히 Int 값만 가지고 구현되었다.
만약 동일한 타입의 다른 값과 비교할 수 있는 모든 변수 타입과 함께 사용된다면,
좀더 범용적으로 사용할 수 있다.
@propertyWrapper
struct MinMaxVal2<V: Comparable> {
var value: V
var max: V
var min: V
init(wrappedValue: V, max: V, min: V) {
value = wrappedValue
self.max = max
self.min = min
}
...
...
}
- Comparable 프로토콜을 사용함으로써 해당 프로토콜을 따르는 타입은 값 비교가 가능한 타입에 사용할 수 있다.
감사합니다.
참고
- 핵심만 골라 배우는 SwiftUI 기반의 iOS 프로그래밍 CHAPTER 13