Chapters

Hide chapters

Swift Apprentice: Beyond the Basics

First Edition · iOS 16 · Swift 5.8 · Xcode 14.3

Section I: Beyond the Basics

Section 1: 13 chapters
Show chapters Hide chapters

9. Property Wrappers
Written by Eli Ganim

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the “Properties” chapter of Swift Apprentice: Fundamentals, you learned about property observers and how you can use them to affect the behavior of properties in a type. Property wrappers take that idea to the next level by letting you name and reuse the custom logic. They do this by moving the custom logic to an auxiliary type, which you may define.

If you’ve worked with SwiftUI, you’ve already run into property wrappers (and their telltale @-based, $-happy syntax). SwiftUI uses them extensively because they allow virtually unlimited customization of property semantics, which SwiftUI needs, to do its view update and data synchronization magic behind the scenes.

The Swift core team worked hard to make property wrappers a general-purpose language feature. For example, they’re already being used outside the Apple ecosystem on the Vapor project. Property wrappers, in this context, let you define a data model and map it to a database like PostgreSQL.

To learn the ins and outs of property wrappers, you’ll continue with some abstractions from the last chapter. You’ll begin with a simple example and then see an implementation for the copy-on-write pattern. Finally, you’ll wrap up with another example showing you some things to watch out for when using this language feature.

Basic Example

Consider the ‘Color’ type from the last chapter to start with a simple use case for property wrappers. It looked like this:

struct Color {
  var red: Double
  var green: Double  
  var blue: Double
}

There was an implicit assumption that the values red, green and blue fall between zero and one. You could have stated that requirement as a comment, but enlisting the compiler’s help is much better. To do that, create a property wrapper like this:

@propertyWrapper                                           // 1
struct ZeroToOne {                                         // 2
  private var value: Double

  private static func clamped(_ input: Double) -> Double { // 3
    min(max(input, 0), 1)
  }

  init(wrappedValue: Double) {
    value = Self.clamped(wrappedValue)                     // 4
  }

  var wrappedValue: Double {                               // 5
    get { value }
    set { value =  Self.clamped(newValue) }
  }
}

What’s so special here? Here’s what’s going on:

  1. The attribute @propertyWrapper says this type can be used as a property wrapper. As such, it must vend a property called wrappedValue.
  2. In every other aspect, it’s just a standard type. In this case, it’s a struct with a private variable value.
  3. The private static clamped(_:) helper method does a min/max dance to keep values between zero and one.
  4. A wrapped value initializer is required for property wrapper types.
  5. The wrappedValue vends the clamped value.

Now, you can use the property wrapper to add behavior to the color properties:

struct Color {
  @ZeroToOne var red: Double
  @ZeroToOne var green: Double
  @ZeroToOne var blue: Double
}

That’s all it takes to guarantee the values are always locked between zero and one. Try it out with this:

var superRed = Color(red: 2, green: 0, blue: 0)
print(superRed) 
// r: 1, g: 0, b: 0

superRed.blue = -2
print(superRed) 
// r: 1, g: 0, b: 0

No matter how hard you try, you can never get it outside the zero-to-one bounds.

You can use property wrappers with function arguments, too. Try this:

func printValue(@ZeroToOne _ value: Double) {
  print("The wrapped value is", value)
}

printValue(3.14)

Here, the wrapped value printed is 1.0. @ZeroToOne adds clamping behavior to passed values. Pretty cool.

Projecting Values With $

In the above example, you clamp the wrapped value between zero and one but potentially lose the original value. To remedy this, you can use another feature of property wrappers. In addition to wrappedValue, property wrappers may vend another type called projectedValue. You can use this to offer direct access to the unclamped value like this:

@propertyWrapper
struct ZeroToOneV2 {
  private var value: Double

  init(wrappedValue: Double) {
    value = wrappedValue
  }

  var wrappedValue: Double {
    get { min(max(value, 0), 1) }
    set { value = newValue }
  }

  var projectedValue: Double { value }
}
func printValueV2(@ZeroToOneV2 _ value: Double) {
  print("The wrapped value is", value)
  print("The projected value is", $value)
}

printValueV2(3.14)

Adding Parameters

The example clamps between zero and one, but you could imagine wanting to clamp between zero and 100 — or any other number greater than zero. You can do that with another parameter: upper. Try this definition:

@propertyWrapper
struct ZeroTo {
  private var value: Double
  let upper: Double

  init(wrappedValue: Double, upper: Double) {
    value = wrappedValue
    self.upper = upper
  }

  var wrappedValue: Double {
    get { min(max(value, 0), upper) }
    set { value = newValue }
  }

  var projectedValue: Double { value }
}
func printValueV3(@ZeroTo(upper: 10) _ value: Double) {
  print("The wrapped value is", value)
  print("The projected value is", $value)
}

printValueV3(42)

Going Generic

You used a Double for the wrapped value in the previous example. The property wrapper can also be generic with respect to the wrapped value. Try this:

@propertyWrapper
struct ZeroTo<Value: Numeric & Comparable> {
  private var value: Value
  let upper: Value

  init(wrappedValue: Value, upper: Value) {
    value = wrappedValue
    self.upper = upper
  }

  var wrappedValue: Value {
    get { min(max(value, 0), upper) }
    set { value = newValue }
  }

  var projectedValue: Value { value }
}

Implementing Copy-on-Write

Now that you have the basic mechanics of property wrappers, it’s time to look at more detailed examples.

struct PaintingPlan { // a value type, containing ...
  // ... 

  // a computed property facade over deep storage
  // with copy-on-write and in-place mutation when possible
  var bucketColor: Color {
    get {
      bucket.color
    }
    set {
      if isKnownUniquelyReferenced(&bucket) {
        bucket.color = bucketColor
      } else {
        bucket = Bucket(color: newValue)
      }
    }
  }
}
struct PaintingPlan {
  @CopyOnWriteColor var bucketColor = .blue
}

Compiler Expansion

The compiler automatically expands @CopyOnWriteColor var bucketColor = .blue into the following:

private var _bucketColor = CopyOnWriteColor(wrappedValue: .blue)

var bucketColor: Color {
  get { _bucketColor.wrappedValue }
  set { _bucketColor.wrappedValue = newValue }
}
@propertyWrapper
struct CopyOnWriteColor {
  private var bucket: Bucket

  init(wrappedValue: Color) {
    self.bucket = Bucket(color: wrappedValue)
  }

  var wrappedValue: Color {
    get {
      bucket.color
    }
    set {
      if isKnownUniquelyReferenced(&bucket) {
        bucket.color = newValue
      } else {
        bucket = Bucket(color:newValue)
      }
    }
  }
}
struct PaintingPlan {
  var accent = Color.white

  @CopyOnWriteColor var bucketColor = .blue
  @CopyOnWriteColor var bucketColorForDoor = .blue
  @CopyOnWriteColor var bucketColorForWalls = .blue
  // ...
}

Wrappers, Projections and Other Confusables

When you think about property wrappers as shorthand that the compiler automatically expands, it’s clear that there’s nothing magical about them — but if you aren’t careful, thinking about them only in this way can tempt you to create unintuitive ones. To work with them day-to-day, you only need to focus on a few key terms: property wrapper, wrapped value and projected value.

Projected Values are Handles

A projected value is nothing more than an additional handle that a property wrapper can offer. As you saw earlier, it’s defined by projectedValue and exposed as $name, where “name” is the name of the wrapped property.

struct Order {
  @ValidatedDate var orderPlacedDate: String
  @ValidatedDate var shippingDate: String
  @ValidatedDate var deliveredDate: String
}
@propertyWrapper
public struct ValidatedDate {
  private var storage: Date? = nil
  private(set) var formatter = DateFormatter()

  public init(wrappedValue: String) {
    self.formatter.dateFormat = "yyyy-mm-dd"
    self.wrappedValue = wrappedValue
  }

  public var wrappedValue: String {
    set {
      self.storage = formatter.date(from: newValue)
    }
    get {
      if let date = self.storage { 
        return formatter.string(from: date) 
      } else {
        return "invalid"
      }
    }
  }
}
@propertyWrapper
public struct ValidatedDate {
  // ... as above ...

  public var projectedValue: DateFormatter {
    get { formatter }
    set { formatter = newValue }
  }
}
var o = Order()
// store a valid date string
o.orderPlacedDate = "2014-06-02"
o.orderPlacedDate // => 2014-06-02

// update the date format using the projected value
let otherFormatter = DateFormatter()
otherFormatter.dateFormat = "mm/dd/yyyy"
order.$orderPlacedDate = otherFormatter

// read the string in the new format
order.orderPlacedDate // => "06/02/2014"

Challenges

Challenge 1: Create a Generic Property Wrapper for CopyOnWrite

Consider the property wrapper CopyOnWriteColor you defined earlier in this chapter. It lets you wrap any variable of type Color. It manages the sharing of an underlying storage type, Bucket, which owns a single Color instance. Thanks to structural sharing, multiple CopyOnWriteColor instances might share the same Bucket instance — thus sharing its Color instance and saving memory.

private class StorageBox<StoredValue> {

  var value: StoredValue
  
  init(_ value: StoredValue) {
    self.value = value
  }
}

Challenge 2: Implement @ValueSemantic

Using StorageBox from the last challenge and the following protocol, DeepCopyable, as a constraint, write the definition for a generic property wrapper @ValueSemantic. Then use it in an example to verify that wrapped properties have value semantics even when wrapping an underlying type that doesn’t. Example: NSMutableString is an example of a non-value semantic type. Make it conform to DeepCopyable and test it with @ValueSemantic.

protocol DeepCopyable {

  /* Returns a deep copy of the current instance.

     If `x` is a deep copy of `y`, then:
        - The instance `x` should have the same value as `y` 
          (for some sensible definition of value – not just 
          memory location or pointer equality!)
        - It should be impossible to do any operation on `x` 
          that will modify the value of the instance `y`.

    Note: A value semantic type implementing this protocol can just 
          return `self` since that fulfills the above requirement.
  */

  func deepCopy() -> Self
}

Key Points

Property wrappers have a lot of flexibility and power, but you also need to use them carefully. Here are some things to remember:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now