Property Wrappers는 Swift 5.1 버전부터 도입된 기능으로, 프로퍼티의 접근을 특정 로직을 통해 제어할 수 있게 해주는 유용한 기능입니다. 이를 통해 코드 중복을 줄이고, 프로퍼티의 사용법을 명확하게 할 수 있습니다.
Property Wrapper 정의
프로퍼티 래퍼를 정의하려면 먼저 @propertyWrapper
속성을 사용하여 구조체를 만듭니다.
그런 다음 wrappedValue
라는 이름의 변수를 사용하여 래핑되는 값의 타입을 정의하고, 필요한 초기화 및 접근 로직을 구현합니다.
@propertyWrapper
struct MyPropertyWrapper<T> {
private var value: T
init(wrappedValue: T) {
self.value = wrappedValue
}
var wrappedValue: T {
get {
return value
}
set {
value = newValue
}
}
}
Property Wrapper 적용
프로퍼티 래퍼를 적용하려면 클래스나 구조체의 프로퍼티 앞에 @
기호와 함께 래퍼 이름을 사용합니다.
이렇게 하면 프로퍼티에 대한 접근 로직이 프로퍼티 래퍼에서 정의한 대로 처리됩니다.
class MyClass {
@MyPropertyWrapper
var value: Int = 0
}
간단한 예제로, UserDefaults로 로컬 스토리지에 값을 저장하는 프로퍼티를 만들어보겠습니다.
@propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
init(key: String, defaultValue: Value) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: Value {
get {
return UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
위 코드에서 @propertyWrapper
어노테이션을 사용하여 UserDefaultsBacked
라는 프로퍼티 래퍼를 생성했습니다. 이를 이용하여 로컬 스토리지를 접근하는 프로퍼티를 쉽게 만들 수 있습니다.
class Settings {
@UserDefault(key: "isDarkModeEnabled", defaultValue: false)
var isDarkModeEnabled: Bool
@UserDefault(key: "lastUpdated", defaultValue: nil)
var lastUpdated: Date?
}
여러 스레드에서 synchronous하게 접근할 수 있는 프로퍼티를 생성하기 위해 Property Wrapper를 사용할 수 있습니다. 예를 들어, DispatchQueue
를 사용하여 프로퍼티에 대한 동시 접근을 제어할 수 있습니다.
@propertyWrapper
struct Synchronized<Value> {
private var value: Value
private let queue = DispatchQueue(label: "Synchronized")
init(wrappedValue: Value) {
self.value = wrappedValue
}
var wrappedValue: Value {
get {
return queue.sync { value }
}
set {
queue.sync { value = newValue }
}
}
}
class Counter {
@Synchronized
var count: Int = 0
}
관련 구현에 대한 심화적인 정보는 해당 아티클을 참고:
Property Wrappers를 사용하여 입력 값에 대한 유효성 검사를 적용할 수 있습니다.
예를 들어, 이메일 주소의 형식이 올바른지 확인하는 프로퍼티를 만들 수 있습니다.
@propertyWrapper
struct ValidatedEmail {
private var value: String
init(wrappedValue: String) {
self.value = wrappedValue
}
var wrappedValue: String {
get {
return value
}
set {
if isValidEmail(newValue) {
value = newValue
}
}
}
private func isValidEmail(_ email: String) -> Bool {
// 이메일 유효성 검사 로직
}
}
class User {
@ValidatedEmail
var email: String
}
다크모드, 라이트모드에 따라 동적으로 결정되는 UIColor 세팅
@propertyWrapper
public struct DynamicUIColor {
public enum Style {
case light, dark
}
let light: UIColor
let dark: UIColor
let styleProvider: () -> Style?
public init(
light: UIColor,
dark: UIColor,
style: @autoclosure @escaping () -> Style? = nil
) {
self.light = light
self.dark = dark
self.styleProvider = style
}
public var wrappedValue: UIColor {
switch styleProvider() {
case .dark: return dark
case .light: return light
case .none:
return UIColor { traitCollection -> UIColor in
switch traitCollection.userInterfaceStyle {
case .dark: return self.dark
case .light, .unspecified: return self.light
@unknown default: return self.light
}
}
}
}
}
사용 예시
@DynamicUIColor(light: .white, dark: .black)
var backgroundColor: UIColor
// The color will automatically update when traits change
view.backgroundColor = backgroundColor
프로퍼티 래퍼와 관련된 추가 정보나 기능을 노출하려면 projectedValue
를 사용합니다. 이를 통해 보조 데이터를 제공하거나, 프로퍼티 래퍼에게 추가적인 동작을 수행하도록 할 수 있습니다.
projectedValue
를 사용하려면 프로퍼티 래퍼 구조체에 projectedValue
를 정의해야 합니다. 그리고 원하는 값을 반환하도록 구현하면 됩니다.
@propertyWrapper
struct WrapperWithProjectedValue<T> {
var wrappedValue: T
var projectedValue: String
init(wrappedValue: T, projectedValue: String) {
self.wrappedValue = wrappedValue
self.projectedValue = projectedValue
}
}
프로퍼티에 프로퍼티 래퍼를 적용한 후 $
기호를 사용하여 projectedValue
에 접근할 수 있습니다.
class MyClass {
@WrapperWithProjectedValue(projectedValue: "Example")
var value: Int = 0
}
let myInstance = MyClass()
print(myInstance.$value) // 출력: Example
@Published
의 projectedValue
Publisher는 @Published의
projectedValue
로 제공
컴바인을 따라 만든 오픈소스 라이브러리인 OpenCombine의 코드를 참고
@available(swift, introduced: 5.1)
@propertyWrapper
public struct Published<Value> {
...
public var projectedValue: Publisher {
mutating get {
return **getPublisher**()
}
set { // swiftlint:disable:this unused_setter_value
switch storage {
case .value(let value):
storage = .publisher(Publisher(value))
case .publisher:
break
}
}
}
internal func **getPublisher**() -> Publisher {
switch storage {
case .value(let value):
let publisher = Publisher(value)
storage = .publisher(publisher)
return publisher
case .publisher(let publisher):
return publisher
}
}
...
private enum Storage {
case value(Value)
case publisher(Publisher)
}
@propertyWrapper
private final class Box {
var wrappedValue: Storage
init(wrappedValue: Storage) {
self.wrappedValue = wrappedValue
}
}
...
public struct Publisher: OpenCombine.Publisher {
public typealias Output = Value
public typealias Failure = Never
fileprivate let subject: PublishedSubject<Value>
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Value, Downstream.Failure == Never
{
subject.subscribe(subscriber)
}
fileprivate init(_ output: Output) {
subject = .init(output)
}
}
class Counter: ObservableObject {
@Published var value: Int = 0
}
let counter = Counter()
let cancellable = counter.**$value**
.sink { newValue in
print("Counter value changed to \(newValue)")
}
counter.value = 1
counter.value = 2
counter.value = 3
값 검증을 property Wrapper 로 쉽게 해주는 라이브러리
https://github.com/SvenTiigi/ValidatedPropertyKit
잘 테스트된 Swift 속성 래퍼 모음 → 사용 예시 학습에 좋을 듯
https://github.com/guillermomuntaner/Burritos
Codable 을 구현할 때 귀찮은
init(from decoder: Decoder) throws
를 안해도 되게 만든 라이브러리
- ex) 디코딩하는 배열, 딕셔너리의 값이
null
로 넘어올 때 자동으로 제거하거나 빈값으로 변환해주게…
https://github.com/marksands/BetterCodable
공식문서:
아티클:
Proposal:
swift-evolution/0258-property-wrappers.md at main · apple/swift-evolution
WWDC: