The hose again

It does not set translatesAutoresizingMaskIntoConstraints = false on a UIView being assigned to tableView.tableFooterView or else it gets the hose again. Yes, it will, Precious, won’t it? It will get the hose!

How (not to) encode an array

Sounds simple, right? But yesterday I messed up this simple operation and was stuck for a couple hours wondering why an API request wasn’t working.

Background

A pattern I follow is to use Swift struct‘s to represent each network request my app needs. Each struct has only let properties and conforms to Encodable so that it can be serialized to JSON and sent to the server by my networking class. Typically this is straightforward and the top-level JSON object is almost always a dictionary.

For example:

struct MyRequest: Encodable {
  let uid: String
  let date: Date
  let bar: Int?
}

Thanks to the magic of the compiler, this will encode to a JSON dictionary just fine without any boiler plate code on my side.

Occasionally if the names of the properties don’t match the names of the parameters the API expects (typically because the API is not in my control and follows some naming convention I don’t like – or even no naming convention whatsoever), then I need to declare CodingKeys to properly map the encoding.

Some times there’s no avoiding it though and I need to implement the encode function myself. I often need this when the server expects some weird Date format or some business logic is needed for the encoding.

The Issue

Yesterday I implemented a simple network request that needed to be serialized as an array.

struct CallsRequest: Encodable {
  let calls: [Call] // Call conforms to Encodable
}

The trouble is that this would encode to a Dictionary with one key “calls” whose value is the array. What I needed was to encode to an Array instead.

func encode(to encoder: Encoder) throws {
  var values = encoder.unkeyedContainer()
  try values.encode(calls)
}

I had never used unkeyedContainer() before. Previously I’ve always used container(keyedBy:), which encodes as a dictionary (hence keyed).

The output of the approach above was an object of type [[Call]]. That is, it encodes an array of arrays instead of just an array. (Someone else spotted the extra brackets in the JSON output. Thanks Anton!)

It took me a while to realize where the extra level of Array was coming from, but then it dawned on me: the unkeyedContainer() is an Array, just as the container(keyedBy:) is a Dictionary.

So what I really needed was something like this:

func encode(to encoder: Encoder) throws {
  var values = encoder.unkeyedContainer()
  for call in calls {
    try values.encode(call)
  }
}

This delivered the expected output of type [Call]. Problem solved.

Anyway, I hope that someday this might save someone a few hours of frustration.

 

Update

Where the real JSON encoding happens is via another custom protocol that I didn’t mention. Basically I want objects to be able to convert themselves to Data so that they could be set as the body to an HTTP PUT/POST/PATCH/DELETE request.

public protocol ParametersBuilder {
    func data() throws -> Data
    func dictionary() throws -> [String: Any]
}

dictionary() is for GET requests or for .formUrlEncoded request bodies. But data() is used for everything else.

The default implementation of data() is this:

extension Encodable {
  public func data() throws -> Data {
    return try JSONEncoder().encode(self)
  }
}

(which is why CallsRequest conforms to Encodable and what by default would turn it into a dictionary)

A simpler solution to my problem would have been to provide a custom implementation of data() on CallsRequest instead of conforming to Encodable:

struct CallsRequest: ParametersBuilder {
  let calls: [Call] // Call conforms to Encodable
    
  public func data() throws -> Data {
    return try JSONEncoder().encode(calls)
  }
}


 

Update 2

Rob Napier pointed out to me that I could use singleValueContainer to skip having to loop through the array.

func encode(to encoder: Encoder) throws {
  var values = encoder.singleValueContainer()
  try values.encode(calls)
}

Level 8

I restarted my play through of The Adventure of Link (NES), only this time I spent a few hours xp farming in the first palace. I finished Parapa Palace with Attack, Magic, and Life fully leveled up. That should make the remainder of the game considerably easier.