Property Wrapper

Mason Kim·2023년 6월 29일
0

swift

목록 보기
4/7

정의

Property Wrappers는 Swift 5.1 버전부터 도입된 기능으로, 프로퍼티의 접근을 특정 로직을 통해 제어할 수 있게 해주는 유용한 기능입니다. 이를 통해 코드 중복을 줄이고, 프로퍼티의 사용법을 명확하게 할 수 있습니다.

💼 기본 사용법

  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
            }
        }
    }
    
  2. Property Wrapper 적용

    프로퍼티 래퍼를 적용하려면 클래스나 구조체의 프로퍼티 앞에 @ 기호와 함께 래퍼 이름을 사용합니다.

    이렇게 하면 프로퍼티에 대한 접근 로직이 프로퍼티 래퍼에서 정의한 대로 처리됩니다.

    class MyClass {
        @MyPropertyWrapper
        var value: Int = 0
    }

✅ 활용 예시

1. UserDefaults

간단한 예제로, 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?
}

2. 스레드 동기화

여러 스레드에서 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
}

관련 구현에 대한 심화적인 정보는 해당 아티클을 참고:

Swift Atomic Properties with Property Wrappers

3. 값 검증

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
}

4. DynamicUIColor

다크모드, 라이트모드에 따라 동적으로 결정되는 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

💥 심화

Projected Value

프로퍼티 래퍼와 관련된 추가 정보나 기능을 노출하려면 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

@PublishedprojectedValue

Publisher@PublishedprojectedValue로 제공

내부 구현

컴바인을 따라 만든 오픈소스 라이브러리인 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

📚 활용 라이브러리

ValidatedPropertyKit

값 검증을 property Wrapper 로 쉽게 해주는 라이브러리

https://github.com/SvenTiigi/ValidatedPropertyKit

Burritos

잘 테스트된 Swift 속성 래퍼 모음 → 사용 예시 학습에 좋을 듯

https://github.com/guillermomuntaner/Burritos

BetterCodable

Codable 을 구현할 때 귀찮은 init(from decoder: Decoder) throws 를 안해도 되게 만든 라이브러리

  • ex) 디코딩하는 배열, 딕셔너리의 값이 null 로 넘어올 때 자동으로 제거하거나 빈값으로 변환해주게…

https://github.com/marksands/BetterCodable

📖 참고

공식문서:

Documentation

아티클:

Property wrappers in Swift | Swift by Sundell

Proposal:

swift-evolution/0258-property-wrappers.md at main · apple/swift-evolution

WWDC:

Modern Swift API Design - WWDC19 - Videos - Apple Developer

profile
iOS developer

0개의 댓글