And that's a Wrap!

And that's a Wrap!

My talk about "Property Wrappers" that I gave at FrenchKit 2019.

Property Wrappers are a new feature in Swift 5.1 which lets you to create reusable get/set behaviors with custom logic and semantic meaning, and use them as annotations for your properties in Swift

Fb60bbce2fadcc41e950ef8dea3f29e5?s=128

AliSoftware

October 08, 2019
Tweet

Transcript

  1. And that’s a Wrap! Getting a taste of Property Wrappers

    FrenchKit October 2019 aligatr Swift 5.1 * burritos not provided *
  2. aligatr FrenchKit 2019 Imagine you want this class Movie {

    var progress: Double = 0.0 { didSet { self.progress = max(0.0, min(progress, 1.0)) } } var volume: Double = 1.0 { didSet { self.volume = max(0.0, min(volume, 1.0)) } } } let movie = Movie() movie.progress = 42.0 print(movie.progress)
  3. aligatr FrenchKit 2019 Create reusable behavior struct Clamped { var

    wrappedValue: Double { didSet { self.wrappedValue = max(0.0, min(wrappedValue, 1.0)) } } } class Movie { var progress: Clamped var volume : Clamped } let movie = Movie() movie.progress.wrappedValue = 42.0 print(movie.progress.wrappedValue)
  4. aligatr FrenchKit 2019 Create reusable behavior struct Clamped { var

    wrappedValue: Double { didSet { self.wrappedValue = max(0.0, min(wrappedValue, 1.0)) } } } class Movie { var progress: Clamped = Clamped(wrappedValue: 0.0) var volume : Clamped = Clamped(wrappedValue: 1.0) } let movie = Movie() movie.progress.wrappedValue = 42.0 print(movie.progress.wrappedValue)
  5. aligatr FrenchKit 2019 Property Wrappers to the rescue! It’s a

    bird! It’s a plane! It’s… wift 5.1!
  6. aligatr FrenchKit 2019 Introducing Property Wrappers struct Clamped { var

    wrappedValue: Double { didSet { self.wrappedValue = max(0.0, min(wrappedValue, 1.0)) } } } class Movie { var progress = Clamped(wrappedValue: 0.0) var volume = Clamped(wrappedValue: 1.0) } let movie = Movie() movie.progress.wrappedValue = 42.0 print(movie.progress.wrappedValue)
  7. aligatr FrenchKit 2019 Introducing Property Wrappers @propertyWrapper struct Clamped {

    var wrappedValue: Double { didSet { self.wrappedValue = max(0.0, min(wrappedValue, 1.0)) } } } class Movie { var progress = Clamped(wrappedValue: 0.0) var volume = Clamped(wrappedValue: 1.0) } let movie = Movie() movie.progress.wrappedValue = 42.0 print(movie.progress.wrappedValue)
  8. aligatr FrenchKit 2019 Introducing Property Wrappers @propertyWrapper struct Clamped {

    var wrappedValue: Double { didSet { self.wrappedValue = max(0.0, min(wrappedValue, 1.0)) } } } class Movie { @Clamped var progress = 0.0 // Double @Clamped var volume = 1.0 // Double } let movie = Movie() movie.progress = 42.0 print(movie.progress) ✨
  9. aligatr FrenchKit 2019 Compiler-Generated Code // We type this @Clamped

    var progress = 0.0 // And compiler generates this for us private var _progress: Clamped = Clamped(wrappedValue: 0.0) var progress: Double { get { return _progress.wrappedValue } set { _progress.wrappedValue = newValue } }
  10. aligatr FrenchKit 2019 Generics and Custom Range @propertyWrapper struct Clamped

    { var wrappedValue: Double { didSet { self.wrappedValue = max(0.0, min(wrappedValue, 1.0)) } } } class Movie { @Clamped var progress = 0.0 @Clamped var volume = 1.0 }
  11. aligatr FrenchKit 2019 Generics and Custom Range @propertyWrapper struct Clamped<Value:

    Comparable> { var wrappedValue: Value { didSet { self.wrappedValue = max(range.lowerBound, min(wrappedValue, range.upperBound)) } } let range: ClosedRange<Value> } class Movie { @Clamped var progress = 0.0 @Clamped var volume = 1.0 }
  12. aligatr FrenchKit 2019 Generics and Custom Range @propertyWrapper struct Clamped<Value:

    Comparable> { var wrappedValue: Value { didSet { self.wrappedValue = max(range.lowerBound, min(wrappedValue, range.upperBound)) } } let range: ClosedRange<Value> } class Movie { @Clamped(range: 0...1) var progress = 0.0 // Double @Clamped(range: 0...1) var volume = 1.0 // Double @Clamped(range: 0...5) var rating = 4 // Int }
  13. aligatr FrenchKit 2019 UserDefaults let defaults: UserDefaults = .standard struct

    MainUser { var name: String { get { defaults.object(forKey: "username") as? String ?? "Olivier" } set { defaults.set(newValue, forKey: "username") } } func demo() { print(name) name = "AliGator" } }
  14. aligatr FrenchKit 2019 UserDefaults let defaults: UserDefaults = .standard struct

    MainUser { var name: String { get { defaults.object(forKey: "username") as? String ?? "Olivier" } set { defaults.set(newValue, forKey: "username") } } func demo() { print(name) name = "AliGator" } }
  15. aligatr FrenchKit 2019 UserDefaults @propertyWrapper class UserDefault<T> { let defaults:

    UserDefaults = .standard var wrappedValue: T { get { defaults.object(forKey: "username") as? T ?? "Olivier" } set { defaults.set(newValue, forKey: "username") } } }
  16. aligatr FrenchKit 2019 UserDefaults @propertyWrapper class UserDefault<T> { let defaults:

    UserDefaults = .standard let key: String let defaultValue: T var wrappedValue: T { get { defaults.object(forKey: key) as? T ?? defaultValue } set { defaults.set(newValue, forKey: key) } } init(wrappedValue: T, key: String) { self.defaultValue = wrappedValue self.key = key } } At call site, this: @UserDefault(key: "username") var name = "Olivier"
  17. aligatr FrenchKit 2019 UserDefaults struct MainUser { @UserDefault(key: "username") var

    name: String = "Olivier" func demo() { print(name) // object(forKey: "username") name = "AliGator" // set("AliGator", forKey: “username") } }
  18. aligatr FrenchKit 2019 UserDefaults struct MainUser { @UserDefault(key: "username") var

    name: String = "Olivier" func demo() { print(name) // object(forKey: "username") name = "AliGator" // set("AliGator", forKey: “username") } } Compiler will generate:
 private var _name = UserDefault(wrappedValue: "Olivier", key: "username") var name: String { /* get & set proxying wrappedValue */ }
  19. aligatr FrenchKit 2019 Accessing the Wrapper struct MainUser { @UserDefault(key:

    "username") var name: String = "Olivier" func demo() { print(name) // object(forKey: "username") name = "AliGator" // set("AliGator", forKey: “username") print("\(_name.key) = \(name)") } } Compiler will generate:
 private var _name = UserDefault(wrappedValue: "Olivier", key: "username") var name: String { /* get & set proxying wrappedValue */ }
  20. aligatr FrenchKit 2019 Accessing the Wrapper @propertyWrapper struct UserDefault<T> {

    /* … */ let key: String var wrappedValue: T { … } } let user = MainUser() user.name /* access the wrapper’s wrappedValue */ user._name.key /* error: _name is private */ Compiler will generate: private var _name: UserDefault<T> var name: T {
 _name.wrappedValue
 } 
 

  21. aligatr FrenchKit 2019 Projected Value @propertyWrapper struct UserDefault<T> { /*

    … */ let key: String var wrappedValue: T { … } var projectedValue: String { key } } let user = MainUser() user.name /* access the wrapper’s wrappedValue */ user._name.key /* error: _name is private */ Compiler will generate: private var _name: UserDefault<T> var name: T {
 _name.wrappedValue
 } var $name: String {
 _name.projectedValue
 }
  22. aligatr FrenchKit 2019 Projected Value @propertyWrapper struct UserDefault<T> { /*

    … */ let key: String var wrappedValue: T { … } var projectedValue: String { key } } let user = MainUser() user.name /* access the wrapper’s wrappedValue */ user.$name /* access the wrapper’s projectedValue */ Compiler will generate: private var _name: UserDefault<T> var name: T {
 _name.wrappedValue
 } var $name: String {
 _name.projectedValue
 }
  23. aligatr FrenchKit 2019 Some More Inspiration @Versioned var name: String

    = "" // and $name returns the history @Undoable var name: String = "" // and $name.undo() restores previous values @Expirable(duration: 60) var token: String? @Validated({ $0.count > 8 }) var password: String = “Fr3nchKu1te" // and $password tells if it’s valid @Rounded(digits: 2) var price: Double = 0.0 @DBField(name: “firstname") var name: String @State var user: User // $user returns the Binding @Published var age: Int // $age.sink() { … } to subscribe
  24. aligatr FrenchKit 2019 One More Thing…

  25. aligatr FrenchKit 2019 PropertyWrappers and Codable struct Movie: Codable {

    let title: String let director: String let plot: String var releaseDate: Date? } /* { "director" : "…", "title" : "…", "plot" : "…", "releaseDate" : 590372414.71850204 } */ Need to implement Codable manually for every single property…
  26. aligatr FrenchKit 2019 PropertyWrappers and Codable struct Movie: Codable {

    let title: String let director: String let plot: String var releaseDate: Date? } /* { "director" : "…", "title" : "…", "plot" : "…", "releaseDate" : 590372414.71850204 } */ @propertyWrapper struct YMD { var wrappedValue: Date? } extension YMD: Codable { /* convert Date from/to YMD string via singleValueContainer */ } func encode(to encoder: Encoder) throws { … } init(from decoder: Decoder) throws { … }
  27. aligatr FrenchKit 2019 PropertyWrappers and Codable struct Movie: Codable {

    let title: String let director: String let plot: String @YMD var releaseDate: Date? } /* { "director" : "…", "title" : "…", "plot" : "…", "releaseDate" : "2019-9-17" } */ @propertyWrapper struct YMD { var wrappedValue: Date? } extension YMD: Codable { /* convert Date from/to YMD string via singleValueContainer */ } func encode(to encoder: Encoder) throws { … } init(from decoder: Decoder) throws { … }
  28. aligatr FrenchKit 2019 PropertyWrappers and Codable struct Movie: Codable {

    let title: String let director: String let plot: String @YMD var releaseDate: Date? } /* { "director" : "…", "title" : "…", "plot" : "…", "releaseDate" : "2019-9-17" } */ @propertyWrapper struct YMD { var wrappedValue: Date? } extension YMD: Codable { /* convert Date from/to YMD string via singleValueContainer */ }
  29. aligatr FrenchKit 2019 And that’s a Wrap! • SE-258: The

    official Proposal
 https://github.com/apple/swift-evolution/ ... /0258-property-wrappers.md • SwiftLee
 https://www.avanderlee.com/swift/property-wrappers/ • NSHipster
 https://nshipster.com/propertywrapper/ • Burritos
 https://github.com/guillermomuntaner/Burritos • Re-implementing State and Binding manually to understand the wrappers
 https://gist.github.com/AliSoftware/ecb5dfeaa7884fc0ce96178dfdd326f8