Jul 22, 2019

Value mapping using custom operators

A common task when initializing classes or structures is to map its values from another object. For example by injecting a JSON dictionary into its init method and then mapping the data. This could look something like the Person structure we are initializing here:

/// Gender enumeration
enum Gender: String {
  
  case male
  case female
  case lgbt
}

/// Person structure containing name, age and gender
struct Person {
  
  var firstName: String?
  var lastName: String?
  var age: Int?
  var gender: Gender?
  
  init(dictionary: [String : Any]) {
    firstName = dictionary["firstName"] as? String
    lastName = dictionary["lastName"] as? String
    age = dictionary["age"] as? Int
    if let rawValue = dictionary["gender"] as? String {
      gender = Gender(rawValue: rawValue)
    }
  }
}

This works just fine but the way the JSON dictionary is mapped is a little hard to read. Luckily Swift provides us with a way to reduce the written code and make it more legible. For this we have to define a new custom operator.

Instead of using the default assigning operator =, we will define our own operator for assigning the value right of <- to the left.

/// operator declaration
infix operator <-

But just because we declared the operator, the compiler still doesn’t know what to do with it. So lets define a func that actually handles assigning the values.

/// Maps right instance to left if of same type
///
/// - Parameters:
///   - left: optional instance to be mapped
///   - right: optional mapping value
func <- <T>(left: inout T?, right: Any?) {
  if right != nil {
    left = right as? T
  } else {
    ()
  }
}

The function takes two values and assigns the right one to the generic left if it is of the same type. If we now replace the = operator with <- in our init method, it will look something like this:

/// Person structure containing name, age and gender
struct Person {
  
  var firstName: String?
  var lastName: String?
  var age: Int?
  var gender: Gender?
  
  init(dictionary: [String : Any]) {
    firstName <- dictionary["firstName"]
    lastName <- dictionary["lastName"]
    age <- dictionary["age"]
    if let rawValue = dictionary["gender"] as? String {
      gender = Gender(rawValue: rawValue)
    }
  }
}

As you can see, the code is already much cleaner and more legible. The only thing that has not changed is the mapping of the gender. Since we are mapping the raw value of Gender, the function for our custom operator will fail because it is not of the same type as the attribute gender in the Person structure. We must define an additional function for our operator to also handle generic types that are RawRepresentable.

// Maps right instance to left if of same type
//
// - Parameters:
///   - left: optional instance to be mapped
///   - right: optional mapping value
func <- <T: RawRepresentable>(left: inout T?, right: Any?) {
  if let right = right as? T {
    left = right
    return
  }
  if let right = right as? T.RawValue {
    left = T(rawValue: right)
  } else {
    ()
  }
}

This now allows us to map any enumerations that are RawRepresentable from a raw value or a case of its own type. Using this we can now complete the mapping in the init method.

/// Person structure containing name, age and gender
struct Person {
  
  var firstName: String?
  var lastName: String?
  var age: Int?
  var gender: Gender?
  
  init(dictionary: [String : Any]) {
    firstName <- dictionary["firstName"]
    lastName <- dictionary["lastName"]
    age <- dictionary["age"]
    gender <- dictionary["gender"]
  }
}

If we then inject the JSON dictionary into the structure, it will all be mapped correctly.

let dictionary: [String : Any] = ["firstName": "Tom", "lastName": "Sawyer", "age": 10, "gender": "male"]
let tomSawyer = Person(dictionary: dictionary)

I hope this helps you write code that is easier to read and maintain. You can view the complete code here.

Language: Swift 5.0 ยท Written on: iPad Pro