Property wrappers to the rescue! - Davide Repetto - Swift Heroes 2022
@State
, @Binding
, @StateObject
등 SwiftUI 프레임워크 내에서 낯설지 않은 타입@propertyWrapper
어트리뷰트로 정의되어야 함wrappedValue
프로퍼티를 가져야 함@propertyWrapper
struct Capitalized {
private var value: String = ""
var wrappedValue: String {
set {
value = newValue.capitalized
}
get {
return value
}
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
wrappedValue
의 get
, set
을 통해 주어진 value
를 조정capitalized
를 리턴 import Foundation
struct UserModel {
@Capitalized var firstName: String
@Capitalized var lastName: String
}
@
을 통해 해당 프로퍼티를 따른다고 선언하기만 하면 됨 (선언형)import Foundation
struct CapitalizedWrapper {
private var _value: String = ""
var value: String {
set {
_value = newValue.capitalized
}
get {
return _value
}
}
init(value: String) {
self._value = value.capitalized
}
}
@propertyWrapper
없이 해당 방법을 구현하는 로직은 상동import Foundation
struct WrappedUserModel {
var firstName: CapitalizedWrapper
var lastName: CapitalizedWrapper
}
value
를 찾아야 하는 단계가 필요let wrappedUser = WrappedUserModel(firstName: CapitalizedWrapper(value: "junyeong"), lastName: CapitalizedWrapper(value: "park"))
wrappedUser.firstName.value // Junyeong
let user = UserModel(firstName: "junyeong", lastName: "park")
user.firstName // Junyeong
@propertyWrapper
struct Capitalized {
private var value: String = ""
var wrappedValue: String {
set {
value = newValue.capitalized
}
get {
return value
}
}
var projectedValue: Capitalized {
return self
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
projectedValue
라는 자기 자신을 리턴했다면, $
으로 접근 가능user.$firstName
$
을 통해 해당 주소 접근 가능@propertyWrapper
struct PlainUserDefaultsBacked<T> {
let key: String
let defaultValue: T
var storage: UserDefaults = .standard
var wrappedValue: T {
get {
let value = storage.value(forKey: key) as? T
return value ?? defaultValue
}
set {
storage.setValue(newValue, forKey: key)
}
}
}
import Foundation
@propertyWrapper
struct CodableUserDefaultsBacked<T: Codable> {
let key: String
let defaultValue: T
var storage: UserDefaults = .standard
var wrappedValue: T {
get {
guard
let data = storage.value(forKey: key) as? Data,
let value = try? JSONDecoder().decode(T.self, from: data) else {
return defaultValue
}
return value
}
set {
let data = try? JSONEncoder().encode(newValue)
storage.setValue(data,forKey: key)
}
}
}
Codable
프로토콜을 따르는 제네릭을 받은 뒤 구조체 내에서 자동으로 인코딩과 디코딩을 해줄 수 있음import Foundation
struct NoteModel: Codable {
let title: String
}
Codable
프로토콜을 따르는 데이터 모델import Foundation
class UserDefaultsDataSource {
@PlainUserDefaultsBacked(key: "is_first_launch", defaultValue: true)
static var isFirstLaunch: Bool
@PlainUserDefaultsBacked(key: "user_name", defaultValue: "unknown")
static var userName: String
@PlainUserDefaultsBacked(key: "counter", defaultValue: 0, storage: .standard)
static var counter: Int
@CodableUserDefaultsBacked(key: "notes", defaultValue: nil)
static var notes: [NoteModel]?
}
static
으로 선언한 해당 변수를 통해 현 시점의 유저 디폴트 값에 접근하거나 수정 가능import UIKit
class PropertyWrapperController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print(UserDefaultsDataSource.isFirstLaunch) // true
print(UserDefaultsDataSource.userName) // unknown
print(UserDefaultsDataSource.counter) // 0
UserDefaultsDataSource.isFirstLaunch = false
UserDefaultsDataSource.userName = "Junyeong"
UserDefaultsDataSource.counter += 1
print(UserDefaultsDataSource.isFirstLaunch) // false
print(UserDefaultsDataSource.userName) // Junyeong
print(UserDefaultsDataSource.counter) // 1
print(UserDefaultsDataSource.notes) // nil
UserDefaultsDataSource.notes = [NoteModel(title: "First Note")]
print(UserDefaultsDataSource.notes)
// [NoteModel(title: "First Note")]
UserDefaultsDataSource.notes?.append(NoteModel(title: "Second Note"))
print(UserDefaultsDataSource.notes)
// [NoteModel(title: "First Note"), NoteModel(title: "Second Note")]
}
}
@propertyWrapper
struct ImageAsset {
let key: String
init(_ key: String) {
self.key = key
}
func image(for name: String) -> UIImage {
UIImage(named: name) ?? .init()
}
var projectedValue: String {
return key
}
var wrappedValue: UIImage {
self.image(for: key)
}
}
projectedValue
는 키를 리턴wrappedValue
는 이니셜라이즈에 사용된 키를 통해 내장 함수 image
를 사용, 주어진 이미지를 리턴enum Asset {
@ImageAsset("menu_icon") static var menuIcon: UIImage
@ImageAsset("search_icon") static var searchIcon: UIImage
@ImageAsset("settings_icon") static var settingsIcon: UIImage
@ImageAsset("plus_icon") static var plusIcon: UIImage
}
static
으로 가지고 있는 이넘을 통해 손쉽게 어셋 이미지 접근 가능import UIKit
class AssetViewController: UIViewController {
private let imageView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = Asset.menuIcon
imageView.image = Asset.plusIcon
imageView.image = Asset.searchIcon
imageView.image = Asset.settingsIcon
}
}
enum Colors {
static var mainRed: UIColor {
UITraitCollection.current.userInterfaceStyle == .dark ? UIColor.black : UIColor.red
}
}
@propertyWrapper
struct Color {
var light: UIColor
var dark: UIColor
var isDark: Bool {
UITraitCollection.current.userInterfaceStyle == .dark
}
var projectedValue: Color { return self }
var wrappedValue: UIColor {
if isDark {
return dark
} else {
return light
}
}
}
enum Colors {
@Color(light: .red, dark: .black) static var mainRed2
}
import UIKit
class ColorViewController: UIViewController {
private let imageView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Colors.mainRed2
let lightColor: UIColor = Colors.$mainRed2.light
let darkColor: UIColor = Colors.$mainRed2.dark
let usingDarkColor: Bool = Colors.$mainRed2.isDark
}
}
projectedValue
로 리턴해주고 있기 때문에 $
를 통해 접근 가능Color
를 선언할 때 파라미터로 넣은 값에 접근 가능unwrapped
) 프로퍼티처럼 사용 가능projectedValue
가 있다면 $
마크를 통해 접근 가능 → 값 자체에 접근할 수 있기 때문에 위험할 수 있음항상 주어진
@State
,@Binding
등에서만 접하면 프로퍼티 래퍼를 직접 커스텀하고 선언해보면서 바깥에서 물론 작업해줄 수 있는 일이지만 보다 불필요한 코드를 줄이고 코드를 클린하게 만들어줄 수 있는 해당 기능에 감탄했다.