Thoughts on Combine in an async/await world

When Apple announced their own Functional Reactive Programming framework at WWDC 2019 I was super excited. Finally, a simplified, easy to use framework that we could use to dip our toes in FRP.

What made it even better is that SwiftUI makes heavy use of Combine, which means that Apple had to buy in to the technology themselves. This made it seem unlikely that Apple would abandon the technology any time soon.

Then WWDC 2020 came around and there weren’t any meaningful changes to Combine. It was hardly even mentioned in the sessions. To me, this was a signal that Apple was happy with where Combine’s at and it didn’t need a ton of updates which is absolutely fine.

And then the Swift team started implementing Swift’s overhauled concurrency features like async/await, structured concurrency and even asynchronous collections. Once people saw this, I regularly got questions like “Will these features replace Combine or make Combine obsolete?”.

And I honestly didn’t know what async/await would mean for Combine at the time. And I still don’t know for sure, but I have some thoughts.

Combine is great at observing things and receiving changes. This is also how I often explain Functional Reactive Programming. It’s about responding to changes. When a property changes, a user taps something, or a notification is sent through Notification Center, these are things you might want to receive and react to in some way.

This output can be transformed, and eventually displayed to the user, or used to influence something else in your app. The main idea is that you reacted to a specific change in your app’s environment.

As far as I know, async/await won’t be very well suited to observe an @Published property, or to listen for notifications in Notification Center. These will most likely be the places where Combine continues to excel and be a fantastic way to react to changes in your app, altough at the same time Async Streams might change this in the future.

On the other hand, there’s a whole range of uses cases where Combine can be used today that will eventually be obsoleted when iOS 15 is your minimum deployment target since async/await is not currently backwards deployable.

An example of this is networking.

Combine (and RxSwift for that matter) is commonly used in networking layers. We treat our data task as publishers, transform their outputs, and eventually we forward this transformed output to our UI for display in one way or the other.

While this is convenient and works well, I don’t think this was ever the best use case for Combine. Swift’s new concurrency features are far better suited for these kinds of tasks that you want to run. After all, Combine never was intended to be a task runner. It was intended to react to streams of values caused by state changes.

I’m curious to see how this unfolds, and I might change my mind on some of this down the line. After all, I’ve only spent a couple of hours seriously looking at Swift’s new concurrency features.

Combine will most likely stick around for a bit, but its uses will for sure be limited once you decide you’re all in on iOS 15 and async/await. Until then, I would recommend you make the best use of Combine you can, and for the time being I won’t be removing networking examples from my Practical Combine book. I will, however update the book to have some callouts where async/await might be a better choice, and I’ll add more examples of where Combine fits in a world where async/await exists.

The iOS Developer’s guide to WWDC 2024

This post was originally published in 2021 and has been touched up for 2024

WWDC is always an exciting time for iOS engineers. It's the one week a year where we're all newcomers to a whole range of features and APIs that Apple has just unleashed upon the world through their latest Xcode, macOS, iOS, tvOS, iPadOS, visionOS, and watchOS betas. The entire community comes out of hiding and we all come together to share thoughts, experiences, opinions, and findings.

For a whole week, Apple releases dozens of sessions on different topics, and we all scramble to watch them as soon as possible to make sure we're all up to date with whatever the latest and greatest is. As a newcomer, or a person with a regular sleeping schedule, this can be quite intimidating.

So that's why I figured I'd put together a little guide to making the most out of WWDC without being completely overwhelmed and intimidated by the sheer volume of content that Apple is about to unleash on us all.

Let's jump right in, shall we.

Tip 1: Breathe

Once Apple opens up that firehose of new content and beta's it's a scramble. You immediately start up the Beta downloads once they're up. In the meantime, you scan the documentation for new things and you're looking at the Apple Developer app to try and figure out which sessions you're going to watch and when.

It's a lot to take in all at once.

When you blink twice, you'll see the first people Tweet about the cool things they're already doing with the new Xcode beta while you're still waiting for that .xip file to finally finish extracting. The FOMO becomes more real with the second.

Just breathe. Take a moment, and breathe.

The Xcode betas aren't going anywhere. You'll have plenty of time to mess around with new APIs if you want to. Because even though it looks like everybody is exploring all the new things in depth within seconds of the first betas becoming available, a lot of us are much, much slower. In fact, I'd say a majority of us are not on top of everything all the time, and you don't have to be either.

Sit back, breathe, and proceed at a comfortable pace.

You'll find that breathing, and taking it easy are a recurring theme in this guide. That's because it's important that you do.

Tip 2: Figure out what's relevant to you

When you keep an eye on Twitter throughout the week of WWDC you'll find that everybody is talking about the latest and greatest. For WWDC 2021 I'm sure everybody's going to be all over what's new in SwiftUI, Swift 5.5's async/await, and whatever other new and exciting frameworks and APIs Apple releases.

Personally, I love exploring the new things too. It's new, it's fresh, it's exciting. But let's be honest; it's completely irrelevant for most of us in the short term. Apple isn't going to officially release anything they announce until more than three months later, so while it's fun to explore new things, it's not likely that you'll need to be aware of everything.

Whenever I'm going through WWDC content I try to focus on two types of sessions:

  • The super interesting sessions on new technology (for fun)
  • Sessions that cover best practices or advances in technologies I'm already familiar with

That's right, I love watching sessions on stuff that's not new at all. Sometimes these sessions don't even involve any new features and instead cover (usually undocumented) tips, tricks, and best practices that I can incorporate into my work almost immediately. These sessions are most exciting to me because they are most relevant to me in the short term.

So when you're in doubt about which sessions to watch, I would recommend to focus on what's relevant to you at the time rather than a new technology that everybody on the internet seems to be all over.

Tip 3: Move at a comfortable pace

I can't stress it enough; that firehose of content that Apple unleashes during WWDC is intimidating. Apple will upload dozens of videos and each one will look more attractive than the other. You might feel the pressure to watch as many videos during the day as you can. Heck, you might even be so excited that it's excitement that drives you. And my advice to you is that you don't give in to that urge.

Watch videos in a pace that is comfortable to you. If you're going over more than a couple of topics in the timespan of a few hours there's a good chance the contents won't really stick, and you'll be too tired to explore the APIs you've just learned about.

Move at a pace that is comfortable to you. If that means watching two videos a day and spending a few hours exploring in code, then that's exactly what you should do. If you prefer watching more videos and not exploring until later,that's fine too! And if you want to skip the videos and just code all day long, guess what? All good!

The videos aren't going anywhere for a long time. Watch them as needed. Maybe even pick a few videos from last year's WWDC if they're referred to from one of this year's videos. It's your choice.

WWDC is not a race, so focus on having fun with it.

Tip 4: Don't skip the State of the Union

If you've ever attended an in-person WWDC you'll know that the main keynote that starts at 10AM PDT on the Monday of WWDC is the main event. People get in line for a good seat early in the morning; some even start queueing at 11PM the day before. It's where we see a first glimpse of all the goodness we'll get to learn more about in the week to come. However, as a developer this main event is probably not the most interesting first day presentation you can watch.

After the main Keynote event, at 2PM PDT, there's a presentation called State of the Union. This presentation is the one to look out for as a developer in my opinion. It's where Apple will dig deeper into the APIs and Frameworks that were introduced, and they'll show off new OS features in-depth.

I know that a lot of developers skip the State of the Union in favor of playing with new APIs and resting up, but I personally love watching the State of the Union. It usually gets me much more pumped up for all the good stuff than the main Keynote does.

Tip 5: Prepare for your lab sessions

WWDC is a unique chance for developers to ask questions to Apple engineers directly. Once WWDC is underway we have the opportunity to request appointments with Apple engineers, and we can show them our projects and ask for input on ways to improve our code, solve bugs, or even just clarify how certain things are supposed to work. Labs is arguably the most valuable part of WWDC, so when WWDC went online last year it was a relieve to see that labs were brought online too. And this year is no different.

When you book a lab appointment, try to make sure you come as prepared as possible. If you want to ask about (what you believe to be) a missing feature, or a bug, make sure you file feedback in Apple's feedback assistant ahead of time. This will make it so your lab session can be associated with the feedback which makes the Apple engineer's job a lot easier. It'll also allow the engineer to pull up your feedback ahead of time.

Possibly more important than filing feedback is that you make sure you have a demo project ready to show off. If your lab starts and you have to work your way through your full codebase, it will make it harder for you and the engineer to work through your question, and potentially fix your problem.

A bare-bones example of what you need will improve this a lot, and will help you make the most out of your lab appointment.

Tip 6: File feedback

This tip applies mostly to the period after WWDC. Once WWDC is over and we start spending actual time with new APIs, and we start testing our own apps on the latest Xcode version, bug will start showing themselves. Presented APIs might be missing features that you'd love to have, iOS 15 might do interesting things to your app, or maybe Xcode 13 crashes every time you do something specific.

No matter what your feedback is, file it as soon as you can.

Don't expect that others will already have filed your feedback. Even if they have, the more duplicates Apple gets, the more likely it is that your feedback is eventually bumped to the top of the list. This means that it's more likely that your bugs get fixed, and that your feature requests get implemented when you file your feedback.

I know that Apple has a history of not always responding to feedback, and that it can be very time consuming to file feedback. But if there is a perfect time for you to do it, it's during the big beta cycle that starts with WWDC.

In Summary

WWDC is a hectic time for everybody. I think it's most important that you focus on taking it easy, not giving in to that fear of missing out, and not trying to learn everything in one week. Consume WWDC content at your own pace, and focus on what's relevant to you unless it's simply something you're super interested in even though it might not be something that you can apply in your work just yet.

I hope that these tips help you make the most of your WWDC experience, and that you'll be able to enjoy this WWDC as much as I've always enjoyed mine!

What’s the difference between a singleton and a shared instance in Swift?

A common pattern on iOS, and in Swift, is to define an instance of an object that you can access from any place in your app. Common examples are URLSession.shared, FileManager.default, and UserDefaults.standard.

These objects can all be considered shared instances, or globally available instances. Defining a shared instance is commonly done as follows:

struct DataProvider {
  static let shared = DataProvider()

  // useful properties and methods
}

It's common for developers to call this a singleton, or a singleton instance.

The singleton pattern is a pattern that allows developers to specify an object that can only ever have one instance within the context of an application. This brings us to the reason that this DataProvider is not an actual singleton object.

There's nothing stopping you from writing the following code:

let providerOne = DataProvider()
let providerTwo = DataProvider()

Since we can create more instances of DataProvider, this means that DataProvider.shared is not a singleton. It's simply a convenient, shared, instance of DataProvider that's associated with the DataProvider type. This means that we can easily access this shared instance through DataProvider.shared.

If we wanted to write a more correct singleton object, we could make the initializer for DataProvider private. This would mean that we can no longer create new instances of DataProvider:

struct DataProvider {
  static let shared = DataProvider()

  // useful properties and methods

  private init() {
    // now we can only create new instances within DataProvider
  }
}

Of course, there's still a possibility that you add an extra static property to DataProvider and that you create more than one instance that way.

Personally, I would consider the above implementation good enough though, and I think you understand the point.

One important note about this example of a singleton (and the shared instance too for that matter) is that even though a static property is evaluated lazily, it's thread-safe. This means that we're guaranteed that the assigningment to the static property is atomic, and that we don't end up with multiple instances even if we access the property from multiple threads at the same time.

To learn more about how this works, check out this post from Jesse Squires. If you're not sure what atomic means in programming, read this post on my website.

Also, big thanks to Josh Asbury for telling me more about how static let is assigned atomically, and pointing me to Jesse's site.

In summary, a singleton is an object that you can only have one instance of. A shared instance is a convenience instance but it's not necessarily the only instance of an object.

An extensive guide to sorting Arrays in Swift

When you're working with Arrays in Swift, it's likely that you'll want to sort them at some point. In Swift, there are two ways to sort an Array:

  1. Through the Comparable implementation for each element in your array
  2. By providing a closure to perform a manual/specialized comparison between elements

If you have a homogenous array of elements, for example [String], you can rely on String's implementation of Comparable to sort an array of String in some sensible manner. There are two ways to sort an array of Comparable elements:

var strings = ["Oh", "Hello", "World", "This", "Is", "An", "Unsorted", "Array"]

// sort in-place
strings.sort()

// create a new array with sorted elements
let sorted = strings.sorted()

If you're not familiar with the Comparable protocol in Swift, take a look at the documentation for this protocol. In short, objects that are Comparable can be compared using < to determine order and with == to determine equality. If two elements are equal according to == then comparing them with < is expected to be false. This means that you can sort an array through the Comparable protocol because that provides all the tools to determine order.

The output from the code sample above is the following:

["An", "Array", "Hello", "Is", "Oh", "This", "Unsorted", "World"]

Based on this output, you can derive that String's default Comparable implementation sorts elements in increasing alphabetical order.

Whenever Swift's default sorting algorithm encounters an element that is equal to another element, it will preserve the original order of these elements in the output. In other words, Swift's sorting algorithm is a so-called stable sorting algorithm. If you want to learn more about how Swift's sort() is implemented, it uses an algorithm called IntroSort and you can learn more about it in this Swift Algorithms Club article.

If the default sorting behaviour for String isn't the behavior you're looking for, or you want to sort an array of items that aren't Comparable, you can use the sort(by:) and sorted(by:) sorting methods to take control of how your array is sorted. The difference between sort(by:) and sorted(by:) is the same as the difference between sort() and sorted(); the former sorts the array in-place, the latter returns a new array.

Here's an example of how sorted(by:) can be used to sort an array of strings by their length in descending order:

let sortedByLength = strings.sorted(by: { lhs, rhs in
  return lhs.count > rhs.count
})

The closure that's passed to sorted(by:) is called as often as needed with two elements to determine their order and you are expeced to return true if the first element (lhs) should precede the second element (rhs) is in the final result.

Because this sorting method is defined as O(n log n) you should be cautious about the amount of work you do in your closure. Doing too much work will result in a very slow sort and you shouldn't want that. This also applies to the regular sort() implementation, so if you're implementing your own Comparable conformance to make sorting an array of your own structs more straightforward, make sure your implementation is as efficient as possible.

Tip: If you're not familiar with Big-O notation, take a look at this post to learn more.

I mentioned earlier that if Swift's sorting algorithm encounters two equal elements, this also applies when you use sorted(by:) even though you can't express equality (you either return true or false). Swift's sorting algorithm processes your array in-order so it knows that if you return false from your sorting closure, an element should not precede the element that it's being compared to. If they're equal, any subsequent comparisons between both elements will return the same value, which means that these two elements will not relative positions unless they're not equal.

For example, sorting our original array while printing all performed comparisons yields this result:

Hello > Oh: true
World > Oh: true
World > Hello: false
This > Oh: true
This > World: false
Is > Oh: false
An > Is: false
Unsorted > An: true
Unsorted > Is: true
Unsorted > Oh: true
Unsorted > This: true
Unsorted > World: true
Unsorted > Hello: true
Array > An: true
Array > Is: true
Array > Oh: true
Array > This: true
Array > World: false

This output gives us a lot of insight into what happens. First Oh and Hello are compared and I return true. This means that Hello should precede Oh. This puts Hello at the first position in the sorted array and Oh is at the second position. Next, the third item in the array (World) is compared to the second (Oh). I return true Because World should precede Oh. Next, the algorithm wants to know whether World should precede Hello. They're equal in length so I return false ("Hello".count > "World".count is false).

The next interesting lines are:

Unsorted > World: true
Unsorted > Hello: true

The word Unsorted is longer than both World and Hello so it preceded both words. However, placing Unsorted before Hello does not change the order in which Hello and World appear in the final result.

The last comparison in our output is Array > World. This comparison returns false because these words are of equal length. And because Array occurs after World in the original, that order is maintained; we stop moving Array upward in the final result as soon as we return false.

While this experiment was useful to provide you with a basic understanding of how Swift's sorting works at the time of writing, you shouldn't rely on the details too much. Your main takeways should be:

  • Swift's sorting is stable; this means that equal elements keep their relative order
  • Swift's sorting is O(n log n) at worst

And of course the most important takeaway by far is that you can sort an array of Comparable elements with sort() and sorted(). If you need more control over how your elements are sorted, or if they aren't Comparable you can use sort(by:) or sorted(by:).

If you have any questions about this tip, or if you have feedback for me, make sure to reach out on Twitter.

Splitting a JSON object into an enum and an associated object with Codable

Decoding data, like JSON, is often relatively straightforward. For a lot of use cases, you won't need to know or understand a lot more than what I explain in this post. However, sometimes you need to dive deeper into Codable, and you end up writing custom encoding or decoding logic like I explain in this post.

In more advanced scenarios, you might need to have an extremely flexible approach to decoding data. For example, you might want to decode your data into an enum that has an associated value depending on one or more properties that exist in your JSON data.

I know that this post sounds like it's most likely covered by this Swift evolution proposal. The proposal covers a slightly different case than the case that I'd like to show you in this post.

So before I dive in, I want to show you the problem that SE-0295 solves before I move on to the main topic of this post.

Understanding the problem that SE-0295 solves

Without going into too much detail, the SE-0295 Swift Evolution proposal covers coverting data that has a key + value pair into an enum with an associated value by using the key from the data as the enum case, and the value for a given key as an associated value.

So for example (this example is copied from the linked proposal), it would allow you to decode the following JSON:

{
  "load": {
    "key": "MyKey"
  }
}

Into the following Decodable enum:

enum Command: Decodable {
  case load(key: String)
}

This is neat, but it doesn't quite cover every possible use case.

For example, imagine that you have a JSON response that looks a bit like this:

{
  "coupons": [
    {
      "type": "promo",
      "discount": 0.2,
      "code": "AEHD36"
    },
    {
      "type": "giftcard",
      "value": 12.0,
      "code": "GIFT_2816"
    }
  ]
}

Decoding this into a Coupon enum that has a promo and giftcard case, both with associated values for the other fields, is not possible with SE-0295. Instead. We can use a custom init(from:) and encode(to:) to add support for this.

Decoding a JSON object into an enum case with associated values

Given the JSON from the previous section, I would like to decode this into the following Decodable models:

struct Checkout: Decodable {
  let coupons: [Coupon]
}

enum Coupon: Decodable {
  case promo(discount: Float, code: String)
  case giftcard(value: Float, code: String)
}

When you write these models, you'll immediately see that Xcode complains. Coupon does not conform to Decodable because the compiler can't synthesize the init(from:) implementation for Coupon. Luckily, we can write this initializer ourselves:

enum Coupon: Decodable {
  case promo(discount: Float, code: String)
  case giftcard(value: Float, code: String)

  enum CodingKeys: String, CodingKey {
    case type, discount, code, value
  }

  enum CouponType: String, Decodable {
    case promo, giftcard
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let type = try container.decode(CouponType.self, forKey: .type)
    let code = try container.decode(String.self, forKey: .code)

    switch type {
    case .promo:
      let discount = try container.decode(Float.self, forKey: .discount)
      self = .promo(discount: discount, code: code)
    case .giftcard:
      let value = try container.decode(Float.self, forKey: .value)
      self = .giftcard(value: value, code: code)
    }
  }
}

There's a lot to look at in this code snippet. If you haven't seen a custom implementation of init(from:) before, you might want to take a look at this post where I explain how you can write custom decoding and encoding logic for your Codable models.

The CodingKeys that I defined for this enum contains all of the keys that I might be interested in. These coding keys match up with the fields in my JSON, but they do not line up with my enum cases. That's because I'll use the value of the type in the data we're decoding to determine which enum case we're decoding. Since I'd like to have some type safety here, I decode the value of type into another enum. This enum only contains the cases that I consider valid and usually you'll find that they mirror your main enum's cases without their associated values.

In my init(from:) implementation you'll notice that I create a container using the CodingKeys. Next, I create two local variables that hold the type and code from the data that I'm decoding. Next, I use a switch to check what the value of type is. Based on the value of type I know which other value(s) to decode, and I know which enum case I should use for self.

This is pretty similar to how I added an other case to an enum in the post I linked to earlier.

This approach will work fine if our enum cases are simple. But what if we might have more complex objects that would total up to an unreasonable number of associated values?

In that case, you can use another Decodable object as each enum case's single associated value. Let's take a look at an updated version of the Coupon enum without the decoding logic:

enum Coupon: Decodable {
  case promo(PromoCode)
  case giftcard(Giftcard)

  enum CodingKeys: String, CodingKey {
    case type
  }
}

Both enum cases now have a single associated value that's represented by a struct. The CodingKeys object now has a single case; type. We'll write the two structs for the associated values first. After that, I'll show you the updated decoding logic for Coupon:

struct PromoCode: Decodable {
  let discount: Float
  let code: String
}

struct Giftcard: Decodable {
  let value: Float
  let code: String
}

These two structs shouldn't surprise you. They're just plain structs that match the JSON data that they're supposed to represent. The real magic is in the init(from:) for the Coupon:

// In Coupon
init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: CodingKeys.self)
  let type = try container.decode(CouponType.self, forKey: .type)

  let associatedContainer = try decoder.singleValueContainer()

  switch type {
  case .promo:
    let promo = try associatedContainer.decode(PromoCode.self)
    self = .promo(promo)
  case .giftcard:
    let card = try associatedContainer.decode(Giftcard.self)
    self = .giftcard(card)
  }
}

This init(from:) still extracts and switches on the type key from the data we're decoding. The real trick is in how I create instances of PromoCode and Giftcard using the single value container that I create right before the switch:

let promo = try associatedContainer.decode(PromoCode.self)
// and
let card = try associatedContainer.decode(Giftcard.self)

Instead of asking my container to decode an instance of PromoCode for a given key, I ask the decoder for a single value container. This is a new container based on all the data that we're decoding. I ask that container to decode a PromoCode. The reason I can't call container.decode on the original container to decode a PromoCode is that container.decode expects to decode an object for a key. This means that if you're thinking of it in terms of JSON, the data should look like this for container.decode to work:

{
  "type": "promo",
  "data": {
    "discount": 0.1,
    "code": "AJDK9"
  }
}

If we'd have data that looked like that, we would be able to decode PromoCode for a data key.

However, the data isn't nested; it's flat. So what we want is to decode two Swift objects for a single data object. To do that, you can pass create a second container that expects to extract a single value from the data. In this case, this means that we have one container based on our data to extract a type, and another container that's used to decode the full JSON object into the decodable object we're looking for. Pretty neat, right?

Once the associated value is decoded, I can assign the appropriate enum case and associated value to self, and the decoding is done. The error from earlier is unchanged in the updated code for Coupon.

With these shenanigans in place, we should also look at what the encoding logic for this enum with an associated object looks like.

Encoding an enum with an associated value into a single JSON object

Encoding the Coupon enum from the previous section unsurprisingly follows a similar pattern. We can first encode the type property, and then pass our encoder to the encode(to:) method on our associated value to encode the associated value. If you're following along, make sure you mark the Giftcard and PromoCode structs as Codable (or Encodable).

Let's look at the updated Coupon:

enum Coupon: Codable {
  case promo(PromoCode)
  case giftcard(Giftcard)

  enum CodingKeys: String, CodingKey {
    case type
  }

  init(from decoder: Decoder) throws {
    // existing implementation for init(from:)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)

    switch self {
    case .promo(let promo):
      try container.encode("promo", forKey: .type)
      try promo.encode(to: encoder)
    case .giftcard(let card):
      try container.encode("giftcard", forKey: .type)
      try card.encode(to: encoder)
    }
  }
}

The encoding logic is hopefully pretty much what you expected. I check what self is, and based on that I encode a type. Next, I grab the associated value and call encode(to:) on the associated value with the Encoder that our instance of Coupon received. The end result is that both the type and the encoded associated value both exist on the same encoded object.

To check whether the encoding logic works as expected, you can take the JSON string from the beginning of this post, convert it to data, decode it, and then encode it again:

let decoder = JSONDecoder()
let checkout = try! decoder.decode(Checkout.self, from: Data(jsonString.utf8))

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(checkout)
print(String(data: data, encoding: .utf8)!)

The structure of the printed JSON will mirror that of the JSON you saw at the start of this post.

Good stuff!

In Summary

In this post, you saw how you can use some interesting techniques to decode a single JSON object (or any data object that can be decoded using Decodable) into multiple objects by passing around your Decoder to different objects. In this example, I used an enum with an associated value but I'm sure you can imagine that you could make this approach to decode into two or more structs if needed. I personally couldn't think of a good reason to do this so I decided to not cover it in this post. Just know that the approach would be identical to what you've seen in this post.

A huge thank you goes out to Josh Asbury for pointing out a couple of neat improvements to the init(from:) that I ended up using for the Coupon object.

If you want to learn more about interesting things that you can do with Codable, make sure to check out the codable category on this website.

Writing custom JSON encoding and decoding logic

The default behavior for Codable is often good enough, especially when you combine this with custom CodingKeys, it's possible to encode and decode a wide variety of JSON data without any extra work.

Unfortunately, there are a lot of situations where you'll need to have even more control. The reasons for needing this control are varied. You might want to flatten a deeply nested JSON structure into a single Codable object. Or maybe you want to assign a default value to a property if it's not possible to extract this value from the received JSON data. Or maybe you want to make your an NSManagedObject subclass work with Codable.

No matter what your reason for needing to implement custom JSON encoding or decoding logic is for your Codable objects, the approach is always the same. In this post you'll learn how you can implement a custom init(from:) to decode JSON data, and a custom encoding(using:) method to encode a Codable object to JSON data.

Implementing custom JSON decoding logic

Decoding JSON data into a Decodable object is done through a special initializer that's required by the Decodable protocol. The initializer is called init(from decoder: Decoder), or as I like to write it init(from:).

This initializer is normally generated for you, but you can also implement it yourself if you need an extremely high level of customization.

The init(from:) initializer receives an object that conforms to the Decoder protocol, and it could be a JSONDecoder but that's not guaranteed. Swift's Decodable protocol was designed so it could work with different kinds of data.

In your initializer, you'll obtain a container object that knows how to extract values from the Data that's being decoded using your CodingKeys to look up these values. Note that as soon as you define your own init(from:), Swift will no longer generate your CodingKeys enum for you (even though Swift will generate an init(from:) if you define your own CodingKeys).

Let's look at a simple example of a custom init(from:):

struct User: Decodable {
  enum CodingKeys: String, CodingKey {
    case id, fullName, isRegistered, email
  }

  let id: Int
  let fullName: String
  let isRegistered: Bool
  let email: String

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.id = try container.decode(Int.self, forKey: .id)
    self.fullName = try container.decode(String.self, forKey: .fullName)
    self.isRegistered = try container.decode(Bool.self, forKey: .isRegistered)
    self.email = try container.decode(String.self, forKey: .email)
  }
}

This User struct is fairly standard, and if you look at it there's nothing fancy happening here. My CodingKeys all use their generated raw value which means that I expect my JSON to perfectly mirror the properties in this struct. In the init(from:) initializer, I obtain an instance of KeyedCodingContainer by calling container(keyedBy:) on the Decoder object that was passed to my initializer. This will create an instance of KeyedCodingContainer that will use the raw values for the cases on CodingKeys to look up information in my JSON data.

I can then call decode(_:forKey:) on my container object to extract an object of a given Decodable type (for example Int, String, or Bool) from my (JSON) data using the key that I passed as the forKey argument.

So for example, self.id = try container.decode(Int.self, forKey: .id) will attempt to look up a value for the key "id", and try to cast it to an Int. This is repeated for all properties on my struct.

This initial example shows how you can decode data that's consistent and always follows the same format. But what happens if the data is slightly less consistent, and we might need to work with default values in case a certain key is missing from the source data.

Assigning fallback values using a custom init(from:) method

Imagine that you are given the following JSON:

[
  {
    "id": 10,
    "fullName": "Donny Wals",
    "isRegistered": true,
    "email": "[email protected]",
  },
  {
    "id": 11,
    "fullName": "Donny Wals",
    "email": "[email protected]",
  }
]

Note how this is an array of two objects. One has an isRegistered property, and the other doesn't. You could say well, that should be a Bool? then so if isRegistered is missing, its value will be nil. That would work just fine.

However, we have a special requirement. If isRegistered is missing from the JSON data, we want the value for this property to be false.

You could attempt to define your User struct like this:

struct User: Decodable { 
  let id: Int
  let fullName: String
  let isRegistered = false
  let email: String
}

Unfortunately, this produces the following warning:

Immutable property will not be decoded because it is declared with an initial value which cannot be overwritten

That's a shame because we do want to use the isRegistered value from the JSON data if it's present. Luckily, we can achieve this through a custom init(from:) implementation.

struct User: Decodable {
  enum CodingKeys: String, CodingKey {
    case id, fullName, isRegistered, email
  }

  let id: Int
  let fullName: String
  let isRegistered: Bool
  let email: String

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.id = try container.decode(Int.self, forKey: .id)
    self.fullName = try container.decode(String.self, forKey: .fullName)
    self.isRegistered = try container.decodeIfPresent(Bool.self, forKey: .isRegistered) ?? false
    self.email = try container.decode(String.self, forKey: .email)
  }
}

Note that all my properties are defined as non-optional. That's because we know that isRegistered should always have a value, even if we didn't receive one in our JSON data.

In the init(from:) implementation, I use decodeIfPresent instead of decode to extract the value for isRegistered. I can't do this with decode because decode assumes that a value exists for the key that you pass, and it assumes that this value has the type that you're looking for. If no value exists for the given key, or this value can't be casted to the desired type, decode will throw a decoding error.

To work around this you could use try? and hide the error, but then you might be hiding far more important mistakes. A better solution to only decode a value if it exists is to use decodeIfPresent. This method will attempt to look up a value for the given key, and if no value was found this method will return nil. If a value was found but it can't be cast to the desired type, an error is thrown because that means your Data isn't structured as expected.

Because decodeIfPresent returns and optional value (in this case Bool? because I attempt to decode to Bool), you can use a nil-coalescing operator to assign a default value (?? false).

This example is relatively simple, but it's also quite powerful. It shows how you can leverage the convenient APIs that were designed for Decoder to decode Data into a Swift object without actually knowing which type of Data you're dealing with. It's easy to forget but none of the code in init(from:) is aware that we're decoding JSON data with a JSONDecoder.

Using a custom init(from:) implementation to future proof decoding for enums

You already know that enums in Swift can be Decodable (and Encodable) as long as their raw value matches the value used in your JSON data. However, you might run into trouble and decoding failures when your service returns an enum case that you didn't know about when you defined your model. Luckily, you can use a custom init(from:) to safely decode unkown enum cases into an other case with an associated value when you encounter an unkown value.

For example, imagine a Product struct that has a status property. This status is initially defined as follows:

enum Status: String, Decodable {
  case completed, inProgress
}

This is simple enough, and will work perfect as long as your back-end only returns "completed" or "inProgress" for the value of status on a product object.

However, in programming, we often have to account for the unkown. Especially if you do not control the server, or if your back-end is maintained by a different team, you might want to make sure your status can handle other values too. After all, you might not want your decoding to fail just because you encountered an unknown status string. Especially if your server team can't provide any guarantees about whether they might add new enum cases on the server side that you don't know about.

Imagine that you need to decode the following (partial) JSON data:

[
  {
    "status": "completed"
  },
  {
    "status": "inProgress"
  },
  {
    "status": "archived"
  }
]

There's a new, unkown "archived" status in this JSON data. Normally, decoding this data would fail because your Status enum is not aware of this new value. However, a custom init(from:) will help you decode this value into a new other(String) case:

enum Status: Decodable {
  case completed, inProgress
  case other(String)

  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let value = try container.decode(String.self)

    switch value {
    case "completed": self = .completed
    case "inProgress": self = .inProgress
    default: self = .other(value)
    }
  }
}

Note that I've removed the String raw value for Status. That's because an enum with a raw value can't have enum cases with associated values.

In the custom init(from:), I use decoder.singleValueContainer() to obtain a container that will only decode a single value. In this case, that's the string that's uses as the value for my product's status property. I'll show you how I've defined Product in a moment to clarify this.

I ask the container to decode its single value into a String, and then I use a switch to check the value of this string, and I use it to assign the appropriate enum case to self. If I find an unkown value, I assign the decoded value as the associated type for other. This will ensure decoding always works, even if the back-end team adds a new value without hiding any errors. Instead, you can check for the other case in your code, and handle this case in a way that is appropriate for your app.

Here's what the Product struct and my decoding code looks like:

struct Product: Decodable {
  let status: Status
}

let decoder = JSONDecoder()
let products = try decoder.decode([Product].self, from: jsonData)

As you can see this all looks very standard. The main takeaway here is that you can use a single value container to extract the value of a property in your JSON that isn't a JSON object/dictionary. You'll mostly find yourself use decoder.singleValueContainer() in the context of decoding enums.

Using RawRepresentable as an alternative to enums when handling unknown values

As with many things in programming, there's more than one way to implement a future-proof Decodable model. In the previous section I showed you how to use an enum with an other case to allow the decoding of new, and unknown values. If you don't like this, you can use a slightly different approach that was pointed out to me by Ian keen.

This alternative approach involes using a struct that's RawRepresentable by a String, as well as Decodable with static values for your known values. Here's what that would look like:

struct Status: Decodable, RawRepresentable {
  typealias RawValue = String

  static let completed = Status(rawValue: "completed")
  static let inProgress = Status(rawValue: "inProgress")

  let rawValue: String

  init?(rawValue: String) {
    self.rawValue = rawValue
  }
}

The nice thing about this is that you don't need to write any custom decoding (or encoding) logic at all. If you make Status conform to Equatable, you could even write comparison logic that looks a lot like you're used to with enums:

if let product = products.first, product.status == .completed {
  print(product status is completed)
}

Personally, I don't have a strong preference for either approach in this case. If you need to handle cases where you got an unknown value explicitly, an other enum might be a little nicer since you could easily compare to other. If you can handle any value just fine and only want to make sure you can decode an unknown value, the RawRepresentable struct might be a little nicer. It's up to you to decide the better fit.

There are many more neat little tricks that you can do with custom decoders, but for now you know everything you need to know write custom decoders for the most common situations you might encounter.

Implementing custom JSON encoding logic

Now that you know about decoding data into a Decodable object, it only makes sense to take a look at encoding an Encodable object into data too. Note how I didn't say JSON data. I did that on purpose because both your custom init(from:) and encode(to:) work without knowing what the format of the data is.

The custom decoding logic that I've shown you earlier used a container object to extract and convert data into the required data types like String, Int, or even your own Decodable objects.

Let's see how we apply this knowledge to a custom encode(to:) method for the User struct that I've shown you in the section on decoding. Here's the User struct from the previous section with the encode(to:) method already added to it:

struct User: Codable {
  enum CodingKeys: String, CodingKey {
    case id, fullName, isRegistered, email
  }

  let id: Int
  let fullName: String
  let isRegistered: Bool
  let email: String

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.id = try container.decode(Int.self, forKey: .id)
    self.fullName = try container.decode(String.self, forKey: .fullName)
    self.isRegistered = try container.decodeIfPresent(Bool.self, forKey: .isRegistered) ?? false
    self.email = try container.decode(String.self, forKey: .email)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(id, forKey: .id)
    try container.encode(fullName, forKey: .fullName)
    try container.encode(isRegistered, forKey: .isRegistered)
    try container.encode(email, forKey: .email)
  }
}

The code in encode(to:) is very similar to the code in init(from:). The main difference is in how you obtain a container. When you encode a struct to an Encoder, you need to obtain a mutable container that uses your CodingKeys as its mapping from your Encodable to the output format (usually JSON). In the case of encoder.container(keyedBy: CodingKeys.self), obtaining a container can't fail so you don't prefix this call with try.

Because you'll be encoding values into the container, the container needs to be a var. You'll mutate the container every time you ask it to encode a value.

To encode values, you call encode(_:forKey:) with the property you want to encode, and what key this property should be decoded to.

Sometimes, you'll want to send your encoded data to a server, and this server might expect you to omit nil values from your output. If that's the case, you should use encodeIdPresent(_:forKey:). This method will check whether the provided value is nil, and if it is, the key/value pair will be omitted from the container's output.

Next, let's take a look at how the encode(to:) for the Status enum from the previous section should be written since the Swift compiler can't properly account for the other enum case.

enum Status: Codable {
  case completed, inProgress
  case other(String)

  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let value = try container.decode(String.self)

    switch value {
    case "completed": self = .completed
    case "inProgress": self = .inProgress
    default: self = .other(value)
    }
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()

    switch self {
    case .completed: try container.encode("completed")
    case .inProgress: try container.encode("inProgress")
    case .other(let val): try container.encode(val)
    }
  }
}

As you might have expected, the implementation for encode(to:) looks similar to init(from:). The encode(to:) method obtains a single value container, and I use a switch to check the value of self so I can determine which string should be encoded by the single value container. If I encounter my .other case, I extract the associated value and tell my container to encode that associated value. This will make sure that we always properly encode and send our enum to the server (or that we can persist it to disc) without discarding the original unkown value.

Of course if you don't use an enum but instead opt to use the RawRepresentable alternative from the previous section, the encoding will work fine out of the box, just like the decoding did.

In Summary

In this post, you learned how you can override Swift's generated init(from:) and encode(to:) methods that are added for Decodable and Encodable objects respectively.

You learned that Swift uses a container object to read and write values from and to a JSON object, and you saw how you can use this container to extract and encode data. You also learned how you can leverage custom encoding and decoding logic to write enums that can decode cases that weren't known at the time of defining your enum. This is incredibly useful to make sure your code is as future proof as possible.

I also showed you an alternative to using an enum that's based on using a RawRepresentable struct that has static members for what would normally be your known enum cases. One of the benefits of this approach is that the Decodable and Encodable conformances can be generated by the compiler so you don't need to do any extra work.

In this post, you'll learn how you can use custom encoding and decoding logic to work with arbitrary enum cases that have associated values by passing around your Decoder object to decode the enum case and associated value seperately from the same underlying data object. This sounds similar to this Swift evolution proposal, but as you'll find out in the post it's quite different.

Another interesting thing you can do with a custom init(from:) is flattening nested data into a single struct, or expand a single struct into nested data using encode(to:). You'll learn how to do this in this post.

With the knowledge from this post you'll be able to implement highly customized JSON encoding and decoding flow up to the point where your JSON data and Codable object are almost nothing alike.

Customizing how Codable objects map to JSON data

In the introductory post for this series you learned the basics of decoding and encoding JSON to and from your Swift structs. In that post, you learned that your JSON object is essentially a dictionary, and that the JSON's dictionary key's are mapped to your Swift object's properties. When encoding, your Swift properties are used as keys in the encoded JSON output dictionary.

Unfortunately, we don't always have the luxury of a 1 to 1 mapping between JSON and our Swift objects.

For example, the JSON you're working with might use snake case (snake_case) instead of camel case (camelCase) for its keys. Of course, you could write your Decodable object's properties using snake case so they match the JSON you're decoding but that would lead to very unusual Swift code.

In addition to the case styling not being the same, your JSON data might even have completely different names for some things that you don't want to use in your Swift objects.

Fortunately, both of these situations can be solved by either setting a key decoding (or encoding) strategy on your JSONDecoder or JSONEncoder, or by specifying a custom list of coding keys that map JSON keys to the properties on your Swift object.

If you'd prefer to learn about mapping your codable objects to JSON in a video format, you can watch the video on YouTube:

Automatically mapping from and to snake case

When you're interacting with data from a remote source, it's common that this data is returned to you as a JSON response. Depending on how the remote server is configured, you might receive a server response that looks like this:

{
    "id": 10,
    "full_name": "Donny Wals",
    "is_registered": false,
    "email_address": "[email protected]"
}

This JSON is perfectly valid. It represents a single JSON object with four keys and several values. If you were to define this model as a Swift struct, it'd look like this:

struct User: Codable {
  let id: Int
  let full_name: String
  let is_registered: Bool
  let email_address: String
}

This struct is valid Swift, and if you would decode User.self from the JSON I showed you earlier, everything would work fine.

However, this struct doesn't follow Swift conventions and best practices. In Swift, we use camel case so instead of full_name, you'll want to use fullName. Here's what the struct would look like when all properties are converted to camel case:

struct User: Codable {
  let id: Int
  let fullName: String
  let isRegistered: Bool
  let emailAddress: String
}

Unfortunately, we can't use this struct to decode the JSON from earlier. Go ahead and try with the following code:

let jsonData = """
{
  "id": 10,
  "full_name": "Donny Wals",
  "is_registered": false,
  "email_address": "[email protected]"
}
""".data(using: .utf8)!

do {
  let decoder = JSONDecoder()
  let user = try decoder.decode(User.self, from: jsonData)
  print(user)
} catch {
  print(error)
}

You'll find that the following error is printed to the console:

keyNotFound(CodingKeys(stringValue: "fullName", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"fullName\", intValue: nil) (\"fullName\").", underlyingError: nil))

This error means that the JSONDecoder could not find a corresponding key in the JSON data for the fullName.

To make our decoding work, the simplest way to achieve this is to configure the JSONDecoder's keyDecodingStrategy to be .convertFromSnakeCase. This will automatically make the JSONDecoder map full_name from the JSON data to fullName in struct by converting from snake case to camel case.

Let's look at an updated sample:

do {
  let decoder = JSONDecoder()
  decoder.keyDecodingStrategy = .convertFromSnakeCase
  let user = try decoder.decode(User.self, from: jsonData)
  print(user)
} catch {
  print(error)
}

This will succesfully decode a User instance from jsonData because all instances of snake casing in the JSON data are automatically mapped to their camel cased counterparts.

If you need to encode an instance of User into a JSON object that uses snake casing, you can use a JSONEncoder like you would normally, and you can set its keyEncodingStrategy to convertToSnakeCase:

do {
  let user = User(id: 1337, fullName: "Donny Wals",
                  isRegistered: true,
                  emailAddress: "[email protected]")

  let encoder = JSONEncoder()
  encoder.keyEncodingStrategy = .convertToSnakeCase
  let data = try encoder.encode(user)

  print(String(data: data, encoding: .utf8)!)
} catch {
  print(error)
}

The output for this code is the following:

{"id":1337,"full_name":"Donny Wals","email_address":"[email protected]","is_registered":true}

The ability to automatically convert from/to snake case is really useful when you're dealing with a server that uses snake case instead of camel casing.

Using a custom key decoding strategy

Since there is no single standard for what a JSON response should look like, some servers use arbitrary patterns for their JSON keys. For example, you might encounter service that uses keys that look like this: USER_ID. In that case, you can specify a custom key encoding- or decoding strategy.

Let's take a look at a slightly modified version of the JSON you saw earlier:

{
  "ID": 10,
  "FULL_NAME": "Donny Wals",
  "IS_REGISTERED": false,
  "EMAIL_ADDRESS": "[email protected]"
}

Since these keys all follow a nice and clear pattern, we can specify a custom strategy to convert our keys to lowercase, and then convert from snake case to camel case. Here's what that would look like:

do {
  let decoder = JSONDecoder()

  // assign a custom strategy
  decoder.keyDecodingStrategy = .custom({ keys in
    return FromUppercasedKey(codingKey: keys.first!)
  })

  let user = try decoder.decode(User.self, from: uppercasedJson)
  print(user)
} catch {
  print(error)
}

The closure that's passed to the custom decoding strategy takes an array of keys, and it's expected to return a single key. We're only interested in a single key so I'm grabbing the first key here, and I use it to create an instance of FromUppercasedKey. This object is a struct that I defined myself, and it conforms to CodingKey which means I can return it from my custom key decoder.

Here's what that struct looks like:

struct FromUppercasedKey: CodingKey {
  var stringValue: String
  var intValue: Int?

  init?(stringValue: String) {
    self.stringValue = stringValue
    self.intValue = nil
  }

  init?(intValue: Int) {
    self.stringValue = String(intValue)
    self.intValue = intValue
  }

  // here's the interesting part
  init(codingKey: CodingKey) {
    var transformedKey = codingKey.stringValue.lowercased()
    let parts = transformedKey.split(separator: "_")
    let upperCased = parts.dropFirst().map({ part in
      return part.capitalized
    })

    self.stringValue = (parts.first ?? "") + upperCased.joined()
    self.intValue = nil
  }
}

Every CodingKey must have a stringValue and an optional intValue property, and two initializers that either take a stringValue or an intValue.

The interesting part in my custom CodingKey is init(codingKey: CodingKey).

This custom initializer takes the string value for the coding key it received and transforms it to lowercase. After that I split the lowercased string using _. This means that FULL_NAME would now be an array that contains the words full, and name. I look over all entries in that array, except for the first array and I captialize the first letter of each word. So in the case of ["full", "name"], upperCased would be ["Name"]. After that I can create a string using the first entry in my parts array ("full"), and add the contents of the uppercase array after it (fullName). The result is a camel cased string that maps directly to the corresponding property in my User struct.

Note that the work I do inside init(codingKey: CodingKey) isn't directly related to Codable. It's purely string manipulation to convert strings that look like FULL_NAME to strings that look like fullName.

This example is only made to work with decoding. If you want it to work with encoding you'll need to define a struct that does the opposite of the struct I just showed you. This is slightly more complex because you'll need to find uppercase characters to determine where you should insert _ delimiters to match the JSON that you decoded initially.

A custom key decoding strategy is only useful if your JSON response follows a predicatable format. Unfortunately, this isn't always the case.

And sometimes, there's nothing wrong with how JSON is structured but you just prefer to map the values from the JSON you retrieve to different fields on your Decodable object. You can do this by adding a CodingKeys enum to your Codable object.

Using custom coding keys

Custom coding keys are defined on your Codable objects as an enum that's nested within the object itself. They are mostly useful when you want your Swift object to use keys that are different than the keys that are used in the JSON you're working with.

It's not uncommon for a JSON response to contain one or two fields that you would name differently in Swift. If you encounter a situation like that, it's a perfect reason to use a custom CodingKeys enum to specify your own set of coding keys for that object.

Consider the following JSON:

{
  "id": 10,
  "fullName": "Donny Wals",
  "registered": false,
  "e-mail": "[email protected]",
}

This JSON is slightly messy, but it's not invalid by any means. And it also doesn't follow a clear pattern that we can use to easily transform all keys that follow a specific pattern to something that's nice and Swifty. There are two fields that I'd want to decode into a property that doesn't match the JSON key: registered and e-mail. These fields should be decoded as isRegistered and email respectively.

To do this, we need to modify the User struct that you saw earlier in this post:

struct User: Codable {
  enum CodingKeys: String, CodingKey {
    case id, fullName
    case isRegistered = "registered"
    case email = "e-mail"
  }

  let id: Int
  let fullName: String
  let isRegistered: Bool
  let email: String
}

The only think that's changed in this example is that User now has a nested CodingKeys enum. This enum defines all keys that we want to extract from the JSON data. You must always add all keys that you need to this enum, so in this case I added id and fullName without a custom mapping. For isRegistered and email, I added a string that represents the JSON key that this property should map to. So isRegistered on User will be mapped to registered in the JSON.

To decode an object that uses custom coding keys, you don't need to do anything special:

do {
  let decoder = JSONDecoder()
  let user = try decoder.decode(User.self, from: jsonData)
  print(user)
} catch {
  print(error)
}

Swift will automatically use your CodingKeys enum when decoding your object from JSON, and it'll also use them when encoding your object to JSON. This is really convenient since there's nothing you need to do other than defining your CodingKeys.

Your CodingKeys enum must conform to the CodingKey playform and should use String as its raw value. It should also contain all your struct properties as its cases. If you omit a case, Swift won't be able to decode that property and you'd be in trouble. The case itself should always match the struct (or class) property, and the value should be the JSON key. If you want to use the same key for the JSON object as you use in your Codable object, you can let Swift infer the JSON key using the case name itself because enums will have a string representation of case as the case's raw value unless you specify a different raw value.

In Summary

In this post, you learned how you can use different techniques to map JSON to your Codable object's properties. First, I showed you how you can use built-in mechanisms like keyDecodingStrategy and keyEncodingStrategy to either automatically convert JSON keys to a Swift-friendly format, or to specificy a custom pattern to perform this transformation.

Next, you saw how you can customize your JSON encoding and decoding even further with a CodingKeys enum that provides a detailed mapping for your JSON <-> Codable conversion.

Using CodingKeys is very common when you work with Codable in Swift because it allows you to make sure the properties on your Codable object follow Swift best practices without enforcing a specific structure on your server data.

An introduction to JSON parsing in Swift

Virtually every modern application needs some way to retrieve, and use, data from a remote source. This data is commonly fetched by making a network request to a webserver that returns data in a JSON format.

When you're working with Javascript, this JSON data can be easily decoded into a Javascript object. Javascript doesn't have strong typing, so a JSON object in Javascript is really just a JavaScript Object.

Objects in Javascript are very comparable to dictionaries in Swift, except they aren't strongly typed and they have a couple of extra features. But that's way beyond what I want to cover in this post...

In this post, I want to take a look at Swift's Codable protocol.

So why start with JSON?

Well, JSON is arguably the most common data format that we use to exchange data on the web. And Swift's Codable protocol was designed to provide a powerful and useful mechanism to convert JSON data into Swift structs.

What's nice about Codable is that it was designed to not be limited to JSON. Out of the box, Codable can also be used to decode a .plist file into Swift structs, or to convert Swift structs into data for a .plist file.

The post you're looking at is intended to provide an introduction into Swift's Codable protocol, and it's part of a series of posts on this topic. I will focus on showing you how to work with JSON and Codable in Swift. It's good to understand that the principles in this series can be applied to both JSON data, as well as .plist files.

I'll start by explaining what Swift's Codable is. After that, I'll show you how to define a struct that implements the Codable protocol, and I'll explain the basics of encoding and decoding JSON data.

If you prefer to consume the contents of this post as a video, you can watch the video below.

Understanding what Swift's Codable is

The Codable protocol in Swift is really a union of two protocols: Encodable and Decodable. These two protocols are used to indicate whether a certain struct, enum, or class, can be encoded into JSON data, or materialized from JSON data.

When you only want to convert JSON data into a struct, you can conform your object to Decodable. If you only want to transform instances of your struct into Data, you can conform your object to Encodable, and if you want to do both you can conform to Codable.

A lot of Swift's built-in types already conform to Codable by default. For example, Int, String, and Bool are Codable out of the box.

Even dictionaries and arrays are Codable by default as long as the objects that you store in them conform to Codable.

This means that an array defined as Array<String> conforms to Codable already. A dictionary that's defined as Dictionary<String: String> is Codable too.

Arrays and dictionaries both play important roles in JSON because everything in JSON is defined using the equivalent of Swift's arrays and dictionaries.

For example, the following is valid JSON for an array of strings:

["hello", "world"]

And the following is an example of a dictionary in JSON:

{
    "hello": "world",
    "someInt": 10,
    "someBool": true
}

Notice how this dictionary has String as its key and three different kinds of values as its value. In Swift, you might represent a dictionary like this as [String: Any]. If we want to decode this JSON into something useful, we can't use [String: Any]. Because Any isn't Codable, a dictionary that has Any as its key can't be Codable either.

Luckily, all values for this object are Codable. Remember, Swift's String, Int, and Bool are all Codable!

Earlier I wrote that your structs, enums, and classes can conform to Codable. Swift can generate the code needed to extract data to populate a struct's properties from JSON data as long as all properties conform to Codable.

In this case, that means we would define a struct that has three properties with types String, Int, and Bool. Swift will take care of the rest.

Let's take a look at an example.

Defining a Codable struct

Given a specific JSON object, it's possible for us to figure out and define structs, classes, and enums that represent this JSON data in Swift.

The easiest way to do this, is to mirror the JSON structure 1-on-1. In this post, you will learn how you can customize the mapping between your Codable object an the JSON data you want to encode or decode. In this post, you will learn how to write custom logic to extract JSON data for a struct that's completely different from the JSON data that's used to populate the struct. For now, we'll focus on a direct mirror.

Earlier, I showed you this JSON:

{
    "hello": "world",
    "someInt": 10,
    "someBool": true
}

If we'd model this data using a Swift struct, we'd write the following:

struct ExampleStruct: Decodable {
    let hello: String
    let someInt: Int
    let someBool: Bool
}

Notice how I declared my struct as ExampleStruct: Decodable. This means that my struct conforms to Decodable, and I can decode JSON into instances of this struct. If I'd want to encode instances of my struct into JSON data, I would declare my struct as ExampleStruct: Encodable, and to convert in both directions I'd use ExampleStruct: Codable.

In this case, I only want to decode so I'm declaring my struct as Decodable.

Notice how the property names for my struct exactly match the keys in my JSON dictionary. This is important because the code that Swift generates behind the scenes for you when you compile your code assumes that the keys in your JSON match the property names of your Decodable object.

The properties of my struct are all Decodable themselves, this means that Swift can automatically generate the code needed to decode JSON data into my struct.

Let's take a look at a more complex JSON structure:

{
  "status": "active",
  "objects": [
    {
      "id": 1,
      "name": "Object one",
      "available": true
    },
    {
      "id": 2,
      "name": "Object two",
      "available": false
    },
  ]
}

In this example, we have a JSON object with two keys, one of them has an array as its value as you can tell by the [] that wrap the value for objects. The array contains more JSON objects. JSON objects are always wrapped by {}.

If we look at this JSON data from the point of view of our struct, we can see that we should define one struct with two properties (status and objects), and that objects should be an array of sorts. This array will hold instances of another struct that has three properties (id, name, and available).

Here's what our Swift models might look like:

struct Response: Decodable {
  let status: String
  let objects: [Product]
}

struct Product: Decodable {
  let id: Int
  let name: String
  let available: Bool
}

Swift can generate code to decode JSON into these structs because Product's properties are all Decodable. This means that Response's properties are also all Decodable since [Product] is Decodable. Remember, arrays are Decodable (or Codable) as long as their Element is Decodable (or Codable).

What's interesting about Codable, is that we can also make enums Codable, as long as they have a raw value that is Codable. For example, we could change the Response's status property to a ResponseStatus enum as follows:

struct Response: Decodable {
  let status: ResponseStatus
  let objects: [Product]
}

enum ResponseStatus: String, Decodable {
  case active = "active"
  case inactive = "inactive"
}

When we attempt to decode our JSON data into Response, the decoding will fail if we receive an unkown value for ResponseStatus.

Depending on your use case, this might be desired, or a problem. In this post, you'll learn how you can write custom decoding logic that will allow you to decode unkown values into a special other case that has an associated value (case other(String)) that can be used to represent new and unkown enum cases for a Decodable enum.

Now that you've seen some examples of how you can define a Decodable struct, let's see how you can decode JSON data into a Decodable struct with a JSONDecoder.

Decoding JSON into a struct

When you've obtained a Data object that represents JSON data, you'll want to decode this data into your Swift struct (or class of course). If you don't have a remote API to practice with, you can define some dummy JSON data using Swift's multiline string syntax as follows:

let exampleData = """
{
  "status": "active",
  "objects": [
    {
      "id": 1,
      "name": "Object one",
      "available": true
    },
    {
      "id": 2,
      "name": "Object two",
      "available": false
    },
  ]
}
""".data(using: .utf8)!

You can call data(using:) on any Swift string to obtain a data representation for that string.

To convert your Data to an instance of your struct, you need a JSONDecoder instance. You can create one as follows:

let decoder = JSONDecoder()

To decode the dummy data I showed you just now into an instance of the Response struct from the previous section, you'd use the following code:

do {
  let jsonDecoder = JSONDecoder()
  let decodedResponse = try jsonDecoder.decode(Response.self,
                                               from: exampleData)

  print(decodedResponse)
} catch {
  print(error)
}

Your JSONDecoder instance has a decode(_:from:) method that you call to convert JSON data into the object of your choosing.

The first argument for this method is the type that you want to decode your data into. In this case, that's Response.self.

The second argument for this method is the data that you want to extract your data from. In this case, that's exampleData.

Because JSON decoding can fail, decode(_:from:) must be called with a try prefix, preferably in a do {} catch {} block.

If something goes wrong we print the error so we can see what went wrong. The error messages that are surfaced by JSONDecoder are generally very helpful. For example, if our struct would contain a type that is not present in the JSON data we would see an error that looks like this:

keyNotFound(CodingKeys(stringValue: "missingObject", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"missingObject\", intValue: nil) (\"missingObject\").", underlyingError: nil))

We can see that we're dealing with a keyNotFound error. We can find out which key wasn't found by reading the CodingKeys declaration that comes after the error case. In this case, the CodingKeys value tells us that we're trying to extract a value for the missingObject key but that key does not exist in the JSON as noted by the debugDescription.

When you see an error like this it usually means that you made a typo, or your JSON object doesn't always contain a specific key. This can happen when your remote data source doesn't include keys with a nil value.

If you made a typo, you should fix it. If your remote data source omits keys with a nil value, you can mark your property as optional. That way the missing property will get a nil value automatically if it's missing in the JSON response.

All errors you might encounter when decoding JSON in Swift follow a similar pattern. Make sure you read your decoding errors if you encounter them because they'll typically provide you with very useful information to debug and fix your models.

Now that you've seen how to decode data, let's take a look at doing the opposite; encoding structs into JSON data.

Encoding a struct to JSON

When you encode data from a struct, class, or enum to JSON data, the end result of your encoding will always be Data. In other words, you decode Data into Decodable objects, and you encode an Encodable object into Data. This data can be written to a file, sent to a server, it could even be persisted using a Core Data entity or UserDefaults. However, the most common goal when encoding objects is to either write the data to a file, or to send it to a server.

Take a look at the following Encodable struct:

struct Product: Codable {
    let id: Int
    let name: String
    let available: Bool
}

Now let's see how you can encode an instance of this struct to Data:

let sampleInput = Product(id: 0, name: "test name", available: true)

do {
  let encoder = JSONEncoder()
  let data = try encoder.encode(sampleInput)
  print(data)
} catch {
  print(error)
}

This code is pretty straightforward, and if you run this in a playground, you'll find that the printed output is the following:

44 bytes

That might be surprising to you. After all, you encoded your struct to JSON data, right?

Well, you did. But Data is data and it's represented as bytes. You can inspect the generated JSON by transforming the data to a string:

if let jsonString = String(data: data, encoding: .utf8) {
  print(jsonString)
}

The output for this code is the following:

{"id":0,"name":"test name","available":true}

Neat! That's a nice JSON string.

Note that this output is not what you should typically send to a server or write to a file. Instead, you should use the Data that was returned by the JSON encoder's encode method. That Data is the binary representation of the String that we just printed.

By default, JSONEncoder will encode your objects into a single-line JSON structure like you just saw. The exampleData that I showed you earlier was nicely formatted on multiple lines. It's possible to configure JSONEncoder to insert newlines and tabs into the output, this allows you to inspect a nicely formatted string representation of the JSON data. you can do this by setting the encoder's outputFormatting to .prettyPrinted:

do {
  let encoder = JSONEncoder()

  encoder.outputFormatting = .prettyPrinted

  let data = try encoder.encode(sampleInput)
  if let jsonString = String(data: data, encoding: .utf8) {
    print(jsonString)
  }
} catch {
  print(error)
}

The output for the code below would look like this:

{
  "id" : 0,
  "name" : "test name",
  "available" : true
}

If you're inspecting a large JSON structure, it's nice to use this pretty printed format. It's not common to need this output format when you write your encoded data to a file, or when you send it to a server. The whitespace is only useful for humans, and it doesn't provide any value to machines that interpret the JSON data.

A more important outputFormatting is .sortedKeys. When you set the output formatting to .sortedKeys, the generated Data will have your JSON keys sorted alphabetically. This can be useful if your server expects you to format your keys in a specific way, or if you want to compare to different encoded objects to see if their data is the same. If the keys aren't sorted, two Data instances that hold the same JSON data might not be equal due to differences in how their keys are ordered.

Here's an example of the encoded sampleInput from earlier when using a JSONEncoder that has its outputFormatting set to .sortedKeys:

{"available":true,"id":0,"name":"test name"}

The output isn't pretty printed but notice how the encoded keys are now in alphabetical order.

It's not common to have to encode your JSON data using a specific key sorting, but it's good to know this option exists if needed. I know I've needed it a few times when working with third party APIs that had requirements about how the JSON data I sent it was formatted.

You can combine the .sortedKeys and .prettyPrinted options by setting outputFormatting to an array:

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]

In Summary

In this post, you learned everything you need to know to get started with JSON encoding and decoding in Swift. You learned what the Codable protocol is, you learned how Swift automatically generates encoding and decoding logic for objects that conform to Codable, and you learned that Codable is really a union of two protocols; Encodable and Decodable.

I also showed you several examples of decoding JSON into Swift objects, and of encoding Swift objects into JSON.

In future posts, we'll dive deeper into thinks like CodingKeys, custom encoding- and decoding logic, and more advanced examples of how you can work with complex JSON data.

Flattening a nested JSON response into a single struct with Codable

Often, you'll want you Swift models to resemble JSON that's produced by an external source, like a server, as closely as possible. However, there are times when the JSON you receive is nested several levels deep and you might not consider this appropriate or needed for your application. Or maybe you're only interested in a couple of fields from the JSON response and these fields are hidden several levels deep in the JSON that's returned by a server.

In this post I'll show you how you can use nested containers to decode nested JSON data into a flat struct with a custom init(from:) implementation.

If you're not familiar with implementing a custom init(from:) method, take a look at this post. It describes custom encoding and decoding logic in detail and serves as basis for us to be building a flattening init(from:).

Decoding nested JSON data into a single struct

Consider the follow JSON data:

{
  "id": 10,
  "contact_info": {
    "email": "[email protected]"
  },
  "preferences": {
    "contact": {
      "newsletter": true
    }
  }
}

There's a lot of nesting here, and in this case all of this nesting is kind of noisy but it's very close to the kinds of JSON we sometimes have to work with in production. We can't change the backend in this case, so let's see how this JSON can be decoded into the following struct:

struct User: Decodable {
  let id: Int
  let email: String
  let isSubscribedToNewsletter: Bool
}

This struct does not represent our JSON at all. It's a good representation of the data for usage in an app but we can't go from our JSON to this struct directly without writing a custom init(from:) that leverages multiple CodingKey enums to map the source JSON to our struct.

struct User: Decodable {
  let id: Int
  let email: String
  let isSubscribedToNewsletter: Bool

  enum OuterKeys: String, CodingKey {
    case id, preferences
    case contactInfo = "contact_info"
  }

  enum ContactKeys: String, CodingKey {
    case email
  }

  enum PreferencesKeys: String, CodingKey {
    case contact
  }

  enum ContactPreferencesKeys: String, CodingKey {
    case newsletter
  }

  init(from decoder: Decoder) throws {
    let outerContainer = try decoder.container(keyedBy: OuterKeys.self)
    let contactContainer = try outerContainer.nestedContainer(keyedBy: ContactKeys.self,
                                                              forKey: .contactInfo)
    let preferencesContainer = try outerContainer.nestedContainer(keyedBy: PreferencesKeys.self,
                                                                  forKey: .preferences)
    let contactPreferencesContainer = try preferencesContainer.nestedContainer(keyedBy: ContactPreferencesKeys.self,
                                                                               forKey: .contact)

    self.id = try outerContainer.decode(Int.self, forKey: .id)
    self.email = try contactContainer.decode(String.self, forKey: .email)
    self.isSubscribedToNewsletter = try contactPreferencesContainer.decode(Bool.self, forKey: .newsletter)
  }
}

In this example I've defined several coding key enums. Each enum represents one of the JSON objects that I want to flatten into the User struct.

In the init(from:) method, the first like should look familiar to you if you've written a custom init(from:) before.

let outerContainer = try decoder.container(keyedBy: OuterKeys.self)

This line extracts a container that uses the keys in my OuterKeys enum. The lines after this line are probably new to you:

let contactContainer = try outerContainer.nestedContainer(keyedBy: ContactKeys.self,
                                                          forKey: .contactInfo)
let preferencesContainer = try outerContainer.nestedContainer(keyedBy: PreferencesKeys.self,
                                                              forKey: .preferences)
let contactPreferencesContainer = try preferencesContainer.nestedContainer(keyedBy: ContactPreferencesKeys.self,
                                                                           forKey: .contact)

Instead of extracting a container from the decoder instance, I extract containers from other containers. These containers are keyed by their respective enums and they allow me to dig into the JSON data to get to the data I'm interested in.

In this case, that means that I can extract the id from the outerContainer, the email from the contactContainer and lastly, I can extract the value for isSubscribedToNewsletter from the contactPreferencesContainer.

Using nested container can be a super powerful approach to flattening your JSON data but maybe you're just looking for a way to provide a flattened struct and you don't mind defining the Decodable structs that mirror your JSON data.

If that's the case, you can simplify your init(from:) quite a bit, and you don't need to write custom coding keys for every intermediate object in your JSON. You do, however have to define all intermediate structs which means that your gains are exclusively in the init(from:) as shown in the example below:

struct User: Decodable {
  let id: Int
  let email: String
  let isSubscribedToNewsletter: Bool

  enum CodingKeys: String, CodingKey {
    case id, preferences
    case contactInfo = "contact_info"
  }

  struct ContactInfo: Decodable {
    let email: String
  }

  struct Preferences: Decodable {
    let contact: ContactPreferences

    struct ContactPreferences: Decodable {
      let newsletter: Bool
    }
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let contactInfo = try container.decode(ContactInfo.self, forKey: .contactInfo)
    let preferences = try container.decode(Preferences.self, forKey: .preferences)

    self.id = try container.decode(Int.self, forKey: .id)
    self.email = contactInfo.email
    self.isSubscribedToNewsletter = preferences.contact.newsletter
  }
}

This approach for decoding the data was pointed out to me by Filip Němeček as an alternative that's easier to understand. I definitely agree that not needing the intermediate containers can be a fantastic bonus. I'll leave it up to you to decide which solution you like better; they each have their own merit in my opinion.

Each of these two approaches take a little bit of extra work compared to having a model that mirror your JSON data but the result of this flattening is quite nice and it doesn't make using your JSONDecoder any more complex:

let decoder = JSONDecoder()
let user = try! decoder.decode(User.self, from: jsonData)

While it's nice that we can flatten this data, let's see how we can write a custom encode(to:) implementation that would allow us to encode and send this User object back to a server in its original shape.

Encoding a flat struct into nested JSON data

Sometimes you'll need to be able to encode and decode your data in order to be able to fetch data from a server and then update it as needed. In these cases, you'll need to write some custom encoding logic to allow converting your flat struct back into the nested JSON data you started out with.

As usual, the encoding part of this example is very simliar to the decoding part. Let's look at the encoding counterpart for the first flattening approach:

struct User: Codable {
  let id: Int
  let email: String
  let isSubscribedToNewsletter: Bool

  // coding keys

  init(from decoder: Decoder) throws {
    // unchanged
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: OuterKeys.self)
    var contactContainer = container.nestedContainer(keyedBy: ContactKeys.self,
                                                     forKey: .contactInfo)
    var preferencesContainer = container.nestedContainer(keyedBy: PreferencesKeys.self,
                                                         forKey: .preferences)
    var contactPreferencesContainer = preferencesContainer.nestedContainer(keyedBy: ContactPreferencesKeys.self,
                                                                           forKey: .contact)

    try container.encode(id, forKey: .id)
    try contactContainer.encode(email, forKey: .email)
    try contactPreferencesContainer.encode(isSubscribedToNewsletter, forKey: .newsletter)
  }
}

Note that I've omitted the implementation for init(from:) and the coding key enums. They are unchanged from the previous section.

The implementation for encode(to) follows the exact same pattern as init(from:). I create all the containers using their respective coding keys, and then I encode the properties of User into the appropriate containers.

Let's take a look at the alternative approach that uses intermediate structs instead of coding keys next:

struct User: Codable {
  let id: Int
  let email: String
  let isSubscribedToNewsletter: Bool

  // coding keys and structs

  init(from decoder: Decoder) throws {
    // unchanged
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    let contactPreferences = Preferences.ContactPreferences(newsletter: isSubscribedToNewsletter)
    let preferences = Preferences(contact: contactPreferences)
    let contactInfo = ContactInfo(email: email)

    try container.encode(id, forKey: .id)
    try container.encode(preferences, forKey: .preferences)
    try container.encode(contactInfo, forKey: .contactInfo)
  }
}

In order to encode the original structs into my encoder, I need to create instances of these structs by hand. In this case, that's not a big deal; my structs are very small so this only takes a couple of lines of code.

After initializing my structs, I encode them into my container using the coding keys that I originally used to extract the same structs in my init(from:).

If you would decode the data from the beginning of this post into a User and then back into Data, you'll see that the JSON structure is identical with this approach. Nice!

In Summary

In this post I showed you how you can use a custom init(from:) to flatten nested JSON data into a single struct by writing your own init(from:) that created several keyed containers based on the different nested objects in the JSON data we're decoding. I also showed you an alternative approach that uses intermediate structs to decode the data and eventually assigned values from the decoded objects to my flattened struct. As I said in the section, I'll leave it up to you to decide which approach you prefer; I like them both. After showing you how to decode nested data, you saw how you can encode a flat struct into nested JSON data.

Writing your own encoding and decoding logic to perform radical transformations like this is something you'll rarely do. It's often more work than it's worth, and it's generally good to have your models mirror the data that you fetch from a remote source. Whether flattening JSON data into a single struct is a good idea will always depend on your reasons and use case. This post is not intended to be advice; it's intended to show you one of the many interesting things that can be done with Swift's encoding and decoding tools.

Preventing unwanted fetches when using NSFetchedResultsController and fetchBatchSize

This article covers a topic that is extensively covered in my Practical Core Data book. This book is intended to help you learn Core Data from scratch using modern techniques and every chapter features sample apps in SwiftUI as well as UIKit whenever this is relevant.

When you use Core Data in a UIKit or SwiftUI app, the easiest way to do this is through a fetched results controller. In SwiftUI, fetched results controller is best used through the @FetchRequest property wrapper. In UIKit, a fetched results controller can be conveniently set up to provide diffable data source snapshots for your table- or collection view while SwiftUI's @FetchRequest will conveniently update your UI as needed without requiring any extra work.

If you're somewhat knowledgable in the realm of Core Data, you've heard about the fetchBatchSize property.

This property is used to fetch your data in batches to prevent having to fetch your entire result set in one go. When you're dealing with a large data set, this can be a huge win.

However, when you're using a fetched results controller with diffable data sources and you set a fetchBatchSize, you'll find that your fetched results controller will initially fetch all of your data using your specified batch size. In other words, your data will be retrieved immediately using many small fetches. Once you start scrolling through your list, the fetched results controller will fetch your data again. using the specified batch size

Because SwiftUI's @FetchRequest is built on top of NSFetchedResultsController, you'll see the exact same problem manifest in a SwiftUI app that uses @FetchRequest with a fetch request that has its fetchBatchSize set.

In this post, I will briefly explain what the problem is exactly, and I'll show you a solution for a UIKit solution. A solution for SwiftUI will be published in a seperate post.

Understanding the problem

The easiest way to spot a problem like the one I described in the introduction of this post is through Core Data's debug launch arguments so you can see the SQLite statements that Core Data runs to retrieve and save data.

When you enable these launch arguments in an app that uses fetchBatchSize combined with a fetched results controller that provides diffable data source snapshots, you'll notice the following:

  1. First, all objectIDs are fetched in the correct order so the fetched results controller (or @FetchRequest which uses a fetched results controller under the hood as far as I can tell) knows the number of items in the result set, and so it knows how to page requests.
  2. Then, all managed objects are fetched in batches that match the batch size you've set.
  3. Lastly, your managed objects are fetched in batches that match your batch size as you scroll through your list.

The second point on this list is worrying. Why does a fetched results controller fetch all data when we expect it to only fetch the first batch? After all, you set a batch size so you don't fetch all data in one go. And now your fetched results controller doesn't just fetch all data up front, it does so in many small batches.

That can't be right, can it?

As it turns out, it seems related to how NSFetchedResultsController constructs a diffable data source snapshot.

I'm not sure how it works exactly, but I am sure that generating the diffable data source snapshot is what triggers these unwanted fetch requests. If a UIKit app, you can quickly verify this by commenting out your NSFetchedResultsControllerDelegate's controller(_:didChangeContentWith:) method. One you do this, you'll notice that your fetched results controller no longer fetches all data.

So how can you work around this?

As it turns out, there's no straightforward way to do this. The best way I've found is to stop using diffable data sources completely and instead use the older delegate methods from NSFetchedResultsControllerDelegate to update your table- or collection view.

In the next section, I'll show you how you can implement the appropriate delegate methods and update an existing collection view. How you build the collection view is up to you, as long as you populate your collection view by implementing the UICollectionViewDataSource methods rather than using diffable data sources.

Preventing unwanted requests in a UIKit app

The easiest way to prevent unwanted requests in a UIKit app is to get rid of the controller(_:didChangeContentWith:) delegate method that's used to have your fetched results controller construct diffable data source snapshots. Instead, you'll want to implement the following four NSFetchedResultsControllerDelegate methods:

  • controllerWillChangeContent(_:)
  • controller(_:didChange:atSectionIndex:for:)
  • controller(_:didChange:at:for:newIndexPath:)
  • controllerDidChangeContent(_:)

I like to abstract my fetched results controllers behind a provider object. For example, an AlbumsProvider, UsersProvider, POIsProvider, and so forth. The name of the provider describes the type of object that this provider object will fetch.

Here's a simple skeleton for a UsersProvider:

class UsersProvider: NSObject {
  fileprivate let fetchedResultsController: NSFetchedResultsController<User>

  let controllerDidChangePublisher = PassthroughSubject<[Change], Never>()
  var inProgressChanges: [Change] = []

  var numberOfSections: Int {
    return fetchedResultsController.sections?.count ?? 0
  }

  init(managedObjectContext: NSManagedObjectContext) {
    let request = User.byNameRequest
    self.fetchedResultsController =
      NSFetchedResultsController(fetchRequest: request,
                                 managedObjectContext: managedObjectContext,
                                 sectionNameKeyPath: nil, cacheName: nil)

    super.init()

    fetchedResultsController.delegate = self
    try! fetchedResultsController.performFetch()
  }

  func numberOfItemsInSection(_ section: Int) -> Int {
    guard let sections = fetchedResultsController.sections,
          sections.endIndex > section else {
      return 0
    }

    return sections[section].numberOfObjects
  }

  func object(at indexPath: IndexPath) -> User {
    return fetchedResultsController.object(at: indexPath)
  }
}

I'll show you the NSFetchedResultsControllerDelegate methods that should be implemented in a moment. Let's go over this class first.

The UsersProvider class contains two properties that you wouldn't need when you're using a diffable data source:

let controllerDidChangePublisher = PassthroughSubject<[Change], Never>()
var inProgressChanges: [Change] = []

The first of these two properties provides a mechanism to tell a view controller that the fetched results controller has informed us of changes. You could use a different mechanism like a callback to achieve this, but I like to use a publisher.

The second property provides an array that's used in the NSFetchedResultsControllerDelegate to collect the different changes that our fetched result controller sends us. These changes are communicated through multiple delegate callbacks because there's one call to a delegate method for each object or section that's changed.

The rest of the code in UsersProvider is pretty straightforward. We have a computed property to extract the number of sections in the fetched results controller, a method to extract the number of items in the fetched results controller, and lastly a method to retrieve an object for a specific index path.

Note that the controllerDidChangePublisher published an array of Change objects. Let's see what this Change object looks like next:

enum Change: Hashable {
  enum SectionUpdate: Hashable {
    case inserted(Int)
    case deleted(Int)
  }

  enum ObjectUpdate: Hashable {
    case inserted(at: IndexPath)
    case deleted(from: IndexPath)
    case updated(at: IndexPath)
    case moved(from: IndexPath, to: IndexPath)
  }

  case section(SectionUpdate)
  case object(ObjectUpdate)
}

The Change enum is an enum I've defined to encapsulate changes in the fetched result controller's data.

Now let's move on to the delegate methods. I'll show them all in one go:

extension AlbumsProvider: NSFetchedResultsControllerDelegate {
  func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    inProgressChanges.removeAll()
  }

  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
    if type == .insert {
      inProgressChanges.append(.section(.inserted(sectionIndex)))
    } else if type == .delete {
      inProgressChanges.append(.section(.deleted(sectionIndex)))
    }
  }

  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    // indexPath and newIndexPath are force unwrapped based on whether they should / should not be present according to the docs.
    switch type {
    case .insert:
      inProgressChanges.append(.object(.inserted(at: newIndexPath!)))
    case .delete:
      inProgressChanges.append(.object(.deleted(from: indexPath!)))
    case .move:
      inProgressChanges.append(.object(.moved(from: indexPath!, to: newIndexPath!)))
    case .update:
      inProgressChanges.append(.object(.updated(at: indexPath!)))
    default:
      break
    }
  }

  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    controllerDidChangePublisher.send(inProgressChanges)
  }
}

There's a bunch of code here, but the idea is quite simple. First, the fetched results controller will inform us that it's about to send us a bunch of changes. This is a good moment to clear the inProgressChanges array so we can populate it with the changes that we're about to receive.

The following two methods are called by the fetched results controller to tell us about changes in objects and sections. A section can only be inserted or deleted according to the documentation.

Managed objects can be inserted, moved, deleted, or updated. Note that a moved object might also be updated (it usually is because it wouldn't have moved otherwise). When this happens, you're only informed about the move.

When the fetched results controller has informed us about all changes, we can call send on the controllerDidChangePublisher so we send all changes that were collected to subscribers of this publisher. Usually that subscriber will be your view controller.

Note:
I'm assuming that you understand the basics of Combine. Explaining how publishers work is outside of the scope of this article. If you want to learn more about Combine you can take a look at my free blog posts, or purchase my Practical Combine book.

In your view controller, you'll want to have a property that holds on to your data provider. For example, you might add the following property to your view controller:

let usersProvider: UsersProvider

Your data sources should typically be injected into your view controllers, but view controllers can also initialize their own data provider. Choose whichever approach works best for your app.

What's more interesting is how you should respond to change arrays that are sent by controllerDidChangePublisher. Let's take a look at how I subscribe to this publisher in viewDidLoad():

override func viewDidLoad() {
  super.viewDidLoad()

  // setup code...

  albumsProvider.controllerDidChangePublisher
    .sink(receiveValue: { [weak self] updates in
      var movedToIndexPaths = [IndexPath]()

      self?.collectionView.performBatchUpdates({
        for update in updates {
          switch update {
          case let .section(sectionUpdate):
            switch sectionUpdate {
            case let .inserted(index):
              self?.collectionView.insertSections([index])
            case let .deleted(index):
              self?.collectionView.deleteSections([index])
            }
          case let .object(objectUpdate):
            switch objectUpdate {
            case let .inserted(at: indexPath):
              self?.collectionView.insertItems(at: [indexPath])
            case let .deleted(from: indexPath):
              self?.collectionView.deleteItems(at: [indexPath])
            case let .updated(at: indexPath):
              self?.collectionView.reloadItems(at: [indexPath])
            case let .moved(from: source, to: target):
              self?.collectionView.moveItem(at: source, to: target)
              movedToIndexPaths.append(target)
            }
          }
        }
      }, completion: { done in
        self?.collectionView.reloadItems(at: movedToIndexPaths)
      })
    })
    .store(in: &cancellables)
}

This code is rather long but it's also quite straightforward. I use UICollectionView's performBatchUpdates(_:completion:) method to iterate over all changes that we received. I also define an array before calling performBatchUpdates(_:completion:). This array will hold on to all index paths that were the target of a move operation so we can reload those cells after updating the collection view (the app will crash if you move and reload a cell).

By checking whether a change matches the section or object case I know what kind of a change I'm dealing with. Each case has an associated value that describes the change in more detail. Based on this associated value I can insert, delete, move, or reload cells and sections.

I haven't shown you the UICollectionViewDataSource methods that are needed to provide your collection view will data and cells. I'm sure you know how to do this as it'd be no different from a very plain and boring collection view. Just make sur eto use your data provider's convenient helpers to determine the number of sections and objects in your collection view.

In Summary

Doing all this work is certainly less convenient than using a diffable data source snapshot, but in the end you'll find that when you're using a fetchBatchSize this is approach will make sure your fetched results controller doesn't make a ton of unwanted extra fetch requests.

I'm not sure whether the behavior we see with diffable data sources is expected, but it's most certainly inconvenient. Especially when you have a large set of data, fetchBatchSize should help you reduce the time it takes to load data. When your app then proceeds to fetch all data anyway except with many small requests you'll find that performance is actually worse than it was when you fetched all data in one go.

If you don't want to do any extra work and have a small data set of maybe a couple dozen items, it might be a wise choice to not use fetchBatchSize if you want to utilize diffable data source snapshots. It takes a bunch of extra work to implement fetched results controller without it, and this extra work might not be worth the trouble if you're not seeing any problems in an app that doesn't use fetchBatchSize.

I will publish a follow-up post that details a fix for the same problem in SwiftUI when you use the @FetchRequest property wrapper. If you have any feedback or questions about this post, you can reach out to me on Twitter. If you want to learn more about Core Data, fetched results controllers and analyziing performance in Core Data apps, check out my Practical Core Data book.