본문 바로가기

iOS/Swift

Property wrapper

Property wrapper는 일반적으로 공통되는 부분을 묶고 같은 보일러 플레이트가 작성 될 여지가 있을 때 Property wrapper를 사용하면 좀 더 직관적이고 클린한 코드를 작성할 수 있다.

 

💫 프로젝트에서 사용할 UserDefault를 PropertyWrapper를 사용해 관리해보자

Before

class Settings {
    static let shared = Settings()
    private init() {}
    
    var isDarkMode: Bool {
        get { UserDefaults.standard.bool(forKey: "isDarkMode") }
        set { UserDefaults.standard.set(newValue, forKey: "isDarkMode") }
    }
}

isDarkMode라는 프로퍼티는 UserDefault에 저장되어 있는 "isDarkMode" key의 값을 불러온다. 이러한 프로퍼티가 여러개가 필요할 때 같은 코드를 여러번 작성하는 불필요한 작업이 필요하다. 이 때 PropertyWrapper를 사용해 프로퍼티의 get, set 부분을 보일러플레이트로 묶어 쉽게 구현이 가능하다.

After

@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T
    
    var wrappedValue: T {
        get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
        set { UserDefaults.standard.set(newValue, forKey: key) }
    }
}

@propertyWrapper를 사용하면 쉽게 프로퍼티 래퍼를 구현할 수 있다.

class Settings {
    static let shared = Settings()
    private init() {}
    
    var isDarkMode: Bool {
        get { UserDefaults.standard.bool(forKey: "isDarkMode") }
        set { UserDefaults.standard.set(newValue, forKey: "isDarkMode") }
    }
    
    // 👇 UserDefault Property Wrapper를 사용
    @UserDefault(key: "isSilentMode", defaultValue: false)
    var isSilentMode: Bool
}

print("\(Settings.shared.isSilentMode)") // false
Settings.shared.isSilentMode.toggle()
print("\(Settings.shared.isSilentMode)") // true

이제 위에서 만든 UserDefault property wrapper를 적용할 변수의 앞 or 상단에 추가하여 작성하면 해당 프로퍼티에 프로퍼티 래퍼가 적용된다.

 

 

 

 

 

+) Apple swift-evolution에서 소개 된 COW PropertyWrapper 예제

protocol Copyable: AnyObject {
  func copy() -> Self
}

@propertyWrapper
struct CopyOnWrite<Value: Copyable> {
  init(wrappedValue: Value) {
    self.wrappedValue = wrappedValue
  }
  
  private(set) var wrappedValue: Value
  
  var projectedValue: Value {
    mutating get {
      if !isKnownUniquelyReferenced(&wrappedValue) {
        wrappedValue = wrappedValue.copy()
      }
      return wrappedValue
    }
    set {
      wrappedValue = newValue
    }
  }
}
@CopyOnWrite var storage: MyStorageBuffer

// Non-modifying access:
let index = storage.index(of: …)

// For modification, access $storage, which goes through `projectedValue`:
$storage.append(…)