DEV Community

Cover image for Asynchronous Object Initialisation in Swift
Hugh Jeremy
Hugh Jeremy

Posted on

Asynchronous Object Initialisation in Swift

Baby birds, rockets, freshly roasted coffee beans, and … immutable objects. What do all these things have in common? I love them.

An immutable object is one that cannot change after it is initialised. It has no variable properties. This means that when using it in a program, my pea brain does not have to reason about the state of the object. It either exists, fully ready to complete its assigned duties, or it does not.

Asynchronous programming presents a challenge to immutable objects. If the creation of an object requires network I/O, then we will have to unblock execution after we have decided to create the object.

As an example, let’s consider the Transaction class inside Amatino Swift. Amatino is a double entry accounting API, and Amatino Swift allows macOS & iOS developers to build finance capabilities into their applications.

To allow developers to build rich user-interfaces, it is critical that Transaction operations be smoothly asynchronous. We can’t block rendering the interface while the Amatino API responds! To lower the cognitive load demanded by Amatino Swift, Transaction should be immutable.

We’ll use a simplified version of Transaction that only contains two properties: transactionTime and description. Let’s build it out from a simple synchronous case, to a full fledged asynchronous case.

class Transaction {
  let description: String
  let transactionTime: Date 

  init(description: String, transactionTime: Date) {
    self.description = description
    self.transactionTime = transactionTime
  }
}
Enter fullscreen mode Exit fullscreen mode

So far, so obvious. We can instantly initialise Transaction. In real life, Transaction is not initialised with piecemeal values, it is initialised from decoded JSON data received from an HTTP request. That JSON might look like this:

{
  "transaction_time": "2008-08",
  "description": "short Lehman Bros. stock"
}
Enter fullscreen mode Exit fullscreen mode

And we can decode that JSON into our Transaction class like so:

/* Part of Transaction definition */
enum JSONObjectKeys: String, CodingKey {
  case txTime = "transaction_time"
  case description = "description"
}

init(from decoder: Decoder) throws {
  let container = try decoder.container(
    keyedBy: JSONObjectKeys.self
  )
  description = try container.decode(
    String.self,
    forKey: .description
  )
  let dateFormatter = DateFormatter()
  dateFormatter.dateFormat = "yyyy-MM" //...
  let rawTime = try container.decode(
    String.self,
    forKey: .txTime
  )
  guard let txTime: Date = dateFormatter.date(
    from: rawTime
  ) else {
    throw Error
  }
  transactionTime = txTime
  return
}
Enter fullscreen mode Exit fullscreen mode

Whoah! What just happened! We decoded a JSON object into an immutable Swift object. Nice! That was intense, so lets take a breather and look at a cute baby bird:

Break time is over! Back to it: Suppose at some point in our application, we want to create an instance of Transaction. Perhaps a user has tapped ‘save’ in an interface. Because the Amatino API is going to (depending on geography) take ~50ms to respond, we need to perform an asynchronous initialisation.

We can do this by giving our Transaction class a static method, like this one:

/* Part of Transaction definition */
static func create(
  description: String,
  transactionTime: Date,
  callback: @escaping (Error?, Transaction?) -> Void
) throws {
  /* dummyHTTP() stands in for whatever HTTP request
     machinery you use to make an HTTP request. */
  dummyHTTP() { (data: Data?, error: Error?) in
    guard error == nil else { 
      callback(error, nil)
      return
    }
    guard dataToDecode: Data = data else {
      callback(Error(), nil)
      return
    }
    let transaction: Transaction
    guard transaction = JSONDecoder().decode(
      Transaction.self,
      from: dateToDecode
    ) else {
      callback(Error(), nil)
      return
    }
    callback(nil, transaction)
    return
  }
}
Enter fullscreen mode Exit fullscreen mode

This new Transaction.create() method follows these steps:

  1. Accepts the parameters of the new transaction, and a function to be called once that transaction is available, the callback(Error?:Transaction?). Because something might go wrong, this function might receive an error, (Error?) or it might receive a transaction (Transaction?)

  2. Makes an HTTP request, receiving optional Data and Error in return, which are used in a closure. In this example, dummyHTTP() stands in for whatever machinery you use to make your HTTP requests. For example, check out Apple’s guide to making HTTP requests in Swift

  3. Looks for the presence of an error, or the absence of data and, if they are found, calls back with those errors: callback(error, nil)

  4. Attempts to decode a new instance of Transaction and, if successful, calls back with that transaction: callback(nil, transaction)

We can use the .create() static method like so:

Transaction.create(
  description: "Stash sweet loot",
  transactionTime: Date(),
  callback: { (error, transaction) in 
    // Guard against errors, then do cool stuff
    // with the transaction
})
Enter fullscreen mode Exit fullscreen mode

The end result? An immutable object. We don’t have to reason about whether or not it is fully initialised, it either exists or it does not. Consider an alternative, wherein the Transaction class tracks internal state:

class Transaction {
  var HTTPRequestInProgress: bool
  var hadError: Bool? = nil
  var description: String? = nil
  var transactionTime: Date? = nil

  init(
    description: String,
    transactionTime: Date,
    callback: (Error?, Transaction?) -> Void
  ) {
    HTTPRequestInProgress = true
    dummyHTTP() { data: Data?, error: Error? in 
       /* Look for errors, try decoding, set
          `hadError` as appropriate */
       HTTPRequestInProgress = false
       callback(nil, self)
       return
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we must reason about all sorts of new possibilities. Are we trying to utilise a Transaction that is not yet ready? Have we guarded against nil when utilising a Transaction that is ostensibly ready? Down this path lies a jumble of guard statements, if-else clauses, and sad baby birdies.

Don’t make the baby birdies sad, asynchronously initialise immutable objects! 💕

Further Reading

Hugh

Originally posted on hughjeremy.com

Top comments (0)