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.

Handling deeplinks in iOS 14 with onOpenURL

Starting with iOS 14, we can write apps that are fully built using SwiftUI, dropping the need to have AppDelegate and SceneDelegate files entirely. For as long as I remember, I've handled deeplinks in my AppDelegate and for the past year in the SceneDelegate. So when Apple introduced developers to this new @main annotated App struct style of building apps, I'm sure we all had the same question on our mind. How does the new App struct work with deeplinks and other tasks that are normally performed in the AppDelegate?

Luckily, Apple engineers made sure that handling deeplinks in our apps is still possible with the new onOpenURL(perform:) view modifier.

Handling deeplinks with onOpenURL

The new onOpenURL(perform:) view modifier is new in iOS 14. It allows developers to register a URL handler on their views so they can respond to URLs by modifying state for their views as needed.

This is vastly different from how we're used to dealing with URLs in UIKit and the SceneDelegate flow.

The old way of handling deeplinks requires you to handle each link in the SceneDelegate (or AppDelegate). You would have to manipulate the selected tab in a UITabBarViewController, or present a UIViewController by inspecting the current view controller hierarchy and pushing the needed UIViewController from right inside of the SceneDelegate.

In SwiftUI, you can use the onOpenURL(perform:) on the root of your scene as follows:

@main
struct MyApplication: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
        .onOpenURL { url in
          // handle the URL that must be opened
        }
    }
  }
}

I will cover what it means exactly to handle the url in the next section of this article, but usually it will involve mutating some state to load and display the view associated with the URL that must be opened.

What's really neat is that you can specify multiple onOpenURL handlers throughout your app. This means that you can make multiple, smaller changes to your app state which means that you no longer have one place where all of your deeplink handling and view manipulation takes place.

Furthermore, onOpenURL is called when your app is in the foreground, background or not running at all. This means that there is now a single entry point for your app to handle URLs. Even if your app is relaunched after being force-closed.

In the next section, I will show you an example of how you can select a tab in a TabView depending on the URL that your app is requested to open. After that, I will show you how to navigate to a list item in a view that's embedded in a TabView by adding a second onOpenURL view modifier on a child View that contains a List.

Activating a tab in a TabView when opening a URL

In SwiftUI, views are a function of their state. This means that virtually everything in a SwiftUI application can be represented and manipulated as a data model. This means that we can represent the currently selected tab in a SwiftUI TabView as a property on an App struct.

The following code shows how:

struct MyApplication: App {
  @State var activeTab = 0

  var body: some Scene {
    WindowGroup {
      TabView(selection: $activeTab) {
        HomeView()
          .tabItem {
            VStack {
              Image(systemName: "house")
              Text("Home")
            }
          }
          .tag(0)

        SettingsView()
          .tabItem {
            VStack {
              Image(systemName: "gearshape")
              Text("Settings")
            }
          }
          .tag(1)
      }
      .onOpenURL { url in
        // determine which tab should be selected and update activeTab
      }
    }
  }
}

What's important to notice here is the activeTab property. This property is marked as @State and represents the selected tab in the TabView. When creating the TabView, I pass a binding to activeTab to the TabView's initializer. Setting the TabView up like this means that updating activeTab will cause the TabView to update its selected tab as well.

Notice that I set a tag on the views that are added to the TabView. This tag is used to identify the TabView's items. When activeTab matches one of the tags associated with your views, the TabView will activate the matching tab.

In this case that means setting activeTab to 1 would activate the tab that displays SettingsView.

Let's see how you can implement onOpenURL to figure out and activate the correct tab. To do this, I'm going to introduce an extension on URL, and a new type called TabIdentifier:

enum TabIdentifier: Hashable {
  case home, settings
}

extension URL {
  var isDeeplink: Bool {
    return scheme == "my-url-scheme" // matches my-url-scheme://<rest-of-the-url>
  }

  var tabIdentifier: TabIdentifier? {
    guard isDeeplink else { return nil }

    switch host {
    case "home": return .home // matches my-url-scheme://home/
    case "settings": return .settings // matches my-url-scheme://settings/
    default: return nil
    }
  }
}

The code above is just a convient way to figure out which tab belongs to a URL without having to duplicate logic all over the app. If you decide to implement a similar object, the isDeeplink computed property should be updated according to the URLs you want to support. If you're implementing Universal Links, you'll want to check whether the URL's host property matches your hostname. I've set up a very minimal check here for demonstration purposes where I only care about the URL scheme.

The tabIdentifier property is a computed property that uses the host property to determine which tab should be selected. For a Universal Link you'll probably want to use the pathComponents property and compare using the second entry in that array, depending on your mapping strategy. Again, I set this up to be very basic.

You can use this basic setup in the App struct as follows:

struct MyApplication: App {
  @State var activeTab = TabIdentifier.home

  var body: some Scene {
    WindowGroup {
      TabView(selection: $activeTab) {
        HomeView()
          .tabItem {
            VStack {
              Image(systemName: "house")
              Text("Home")
            }
          }
          .tag(TabIdentifier.home) // use enum case as tag

        SettingsView()
          .tabItem {
            VStack {
              Image(systemName: "gearshape")
              Text("Settings")
            }
          }
          .tag(TabIdentifier.settings) // use enum case as tag
      }
      .onOpenURL { url in
        guard let tabIdentifier = url.tabIdentifier else {
          return
        }

        activeTab = tabIdentifier
      }
    }
  }
}

Because I made TabIdentifier Hashable, it can be used as the activeTab identifier. Each tab in the TabView is associated with a TabIdentifier through their tags, and by reading the new tabIdentifier that I added to URL in my extension, I can easily extract the appropriate tab identifier associated with the URL that I need to open.

As soon as I assign the acquired tabIdentifier to activeTab, the TabView is updated marking the appropriate tab as selected along with displaying the appropriate View.

Of course this is only half of what you'll want to typically do when opening a deeplink. Let's take a look at activating a NavigationLink in a different view next.

Handling a URL by activating the correct NavigationLink in a List

You already know how to activate a tab in a TabView when your app needs to handle a URL. Often you'll also need to navigate to a specific detail page in the view that's shown for the selected tab item. The cleanest way I have found to do this, is by adding a second onOpenURL handler that's defined within the detail view that should activate your navigation link.

When you define multiple onOpenURL handlers, the system will call them all, allowing you to make small, local changes to your view's data model. Like selecting a tab in the App struct, and activating a NavigationLink in a child view. Before I show you how to do this, We'll need another extension on URL to extract the information we need to activate the appropriate NavigationLink in a List:

enum PageIdentifier: Hashable {
  case todoItem(id: UUID)
}

extension URL {
  var detailPage: PageIdentifier? {
    guard let tabIdentifier = tabIdentifier,
          pathComponents.count > 1,
          let uuid = UUID(uuidString: pathComponents[1]) else {
      return nil
    }

    switch tabIdentifier {
    case .home: return .todoItem(id: uuid) // matches my-url-scheme://home/<item-uuid-here>/
    default: return nil
    }
  }
}

I've added a new enum called PageIdentifier. This enum has a single case with an associated value. This associated value represents the identifier of the object that I want to deeplink to. My app is a to-do app, and each to-do item uses a UUID as its unique identifier. This is also the identifier that's used in the deeplink. The approach above is similar to what I've shown in the previous section and if you decide you like my URL parsing approach, you'll have to make some modifications to adapt it in your app.

The next step is to implement the HomeView, and select the appropriate item from its list of items:

struct HomeView: View {
  @StateObject var todoItems = TodoItem.defaultList // this is just a placeholder.  
  @State var activeUUID: UUID?

  var body: some View {
    NavigationView {
      List(todoItems) { todoItem in
        NavigationLink(destination: TodoDetailView(item: todoItem), tag: todoItem.id, selection: $activeUUID) {
          TaskListItem(task: todoItem)
        }
      }
      .navigationTitle(Text("Home"))
      .onOpenURL { url in
        if case .todoItem(let id) = url.detailPage {
          activeUUID = id
        }
      }
    }
  }
}

Notice that my HomeView has a property called activeUUID. This property serves the exact same purpose that activeTab fulfilled in the previous section. It represents the identifier for the item that should be selected.

When creating my NavigationLink, I pass a tag and a binding to activeUUID to each NavigationLink object. When SwiftUI notices that the tag for one of my navigation links matches the activeUUID property, that item is selected and pushed onto the NavigationView's navigation stack. If you already have a different item selected, SwitUI will first go back to the root of your NavigationView (deactivating that link) and then navigate to the selected page.

In onOpenURL I check whether url.detailPage points to a todoItem, and if it does I extract its UUID and set it as the active UUID to navigate to that item.

By definiing two onOpenURL handlers like I just did, I can make small changes to local state and SwiftUI takes care of the rest. Both of these onOpenURL handlers are called when the app is expected to handle a link. This means that it's important for each View to check whether it can (and should) handle a certain link, and to make small changes to the view it belongs to rather than making big app-wide state changes like you would in a UIKit app.

In Summary

This week, you saw how you can use iOS 14's new onOpenURL view modifier to handle open URL requests for your app. You learned that you can define more than one handler, and that onOpenURL is called for all scenarios where your app needs to open a URL. Even if your app is launched after being force closed.

First, I showed you how you can parse a URL to determine which tab in a TabView should be selected. Then I showed you how you can change the active tab in a TabView by tagging your views and passing a selection biding to the TabView's initializer. After that, you saw how you can navigate to a detail view by doing more URL parsing, and passing a tag and binding to your NavigationLink.

I was rather surprised when I learned that deeplink handling on iOS 14 with SwiftUI is this powerful and flexible. Since we can specify onOpenURL handlers wherever they are needed, it's much easier to decouple and compose your app using small parts. You no longer need a SceneDelegate that knows exactly how your entire app is structured because it needs to manipulate your app's entire navigation state from a single place. It feels much cleaner to handle URLs on a local level where the side-effects are limited to the view that's handling the URL.

If you have any comments, questions, or feedback about this post please reach out on Twitter. I love hearing from you.

Implementing an infinite scrolling list with SwiftUI and Combine

Tons of apps that we build feature lists. Sometimes we build lists of settings, lists of todo items, lists of our favorite pictures, lists of tweets, and many other things. Some of these lists could scroll almost endlessly. Think of a Twitter timeline, a Facebook feed or a list of posts on Reddit.

You might argue that knowing how to build a list that scrolls infinitely and fetches new content whenever a user reaches the end of the list is an essential skill of any iOS developer. That's why as one of my first posts that covers SwiftUI I wanted to explore building a list that can scroll forever.

And to be honest, I was surprised with how simple SwiftUI makes implementing this feature on iOS 14, even though we can't read the current scroll offset of a list like we can in UIKit. Instead of reading a scroll offset we can use a list item's onAppear modifier to trigger a new page load.

Let's find out how.

Implementing the SwiftUI portion of an infinite scrolling list

Before I explain, let's look at some code:

struct EndlessList: View {
  @StateObject var dataSource = ContentDataSource()

  var body: some View {
    List(dataSource.items) { item in
      Text(item.label)
        .onAppear {
          dataSource.loadMoreContentIfNeeded(currentItem: item)
        }
        .padding(.all, 30)
    }
  }
}

This code uses @StateObject which is new in iOS 14. Read more about @StateObject and how it compares to @ObservedObject here.

I'll show you the data source in a moment, but let's talk about the couple of lines of code in this snippet first. Surely this can't be all we need to support infinite scrolling, right? Well... it turns out it is all we need.

In SwiftUI, onAppear is called when a view is rendered by the system. This doesn't mean that the view will be rendered within the user's view, or that it ever makes it on screen so we're relying on List's performance optimizations here and trust that it doesn't render all of its views at once.

A List will only keep a certain number of views around while rendering so we can use onAppear to hook into List's rendering. Since we have access to the item that's being rendered, we can ask the data source to load more data if needed depending on the item that's being rendered. If this is one of the last items in the data source, we can kick off a page load and add more items to the data source.

Implementing the data source

Let's look at the data source for this example:

class ContentDataSource: ObservableObject {
  @Published var items = [ListItem]()
  @Published var isLoadingPage = false
  private var currentPage = 1
  private var canLoadMorePages = true

  init() {
    loadMoreContent()
  }

  func loadMoreContentIfNeeded(currentItem item: ListItem?) {
    guard let item = item else {
      loadMoreContent()
      return
    }

    let thresholdIndex = items.index(items.endIndex, offsetBy: -5)
    if items.firstIndex(where: { $0.id == item.id }) == thresholdIndex {
      loadMoreContent()
    }
  }

  private func loadMoreContent() {
    guard !isLoadingPage && canLoadMorePages else {
      return
    }

    isLoadingPage = true

    let url = URL(string: "https://s3.eu-west-2.amazonaws.com/com.donnywals.misc/feed-\(currentPage).json")!
    URLSession.shared.dataTaskPublisher(for: url)
      .map(\.data)
      .decode(type: ListResponse.self, decoder: JSONDecoder())
      .receive(on: DispatchQueue.main)
      .handleEvents(receiveOutput: { response in
        self.canLoadMorePages = response.hasMorePages
        self.isLoadingPage = false
        self.currentPage += 1
      })
      .map({ response in
        return self.items + response.items
      })
      .catch({ _ in Just(self.items) })
      .assign(to: $items)
  }
}

This code uses Combine's new assign(to:) function. Read more about it here.

There's a lot to unpack in that snippet but I think the most interesting bit is loadMoreContent. The rest of the code kind of speaks for itself.

In loadMoreContent I check whether I'm already loading a page, and whether there are more pages to load. I set isLoadingPage to true, and I construct a URL for a page which points to a feed file that I've uploaded to Amazon S3. This would normally be a URL that points to the page that you want to load in your list. I create a dataTaskPublisher so I can load the URL and I use Combine's handleEvents operator to apply side-effects to my data source when a response was loaded.

Next, I update the canLoadMorePages boolean, set isLoadingPage back to false because the load is complete and increment the currentPage. I prefixed handleEvents with receive(on: DispatchQueue.main) because I modify an @Published property in the handleEvents operator which might change my view and that must be done on the main thread. I don't do this in my map that's applied after handleEvents because map is supposed to be pure and not apply side-effects.

In the map I return a value that merges the current list of items with the newly loaded items. Lastly, I catch any erros that might have occured during the page load and replace them with a publisher that re-emits the current list of items. To update the items property I use Combine's new assign(to:) operator. This operator can pipe the output from a publisher directly into an @Published property without needing to manually subscribe to it.

While it's a lot of code, I think this pipeline is relatively straightforward once you understand all of the operators that are used.

Since I made the ContentDataSource's isLoadingPage property @Published, we can use it to add a loading indicator to the bottom of the list to show the user we're loading a new page in case the page isn't loaded by the time the user reaches the end of the list:

struct EndlessList: View {
  @StateObject var dataSource = ContentDataSource()

  var body: some View {
    List {
      ForEach(dataSource.items) { item in
        Text(item.label)
          .onAppear {
            dataSource.loadMoreContentIfNeeded(currentItem: item)
          }
          .padding(.all, 30)
      }

      if dataSource.isLoadingPage {
        ProgressView()
      }
    }
  }
}

This if statement will conditionally show and hide a ProgressView depending on whether we're fetching a new page.

We can modify this example to build in infinite scrolling list using a ScrollView and ForEach through a LazyVStack on iOS 14.

Building an endless scrolling LazyVStack

On iOS 13 it's possible to build scrolling lists using ForEach and VStack. Unfortunately, these components don't work well with the technique for building an infinite list that I just demonstrated. A VStack combined with ForEach builds its entire view hierarchy at once rather than lazily like a List does. This would mean that we'd immediately begin loading items from the server and continue to load more until all pages are loaded without any action from the user. This happens because onAppear is called when a view is added to the view hierarchy rather than when the view actually becomes visible.

Luckily, iOS 14 introduces a LazyVStack that builds its view hierarchy lazily, which means that new items are added to its layout as the user scrolls. This means that the onAppear method for items created in ForEach is called at a similar time as it is for items inside a List, and that we can use it to build our infinite scrolling list without using a List:

struct EndlessList: View {
  @StateObject var dataSource = ContentDataSource()

  var body: some View {
    ScrollView {
      LazyVStack {
        ForEach(dataSource.items) { item in
          Text(item.label)
            .onAppear {
              dataSource.loadMoreContentIfNeeded(currentItem: item)
            }
            .padding(.all, 30)
        }

        if dataSource.isLoadingPage {
          ProgressView()
        }
      }
    }
  }
}

Pretty nifty, right?

In Summary

In this week's post I finally went all-in on SwiftUI. With iOS 14 I think it has reached a level of maturity that makes it attractive to learn, and since all SwiftUI code from iOS 13 also works on iOS 14 I think it's unlikely that Apple will make huge breaking changes to SwiftUI in the near future.

You saw how you can use SwiftUI to build an infinite scrolling list using the onAppear modifier, and how you can back this up with a data source object that's implemented in Combine. I also showed you the new LazyVStack that was added to SwiftUI in iOS 14 which allows you to apply the technique we used to build an inifite scrolling list to a VStack.

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

Using multi-colored icons in iOS 14 with SF Symbols 2

Apple introduced SF Symbols in iOS 13. SF Symbols allow developers to easily integrate icons in their apps. The SF Symbols icons integrate really well with the default system font, and provide a consistent look throughout the system.

In iOS 14, Apple added over 750 new icons to the SF Symbols library for developers to use in their apps. Additionally, Apple has expanded SF Symbols to include multi-colored icons. For a full overview of the available SF Symbols that are available, including the newly added and multicolor symbols, download the SF Symbols 2 app from Apple's SF Symbols page.

grid of new symbols

To use a multicolored symbol in your app, all you need to do is set the correct rendering mode for your image.

To use a multi-colored icon in SwiftUI you can use the following code:

Image(systemName: "thermometer.sun.fill")
  .font(.largeTitle)
  .renderingMode(.original)

In a UIKit based app, you can set the icon's tint color as follows:

let image = UIImage(systemName: "star.fill")?
  .withRenderingMode(.alwaysOriginal)

Note that at the time of writing I have not managed to get multi-colored SF Symbols to actually work with UIKit and that only a handful of SF Symbols properly show up with multiple colors when used in SwiftUI depending on the device you're using. The iPhone 11 simulator appears to render all icons correctly but the iOS 14 beta on an iPhone 7 doesn't. There's also a bug currently where setting an icon's font-size can cause it to not be colored correctly.

The ability to use multi-colored symbols in your app is a very welcome addition to the SF Symbols feature and I think it can add a really vibrant touch to your apps.