Swift Property Wrappers

A property wrapper in Swift is a simple and clean way to mix behaviors with properties via syntactic sugar. Simple means that to wrap a property you just declare it with an @ sign and the name of wrapper. Clean means that all the code for the injected behavior lives in one place. You could do this all with functions but property wrappers require less cognitive juggling.

Let’s walk through an example of using a property wrapper to mix constraints with a String property so that we can use it represent a Tic Tac Toe game board.

Here are some well known rules of Tic Tac Toe which we want any representation to conform with:

  • The board has 9 squares, no more, no less
  • The only marks a player can make on a board are noughts and crosses.

A Do-Nothing Implementation

Let’s start with a simple property wrapper that represents a Tic Tac Toe board as a String with no rules enforced…

@propertyWrapper
 struct TicTacToeBoard {
     private var state = ""
     
     var wrappedValue: String {
         get { return state }
         set { state = newValue }
     }
     
     init() {
         state = ""
     }
 }

This code handles getting, setting, and initialization of a wrapped property. Getting, Setting, and initialization are the moments in the wrapped property’s life cycle where we will want to mix-in constraints to ensure the TicTacToeBoard conforms to our rules.

To wrap a property with this wrapper we annotate it with the name of the wrapper inside a struct or class…

struct TicTacToeGame {
     @TicTacToeBoard public var gameState: String
 }

(Obviously in a real Tic Tac Toe game we would have many more properties.)

To test our wrapper we can instantiate a game object and see is we can write to and read from the game state property…

var game = TicTacToeGame()
game.gameState = "_"
print(game.gameState)
// output: _

Excellent! We’re at a good starting point. Our wrapper is working but doing nothing. It’s time to mix-in some behavior with the game state property.

Constraint 1

Let’s start by ensuring the length of a Tic Tac Toe game board, as memorialized in the game state property, is always nine squares long.

(I know that in the real world a Tic Tac Toe game board is a 3 x 3 square but from a state point of view that is an implementation detail.)

@propertyWrapper
struct TicTacToeBoard0 {
  private var state = ""
  private var length = 9
 	 	  	 
  var wrappedValue: String {
  get { // ... }
  set {
    // Constraint #1
    // ensure that the length of the newValue is not too 
    // long or too short
    if newValue.count > length {
	  	  	  	  	  	        
    // Constraint #1a	  	  	  	  	  	  
    // truncate a board that is too long
    state = newValue.prefix(length) + "" 
       // the + "" make the Substring a String
    } else {
 	  	  	  	  	  	  	  	  // Constraint #1b
 	  	  	  	  	  	  	  	  // padd a board that is too short
 	  	  	  	  	  	  	  	  state = newValue + String(repeating: "_", count: length - newValue.count)
 	  	  	  	  	  	  	  	  // cound not use String.padding() because of SIGBART
 	  	  	  	  	  	  }
 	  	  	  	  }
 	  	  }
 	 	  	 
 	  	  init() { //... }
 }

We’ve expanded the set clause to check for a state string that is too long or too short. If the state string is too long (newValue.count > length) then we truncate it. We just throw the characters beyond index 8 away. if the state string is too short (newValue.count < length) we pad the right end with underscore characters. We’re making some harsh calls here, throwing away data, which we will deal with later on.

Note that I had to do some tricks to get this simple code to work. Swift has come a long way in making strings, substrings, and characters interoperable but I still had to add an empty string to coerce the result of prefix() into a string. I also could not use the padding() method to pad the state string because doing so resulted in a crash. Maybe these are bugs or maybe I’m not reading deeply enough into Apple’s String class documentation.

When we run our updated code the result is a game board that is always exactly 9 squares in length…

var game = TicTacToeGame()
game.gameState = "_"
print(game.gameState)
// output: _________

Constraint 2

Now let’s ensure that a game board only contains legal characters. In the case of Tic Tac Toe we’ll use the following characters as symbols:

  • “_” for empty square
  • “o” for nought
  • “x” for cross

Now that we’ve defined how our game universe is symbolized it’s time to put these definitions into code..

@propertyWrapper
 struct TicTacToeBoard {
 	  	  private var state = ""
 	  	  private var length = 9
 	  	  private var legalChararacters = "_ox"
 	 	  	 
 	  	  var wrappedValue: String {
 	  	  	  	  get { return state }
 	  	  	  	  set { 	 	  	  	  	  	  	 
 	  	  	  	  	  	  // Constraint #1
 	  	  	  	  	  	  // ...

 	  	  	  	  	  	  // Constraint #2
 	  	  	  	  	  	  // ensure that the newValue only contains legal chars
 	  	  	  	  	  	  let legalizedState = state.map { legalChararacters.contains($0) ? $0 : "_" }
 	  	  	  	  	  	  state = String(legalizedState)
 	  	  	  	  }
 	  	  }
 	 	  	 
 	  	  init() { ... }
 }

For the purposes of this demo we don’t have to define the meaning of each symbol. We just have to let the game know that “_”, “o”, and “x” are the only legal characters.

We’re using a map function to map any illegal characters to an empty square (“_”). Here we are again, throwing away valuable data because it doesn’t fit our model of what a Tic Tac Toe game is. (We’ll correct this problem shortly, I promise.)

Run a test and we’ll see that illegal characters are replaced and the length of the game board remains consistent…

var game = TicTacToeGame()
game.gameState = "_ox_x_z_x_"
print(game.gameState)
// output: _ox_x___x

Retaining Data

It kind of feels like we are done. We have successfully used a property wrapper to inject our model of a Tic Tac Toe game into an ordinary String object. But I’m still nervous about throw away data. It might be important to preserve the original value of the game state before we sanitize it. Future maintainers of this Tic Tac Toe repo might need to use that data to improve the game. Lucky for us Swift’s property wrapper mechanism has a property called projectedValue which we can use to retain the original game state value.

Let’s implement a projectedValue for our Tic Tac Toe game…

@propertyWrapper
 struct TicTacToeBoard {
 	  	  private var state = ""
 	  	  private var length = 9
 	  	  private var legalChararacters = "_ox"
 	  	  var projectedValue = ""
 	 	  	 
 	  	  var wrappedValue: String {
 	  	  	  	  get { return state }
 	  	  	  	  set {
 	  	  	  	  	  	  // Save the original newValue as the projectedValue
 	  	  	  	  	  	  projectedValue = newValue
 	 	  	  	  	  	  	 
 	  	  	  	  	  	  // Constraint #1
 	  	  	  	  	  	  // ensure that the length of the newValue is not too long or too short
 	  	  	  	  	  	  if newValue.count > length {
 	  	  	  	  	  	  	  	  // Constraint #1a
 	  	  	  	  	  	  	  	  // truncate a board that is too long
 	  	  	  	  	  	  	  	  state = newValue.prefix(length) + "" // the + "" make the Substring a String
 	  	  	  	  	  	  } else {
 	  	  	  	  	  	  	  	  // Constraint #1b
 	  	  	  	  	  	  	  	  // padd a board that is too short
 	  	  	  	  	  	  	  	  state = newValue + String(repeating: "_", count: length - newValue.count)
 	  	  	  	  	  	  	  	  // cound not use String.padding() because of SIGBART
 	  	  	  	  	  	  }
 	  	  	  	  	  	  // Constraint #2
 	  	  	  	  	  	  // ensure that the newValue only contains legal chars
 	  	  	  	  	  	  let legalizedState = state.map { legalChararacters.contains($0) ? $0 : "_" }
 	  	  	  	  	  	  state = String(legalizedState)
 	  	  	  	  }
 	  	  }
 	 	  	 
 	  	  init() {
 	  	  	  	  state = "_________"
 	  	  }
 }

All we had to do was add a var called projectedValue and then assign it newValue before we started messing around with it. To access the projectedValue you use a $ in front of the wrapped property name. Let’s update our previous test to print out both the wrapped property and the projected value…

var game = TicTacToeGame()
game.gameState = "_ox_x_z_x_"
print(game.gameState, game.$gameState)
// output: _ox_x___x _ox_x_z_x_

A projectedValue, like a wrapped value, is not persistent. If we wanted to we could log both values to a file with a IOS 8601 date-time stamp.

When to use Wrapped Values

We could have implemented the behavior of our Tic Tac Toe game board in many ways with the traditional methods and properties of a class or struct. We could have made the game board a type or composed of a set of types.

Classes and structs (types) are excellent for managing complexity. As a codebase evolves over time with many maintainers the power of types helps keep the code organized, documented, and testable. But managing complexity requires its own level of complexity. Sometimes the effort involved in maintaining and extending custom types overwhelms the code itself so that we spend more time refactoring old code than we do writing new functionality.

Property wrappers, which form the basis of powerful abstractions, like @State, @Binding, @ObservedObject and @Published, are used to weave together existing types without inheritance, extension, or composition. @TicTacToeBoard is not a new type, it’s a String type that has been decorated with additional code. Property wrappers are a great way of keeping abstractions separate and independent.

Looking back on my example, I’m not sure it is a good one for the property wrapper abstraction. @State is a good as a property wrapper because it makes any property observable. @TicTacToeBoard is not an abstraction that many other properties are going need to worry about.

Going forward I would use a property wrapper for memory management, state management, storage management, logging, and DB connections. These are all behaviors that Apple should just provide–as they have done with state management via @State. But until Apple does, if they do, you’ll want to have a library of management code injectable though property wrappers for all your properties.


Posted

in

by

Tags: