In the previous episode, you learned how to load seed data from a file in the application's bundle. It's a common option that isn't that hard to implement. Once you've laid the groundwork, though, the seed data can come from anywhere. Let me show you what changes we need to make to seed the persistent store with data fetched from a remote server.

What Needs to Change

Even though fetching seed data from a remote server sounds daunting, the changes we need to make are small. The reason is simple. We laid most of the groundwork in the previous episodes. The only difference is the source of the seed data.

In the previous episode, we added a file named seed.json to the application's bundle. I've uploaded this file to a remote server for this episode. Even though we're going to download the contents of a static file, where the data comes from and how it's generated is an implementation detail. The application could ask a REST API for data. That's a detail the application isn't concerned with. It only asks for seed data to populate the persistent store. That's it.

Fetching the Seed Data

We fetch the seed data from an S3 bucket. The simplest solution requires one line of code to change. Open SeedOperation.swift and navigate to the seed() method.

We no longer fetch the seed data from a file in the application's bundle. We create a URL in the guard statement at the top using a string literal. In production, I don't recommend storing the location of the seed data in the SeedOperation class. Create a configuration file for that purpose.

// MARK: - Helper Methods

private func seed() throws {
    // Load Seed Data From Remote Server
    guard let url = URL(string: "https://cocoacasts.s3.amazonaws.com/resources/seeding-a-core-data-persistent-store/seed.json") else {
        throw SeedError.seedDataNotFound
    }

    ...

}

Remove the application from the simulator or your device to start with a clean slate. Run the application to verify that the application is still seeded with data.

I'm sure you didn't expect it to be this simple. Why does this work? We use the URL instance to create a Data instance. This works because the Data struct doesn't care where the data comes from as long as it can load the data.

// MARK: - Helper Methods

private func seed() throws {
    // Load Seed Data From Remote Server
    guard let url = URL(string: "https://cocoacasts.s3.amazonaws.com/resources/seeding-a-core-data-persistent-store/seed.json") else {
        throw SeedError.seedDataNotFound
    }

    // Load Data
    let data = try Data(contentsOf: url)

    ...

}

But there's another reason why this works. Creating a Data instance by loading data from a remote location is a blocking operation. If we were to perform this operation on the main thread, it would impact the application's performance. This isn't an issue for us since we're performing this task in the SeedOperation class. As long as we schedule the operation on an operation queue that does its work on a background thread, we're fine.

Other Options

There are other options to load the seed data, but they're more complex. Let's fetch the seed data using the URLSession API. We implement a helper method, fetchData(), in which we perform a network request to fetch the seed data.

private func fetchData() throws {
    guard let url = URL(string: "https://cocoacasts.s3.amazonaws.com/resources/seeding-a-core-data-persistent-store/seed.json") else {
        throw SeedError.seedDataNotFound
    }

    // Fetch Data
    URLSession.shared.dataTask(with: url) { [weak self] (data, _, _) in
        // Update Data
        self?.data = data
    }.resume()
}

We need to define a property to temporarily store the data we fetch from the remote server.

private var data: Data?

We perform the fetchData() method in the main() method of the SeedOperation class. We safely unwrap the value stored in the data property and pass it to the seed(with:) method.

override func main() {
    do {
        // Fetch Data
        try fetchData()

        if let data = data {
            // Seed With Data
            try seed(with: data)

            // Invoke Completion
            completion?(true)
        } else {
            // Invoke Completion
            completion?(false)
        }
    } catch {
        print("Unable to Save Managed Object Context After Seeding Persistent Store (\(error))")

        // Invoke Completion
        completion?(false)
    }
}

The seed(with:) method accepts the Data instance as an argument.

private func seed(with data: Data) throws {
    // Initialize JSON Decoder
    let decoder = JSONDecoder()

    // Configure JSON Decoder
    decoder.dateDecodingStrategy = .secondsSince1970

    ...
}

If you've worked with operations before, then you probably know what's going to happen if we run the application. Give it a try. Can you guess why the application isn't seeded with data?

Understanding Operations

A non-concurrent operation is completed and deallocated when it exits its main() method. Performing a network request is an asynchronous operation, which means the fetchData() method returns immediately, before the network request has completed.

This is a problem because it means the data property is equal to nil when we unwrap its value in the main() method. In other words, the seed(with:) method is never invoked.

How can we solve this? The solution is surprisingly simple, but it isn't terribly elegant. We add a while loop to the main() method that is repeated as long as the network request hasn't completed, successfully or unsuccessfully.

To make this work, we need to define a helper property, isFetching, that is set to true as long as the network request is in progress.

private var isFetching: Bool = false

With the isFetching property defined, we can update the main() method by adding the while loop.

override func main() {
    do {
        // Fetch Data
        try fetchData()

        while isFetching {}

        if let data = data {
            // Seed With Data
            try seed(with: data)

            // Invoke Completion
            completion?(true)
        } else {
            // Invoke Completion
            completion?(false)
        }
    } catch {
        print("Unable to Save Managed Object Context After Seeding Persistent Store (\(error))")

        // Invoke Completion
        completion?(false)
    }
}

In the fetchData() method, we set isFetching to true. In the completion handler of the data task, we set isFetching to false to notify the operation that the network request has completed, successfully or unsuccessfully. The result is that the while loop is exited and the execution of the main() method continues. Remove the application and run it again. The application's persistent store should be seeded with data again.

Concurrent Operations

The SeedOperation class is a non-concurrent operation and the solution we're using to make this work isn't terribly elegant. It's better to use a concurrent operation, but that's beyond the scope of this episode.

What's Next?

You learned how to seed a persistent store with hard-coded seed data, seed data loaded from a file, and seed data fetched from a remote server. The third and last strategy we cover in this series is seeding a persistent store with generated seed data. This is more complex, but it has a number of benefits.