Configuring error types when using flatMap in Combine

When you're using Combine for an app that has iOS 13 as its minimum deployment target, you have likely run into problems when you tried to apply a flatMap to certain publishers in Xcode 12. To be specific, you have probably seen the following error message: flatMap(maxPublishers:_:) is only available in iOS 14.0 or newer. When I first encountered this error I was somewhat stumped. Surely we had flatMap in iOS 13 too!

If you've encountered this error and thought the same, let me reassure you. You're right. We had flatMap in iOS 13 too. We just gained new flavors of flatMap in iOS 14, and that's why you're seeing this error.

Before I explain more about the new flatMap flavors that we gained in iOS 14, let's figure out the underlying error that causes flatMap(maxPublishers:_:) is only available in iOS 14.0 or newer in your project.

Understanding how flatMap works with Error types

In Combine, every publisher has a defined Output and Failure type. For example, a DataTaskPublisher has a tuple of Data and URLResponse as its Output ((data: Data, response: URLResponse)) and URLError as its Failure.

When you want to apply a flatMap to the output of a data task publisher like I do in the following code snippet, you'll run into a compiler error:

URLSession.shared.dataTaskPublisher(for: someURL)
  .flatMap({ output -> AnyPublisher<Data, Error> in

  })

If you've written code like this before, you'll probably consider the following compiler error to be normal:

Instance method flatMap(maxPublishers:_:) requires the types URLSession.DataTaskPublisher.Failure (aka URLError) and Error be equivalent

I've already mentioned that a publisher has a single predefined error type. A data task uses URLError. When you apply a flatMap to a publisher, you can create a new publisher that has a different error type.

Since your flatMap will only receive the output from an upstream publisher but not its Error. Combine doesn't like it when your flatMap returns a publisher with an error type that does not align with the upstream publisher. After all, errors from the upstream are sent directly to subscribers. And so are errors from the publisher created in your flatMap. If these two errors aren't the same, then what kind of error does a subscriber receive?

To avoid this problem, we need to make sure that an upstream publisher and a publisher created in a flatMap have the same Failure. One way to do this is to apply mapError to the upstream publisher to cast its errors to Error:

URLSession.shared.dataTaskPublisher(for: someURL)
  .mapError({ $0 as Error })
  .flatMap({ output -> AnyPublisher<Data, Error> in

  })

Since the publisher created in the flatMap also has Error as its Failure, this code would compile just fine as long as we would return something from the flatMap in the code above.

You can apply mapError to any publisher that can fail, and you can always cast the error to Error since in Combine, a publisher's Failure must conform to Error.

This means that if you're returning a publisher in your flatMap that has a specific error, you can also apply mapError to the publisher created in your flatMap to make your errors line up:

URLSession.shared.dataTaskPublisher(for: someURL)
  .mapError({ $0 as Error })
  .flatMap({ [weak self] output -> AnyPublisher<Data, Error> in
    // imagine that processOutput is a function that returns AnyPublisher<Data, ProcessingError>
    return self?.processOutput(output) 
      .mapError({ $0 as Error })
      .eraseToAnyPublisher()
  })

Having to line up errors manually for your calls to flatMap can be quite tedious but there's no way around it. Combine can not, and should not infer anything about how your errors should be handled and mapped. Instead, Combine wants you to think about how errors should be handled yourself. Doing this will ultimately lead to more robust code since the way errors propagate through your pipeline is very explicit.

So what's the deal with "flatMap(maxPublishers:_:) is only available in iOS 14.0 or newer" in projects that have iOS 13.0 as their minimum target? What changed?

Fixing "flatMap(maxPublishers:_:) is only available in iOS 14.0 or newer"

Everything you've read up until now applies to Combine on iOS 13 and iOS 14 equally. However, flatMap is slightly more convenient for iOS 14 than it is on iOS 13. Imagine the following code:

let strings = ["https://donnywals.com", "https://practicalcombine.com"]
strings.publisher
  .map({ url in URL(string: url)! })

The publisher that I created in this code is a publisher that has URL as its Output, and Never as its Failure. Now let's add a flatMap:

let strings = ["https://donnywals.com", "https://practicalcombine.com"]
strings.publisher
  .map({ url in URL(string: url)! })
  .flatMap({ url in
    return URLSession.shared.dataTaskPublisher(for: url)
  })

If you're using Xcode 12, this code will result in the flatMap(maxPublishers:_:) is only available in iOS 14.0 or newer compiler error. While this might seem strange, the underlying reason is the following. When you look at the upstream failure type of Never, and the data task failure which is URLError you'll notice that they don't match up. This is a problem since we had a publisher that never fails, and we're turning it into a publisher that emits URLError.

In iOS 14, Combine will automatically take the upstream publisher and turn it into a publisher that can fail with, in this case, a URLError.

This is fine because there's no confusion about what the error should be. We used to have no error at all, and now we have a URLError.

On iOS 13, Combine did not infer this. We had to explicitly tell Combine that we want the upstream publisher to have URLError (or a different error) as its failure type by calling setFailureType(to:) on it as follows:

let strings = ["https://donnywals.com", "https://practicalcombine.com"]
strings.publisher
  .map({ url in URL(string: url)! })
  .setFailureType(to: URLError.self) // this is required for iOS 13
  .flatMap({ url in
    return URLSession.shared.dataTaskPublisher(for: url)
  })

This explains why the compiler error said that flatMap(maxPublishers:_:) is only available on iOS 14.0 or newer.

It doesn't mean that flatMap isn't available on iOS 13.0, it just means that the version of flatMap that can be used on publishers the have Never as their output and automatically assigns the correct failure type if the flatMap returns a publisher that has something other than Never as its failure type is only available on iOS 14.0 or newer.

If you don't fix the error and you use cmd+click on flatMap and then jump to definition you'd find the following definition:

@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *)
extension Publisher where Self.Failure == Never {

  /// Transforms all elements from an upstream publisher into a new publisher up to a maximum number of publishers you specify.
  ///
  /// - Parameters:
  /// - maxPublishers: Specifies the maximum number of concurrent publisher subscriptions, or ``Combine/Subscribers/Demand/unlimited`` if unspecified.
  /// - transform: A closure that takes an element as a parameter and returns a publisher that produces elements of that type.
  /// - Returns: A publisher that transforms elements from an upstream publisher into a publisher of that elementā€™s type.
  public func flatMap<P>(maxPublishers: Subscribers.Demand = .unlimited, _ transform: @escaping (Self.Output) -> P) -> Publishers.FlatMap<P, Publishers.SetFailureType<Self, P.Failure>> where P : Publisher
}

This extension is where the new flavor of flatMap is defined. Notice that it's only available on publishers that have Never as their Failure type. This aligns with what you saw earlier.

Another flavor of flatMap that was added in iOS 14 is similar but works the other way around. When you have a publisher that can fail and you create a publisher that has Never as its Failure in your flatMap, iOS 14 will now automatically keep the Failure for the resulting publisher equal to the upstream's failure.

Let's look at an example:

URLSession.shared.dataTaskPublisher(for: someURL)
  .flatMap({ _ in
    return Just(10)
  })

While this example is pretty useless on its own, it demonstrates the point of the second flatMap flavor really well.

The publisher created in this code snippet has URLError as its Failure and Int as its Output. Since the publisher created in the flatMap can't fail, Combine knows that the only error that might need to be sent to a subscriber is URLError. It also knows that any output from the upstream is transformed into a publisher that emits Int, and that publisher never fails.

If you have a construct similar to the above and want this to work with iOS 13.0, you need to use setFailureType(to:) inside the flatMap:

URLSession.shared.dataTaskPublisher(for: someURL)
  .flatMap({ _ -> AnyPublisher<Int, URLError> in
    return Just(10)
      .setFailureType(to: URLError.self) // this is required for iOS 13
      .eraseToAnyPublisher()
  })

The code above ensures that the publisher created in the flatMap has the same error as the upstream error.

In Summary

In this week's post, you learned how you can resolve the very confusing flatMap(maxPublishers:_:) is only available in iOS 14.0 or newer error in your iOS 13.0+ projects in Xcode 12.0.

You saw that Combine's flatMap requires the Failure types of the upstream publisher that you're flatMapping over and the new publisher's to match. You learned that you can use setFailureType(to:) and mapError(_:) to ensure that your failure types match up.

I also showed you that iOS 14.0 introduces new flavors of flatMap that are used when either the upstream publisher or the publisher created in the flatMap has Never as its Failure because Combine can infer what errors a subscriber should receive in these scenarios.

Unfortunately, these overloads aren't available on iOS 13.0 which means that you'll need to manually align error types if your project is iOS 13.0+ using setFailureType(to:) and mapError(_:). Even if your upstream or flatMapped publishers have Never as their failure type.

If you have any questions or feedback for me about this post, feel free to reach out on Twitter.

3 tips to work through a technical coding interview

If you're a programmer looking for a job it's very likely that you'll have to do a coding interview at some point. Every company conducts these interviews differently. Some may have you work through several heavy computer science problems, others may present you with a task that's related to the job you're interviewing for and others might do both.

No matter what the exact form is, you'll want to nail these interviews as they are a big part of whether you'll get an offer or not.

In my career, I haven't had to go through extensive coding interviews myself. That's in part because I've interviewed with smaller, local companies most of the time and in part because until recently coding challenges just weren't that common where I'm from.

A while ago I was talking to a coworker about coding interviews and not only has he gone through more coding interviews than I have, he's also been on the interviewer's side far more often than I have been.

Based on that conversation, and the brief mock coding challenge I completed to see what our interview process was like (the company I was hired at got bought by my current employer so I never went through their interview process), I would like to share three things with you that I have learned from talking to many people about their interviewing experiences over the past few years.

This list obviously isn't a complete list made up of everything you could possibly do to nail a coding interview, and it might not even apply to every interview out there. After all, every country, company, and interview is different and there's no silver bullet to doing well on every coding interview.

1. Study

The tip I'd like to open this top three with is very important, but also my least favorite tip. If you want to do well during a coding interview you'll have to sit down and study. Many coding interviews will contain problems that are common computer science, or general programming problems and you'll have to understand how to best solve these problems.

However, just learning the solution to each problem might not be enough. You'll want to learn why a certain solution is a good fit for a certain problem.

If you can produce a perfect solution to an interviewer's question but you can't explain how that solution works, why it's a good solution, or how it performs, the interviewer will see that you studied, which might score you some point, but they can't determine whether or not you understand what you're doing.

A resource that I've often seen recommended and sometimes peek into myself is Cracking the Coding Interview. This book contains a ton of interview questions, theory, and answers based on the interview processes at companies like Facebook, Google, Microsoft, and more. This book is a very popular and common resource which means that it's not unlikely for you to encounter questions from this book in an interview.

If you're interviewing for an iOS job, I recommend you take a look at Paul Hudson's iOS interview questions page which contains a ton of questions that are used in iOS interviews regularly.

2. Think out loud

You might not know the answer to every question, and that's fine most of the time. Especially if the interviewer asks you to write the implementation for a specific algorithm on a whiteboard (or a Swift Playground) on the spot.

These kinds of coding on the spot questions can be jarring, especially if you're not confident whether your answer is the correct answer.

Usually, the interviewer isn't only interested in the correct answer. They are interested in seeing how you solve problems, and how you think about them. A good way to demonstrate your thought process is to think out loud.

Thinking out loud can be very scary at first. You might say something silly, or describe something poorly even though you didn't mean to. That's all perfectly fine. I wouldn't recommend rambling every thought you have, but any thoughts about the problem and your solution will help the interviewer see how you think, and how you work through a problem.

Not only will the interviewer be able to pick up on your though process, you might actually help yourself solve the problem much better than you would if you remained quiet. It's almost like you're rubber ducking your way through the problem. Simply talking through it out loud can help you spot flaws in your reasoning and guide you to the correct solution.

3. Ask questions

My third tip flows nicely from the second tip. While you're talking out loud, you might find that you have questions about the problem you're trying to solve. If this happens, you should ask these questions to the interviewer. Sometimes you might discover an edge case and be unsure of whether or not that edge case should be covered in your implementation.

Other times a requirement might not be quite clear and it helps to ask questions and clarify your doubts.

By asking questions you show the interviewer that you pay attention to details, and care about confirming whether or not you understood the task. And a couple of well-asked questions might even show the interviewer that you understand why a specific problem is non-trivial.

It could even show that you understand that a specific question has two valid solutions that each have different pros and cons and that you want to make sure you understand which solution fits best.

(bonus) 4. Be honest

If you really get stuck on a problem, and have no idea where to start don't bluf your way through the problem. The interviewer will be able to tell, and while you're most likely bluffing because you really want the job, the interview might consider that to be reckless.

They will immediately wonder whether or not you might bluf your way through your day to day work too.

It's much better to admit that you're not sure about a solution, and then work your way through the problem. Think out loud, ask questions where needed and apply the principles that you've studies while preparing.

I'm sure you'll be amazed how far you're able to come, and how much knowledge you'll get to show off even if you don't fully understand the problem you're asked to solve during the interview.

In Summary

In this week's post you saw a couple of tips that will hopefully help you get through (and nail) your next coding interview.

Keep in mind that every person is different, and that every interview is different too. Not every company will require a very technical coding interview, and others will have a full day of technical interviews. There's no silver bullet, and no one size fits all solution to do well in any coding interview.

That said, I really hope that these tips will help you prepare for your next interview, and that you'll get the offer you're hoping to get. If you have any questions, additions, or feedback for this list then please let me know on Twitter.

Dispatching async or sync? The differences explained

When writing iOS apps, we regularly run into code that is asynchronous. Sometimes you know you're writing something that will run asynchronously and other times you're passing a completion handler to code that may or may not run asynchronously on a different dispatch queue.

If you're familiar with using DispatchQueue.main, you have probably written code like this:

DispatchQueue.main.async {
  // do something
}

And while writing this, you may have encountered a second method on DispatchQueue.main called sync.

In this week's post I will explain the difference between sync and async, and you will learn when you might want to use these different methods.

Note that this article assumes a little bit of knowledge about DispatchQueue and asynchronous programming as I won't be covering the basics of these topics in this post.

Understanding DispatchQueue.async

Every DispatchQueue instance has an async method. Regardless of whether you're using DispatchQueue.main.async, DispatchQueue.global().async, or if you create a custom queue.

Every dispatch queue has an async method that schedules a chunk of work that's in the closure it receives to be executed at a later time (asynchronously).

When you use a dispatch queue's async method you ask it to perform the work in your closure, but you also tell it that you don't need that work to be performed right now, or rather, you don't want to wait for the work to be done.

Imagine working in a restaurant as a waiter. Your job is to accept orders from guests and relay them to the kitchen. Every time you take a guest's order, you walk to the kitchen and pass them a note with the dishes that they need to prepare and you move on to take the next order. Eventually the kitchen notifies you that they have completed an order and you can pick up the order and take it to the guest's table.

In this analogy, the waiter can be considered its own dispatch queue. You might consider it the main dispatch queue because if the waiter is blocked, no more orders are taken and the restaurant grinds to a halt (assuming that it's a small restaurant with only a single waiter). The kitchen can be thought of as a different dispatch queue that the waiter calls async on with an order every time it asks for an order to be made.

As a waiter you dispatch the work and move on to do the next task. Because you dispatch to the kitchen asynchronously nobody is blocked and everybody can perform their jobs.

The above analogy explains dispatching asyncronously from one queue to another, but it doesn't explain dispatching asyncronously from within the same queue.

For example, there's nothing stopping you from calling DispatchQueue.main.async when you're already on the main queue. So how does that work?

It's quite similar, really. When you dispatch asynchronously within the same queue, then the body of work that should be executed is executed right after whatever it is the queue is currently doing.

Coming back to the waiter analogy, if you walk past a table and tell them "I'll be right with you to take your order" and you're currently delivering drinks to another table, you would essentially be dispatching asyncronously to yourself. You've scheduled a body of work to be done but you also don't want to block what you're doing now.

To summarize, DispatchQueue.async allows you to schedule work to be done using a closure without blocking anything that's ongoing. In most cases where you need to dispatch to a dispatch queue you'll want to use async. However, there are most certainly cases where DispatchQueue.sync makes sense as you'll see in the next section.

Understanding DispatchQueue.sync

Dispatching work synchronously can be a slippery slope. It can be quite tempting to simplify an asyncronous API and replace it with sync. Especially if you're not too familiar with DispatchQueue.sync, you might think that even though you're dispatching synchronously you're not blocking the queue you're on since the work runs on a different queue.

Unfortunately, this isn't entirely true. Let's go back to the restaurant from the previous section.

I explained how a waiter dispatches meal preparation to the kitchen asyncronously. This allows the waiter to continue taking orders and bringing meals to guests.

Now imagine that the waiter would ask the kitchen for an order and then stood there waiting. Doing nothing. The waiter isn't blocked by work they're doing themselves but instead, they're blocked because they have to wait for the chef to prepare the dish they asked for.

This is exactly what DispatchQueue.sync does. When you dispatch a body of work using sync, the current queue will wait for the body of work to complete until it can continue doing any work.

I wrote about DispatchQueue.sync briefly in my post on an @Atomic property wrapper where I explained why that property wrapper doesn't quite work for types like Array and Dictionary.

By far the most common case where I've used DispatchQueue.sync is for a purposeĀ similar to the @Atomic property wrapper, which is to ensure that certain properties or values can only be modified synchronously to avoid multithreading problems.

A very simple example would be this DateFormatterCache object:

class DateFormatterCache {
  private var formatters = [String: DateFormatter]()
  private let queue = DispatchQueue(label: "DateFormatterCache: \(UUID().uuidString)")

  func formatter(using format: String) -> DateFormatter {
    return queue.sync { [unowned self] in
      if let formatter = self.formatters[format] {
        return formatter
      }

      let formatter = DateFormatter()
      formatter.locale = Locale(identifier: "en_US_POSIX")
      formatter.dateFormat = format
      self.formatters[format] = formatter

      return formatter
    }
  }
}

Every time the formatter(using:) function is called on an instance of DateFormatterCache, that work is dispatched to a specific queue synchronously. Dispatch queue are serial by default which means that they perform every task that they're asked to perform one by one, in the same order that the tasks were scheduled in.

This means that we know for sure that there's only one task at a time that accesses the formatters dictionary, reads from it, and caches a new formatter if needed.

If we would call formatter(using:) from multiple threads at the same time we really need this synchronization behavior. If we wouldn't have that, multiple threads would read from the formatters dictionary and write to it which means that we'd end up creating the same date formatter multiple times and we might even run into scenarios where formatters go missing from the cache entirely.

If you prefer to think of this in our restaurant analogy, think of it as all guests in a restaurant being able to write their order on a piece of paper. The waiter is only allowed to pass one piece of paper to the kitchen and that piece of paper must contain all orders. Every time a guest asks the waiter for the ordering paper the waiter will give a copy of its currently known list of orders to the guest.

So if two guests come in at the same time, they both get an empty piece of paper. And when they come back to return the paper to the waiter, the waiter can only keep the last one they receive. The next guests do the same and ultimately the waiter will have a very messy and incomplete order for the kitchen.

This is obviously not what you want so the waiter should process requests synchronously to make sure that every order is written down, and no orders go missing.

In Summary

In this week's post you learned about two very interesting DispatchQueue methods: sync and async. You learned what each of these two methods do, and how they are used in practice.

Keep in mind that async is often the method you want when dispatching work to a queue, and that sync can be very important when you want to have atomic operations and ensure a level of thread safety for dictionaries, arrays, and more.

If you have questions about this post, or if you have feedback for me, I'd love to hear from you on Twitter.

Implementing a one-way sync strategy with Core Data, URLSession and Combine

A common use of a Core Data store is to cache data from a remote resource locally to support offline functionality of an app. There are multiple ways to implement a caching mechanism and many of them don't involve Core Data. For example, you could simply fetch a JSON file from a server and store the contents of that JSON file on disk.

A downside of fetching a full data set every time is that you risk using a lot of bandwidth, especially if your data set is large, or if your data set is expected to grow over time.

An alternative might be to send a parameter to your server to tell the server when you last fetched data, which means that the server can respond with updated records only, and you can merge these updates into your local store. Using Core Data in this case helps, but you can achieve this with plain SQLite too.

Regardless of being able to achieve this functionality with other tools than Core Data, I want to talk about implementing a sync strategy on top of Core Data simply because I like the technology and have been using it for years now with little to no problems.

By the end of this post you should have a solid idea of what a basic sync strategy looks like. We'll only focus on fetching data and caching it. If your application modifies data and sends this modified data back to the server your strategy will need some modifications that I won't cover in this article.

In other words, this article explains how you can implement a one-way sync strategy.

Defining the server-side expectations

Syncing data is usually a non-trivial task, especially if you don't want to fetch all data from the server every time. Throughout this article I'm going to assume that you're working with a server that is capable of sending you only the changes from a given point in time. However, if your server returns a full data set instead of only changes, the sync strategy in this post should work the same. The only difference would be that a full data set uses more bandwidth and might take more time to process depending on the size of the data set.

The exact shape of the objects that a server returns doesn't matter much, the most important point is that objects that your server returns have a unique identifier. Consider the following JSON response:

{
  "events": [
    {
      "id": 1,
      "title": "My Event",
      "location": {
        "id": 2,
        "name": "Donny's House"
      }
    }
  ]
}

This response contains an array of events. Each event has a unique id, and it has a property called location. This location property will be imported as a relationship between an Event object and a Location object.

We can use this structure just fine for new objects and for modified objects. A potential downside of working with this structure is that a modified location would trigger changes on one or more events which means that we do more processing than needed. An easy workaround would be to have the server include a seperate key for modified locations:

{
  "events": [
    {
      "id": 1,
      "title": "My Event",
      "location": {
        "id": 2,
        "name": "Donny's House"
      }
    }
  ],
  "locations": [
    {
      "id": 3,
      "name": "Melkweg, Amsterdam"
    }
  ]
}

Having this seperate locations key is useful if a location changed but the event that it belongs to remains the same. If both the location and the event changed the server should probably omit the location from the locations array in the JSON since it'll also be part of an event in the events array. I consider this to be a server-side optimization that can be made which is outside of the scope of this post.

At this point the JSON can tell us about objects that are new or modified but not about objects that are deleted. Since we don't need a full object to know what to delete we can include a deleted key in the json that contains any deleted event or location ids:

{
  "events": [
    {
      "id": 1,
      "title": "My Event",
      "location": {
        "id": 2,
        "name": "Donny's House"
      }
    }
  ],
  "locations": [
    {
      "id": 3,
      "name": "Melkweg, Amsterdam"
    }
  ],
  "deleted": {
    "events": [2, 8],
    "locations": [4, 7]
  }
}

And lastly, the server should provide us with a key, or timestamp, that we can use for the next request. The reason we want the server to provide a timestamp rather than letting the client determine one is that we shouldn't rely on the client's clock to make decisions on the server. Furthermore, if the server is in charge of generating these keys it can decide for itself whether this key is a timestamp or something else.

This means that the full JSON I'm working from in this post looks like this:

{
  "events": [
    {
      "id": 1,
      "title": "My Event",
      "location": {
        "id": 2,
        "name": "Donny's House"
      }
    }
  ],
  "locations": [
    {
      "id": 3,
      "name": "Melkweg, Amsterdam"
    }
  ],
  "deleted": {
    "events": [2, 8],
    "locations": [4, 7]
  },
  "version_token": "1234567890"
}

Your server's responses might look completely different, and that's perfectly fine. My goal is to explain the strategy I'm building on top of this response in a way that will allow you to write your own sync strategy even if your server is completely different.

Configuring your Core Data store

When you implement a sync strategy that writes remote data to a local Core Data store it's crucial that you prevent data duplication. While Core Data should typically not be treated as a store that has a concept of primary keys, we can apply a unique constraint on one or more properties of a Core Data model.

To do this, open the Core Data model editor, select the Entity that needs a unique contraint and use the Data model inspector in the right-hand sidebar to add Constraints. You can provide a list of comma-separated values if you want to determine uniqueness on a combination of multiple properties.

When you add a unique constraint like this, Core Data will consider it an error if you try to save an object with a duplicate value for the key you provided. So in this case, saving two Event objects with the same id would result in an error.

Before I explain why this error is useful and how you can use it to your advantage, I want to work a little bit on our data importer. In an earlier post I explained how you can build a Core Data abstraction that doesn't rely on the AppDelegate or any of Apple's default templates for SwiftUI or UIKit applications.

I'm going to assume that you have a similar setup in your application but ultimately it won't matter much how you've set up your application. The following code can be used as a simple starting point for your data importer:

class DataImporter {
  let importContext: NSManagedObjectContext

  init(persistentContainer: NSPersistentContainer) {
    importContext = persistentContainer.newBackgroundContext()
    importContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
  }
}

You can create an instance of this DataImporter wherever you need it in your application. Typically this will be somewhere in a ViewModel or other place that you would normally use to make network requests, fetch data, or perform other potentially slow and costly operations.

The most important part of the snippet above is this:

importContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

By setting the mergePolicy for the background context to NSMergeByPropertyObjectTrumpMergePolicy we tell Core Data to resolve any conflicts between the store and the data we're attempting to save using the properties from the object we want to save. This means that if we have a stored Event, and want to save an event with the same id as the Event that's already stored, Core Data will overwrite the stored event's values with the new event's values.

This is perfect for an application where we want to sync data from the server into our local data store. It's save to assume that the server has the most recent and correct version of every object, so whenever we import we'll want to overwrite any existing data with data from the server.

Since we'll be importing data on a background context and the persistent container's viewContext should pick up any changes we make automatically, we'll need to set the viewContext's automaticallyMergesChangesFromParent property to true. If you're using one of Apple's premade templates you can insert container.viewContext.automaticallyMergesChangesFromParent = true at the point where the container is created.

If you're using a custom abstraction you can do the same except you'll be adding this line in code you wrote yourself.

Setting automaticallyMergesChangesFromParent = true will make sure that the viewContext is aware of any changes that were made to the persistent container. When you save a background context, the persistent container is automatically informed of the changes that were made. The viewContext is considered to be a child of the persistent container so when you set automaticallyMergesChangesFromParent to true, the viewContext will automatically be made aware of changes in the persistent container.

This is particularly useful if your UI uses an NSFetchedResultsController. When your viewContext does not automatically merge changes from its parent, your NSFetchedResultsController won't automatically update when your background context saves after running your import.

One last thing you should do before writing your importer logic is to make sure you have a way to transform your JSON into managed objects. One way is to make your managed objects work with Codable. Since I have a full post that already covers this I won't explain how to do this in this post.

Writing your import logic

Now that you have a Core Data store set up and you know what the server's response data looks like, let's implement our importer logic.

Since the server returns a version_token key that should be used to ask the server for changes after we've done an initial import, the importer should keep track of this token.

I'll store it in UserDefaults since it's just a single value and we don't need to keep any history of it:

class DataImporter {
  var versionToken: String? {
    get { UserDefaults.standard.string(forKey: "DataImporter.versionToken") }
    set { UserDefaults.standard.set(newValue, forKey: "DataImporter.versionToken") }
  }

  // existing initializer
}

We'll also need to define a Decodable struct that's used to decode the server's response into:

struct ImporterResponse: Decodable {
  let events: [Event]
  let locations: [Location]
  let deleted: ImporterResponse.Deleted
  let versionToken: String
}

extension ImporterResponse {
  struct Deleted: Decodable {
    let events: [Int]
    let locations: [Int]
  }
}

I'm using a nested struct to decode the deleted items into. I will set my JSONDecoder's keyDecodingStrategy to convertFromSnakeCase so the version_token from the JSON is converted to versionToken in my struct.

Now that we have something to decode the response into we can write the import function:

func runImport() {
  // 1. Build the correct URL
  var url = URL(string: "https://www.mywebsite.com/datasource")!
  if let versionToken = self.versionToken {
    url.appendPathComponent(versionToken)
  }

  URLSession.shared.dataTaskPublisher(for: url)
    .map(\.data)
    .sink(receiveCompletion: { completion in
      if case .failure(let error) = completion {
        print("something went wrong: \(error)")
      }
    }, receiveValue: { [weak self] data in
      guard let self = self
        else { return }

      self.importContext.perform {
        do {
          // 2. Decode the response
          let response = try self.decoder.decode(ImporterResponse.self, from: data)

          // 3. Store the version token
          self.versionToken = response.versionToken

          // 4. Build batch delete requests
          let deletedEventsPredicate = NSPredicate(format: "id IN %@", response.deleted.events)
          let deletedEventsRequest: NSFetchRequest<Event> = Event.fetchRequest()
          deletedEventsRequest.predicate = deletedEventsPredicate
          let batchDeleteEvents = NSBatchDeleteRequest(fetchRequest: deletedEventsRequest)

          let deletedLocationsPredicate = NSPredicate(format: "id IN %@", response.deleted.locations)
          let deletedLocationsRequest: NSFetchRequest<Location> = Location.fetchRequest()
          deletedLocationsRequest.predicate = deletedLocationsPredicate
          let batchDeleteLocations = NSBatchDeleteRequest(fetchRequest: deletedLocationsRequest)

          do {
            // 5. Execute deletions
            try self.importContext.execute(batchDeleteEvents)
            try self.importContext.execute(batchDeleteLocations)

            // 6. Finish import by calling save() on the import context
            try self.importContext.save()
          } catch {
            print("Something went wrong: \(error)")
          }
        } catch {
          print("Failed to decode json: \(error)")
        }
      }

    }).store(in: &cancellables) // store the returned cancellable in a property on `DataImporter`
}

There's a lot of code here but if you follow the comments you'll see that the code I wrote is fairly trivial.

The first step in my code is to construct a valid URL. If we have a versionToken, I append it to the URL. In your app you might have to send your token or timestamp differently, but I'm sure you get the point.

After building the URL a request is made and the response is decoded. My JSON decoder is defined as a lazy var on the DataImporter object as follows:

lazy var decoder: JSONDecoder = {
  let decoder = JSONDecoder()
  decoder.keyDecodingStrategy = .convertFromSnakeCase
  decoder.userInfo[.managedObjectContext] = importContext
  return decoder
}()

Note that I'm using the decoding strategy from my earlier post which means that my JSON response is immediately decoded into NSManagedObject instances using my import context. This means that I don't have to perform any extra work to import the objects from the JSON response into my managed object context since this happens when the JSON is decoded into my managed object. I strongly recommend that you read my earlier post if this seems confusing or weird to you.

Once the response is decoded and the received events and locations are added to my managed object context I can extract the versionToken and store it in UserDefaults for future imports.

The fourth step is to prepare and configure batch delete requests for events and locations to get rid of any events and locations that the server has deleted. A batch delete request takes a regular fetch request and we can use a predicate to make sure only the items that should be deleted actually get deleted.

Lastly, the batch delete requests are executed and we can call save() on the import context to write all the changes we made to the persistent store. If everything was set up properly this will work perfectly and since we defined a mergePolicy on the importContext any conflicts between the old and the new data will be resolved using properties from the object that we're trying to save, which will overwrite the existing data.

While there's a lot to unpack in this flow, it's actually fairly straightforward because we're able to make good use of Core Data's features and a little bit of custom work to make NSManagedObject work with Codable.

In Summary

In this post, you've learned how you can implement a data importing flow that uses Core Data, URLSession and Combine. You saw how a server's response might look when it sends incremental updates, and you learned why this is more convenient than fetching all data everytime your app launches.

Next, I went on to show you how you can set up a Core Data store that applies unique constraints to certain properties of your entities and how you can use a merge policy to resolve conflicts that arise when you attempt to save an object that has an conflicting property. In this article we used NSMergeByPropertyObjectTrumpMergePolicy which will overwrite the stored data with the data from the object that you attempted to store. A perfect policy for a sync like this.

And lastly, I showed you a basic implementation of an import function that performs an import on a background context, deletes any unneeded data and then saves the background context. As long as you set automaticallyMergesChangesFromParent on your viewContext to true, your view contxt will automatically pick up any changes which will in turn trigger an NSFetchedResultController's delegate methods and Core Data will fire the appropriate notifications through NSNotificationCenter.

I hope that this post gave you some valuable insight into how Core Data can be used as a cache for remote data. If you have any questions about this post, or if you've spotted any mistakes, feel free to reach out to me on Twitter. I love hearing from you.

Understanding Swift’s OptionSet

Every once in a while I look at a feature in Swift and I fall down a rabbit hole to explore it so I can eventually write about it. The OptionSet protocol is one of these Swift features.

If you've ever written an animation and passed it a list of options like this, you have already used OptionSet:

UIView.animate(
  withDuration: 0.6, delay: 0, options: [.allowUserInteraction, .curveEaseIn],
  animations: {
    myView.layer.opacity = 0
  }, completion: { _ in })

You may not have realized that you weren't passing an array to the options parameter, and that's not surprising. After all, the options parameter accepts an array literal so it makes a lot of sense to think of the list of options as an array.

But, while it might look like an array, the options parameter accepts an object of type UIView.AnimationOptions which conforms to OptionSet.

Similarly, you may have written something like the following code in SwiftUI:

Rectangle()
  .fill(Color.yellow)
  .edgesIgnoringSafeArea([.leading, .trailing, .bottom])

The edgesIgnoringSafeArea accepts a list of edges that you want to ignore. This list looks a lot like an array since it's passed as an array literal. However, this too is actually an OptionSet.

There are many more examples of where OptionSet is used on Apple's platforms and if you're curious I recommend that you take a look at the documentation for OptionSet under the Relationships section.

In this week's post, I would like to zoom in on what an OptionSet is. And more importantly, I want to zoom in on how an OptionSet works because it's quite an interesting topic.

I'll first show you how you can define a custom OptionSet object and why you would do that. After that, I will explain how an OptionSet works. You will learn about bit shifting and some bitwise operators since that's what OptionSet uses extensively under the hood.

Understanding what an OptionSet is

If you've looked at the documentation for OptionSet already, you may have noticed that it's not terribly complex to create a custom OptionSet. So let's talk about when or why you might want to write an OptionSet of your own before I briefly show you how you can define your own OptionSet objects.

An OptionSet in Swift is a very lightweight object that can be used to represent a fairly large number of boolean values. While you can initialize it with an array literal, it's actually much more like a Set than an array. In fact, OptionSet inherits all of the SetAlgebra that you can apply to sets which means that OptionSet has methods like intersection, union, contains, and several other methods that you might have used on Set.

In the examples I've shown you in the introduction of this post, the OptionSets that were used represented a somewhat fixed set of options that we could either turn off or on. When a certain option is present in the OptionSet we want that option to be on, or true. if we omitted that option we want that option to be ignored. In other words, it should be false.

So when we pass .leading in the list of options for edgesIgnoringSafeArea we want it to be ignored. If we don't pass .leading in the list, we want the view to respect the leading safe area edge because it wasn't present in the list of edges that we want to ignore.

What's interesting about OptionSet in the context of edgesIgnoringSafeArea is that we can also pass .all instead of [.all] if we want to ignore all edges. The reason for this is that OptionSet is an object that can be initialized using an array literal but as I've mentioned before, it is not an array.

Instead, it is an object that stands on its own and it uses a single raw value to represent all options that it holds. Before I explain how that works exactly, let's see how you can define a custom OptionSet because I'm sure that'll provide some useful context.

Defining a custom OptionSet

When you define an OptionSet in Swift, all you do is define a struct (or class) that has a raw value. This raw value can theoretically be any type you want but commonly you will use a type that conforms to FixedWidthInteger, like Int or Int8 because you will get a lot of functionality for free that way (like the conformance to SetAlgebra) and it simply makes more sense.

Next, you should define your options where each option has a unique raw value that's a power of two.

Let's look at an example:

struct NotificationOptions: OptionSet {
  static let daily = NotificationOptions(rawValue: 1)
  static let newContent = NotificationOptions(rawValue: 1 << 1)
  static let weeklyDigest = NotificationOptions(rawValue: 1 << 2)
  static let newFollows = NotificationOptions(rawValue: 1 << 3)

  let rawValue: Int8
}

Looks simple enough right? But what does that << do you might ask. I'm glad you asked and I will talk about it in the next section. Just trust me when I say that the raw values for my options are 1, 2, 4, and 8.

If you'd define this OptionSet in your code you might use it a little bit like this:

class User {
  var notificationPreferences: NotificationOptions = []
}

let user = User()
user.notificationPreferences = [.newContent, .newFollows]

user.notificationPreferences.contains(.newContent) // true
user.notificationPreferences.contains(.weeklyDigest) // false

user.notificationPreferences.insert(.weeklyDigest)

user.notificationPreferences.contains(.weeklyDigest) // true

As you can see you can treat notificationPreferences like a Set even though the type of notificationPreferences is NotificationOptions and your options are represented by a single integer which means that this is an extremely lightweight way to store a set of options that are essentially boolean toggles.

Let's see how this magic works under the hood, shall we?

Understanding how OptionSet works

In the previous section I showed you this OptionSet:

struct NotificationOptions: OptionSet {
  static let daily = NotificationOptions(rawValue: 1)
  static let newContent = NotificationOptions(rawValue: 1 << 1)
  static let weeklyDigest = NotificationOptions(rawValue: 1 << 2)
  static let newFollows = NotificationOptions(rawValue: 1 << 3)

  let rawValue: Int8
}

I told you that the raw values for my options were 1, 2, 4, and 8.

The reason these are my raw values is because I applied a bitshift operator (<<) to the integer 1. Let's take a look at what that means in greater detail.

The integer 1 can be represented in Swift by writing out its bytes as follows:

let one = 0b00000001
print(one == 1) // true

In this case I'm working with an Int8 which uses 8 bits for its storage (you can count the 0s and 1s after 0b to see that there are eight). You can imagine that an Int64 which uses 64 bits as its storage would mean that I have to type a lot of zeroes to represent the full storage in this example.

When we take the integer 1 (or 0b00000001) and apply << 1 to this value, we shift all of its bits to left by one step. This means that the last bit in my integer becomes 0 and the bit that came before the last bit becomes 1 since the last bit shifts leftward by 1. So that means our value is now 0b00000010 which happens to be how the integer two is represented. If apply << 2 to 1, we end up with the following bits: 0b00000100 which happen to be how four is represented. Shifting to left once more would result in the integer eight, and so forth. With a raw value of Int8 we can shift to the left seven times before we reach 0b00000000 and get the integer 0. So that means that an OptionSet with Int8 as its raw value can hold eight options. Int16 can hold sixteen options all the way up to Int64 which will hold up to sixty-four values.

That's a lot of options that can be represented with a single integer!

Now let's see what happens when we add a new static let to represent all options:

static let all: NotificationOptions = [.daily, .newContent, .weeklyDigest, .newFollows]

What's the raw value for all? You know it's not an array of integers since the type of all is NotificationOptions so that list of options must be represented as a single Int8.

If you're curious about the answer, it's 15. But why is that list of options represented as 15 exactly? The simple explanation is that all individual options are added together: 1 + 2 + 4 + 8 = 15. The more interesting explanation is that all options are added together using a bitwise OR operation.

A bitwise OR can be performed using the | operator in Swift:

print(1 | 2 | 4 | 8) // 15

A bitwise OR compares all the bits in each integer and whenever it encounters a bit that's set to 1, it's set to 1 in the final result. Let's look at this by writing out the bits again:

0b00000001 // 1
0b00000010 // 2
0b00000100 // 4
0b00001000 // 8
---------- apply bitwise OR
0b00001111 // 15

If you want to write this out in a Playground, you can use the following:

print(0b00000001 | 0b00000010 | 0b00000100 | 0b00001000 == 0b00001111) // true

Pretty cool, right?

With this knowledge we can try to understand how contains and insert work. Let's look at insert first because that's the simplest one to explain since you already know how that works.

An insert would simply bitwise OR another value onto the current value. Let's use the following code as a starting point:

class User {
  var notificationPreferences: NotificationOptions = []
}

let user = User()
user.notificationPreferences = [.newContent, .newFollows]

In this code we use two options which can be represented as follows: 0b00000010 | 0b00001000. This results in 0b00001010 meaning that we have a raw value of 10. If we then insert a new option, for example .daily, the OptionSet will simply take that raw value of 10 and bitwise OR the new option on top: 0b00001010 | 0b00000001 which means we get 0b00001011 which equals eleven.

To check whether an OptionSet contains a specific option, we need to use another bitwise operator; the &.

The & bitwise operator, or bitwise AND compares two values and sets any bits that are 1 in both values to 1. All other bits are 0. Let's look at an example based on the code from before again:

user.notificationPreferences = [.newContent, .newFollows]

You know that the notificationPreferences's raw value is 10 and that we can represent that as 0b00001010. So let's use the bitwise AND to see if 0b00001010 contains the .newContent option:

0b00001010 // the option set
0b00000010 // the option that we want to find
---------- apply bitwise AND
0b00000010 // the result == the option we want to find

Because the result of applying the bitwise AND equals the value we were trying to find, we know that the option set contains the option we were looking for. Let's look at another example where we check if 0b00001010 contains the weeklyDigest option:

0b00001010 // the option set
0b00000100 // the option that we want to find
---------- apply bitwise AND
0b00000000 // the result == 0

Since the bits that we wanted to find weren't present in the option set, the output is 0 since all bits are 0 in the result of our operation.

With this knowledge you can also perform more complicated SetAlgebra operations. For example, at the start of this article I mentioned that OptionSet has a union method that's provided by SetAlgebra. The union method returns the combination of two OptionSet objects. We can easily calculate this using a bitwise OR operator. Let's assume that we have two OptionSet objects:

let left: NotificationOptions = [.weeklyDigest, .newFollows]
let right: NotificationOptions = [.newContent, .weeklyDigest]

We can calculate the union using left.union(right) which would give an OptionSet that contains weeklyDigest, newFollows and newContent, but let's see how we can calculate this union ourselves using the bitwise OR:

0b00001100 // weeklyDigest and newFollows
0b00000110 // newContent and weeklyDigest
---------- apply bitwise OR
0b00001110 // newContent, weeklyDigest, and newFollows

While you don't have to understand how all of these bitwise operations work, I do think it's very valuable to have this knowledge, even if it's just to help you see how nothing is truly magic, and everything can be explained.

The key information here isn't that you can do bit shifting in Swift, or that you can apply bitwise operators. That's almost a given for any programming language.

The important information here is that OptionSet can store a tremendous amount of information in a single integer with just a couple of bits while also providing a very powerful and flexible API on top of this storage.

While I haven't had to define my own OptionSets often, it's very useful to understand how you can define them, you never know when you might run into a case where you need a flexible, lightweight storage object like OptionSet provides.

In Summary

I was planning to write a short and consise article on OptionSet at first. But then I found more and more interesting concepts to explain, and while there still is more to explain I think this article should provide you with a very good understanding of OptionSet and a couple of Swift's bitwise operators. There are more bitwise operators available in Swift and I highly recommend that you go ahead and explore them.

For now, you saw how to use, and define OptionSet objects. You also saw how an OptionSet's underlying storage works, and you learned that while you can express an OptionSet as an array literal, they are nothing like an array. You now know that a list of different options can be fully represented in an integer.

If you have any questions about this article, or if you have any feedback for me, I would love to hear from you on Twitter.

Fetching objects from Core Data in a SwiftUI project

When you've added Core Data to your SwiftUI project and you have some data stored in your database, the next hurdle is to somehow fetch that data from your Core Data store and present it to the user.

In this week's post, I will present two different ways that you can use to retrieve data from Core Data and present it in your SwiftUI application. By the end of this post you will be able to:

  • Fetch data using the @FetchRequest property wrapper
  • Expose data to your SwiftUI views with an observable object and an @Published property.

Since @FetchRequest is by far the simplest approach, let's look at that first.

Fetching data with the @FetchRequest property wrapper

The @FetchRequest property wrapper is arguably the simplest way to fetch data from Core Data in a SwiftUI view. Several flavors of the @FetchRequest property wrapper are available. No matter the flavor that you use, they all require that you inject a managed object context into your view's environment. Without going into too much detail about how to set up your Core Data stack in a SwiftUI app (you can read more about that in this post) or explaining what the environment is in SwiftUI and how it works, the idea is that you assign a managed object context to the \.managedObjectContext keypath on your view's environment.

So for example, you might write something like this in your App struct:

@main
struct MyApplication: App {

  // create a property for your persistent container / core data abstraction

  var body: some Scene {
    WindowGroup {
      MainView()
        .environment(\.managedObjectContext, persistentContainer.viewContext)
    }
  }
}

When you assign the managed object context to the environment of MainView like this, the managed object context is available inside of MainView and it's automatically passed down to all of its child views.

Inside of MainView, you can create a property that's annotated with @FetchRequest to fetch objects using the managed object context that you injected into MainView's environment. Note that not setting a managed object context on the view's environment while using @FetchRequest will result in a crash.

Let's look at a basic example of @FetchRequest usage:

struct MainView: View {
  @FetchRequest(
    entity: TodoItem.entity(),
    sortDescriptors: [NSSortDescriptor(key: "dueDate", ascending: true)]
  ) var items: FetchedResults<TodoItem>

  var body: some View {
    List(items) { item in
      Text(item.name)
    }
  }
}

The version of the @FetchRequest property wrapper takes two arguments. First, the entity description for the object that you want to fetch. You can get a managed object's entity description using its static entity() method. We also need to pass sort descriptors to make sure our fetched objects are sorted property. If you want to fetch your items without sorting them, you can pass an empty array.

The property that @FetchRequest is applied to has FetchedResults<TodoItem> as its type. Because FetchedResults is a collection type, you can use it in a List the same way that you would use an array.

What's nice about @FetchRequest is that it will automatically refresh your view if any of the fetched objects are updated. This is really nice because it saves you a lot of work in applications where data changes often.

If you're familiar with Core Data you might wonder how you would use an NSPredicate to filter your fetched objects with @FetchRequest. To do this, you can use a different flavor of @FetchRequest:

@FetchRequest(
  entity: TodoItem.entity(),
  sortDescriptors: [NSSortDescriptor(key: "dueDate", ascending: true)],
  predicate: NSPredicate(format: "dueDate < %@", Date.nextWeek() as CVarArg)
) var tasksDueSoon: FetchedResults<TodoItem>

This code snippet passes an NSPredicate to the predicate argument of @FetchRequest. Note that it uses a static method nextWeek() that I defined on Date myself.

I'm sure you can imagine that using @FetchRequest with more complex sort descriptors and predicates can get quite wieldy, and you might also want to have a little bit of extra control over your fetch request. For example, you might want to set up relationshipKeyPathsForPrefetching to improve performance if your object has a lot of relationships to other objects.

Note:
You can learn more about relationship prefetching and Core Data performance in this post.

You can set up your own fetch request and pass it to @FetchRequest as follows:

// a convenient extension to set up the fetch request
extension TodoItem {
  static var dueSoonFetchRequest: NSFetchRequest<TodoItem> {
    let request: NSFetchRequest<TodoItem> = TodoItem.fetchRequest()
    request.predicate = NSPredicate(format: "dueDate < %@", Date.nextWeek() as CVarArg)
    request.sortDescriptors = [NSSortDescriptor(key: "dueDate", ascending: true)]

    return request
  }
}

// in your view
@FetchRequest(fetchRequest: TodoItem.dueSoonFetchRequest)
var tasksDueSoon: FetchedResults<TodoItem>

I prefer this way of setting up a fetch request because it's more reusable, and it's also a lot cleaner when using @FetchRequest in your views.

While you can fetch data from Core Data with @FetchRequest just fine, I tend to avoid it in my apps. The main reason for this is that I've always tried to separate my Core Data code from the rest of my application as much as possible. This means that my views should be as unaware of Core Data as they can possibly be. Unfortunately, @FetchRequest by its very definition is incompatible with this approach. Views must have access to a managed object context in their environment and the view manages an object that fetches data directly from Core Data.

Luckily, we can use ObservableObject and the @Published property wrapper to create an object that fetches objects from Core Data, exposes them to your view, and updates when needed.

Building a Core Data abstraction for a SwiftUI view

There is more than one way to build an abstraction that fetches data from Core Data and updates your views as needed. In this section, I will show you an approach that should fit common use cases where the only prerequisite is that you have a property to sort your fetched objects on. Usually, this shouldn't be a problem because an unsorted list in Core Data will always come back in an undefined order which, in my experience, is not desirable for most applications.

The simplest way to fetch data using a fetch request while responding to any changes that impact your fetch request's results is to use an NSFetchResultsController. While this object is commonly used in conjunction with table views and collection views, we can also use it to drive a SwiftUI view.

Let's look at some code:

class TodoItemStorage: NSObject, ObservableObject {
  @Published var dueSoon: [TodoItem] = []
  private let dueSoonController: NSFetchedResultsController<TodoItem>

  init(managedObjectContext: NSManagedObjectContext) {
    dueSoonController = NSFetchedResultsController(fetchRequest: TodoItem.dueSoonFetchRequest,
    managedObjectContext: managedObjectContext,
    sectionNameKeyPath: nil, cacheName: nil)

    super.init()

    dueSoonController.delegate = self

    do {
      try dueSoonController.performFetch()
      dueSoon = dueSoonController.fetchedObjects ?? []
    } catch {
      print("failed to fetch items!")
    }
  }
}

extension TodoItemStorage: NSFetchedResultsControllerDelegate {
  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    guard let todoItems = controller.fetchedObjects as? [TodoItem]
      else { return }

    dueSoon = todoItems
  }
}

While there's a bunch of code in the snippet above, the contents are fairly straightforward. I created an ObservableObject that has an @Published property called dueSoon. This is the item that a SwiftUI view would use to pull data from.

Note that my TodoItemStorage inherits from NSObject. This is required by the NSFetchedResultsControllerDelegate protocol that I'll talk about in a moment.

In the initializer for TodoItemStorage I create an instance of NSFetchedResultsController. I also assign a delegate to my fetched results controller so we can respond to changes, and I call performFetch to fetch the initial set of data. Next, I assign the fetched results controller's fetched objects to my dueSoon property. The fetchedObject property of a fetched results controller holds all of the managed objects that it retrieved for our fetch request.

The controllerDidChangeContent method in my extension is an NSFetchedResultsControllerDelegate method that's called whenever the fetched results controller changes its content. This method is called whenever the fetched results controller adds, removes, or updates any of the items that it fetched. By assigning the fetched result controller's fetchedObjects to dueSoon again, the @Published property is updated and your SwiftUI view is updated.

Let's see how you would use this TodoItemStorage in an application:

@main
struct MyApplication: App {

  let persistenceManager: PersistenceManager
  @StateObject var todoItemStorage: TodoItemStorage

  init() {
    let manager = PersistenceManager()
    self.persistenceManager = manager

    let managedObjectContext = manager.persistentContainer.viewContext
    let storage = TodoItemStorage(managedObjectContext: managedObjectContext)
    self._todoItemStorage = StateObject(wrappedValue: storage)
  }

  var body: some Scene {
    WindowGroup {
      MainView(todoItemStorage: todoItemStorage)
    }
  }
}

Before we look at what MainView would look like in this example, let's talk about the code in this snippet. I'm using a PersistenceManager object in this example. To learn more about this object and what it does, refer back to an earlier post I wrote about using Core Data in a SwiftUI 2.0 application.

Note that the approach I'm using in this code works for iOS 14 and above. However, the principle of this code applies to iOS 13 too. You would only initialize the TodoItemStorage in your SceneDelegate and pass it to your MainView from there rather than making it an @StateObject on the App struct.

In the init for MyApplication I create my PersistenceManager and extract a managed object context from it. I then create an instance of my TodoItemStorage, and I wrap it in a StateObject. Unfortunately, we can't assign values to an @StateObject directly so we need to use the _ prefixed property and assign it an instance of StateObject.

Lastly, in the body I create MainView and pass it the todoItemStorage.

Let's look at MainView:

struct MainView: View {
  @ObservedObject var todoItemStore: TodoItemStorage

  var body: some View {
    List(todoItemStore.dueSoon) { item in
      return Text(item.name)
    }
  }
}

Pretty lean, right? All MainView knows is that it has a reference to an instance of TodoItemStorage which has an @Published property that exposes todo items that are due soon. It doesn't know about Core Data or fetch requests at all. It just knows that whenever TodoItemStorage changes, it should re-render the view. And because TodoItemStorage is built on top of NSFetchedResultsController we can easily update the dueSoon property when needed.

While this approach is going to work fine, it does sacrifice some of the optimizations that you get with NSFetchedResultsController. For example, NSFetchedResultsController frees up memory whenever it can by only keeping a certain number of objects in memory and (re-)fetching objects as needed. The wrapper I created does not have such an optimization and forces the fetched results controller to load all objects into memory, and they are then kept in memory by the @Published property. In my experience this shouldn't pose problems for a lot of applications but it's worth pointing out since it's a big difference with how NSFetchedResultsController works normally.

While the approach in this post might not suit all applications, the general principles are almost universally applicable. If an NSFetchedResultsController doesn't work for your purposes, you could listen to Core Data related notifications in NotificationCenter yourself and perform a new fetch request if needed. This can all be managed from within your storage object and shouldn't require any changes to your view code.

In my opinion, this is one of the powers of hiding Core Data behind a simple storage abstraction.

In Summary

In this week's post, we took a look at fetching objects from Core Data in a SwiftUI application. First, you learned about the built-in @FetchRequest property wrapper and saw several different ways to use it. You also learned that this property wrapper creates a tight coupling between your views and Core Data which, in my opinion, is not great.

After that, you saw an example of a small abstraction that hides Core Data from your views. This abstraction is built on top of NSFetchedResultsContoller which is a very convenient way to fetch data using a fetch request, and receive updates whenever the result of the fetch request changes. You saw that you can update an @Published property whenever the fetched results controller changes its contents. The result of doing this was a view that is blissfully unaware of Core Data and fetch requests.

If you have any questions about this post, or if you have feedback for me, don't hesitate to reach out to me on Twitter.

Using Codable with Core Data and NSManagedObject

If you've ever wanted to decode a bunch of JSON data into NSManagedObject instances you've probably noticed that this isn't a straightforward exercise. With plain structs, you can conform your struct to Codable and you convert the struct from and to JSON data automatically.

For an NSManagedObject subclass it's not that easy.

If your Core Data data model is configured to automatically generate your entity class definitions for you (which is the default), you may have tried to write the following code to conform your managed object to Decodable:

extension MyManagedObject: Decodable { }

If you do this, the compiler will tell you that it can't synthesize an implementation for init(from:) for a class that's defined in a different file. Xcode will offer you some suggestions like adding an initializer, marking it as convenience and eventually the errors will point you towards making your init required too, resulting in something like the following:

extension MyManagedObject: Decodable {
  required convenience public init(from decoder: Decoder) throws {
  }
}

Once you've written this you'll find that Xcode still isn't happy and that it presents you with the following error:

'required' initializer must be declared directly in class 'MyManagedObject' (not in an extension)

In this week's post, you will learn how you can manually define your managed object subclass and add support for Swift's JSON decoding and encoding features by conforming your managed object to Decodable and Encodable. First, I'll explain how you can tweak automatic class generation and define your managed object subclasses manually while still generating the definition for all of your entity's properties.

After that, I'll show you how to conform your managed object to Decodable, and lastly, we'll add conformance for Encodable as well to make your managed object conform to the Codable protocol (which is a combined protocol of Decodable and Encodable).

Tweaking your entity's code generation

Since we need to define our managed object subclass ourselves to add support for Codable, you need to make some changes to how Xcode generates code for you.

Open your xcdatamodeld file and select the entity that you want to manually define the managed object subclass for. In the sidebar on the right, activate the Data model inspector and set the Codegen dropdown to Category/Extension. Make sure that you set Module to Current product module and that Name is set to the name of the managed object subclass that you will define. Usually, this class name mirrors the name of your entity (but it doesn't have to).

After setting up your data model, you can define your subclasses. Since Xcode will generate an extension that contains all of the managed properties for your entity, you only have to define the classes that Xcode should extend:

class TodoItem: NSManagedObject {
}

class TodoCompletion: NSManagedObject {
}

Once you've defined your managed object subclasses, Xcode generates extensions for these classes that contain all of your managed properties while giving you the ability to add the required initializers for the Decodable and Encodable protocols.

Let's add conformance for Decodable first.

Conforming an NSManagedObject to Decodable

The Decodable protocol is used to convert JSON data into Swift objects. When your objects are relatively simple and closely mirror the structure of your JSON, you can conform the object to Decodable and the Swift compiler generates all the required decoding code for you.

Unfortunately, Swift can't generate this code for you when you want to make your managed object conform to Decodable.

Because Swift can't generate the required code, we need to define the init(from:) initializer ourselves. We also need to define the CodingKeys object that defines the JSON keys that we want to use when decoding JSON data. Adding the initializer and CodingKeys for the objects from the previous section looks as follows:

class TodoCompletion: NSManagedObject, Decodable {
  enum CodingKeys: CodingKey {
    case completionDate
  }

  required convenience init(from decoder: Decoder) throws {
  }
}

class TodoItem: NSManagedObject, Decodable {
  enum CodingKeys: CodingKey {
    case id, label, completions
  }

  required convenience init(from decoder: Decoder) throws {
  }
}

Before I get to the decoding part, we need to talk about managed objects a little bit more.

Managed objects are always associated with a managed object context. When you want to create an instance of a managed object you must pass a managed object context to the initializer.

When you're initializing your managed object with init(from:) you can't pass the managed object context along to the initializer directly. And since Xcode will complain if you don't call self.init from within your convenience initializer, we need a way to make a managed object context available within init(from:) so we can properly initialize the managed object.

This can be achieved through JSONDecoder's userInfo dictionary. I'll show you how to do this first, and then I'll show you what this means for the initializer of TodoItem from the code snippet I just showed you. After that, I will show you what TodoCompletion ends up looking like.

Since all keys in JSONDecoder's userInfo must be of type CodingUserInfoKey we need to extend CodingUserInfoKey first to create a managed object context key:

extension CodingUserInfoKey {
  static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
}

We can use this key to set and get a managed object context from the userInfo dictionary. Now let's create a JSONDecoder and set its userInfo dictionary:

let decoder = JSONDecoder()
decoder.userInfo[CodingUserInfoKey.managedObjectContext] = myPersistentContainer.viewContext

When we use this instance of JSONDecoder to decode data, the userInfo dictionary is available within the initializer of the object we're decoding to. Let's see how this works:

enum DecoderConfigurationError: Error {
  case missingManagedObjectContext
}

class TodoItem: NSManagedObject, Decodable {
  enum CodingKeys: CodingKey {
    case id, label, completions
 }

  required convenience init(from decoder: Decoder) throws {
    guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext else {
      throw DecoderConfigurationError.missingManagedObjectContext
    }

    self.init(context: context)

    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.id = try container.decode(Int64.self, forKey: .id)
    self.label = try container.decode(String.self, forKey: .label)
    self.completions = try container.decode(Set<TodoCompletion>.self, forKey: .completions) as NSSet
  }
}

In the initializer for TodoItem I try to extract the object at CodingUserInfoKey.managedObjectContext from the Decoder's userInfo dictionary and I try to cast it to an NSManagedObjectContext. If this fails I throw an error that I've defined myself because we can't proceed without a managed object context.

After that, I call self.init(context: context) to initialize the TodoItem and associate it with a managed object context.

The last step is to decode the object as you normally would by grabbing a container that's keyed by CodingKeys.self and decoding all relevant properties into the correct types.

Note that Core Data still uses Objective-C under the hood so you might have to cast some Swift types to their Objective-C counterparts like I had to with my Set<TodoCompletion>.

For completion, this is what the full class definition for TodoCompletion would look like:

class TodoCompletion: NSManagedObject, Decodable {
  enum CodingKeys: CodingKey {
    case completionDate
  }

  required convenience init(from decoder: Decoder) throws {
    guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext else {
      throw DecoderConfigurationError.missingManagedObjectContext
    }

    self.init(context: context)

    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.completionDate = try container.decode(Date.self, forKey: .completionDate)
  }
}

This code shouldn't look surprising; it's basically the same as the code for TodoItem. Note that the decoder that's used to decode the TodoItem is also used to decode TodoCompletion which means that it also has the managed object context in its userInfo dictionary.

If you want to test this code, you can use the following JSON as a starting point:

[
  {
    "id": 0,
    "label": "Item 0",
    "completions": []
  },
  {
    "id": 1,
    "label": "Item 1",
    "completions": [
      {
        "completionDate": 767645378
      }
    ]
  }
]

Unfortunately, it takes quite a bunch of code to make Decodable work with managed objects, but the final solution is something I'm not too unhappy with. I like how easy it is to use once set up properly.

Adding support for Encodable to an NSManagedObject

While we had to do a bunch of custom work to add support for Decodable to our managed objects, adding support for Encodable is far less involved. All we need to do is define encode(to:) for the objects that need Encodable support:

class TodoItem: NSManagedObject, Codable {
  enum CodingKeys: CodingKey {
    case id, label, completions
  }

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

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(id, forKey: .id)
    try container.encode(label, forKey: .label)
    try container.encode(completions as! Set<TodoCompletion>, forKey: .completions)
  }
}

Note that I had to convert completions (which is an NSSet) to a Set<TodoCompletion> explicitly. The reason for this is that NSSet isn't Encodable but Set<TodoCompletion> is.

For completion, this is what TodoCompletion looks like with Encodable support:

class TodoCompletion: NSManagedObject, Codable {
  enum CodingKeys: CodingKey {
    case completionDate
  }

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

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

Note that there is nothing special that I had to do to conform my managed object to Encodable compared to a normal manual Encodable implementation.

In Summary

In this week's post, you learned how you can add support for Codable to your managed objects by changing Xcode's default code generation for Core Data entities, allowing you to write your own class definitions. You also saw how you can associate a managed object context with a JSONDecoder through its userInfo dictionary, allowing you to decode your managed objects directly from JSON without any extra steps. To wrap up, you saw how to add Encodable support, making your managed object conform to Codable rather than just Decodable.

If you have any questions about this post or if you have feedback for me, don't hesitate to shoot me a message on Twitter.

Setting up a Core Data store for unit tests

Unit testing is an important skill in the toolbox of any engineer. Knowing how to write a reliable, robust test suite helps you write reliable and robust code. If you've followed my introduction to unit testing part one and part two, or if you're experienced with unit testing you know that your tests should run isolated from any other tests. You also know that you should make sure that your test relies on as few external dependencies as possible.

When you want to test your Core Data code, it might not be immediately obvious how you can test your Core Data store in isolation.

Luckily, you can configure your NSPersistentContainer to write data to memory rather than disk. There are two ways to achieve this. One option is to create a persistent store description that has its type property set to NSInMemoryStoreType. When you use this method of creating an in-memory store, you will find that certain features like cascading deletes might not work as expected. These features appear to rely on features from the default storage (SQLite) that are not available to an in-memory store.

Luckily we can use an SQLite store that writes to memory by configuring a persistent store that writes data to /dev/null/ instead of an actual SQLite file:

lazy var persistentContainer: NSPersistentContainer = {
  let container = NSPersistentContainer(name: "YourDataStore")

  let description = NSPersistentStoreDescription()
  description.url = URL(fileURLWithPath: "/dev/null")
  container.persistentStoreDescriptions = [description]

  container.loadPersistentStores(completionHandler: { _, error in
    if let error = error as NSError? {
      fatalError("Failed to load stores: \(error), \(error.userInfo)")
    }
  })

  return container
}()

Most of this code should look familiar to you. The only difference between this code and a standard Core Data setup are the following three lines:

let description = NSPersistentStoreDescription()
description.url = URL(fileURLWithPath: "/dev/null")
container.persistentStoreDescriptions = [description]

These three lines of code create a persistent store description that informs the persistent container that it should write data to /dev/null while using the default SQLite storage mechanism.

While it's not documented anywhere publicly, Apple's current recommendation appears to favor using an SQLite store that writes to /dev/null over an NSInMemoryStoreType based store. Writing to /dev/null effectively uses an in-memory store, except you get all the features that you also get from the SQLite store that your app uses. This makes unit testing with a /dev/null based store far more accurate than an NSInMemoryStoreType based store.

Note:
The initial version of this article covered NSInMemoryStoreType. Thanks to some feedback and information from Geoff Pado and Vojta Stavik I found out that writing to /dev/null is the currently preferred way to create an in-memory store. Apple talks about it in this WWDC video, and you can learn more about in-memory SQLite stores here.

Unfortunately, Apple has not updated their documentation for NSInMemoryStoreType to express their latest recommendations so using the /dev/null based approach will probably remain somewhat obscure for a while.

Exposing a Core Data stack for testing from your app

Since you don't want to add any app-related code to your test target, you need to have a way to expose your testing stack to your unit tests from within your app.

I like to create a very lightweight abstraction where I initialize and manage my Core Data stack. A very bare implementation of such an abstraction looks like this:

class CoreDataStore {
  let persistentContainer: NSPersistentContainer

  init() {
    self.persistentContainer = NSPersistentContainer(name: "YourDataStore")

    self.persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in
      if let error = error as NSError? {
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    })
  }
}

The code above is pretty basic but what's nice about it, is that the initialization of my persistent container is hidden from the rest of my app. This means that I have full control over how my persistent container is initialized and that I can set it up however I want.

An abstraction like this works well when you use dependency injection in your app.

Tip:
Did you know you can use dependency injection with storyboards in iOS 13 and above? Learn more here.

When you use dependency injection you can inject a different datastore into the objects that require a data store if you want to use them in your tests.

To make the abstraction I've just shown you useful for unit testing, we need to make a couple of modifications so we can choose whether a specific instance of CoreDataStore should use a regular SQLite backed store or an in-memory store:

enum StorageType {
  case persistent, inMemory
}

class CoreDataStore {
  let persistentContainer: NSPersistentContainer

  init(_ storageType: StorageType = .persistent) {
    self.persistentContainer = NSPersistentContainer(name: "YourDataStore")

    if storageType == .inMemory {
      let description = NSPersistentStoreDescription()
      description.url = URL(fileURLWithPath: "/dev/null")
      self.persistentContainer.persistentStoreDescriptions = [description]
    }

    self.persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in
      if let error = error as NSError? {
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    })
  }
}

With the code above, we can create a new CoreDataStore instance and pass inMemory to its initializer to create an instance of CoreDataStore that's useful for testing. In our application code, we can explicitly pass persistent to the initializer, or pass no arguments at all because persistent is the default store type.

Imagine that you have a view model object called MainViewModel that looks as follows:

struct MainViewModel {
  let storage: CoreDataStore

  // and of course it has a bunch of properties and method
}

You can create an instance of MainViewModel in your AppDelegate, SceneDelegate, or App struct if you're using SwiftUI 2.0 as follows:

let coreDataStore = CoreDataStore()
let viewModel = MainViewModel(storage: coreDataStore)

Note:
Every time you initialize a new CoreDataStore the persistent stores are loaded again. Make sure that you only create a single instance of your Core Data storage object to avoid loading multiple copies of your persistent store.

In your tests you can create an instance of your MainViewModel that uses a temporary in memory Core Data store as follows:

let coreDataStore = CoreDataStore(.inMemory)
let viewModel = MainViewModel(storage: coreDataStore)

The view model does not "know" whether it runs in a testing environment or an app environment. It just knows that a store exists and that this store is used to read and write data.

Every time you create a new in-memory instance of CoreDataStore, you start with a fresh database. This is perfect for unit testing and you should create a new instance of your CoreDataStore for every unit test you run to make sure it has a fresh, non-polluted database that is not modified by external actors.

In Summary

In this week's post, you saw how you can create and use an in-memory version of your Core Data database that is optimized for testing. In-memory storage is wiped as soon as the persistent container associated with the in-memory store goes out of memory, and every persistent container that's created with an in-memory store starts with a completely clean slate, making it perfect for unit tests.

You also saw how you can create a very simple and plain abstraction around your Core Data store to make it very simple to create persistent and in-memory instances of your persistent container.

If you have any questions or feedback for me, make sure to reach out to me on Twitter. I would love to hear from you.

Using Core Data with SwiftUI 2.0 and Xcode 12

In Xcode 12 you can create projects that no longer use an AppDelegate and SceneDelegate to manage the application lifecycle. Instead, we can use Swift's new @main annotation to turn a struct that conforms to the App protocol into the main entry point for our applications.

When you create a new project in Xcode 12, you have the option to use the SwiftUI App application lifecycle for your SwiftUI project.

While Xcode 12 beta 5 introduces an option to include Core Data in your SwiftUI application when you create a new project, you might have an existing SwiftUI project that doesn't use Core Data. Alternatively, you might just be curious how you could manually integrate Core Data in a SwiftUI project since there is nothing magic about the new Core Data template provided by Apple.

Adding Core Data to a SwiftUI project just takes two small steps:

  • Add a Core Data model file to your project
  • Initialize an NSPersistentContainer

Adding a Core Data model file to your project

To add a new Core Data model file to your project select File -> New -> File (cmd + N) and select Data Model from the Core Data section in the file type picker.

New file picker for Core Data model

After selecting this, pick a name for your model. The default that Xcode used to pick is the name of your project but you can choose any name you want.

I personally usually go with the name of my project for no reason other than it feeling familiar since it's the default name Xcode would have chosen in older Xcode versions. Currently, the default name Xcode would pick for you is Model which is a perfectly fine name too.

And that's all there is to it. Your Core Data model file is now added to your project and available to use by an NSPersistentContainer.

Initializing an NSPersistentContainer

Since iOS 10, the recommended way to use and manage a Core Data stack is through NSPersistentContainer. When Xcode generates a Core Data implementation for you, it uses an NSPersistentContainer too. You can initialize an NSPersistentContainer anywhere you want. I will show you how to initialize it as a property in your App struct, but you could just as well initialize the NSPersistentContainer in a dedicated data source object. I would recommend against initializing your NSPersistentContainer from within a View, but it's possible. Keep in mind that you should only load your container once though.

Let's look at the code needed to instantiate an NSPersistentContainer:

struct MyApplication: App {
  let persistentContainer: NSPersistentContainer = {
      let container = NSPersistentContainer(name: "MyApplication")
      container.loadPersistentStores(completionHandler: { (storeDescription, error) in
          if let error = error as NSError? {
              fatalError("Unresolved error \(error), \(error.userInfo)")
          }
      })
      return container
  }()

  var body: some Scene {
    WindowGroup {
      Text("Hello, World!")
    }
  }
}

All you have to do when initializing a persistent container is tell it which model file to load by passing the model name to the initializer, and then call loadPersistentStores on your container instance. After doing this your Core Data stack is initialized and ready to go. From here you can insert the container's viewContext into your app's environment using the @Environment property wrapper in your View, or you can pass it around in other ways.

Remember that you don't have to initialize your persistent container in your App struct. You could also create a PersistenceManager object for example:

class PersistenceManager {
  let persistentContainer: NSPersistentContainer = {
      let container = NSPersistentContainer(name: "MyApplication")
      container.loadPersistentStores(completionHandler: { (storeDescription, error) in
          if let error = error as NSError? {
              fatalError("Unresolved error \(error), \(error.userInfo)")
          }
      })
      return container
  }()
}

struct MyApplication: App {
  let persistence = PersistenceManager()

  var body: some Scene {
    WindowGroup {
      Text("Hello, World!")
    }
  }
}

This would work perfectly fine.

The old version of the Core Data stack generated in your AppDelegate contains one extra feature which is to automatically save any changes when your application goes to the background. You can mimic this behavior by listening for the UIApplication.willResignActiveNotification on NotificationCenter.default.

Note that this auto-save feature is also missing from Apple's new Core Data template for SwiftUI applications.

Unfortunately, I haven't found a way yet to subscribe to this notification from within the App struct because it's a struct and using the closure based listener complains that I capture a mutating self parameter when I access persistentContainer.

The easiest way to work around this is to subscribe within a specialized PersistenceManager like the one I showed you earlier:

class PersistenceManager {
  let persistentContainer: NSPersistentContainer = {
      let container = NSPersistentContainer(name: "MyApplication")
      container.loadPersistentStores(completionHandler: { (storeDescription, error) in
          if let error = error as NSError? {
              fatalError("Unresolved error \(error), \(error.userInfo)")
          }
      })
      return container
  }()

  init() {
    let center = NotificationCenter.default
    let notification = UIApplication.willResignActiveNotification

    center.addObserver(forName: notification, object: nil, queue: nil) { [weak self] _ in
      guard let self = self else { return }

      if self.persistentContainer.viewContext.hasChanges {
        try? self.persistentContainer.viewContext.save()
      }
    }
  }
}

And with that, you should have all the information needed to start using Core Data in your SwiftUI 2.0 applications

In Summary

In this week's post I showed you how you can initialize Core Data from anywhere in your app, allowing you to use it with SwiftUI 2.0's new application lifecycle.

You saw that all you need to do add Core Data to your app, is creating a model file, and initializing an NSPersistentContainer object. This works from anywhere in your app, including apps that use the old AppDelegate based application lifecycle.

If you have any questions about this post, you can find me on Twitter.

Understanding the importance of abstractions

As developers, we constantly deal with layers of abstractions that make our lives easier. We have abstractions over low level networking operations that allow us to make network calls with URLSession. Core Data provides an abstraction over data persistence that can be used to store information in an sqlite database. And there are many, many more abstractions that we all use every day.

Over the past few weeks I have seen many people ask about using Core Data in pure SwiftUI projects created in Xcode 12. These projects no longer require an App- and SceneDelegate, and the checkbox to add Core Data is disabled for these projects. Some folks immediately thought that this meant Core Data can't be used with these projects since Xcode's template always initialized Core Data in the AppDelegate, and since that no longer exists it seems to make sense that Core Data is incompatible with apps that don't have an AppDelegate. How else would you initialize Core Data?

Fortunately, this isn't true. It's still possible to use Core Data in projects, even if they don't have an AppDelegate. In fact, the only thing that AppDelegate has to do with Core Data is that Apple decided that they wanted to setup Core Data in the AppDelegate.

They didn't have to make that choice. Core Data can be initialized from anywhere in your app.

However, this got me thinking about abstractions. Folks who have built a layer of abstraction between their app and Core Data probably already know that you don't need Xcode to generate a Core Data stack for you. They probably also already know that you can initialize Core Data anywhere.

While thinking about this, I started thinking more about abstractions. Adding the right abstractions to your app at the right time can help you build a more modular, portable and flexible code base that can quickly adapt to changes and new paradigms.

That why in this week's post, I would like to talk about abstractions.

Understanding what abstractions are

Abstractions provide a seperation between the interface you program against and the underlying implementation that performs work. In essence you can think of most, if not all, frameworks you use every day on iOS as abstractions that make working with something complex easier.

In programming, we often work with abstractions on top of abstractions on top of more abstractions. And yet, there is value in adding more abstractions yourself. A good abstraction does not only hide complexity and implementation details. It should also be reusable. When your abstraction is reusable it can be used in multiple projects with similar needs.

I could try to make the explanation more wordy, fancy or impressive but that wouldn't help anybody. Abstractions wrap a complex interface and provide an (often simpler) inferface while hiding the wrapped, complex interface as an implementation detail. Good abstractions can be reused.

Knowing when to write an abstraction

Earlier I wrote that adding your own abstractions has value. That said, it's not always obvious to know when you should write an abstraction. Especially since there are no hard or clear rules.

A good starting point for me is to determine whether I will write a certain block of tedious code more than once. Or rather, whether I will write similar blocks of tedious code multiple times. If the answer is yes, it makes sense to try and create a lightweight abstraction to wrap the tedious code and make it less annoying to work with.

Another method I often use to determine whether I should write an abstraction is to ask myself how easily I want to be able to swap a certain mechanism in my app out for testing or to replace it entirely.

Usually the answer to this question is that I want to be able to swap things out as easily as possible. And more often than not this means that I should add an abstraction.

For instance, when I write code that uses Core Data I always wrap it in a small abstraction layer. I don't want my entire app to depend directly on Core Data. Instead, my app uses the abstraction to interface with a persistence layer. The code in my app doesn't know how the persistence layer works. It just knows that such a layer exists, and that it can fetch and save objects of certain types.

Creating an abstraction like this allows me to easily change the underlying storage mechanism in my persistence layer. I could switch to Realm, use sqlite directly, or even move from local persitence to persisting data on a server or in iCloud. The app shouldn't know, and the app shouldn't care. That's the beauty of abstractions.

Designing an abstraction

Once you've decided that you want to write an abstraction, you need to design it. The first thing I always do is make sure that I decide which properties and methods should be publicly available. I then define a protocol that captures this public API for my abstraction. For example:

protocol TodoItemPersisting {
  func getAllTodoItems() -> Future<[TodoItem]>
  func getTodoItem(withId id: UUID) -> Future<TodoItem?>
  func updateItem(_ item: TodoItem)
  func newTodoItem() -> Future<TodoItem>
}

This is a very simple protocol that exposes nothing about the underlying persistence layer. In the rest of my code I will always refer to TodoItemPersisting when I want to use my persistence abstraction:

struct TodoListViewModel {
  private let itemStore: TodoItemPersisting
}

In this example I defined a ViewModel that has an itemStore property. This property conforms to TodoItemPersisting and the object that creates an instance of TodoListViewModel gets to decide which concrete implementation of TodoItemPersisting is injected. And since the protocol for TodoItemPersisting uses Combine Futures, we know that the persistence layer does work asynchronously. The ViewModel doesn't know whether the persistence layer goes to the network, file system, Core Data, Realm, Firebase, iCloud or anywhere else for persistence.

It just knows that items are fetched and created asynchronously.

At this point you're free to create objects that implement TodoItemPersisting as needed. Usually you'll have one or two. One for the app to use, and a second version to use while testing. But you might have more in your app. It depends on the abstraction and what it's used for.

For instance, if your app uses In-App Purchases to provide syncing data to a server you might have a local persistence abstraction, and a premium local + remote persistence abstraction that you can swap out depending on whether the user bought your premium IAP.

By desiginig abstractions as protocols you gain a lot of flexibility and power. So whenever possible I always recommend to design and define your abstractions as protocols.

Things to watch out for when writing abstractions

Once you get the hang of abstracting code, it's very tempting to go overboard. While abstractions provide a lot of power, they also add a layer of indirection. New members of your team might understand the things you've abstracted really well, but if you added to many layers your code will be really hard to understand and your abstractions will be in the way of understanding the code base.

It's also possible that you didn't design your abstractions properly. When this happens, you will find that your abstractions are holding you back rather than helping you write code that does exactly what you want it to do. When you find you're fighting your abstractions it's time to revise your design and make improvements where needed.

And the last word of warning I want to give you is that it's important to limit the levels of abstractions you add. No matter how good your abstractions are, there will come a point where it'll get harder and harder to understand and debug your app when something is wrong. There's no hard cutoff point but eventually you'll develop a sense for when you're going too far. For now it's good to know that you can abstract too much.

In Summary

In this week's post you learned about abstractions in programming. You learned what an abstraction is, what abstractions are used for and how you can determine whether you should write an abstraction of your own.

You learned that abstractions can be extremely useful when you want to write code that's testible, flexible, and maintainable. Good abstractions make difficult work easier, and allow you to hide all implementation details of the thing or process you've written your abstraction for. You also learned that protocols are a fantastic tool to help you define and design your abstraction. Lastly, I gave you some things to watch out for when writing abstractions to make sure you don't overcomplicate matters or abstract too much.

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