Swift 5.1에서 추가된 기능인 Property Wrapper에 대해서 살펴보도록 하겠습니다.
연산 프로퍼티의 중복되는 로직을 프로퍼티 래퍼에 한 번만 구현하고,
이 로직을 필요로 하는 프로퍼티들 자체에 연결하여 사용할 수 있게 하여
코드의 재사용성을 높일 수 있습니다.
struct, class, enum을 만들고 @propertyWrapper 를 맨 앞에 명시해주면 프로퍼티 래퍼를 정의할 수 있습니다.
그리고 반드시 wrappedValue라는 연산 프로퍼티를 구현하여야 합니다.
UserDefaults 예제를 통해 한 번 알아보겠습니다. 먼저 Property Wrapper 전의 코드 상태입니다.
class UserManager {
static var usesTouchID: Bool {
get { return UserDefaults.standard.bool(forKey: "usesTouchID") }
set { UserDefaults.standard.set(newValue, forKey: "usesTouchID") }
}
static var myEmail: String {
get { return UserDefaults.standard.string(forKey: "myEmail") }
set { UserDefaults.standard.set(newValue, forKey: "myEmail") }
}
static var numberOfAccounts: Bool {
get { return UserDefaults.standard.bool(forKey: "numberOfAccounts") }
set { UserDefaults.standard.set(newValue, forKey: "numberOfAccounts") }
}
}
첫째로, UserDefaults를 활용하여 값을 읽거나 쓸 때, 그 key 값으로 항상 String을 사용하고 있는 것을 확인할 수 있습니다.
둘째로, 값을 읽거나 쓸 때, 값의 타입과 관계없이 UserDefaults.standard까지는 동일하게 사용하고 있는 것 역시 확인할 수 있습니다.
마지막으로 가장 중요한건, 위 클래스의 세 연산 프로퍼티가 결국 공통적인 로직을 공유하고 있습니다.
위 세 가지 특징을 파악했으니, 이를 활용하여 Property Wrapper를 이용해서 개선해보도록 하겠습니다.
@propertyWrapper
struct UserDefault<T> {
let key: String // key는 항상 String 타입let defaultValue: T
let defaultValue: T
private let storage: UserDefaults
var wrappedValue: T { // property wrapper를 사용하려면 반드시 이 이름으로 연산 프로퍼티를 구현하여야 함
get {
storage.object(forKey: key) as? T ?? defaultValue
}
set {
storage.set(newValue, forKey: key)
}
}
init(key: String, defaultValue: T, storage: UserDefaults = .standard) {
self.key = key
self.defaultValue = defaultValue
self.storage = storage
}
}
class UserManager {
@UserDefault(key: "usesTouchID", defaultValue: false)
static var usesTouchID: Bool
@UserDefault(key: "myEmail", defaultValue: "")
static var myEmail: String
@UserDefault(key: "numberOfAccounts", defaultValue: 0)
static var numberOfAccounts: Int
}
UserManager.usesTouchID = true // set
print(UserManager.usesTouchID) // get
UserManager.myEmail = "myEmail" // set
print(UserManager.myEmail) // get
UserManager.numberOfAccounts = 3 // set
print(UserManager.numberOfAccounts) // get
위 코드의 결과는 다음과 같습니다.
true
myEmail
3
원하는대로 동작했음을 확인할 수 있습니다.
추가로 제공하고 싶은 정보가 있다면 어떤 타입이라도 projectedValue를 정의하여 사용할 수 있습니다.
이 값은 프로퍼티 래퍼로 감싼 프로퍼티 앞에 $를 붙여 접근할 수 있습니다.
예를 들어, 위의 예시에서 defaultValue 값을 알고는 있지만 UserManager 클래스를 통해 현재 설정되어 있는 defaultValue를 갖고 오고 싶다고 가정을 해보겠습니다. 현재 상태에서는 바로 defaultValue에 접근할 수 없습니다. 이 때, projectValue를 활용하면 쉽게 defaultValue를 얻어낼 수 있습니다.
@propertyWrapper
struct UserDefault<T> {
let key: String // key는 항상 String 타입let defaultValue: T
let defaultValue: T
private let storage: UserDefaults
var wrappedValue: T { // property wrapper를 사용하려면 반드시 이 이름으로 연산 프로퍼티를 구현하여야 함
get {
storage.object(forKey: key) as? T ?? defaultValue
}
set {
storage.set(newValue, forKey: key)
}
}
var projectedValue: T { // 반드시 이 이름을 사용하여야 동작함
defaultValue
}
init(key: String, defaultValue: T, storage: UserDefaults = .standard) {
self.key = key
self.defaultValue = defaultValue
self.storage = storage
}
}
class UserManager {
@UserDefault(key: "usesTouchID", defaultValue: false)
static var usesTouchID: Bool
@UserDefault(key: "myEmail", defaultValue: "")
static var myEmail: String
@UserDefault(key: "numberOfAccounts", defaultValue: 0)
static var numberOfAccounts: Int
}
print(UserManager.$usesTouchID) // projectedValue
print(UserManager.$myEmail) // projectedValue
print(UserManager.$numberOfAccounts) // projectedValue
위 코드의 결과는 다음과 같습니다.
false
0
정상적으로 동작했음을 확인할 수 있습니다.
채고에요!