Hard-coding seed data is quick and easy, but it's not my preferred solution. Loading seed data from a file is a strategy I like more. The idea is simple. You include a file with seed data in the application bundle. The file is loaded at runtime and its contents parsed. The contents is used to seed the persistent store with data.

This strategy has a number of benefits. You can choose the format of the seed data, include multiple files for different purposes, and it's easy to read and update the contents of the seed data. You can also fetch the seed data from a remote server and use it to seed the application with data. This solution makes it possible to update the seed data without updating the application.

In this episode, I show you how to seed an application with data by loading the seed data from a file included in the application's bundle. In the next episode, I show you how to modify the implementation to load the seed data from a remote server.

Creating the Seed Data

One of the benefits of this approach is that you can choose the format of the seed data. I almost always opt for JSON because it's easy to read and write.

In this episode, we use the same seed data we used in the previous episode. The only difference is that we store the seed data in a file in the application bundle. Create a new file in the Seed Data group and name it seed.json. The file contains a dictionary with three keys, tags, categories, and notes. The value of each key is an array of dictionaries. This should look familiar.

{
    "tags": [
        {
            "name": "Tips and Tricks"

        },
        {
            "name": "Libraries"
        },
        {
            "name": "Programming"
        },
        {
            "name": "Patterns"
        },
        {
            "name": "Apple"
        },
        {
            "name": "WWDC"
        },
        {
            "name": "News"
        }
    ],
    "categories": [
        {
            "name": "Core Data",
            "colorAsHex": "F6D14E"
        },
        {
            "name": "Swift",
            "colorAsHex": "5A76B5"
        },
        {
            "name": "iOS",
            "colorAsHex": "292B34"
        },
        {
            "name": "tvOS",
            "colorAsHex": "F2F2F2"
        },
        {
            "name": "macOS",
            "colorAsHex": "CD3227"
        },
        {
            "name": "watchOS",
            "colorAsHex": "E99057"
        },
        {
            "name": "Xcode",
            "colorAsHex": "101410"
        },
        {
            "name": "Objective-C",
            "colorAsHex": "4483F3"
        }
    ],
    "notes": [
        {
            "title": "Getting Started With Core Data",
            "contents": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis nec lacinia risus. Etiam suscipit elementum semper. Nunc gravida cursus pretium. Curabitur ultricies arcu sit amet porttitor rhoncus. Praesent eget convallis nibh, et malesuada nibh. Suspendisse potenti. Nulla id efficitur odio, vitae gravida erat. Nulla at eros a erat auctor venenatis. Nunc lectus libero, euismod eget nisi non, elementum scelerisque odio. Aliquam nec hendrerit sapien. Quisque ut dui posuere, malesuada libero et, vulputate purus. Quisque iaculis enim a faucibus hendrerit. Cras elementum vestibulum vestibulum. Nulla faucibus, leo quis lobortis euismod, leo sem ullamcorper justo, ac cursus metus elit vel orci. Aenean ac mi sollicitudin nisl lobortis dignissim. Vivamus facilisis mauris id nisl vulputate pretium.",
            "createdAt": 1518011139,
            "updatedAt": 1518011139,
            "category": "Swift",
            "tags": [
                "Tips and Tricks"
            ]
        },
        {
            "title": "Welcome to Cocoacasts",
            "contents": "Nunc velit turpis, commodo quis turpis quis, maximus aliquam odio. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis quis quam risus. Duis pretium magna eu imperdiet finibus. In semper erat ac elit rhoncus, et tempor quam aliquet. Suspendisse ullamcorper dolor sed pulvinar lobortis. Nam neque felis, rhoncus et lectus quis, ultricies bibendum magna. Pellentesque a dignissim metus, id faucibus metus. Nullam aliquet magna eu velit accumsan, sed aliquet metus euismod. Cras mauris libero, ultrices ut neque ut, dictum auctor diam. Etiam eu consequat nulla. Suspendisse placerat sit amet arcu sed tempus. Praesent eros lorem, molestie in lorem quis, congue scelerisque metus.",
            "createdAt": 1517987097,
            "updatedAt": 1517987097,
            "category": "iOS",
            "tags": [
                "Libraries"
            ]
        },
        {
            "title": "Five Tips to Become a Better Swift Developer",
            "contents": "Etiam pellentesque fringilla lacus id malesuada. Integer vitae dolor quam. Integer porta tincidunt ipsum, pulvinar porta velit sollicitudin eget. Proin dictum, risus at convallis tincidunt, justo diam congue magna, id condimentum velit nulla et massa. Donec luctus magna sed lacus semper, a varius lacus rhoncus. Nam consectetur mauris sit amet tellus posuere, vel pulvinar justo efficitur. Maecenas luctus porta metus. Vestibulum nunc mauris, vestibulum et accumsan ut, tempor sit amet neque. Donec vestibulum lobortis consequat. Proin a elit interdum, porta odio quis, cursus est. Praesent sit amet pulvinar elit. In elementum quam malesuada finibus tincidunt. Fusce commodo luctus hendrerit. In at arcu non nulla ultricies euismod. Cras malesuada malesuada enim, vitae tristique orci suscipit nec.",
            "createdAt": 1517963055,
            "updatedAt": 1517963055,
            "category": "Xcode",
            "tags": [
                "Libraries"
            ]
        },
        {
            "title": "Three Podcasts for Developers",
            "contents": "Curabitur purus odio, mattis nec ullamcorper eget, vestibulum vitae libero. Vestibulum eleifend, dui non vestibulum porttitor, sem sem rutrum felis, vel imperdiet mi urna eget ipsum. Nulla id lectus dignissim, hendrerit lorem non, suscipit ligula. Etiam lacinia sagittis est. Etiam maximus ante vitae lacus tempor condimentum. Curabitur enim libero, placerat a mauris posuere, imperdiet rutrum odio. Donec iaculis pulvinar risus, bibendum blandit nisi mollis vitae. Vestibulum at imperdiet risus, quis elementum purus. Integer interdum risus sem, eu ultricies augue lacinia id. Proin mattis semper mattis. Integer rutrum turpis mi. Duis consequat id orci non scelerisque. Cras sed eros vitae leo sagittis auctor ut quis leo. Vivamus in leo risus. Suspendisse orci purus, placerat lacinia augue mattis, rutrum dapibus mi. Interdum et malesuada fames ac ante ipsum primis in faucibus.",
            "createdAt": 1517939013,
            "updatedAt": 1517939013,
            "category": "Xcode",
            "tags": [
                "Tips and Tricks",
                "Libraries",
                "Programming",
                "Apple"
            ]
        },
        {
            "title": "What Is a Singleton and How To Create One In Swift",
            "contents": "Sed finibus aliquam cursus. Nam tempor gravida enim ac ullamcorper. Quisque rhoncus tellus ac turpis laoreet consectetur. Duis ornare vulputate faucibus. Morbi laoreet libero eu viverra interdum. Nam accumsan bibendum sapien, ut tincidunt libero varius at. Nulla sapien massa, euismod nec rhoncus eu, cursus sit amet purus. Vestibulum dapibus bibendum felis, a ornare lorem. Donec in lobortis ex, viverra sodales lorem. Morbi pretium, tortor et commodo lacinia, ipsum risus eleifend nulla, et maximus odio libero non arcu. Donec vestibulum tincidunt justo sed ullamcorper. Aenean nibh velit, hendrerit nec elementum sit amet, suscipit et metus. Integer sit amet turpis mauris. Aliquam viverra sed nulla vel imperdiet. Cras non pellentesque turpis, sed interdum tortor. Vivamus placerat arcu id neque accumsan, non viverra odio porta.",
            "createdAt": 1517914971,
            "updatedAt": 1517914971,
            "category": "Swift",
            "tags": [
                "Programming"
            ]
        },
        {
            "title": "Managing View Controllers With Container View Controllers",
            "contents": "Vestibulum eleifend metus at nunc porta, scelerisque consectetur augue ornare. Suspendisse mauris dui, fringilla nec posuere fringilla, vulputate quis arcu. Quisque ullamcorper tincidunt nulla vel consectetur. Ut massa ipsum, varius quis nulla in, iaculis molestie augue. Mauris et sodales lorem. Vivamus eu est iaculis urna faucibus pretium dignissim sed arcu. Nulla feugiat dui orci.",
            "createdAt": 1517890929,
            "updatedAt": 1517890929,
            "category": "watchOS",
            "tags": [
                "Programming"
            ]
        },
        {
            "title": "Reading and Updating Managed Objects With Core Data",
            "contents": "Donec nisi enim, euismod non malesuada condimentum, ullamcorper in velit. Cras hendrerit sit amet nunc eget feugiat. Mauris accumsan facilisis erat, condimentum faucibus leo accumsan id. Cras accumsan elementum diam, vel ultrices quam placerat vitae. Aliquam ligula urna, condimentum eu semper sit amet, vehicula vel diam. Sed nec lectus ac augue porta tincidunt. Praesent tempus ac urna vitae lacinia. Vestibulum consectetur justo non turpis rutrum, id eleifend lacus bibendum. Maecenas luctus metus lorem, vitae dictum erat fermentum ac. Cras auctor vitae eros id venenatis. Sed rhoncus rhoncus hendrerit. Ut quis eros euismod mauris finibus ullamcorper sit amet a ipsum. Sed vitae pulvinar lectus.",
            "createdAt": 1517866887,
            "updatedAt": 1517866887,
            "category": "Objective-C",
            "tags": [
                "Apple"
            ]
        },
        {
            "title": "Nuts and Bolts of Dependency Injection in Swift",
            "contents": "Donec dolor ipsum, iaculis eget mi ut, auctor pretium nisl. Nullam ante libero, lacinia non leo at, feugiat commodo massa. Morbi congue scelerisque arcu, id interdum ante pretium non. Suspendisse in condimentum lacus. Sed mattis nulla a est commodo, vitae sagittis nisl viverra. Integer tincidunt tempor lacinia. Donec convallis blandit dapibus. Nullam a ipsum tristique, dapibus libero vel, convallis neque. Sed posuere lacinia augue, id volutpat lorem tristique nec. Nunc id erat vitae justo porttitor feugiat. Morbi iaculis magna at molestie vestibulum. Sed est nunc, mollis at auctor et, pharetra eget felis. Phasellus elementum eget dui sit amet vestibulum. In pellentesque mi eu imperdiet pretium.",
            "createdAt": 1517842845,
            "updatedAt": 1517842845,
            "category": "tvOS",
            "tags": [
                "Tips and Tricks",
                "Programming",
                "Patterns"
            ]
        },
        {
            "title": "Mastering MVVM With Swift",
            "contents": "Duis blandit mi tellus. In sed lorem nibh. Fusce facilisis quam eu nibh vulputate, id bibendum nisl venenatis. Nam dictum ultrices velit. Sed ut placerat augue. Maecenas sit amet ligula mi. Ut varius efficitur tellus. Duis cursus aliquet convallis. Pellentesque bibendum, turpis vel lobortis fermentum, est metus sagittis diam, maximus hendrerit purus diam a turpis. Praesent pellentesque tortor et quam gravida ullamcorper. Suspendisse semper, lectus a congue interdum, ex massa elementum urna, nec ultricies magna nisi vel turpis.",
            "createdAt": 1517818803,
            "updatedAt": 1517818803,
            "category": "Swift",
            "tags": [
                "Patterns"
            ]
        },
        {
            "title": "Core Data Fundamentals",
            "contents": "Aliquam sagittis magna felis, eget convallis diam convallis et. In ut risus ut lorem mollis euismod. Sed finibus augue in lacus imperdiet suscipit. Morbi accumsan urna ullamcorper porttitor egestas. Sed ex tortor, elementum ac imperdiet eu, ullamcorper sit amet mauris. Praesent volutpat sem a iaculis sodales. Fusce gravida sodales arcu laoreet lacinia. In pretium diam quam, sed vestibulum orci ullamcorper vel. Etiam tincidunt massa ipsum. In sollicitudin molestie accumsan. Duis vel lacinia mauris. Sed hendrerit, nibh nec blandit vulputate, ligula dolor auctor nibh, sodales vestibulum ex turpis at lectus.",
            "createdAt": 1517794761,
            "updatedAt": 1517794761,
            "category": "Swift",
            "tags": [
                "News"
            ]
        }
    ]
}

Decoding JSON

Working with JSON in Swift used to be quite a challenge. That's no longer true since the introduction of the Codable protocol. While I won't cover the basics of the Codable protocol in this episode, don't worry if you're not familiar with it. It's surprisingly easy to adopt.

The objective is to create model objects from the contents of seed.json. It means that we need to parse the contents of the seed data file and convert the JSON data to model objects.

Create a new group, Models, in the Seed Data group. Create a new Swift file and name it TagData.swift. We define a struct TagData that conforms to the Codable protocol. We can't name the struct Tag since the project already defines an NSManagedObject subclass with that name.

import Foundation

struct TagData: Codable {

}

The structure of the TagData struct mirrors the structure of the Tag entity. We define a property, name of type String. That's it. The TagData struct is ready to be used.

import Foundation

struct TagData: Codable {

    // MARK: - Properties

    let name: String

}

We repeat this for the data of a category. We create a file named CategoryData.swift and define a struct CategoryData. The CategoryData struct conforms to the Codable protocol and it defines two properties, name of type String and colorAsHex of type String.

import Foundation

struct CategoryData: Codable {

    // MARK: - Properties

    let name: String
    let colorAsHex: String

}

I'm sure you know what to do next. Create a new file NoteData.swift and try implementing the NoteData struct. Give it a try. There are a few details that need an explanation. The NoteData struct defines a title and a contents property of type String.

import Foundation

struct NoteData: Codable {

    // MARK: - Properties

    let title: String
    let contents: String

}

In seed.json, we define dates as timestamps, that is, the number of seconds that have passed since midnight January 1, 1970. Even though we define dates as numbers in seed.json, we declare the createdAt and updatedAt properties as dates, instances of the Date struct. This isn't a problem as we'll see a moment.

import Foundation

struct NoteData: Codable {

    // MARK: - Properties

    let title: String
    let contents: String

    let createdAt: Date
    let updatedAt: Date

}

The relationships of a note don't pose any problems either. The tags relationship is of type [String] and the category relationship is of type String.

import Foundation

struct NoteData: Codable {

    // MARK: - Properties

    let title: String
    let contents: String

    let createdAt: Date
    let updatedAt: Date

    // MARK: - Relationships

    let tags: [String]
    let category: String

}

With TagData, CategoryData, and NoteData created, it's time to update the implementation of the Seed struct. We start by removing the computed properties that return the hard-coded data.

import Foundation

struct Seed {

}

We conform the Seed struct to the Codable protocol.

import Foundation

struct Seed: Codable {

}

If you take a look at the contents of seed.json, you should already know which properties we need to define.

  • tags of type [TagData]
  • categories of type [CategoryData]
  • notes of type [NoteData]
import Foundation

struct Seed: Codable {

    // MARK: - Properties

    let tags: [TagData]
    let categories: [CategoryData]
    let notes: [NoteData]

}

Loading Seed Data

With the model types created, it's time to load the data in the SeedOperation class. Open SeedOperation.swift and navigate to the seed() method.

We ask the main bundle for the URL of seed.json. We use a guard statement since there's no point to continue if we're not able to find the seed data in the application's bundle.

// MARK: - Helper Methods

private func seed() throws {
    // Load Seed Data From Bundle
    guard let url = Bundle.main.url(forResource: "seed", withExtension: "json") else {

    }

    ...

}

If we're not able to find the seed data in the application's bundle, we throw an error. Let's define the error at the top of the SeedOperation class and name it seedDataNotFound.

import CoreData
import Foundation

class SeedOperation: Operation {

    // MARK: - Error Handling

    enum SeedError: Error {

        case seedDataNotFound

    }

    ...

}

We throw the error in the else clause of the guard statement.

// MARK: - Helper Methods

private func seed() throws {
    // Load Seed Data From Bundle
    guard let url = Bundle.main.url(forResource: "seed", withExtension: "json") else {
        throw SeedError.seedDataNotFound
    }

    ...

}

We use the URL of seed.json to create a Data instance. We prefix the initialization with the try keyword since the operation can fail. There's no need to wrap the instantiation of the Data instance in a do-catch statement since the seed() method is throwing. Any errors that are thrown bubble up to the call site of the seed() method.

// MARK: - Helper Methods

private func seed() throws {
    // Load Seed Data From Bundle
    guard let url = Bundle.main.url(forResource: "seed", withExtension: "json") else {
        throw SeedError.seedDataNotFound
    }

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

    ...

}

To decode the contents of seed.json, we initialize an instance of the JSONDecoder class. This class is defined in the Swift standard library and was introduced alongside the Codable protocol in Swift 4.

// MARK: - Helper Methods

private func seed() throws {
    // Load Seed Data From Bundle
    guard let url = Bundle.main.url(forResource: "seed", withExtension: "json") else {
        throw SeedError.seedDataNotFound
    }

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

    // Initialize JSON Decoder
    let decoder = JSONDecoder()

    ...

}

Before we use the decoder to parse the contents of seed.json, we need to tell it how it should handle dates. Remember that the format we chose is the number of seconds that have passed since midnight January 1, 1970. The JSONDecoder class supports this format out of the box. We set the dateDecodingStrategy property of the JSONDecoder instance to secondsSince1970 and we're set. This is very convenient.

// MARK: - Helper Methods

private func seed() throws {
    // Load Seed Data From Bundle
    guard let url = Bundle.main.url(forResource: "seed", withExtension: "json") else {
        throw SeedError.seedDataNotFound
    }

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

    // Initialize JSON Decoder
    let decoder = JSONDecoder()

    // Configure JSON Decoder
    decoder.dateDecodingStrategy = .secondsSince1970

    ...

}

We're ready to parse the contents of seed.json. We invoke the decode(_:from:) method on the JSONDecoder instance, passing in the type we want to decode and the Data instance. Notice that the decode(_:from:) method is throwing. We store the result in a constant named seed.

// MARK: - Helper Methods

private func seed() throws {
    // Load Seed Data From Bundle
    guard let url = Bundle.main.url(forResource: "seed", withExtension: "json") else {
        throw SeedError.seedDataNotFound
    }

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

    // Initialize JSON Decoder
    let decoder = JSONDecoder()

    // Configure JSON Decoder
    decoder.dateDecodingStrategy = .secondsSince1970

    // Decode Seed Data
    let seed = try decoder.decode(Seed.self, from: data)

    ...

}

That's it. We need to make a few minor adjustments because the structure of the Seed struct is a bit different from its previous implementation.

Tags

The tags property of the Seed struct is an array of TagData instances. We access the name property in the for loop.

for data in seed.tags {
    // Initialize Tag
    let tag = Tag(context: privateManagedObjectContext)

    // Configure Tag
    tag.name = data.name

    // Append to Buffer
    tagsBuffer.append(tag)
}

Notes

We no longer need the index of the item in the for loop of the notes. We can use a regular for loop to create the Note instances.

for data in seed.notes {
    // Initialize Note
    let note = Note(context: privateManagedObjectContext)

    ...
}

The contents of the note is a property of the NoteData instance, which simplifies the implementation of the for loop.

for data in seed.notes {
    // Initialize Note
    let note = Note(context: privateManagedObjectContext)

    // Configure Note
    note.title = data.title
    note.contents = data.contents

    ...
}

Because dates are automatically parsed by the JSONDecoder instance, we no longer need to convert the timestamps to Date instances.

for data in seed.notes {
    // Initialize Note
    let note = Note(context: privateManagedObjectContext)

    // Configure Note
    note.title = data.title
    note.contents = data.contents
    note.createdAt = data.createdAt
    note.updatedAt = data.updatedAt

    ...

}

Build and Run

Start with a clean slate by removing the application from the simulator or your device. Run the application to make sure the updated implementation works as expected. The result should be identical.

What's Next?

Loading seed data from a file brings several benefits to the table. You can add multiple files to your project, you can download data from a remote server, and you can choose the format of the seed data. Loading data from a file is also very convenient if you seed the persistent store in production and need to support multiple languages. You simply create a file for each language you intend to support.

In the next episode, we take it one step further by fetching the seed data from a remote server. The changes we need to make are surprisingly small thanks to the groundwork we laid in this episode.