WWDC24 SALE: Save 50% on all my Swift books and bundles! >>

Loading a specific kind of Codable data

Paul Hudson    @twostraws   

In this app we’re going to load two different kinds of JSON into Swift structs: one for astronauts, and one for missions. Making this happen in a way that is easy to maintain and doesn’t clutter our code takes some thinking, but we’ll work towards it step by step.

First, drag in the two JSON files for this project. These are available in the GitHub repository for this book, under “project8-files” – look for astronauts.json and missions.json, then drag them into your project navigator. While we’re adding assets, you should also copy all the images into your asset catalog – these are in the “Images” subfolder. The images of astronauts and mission badges were all created by NASA, so under Title 17, Chapter 1, Section 105 of the US Code they are available for us to use under a public domain license.

If you look in astronauts.json, you’ll see each astronaut is defined by three fields: an ID (“grissom”, “white”, “chaffee”, etc), their name (“Virgil I. "Gus" Grissom”, etc), and a short description that has been copied from Wikipedia. If you intend to use the text in your own shipping projects, it’s important that you give credit to Wikipedia and its authors and make it clear that the work is licensed under CC-BY-SA available from here: https://creativecommons.org/licenses/by-sa/3.0.

Let’s convert that astronaut data into a Swift struct now – press Cmd+N to make a new file, choose Swift file, then name it Astronaut.swift. Give it this code:

struct Astronaut: Codable, Identifiable {
    let id: String
    let name: String
    let description: String
}

As you can see, I’ve made that conform to Codable so we can create instances of this struct straight from JSON, but also Identifiable so we can use arrays of astronauts inside ForEach and more – that id field will do just fine.

Next we want to convert astronauts.json into a dictionary of Astronaut instances, which means we need to use Bundle to find the path to the file, load that into an instance of Data, and pass it through a JSONDecoder. Previously we put this into a method on ContentView, but here I’d like to show you a better way: we’re going to write an extension on Bundle to do it all in one centralized place.

Create another new Swift file, this time called Bundle-Decodable.swift. This will mostly use code you’ve seen before, but there’s one small difference: previously we used String(contentsOf:) to load files into a string, but because Codable uses Data we are instead going to use Data(contentsOf:). It works in just the same way as String(contentsOf:): give it a file URL to load, and it either returns its contents or throws an error.

Add this to Bundle-Decodable.swift now:

extension Bundle {
    func decode(_ file: String) -> [String: Astronaut] {
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle.")
        }

        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }

        let decoder = JSONDecoder()

        guard let loaded = try? decoder.decode([String: Astronaut].self, from: data) else {
            fatalError("Failed to decode \(file) from bundle.")
        }

        return loaded
    }
}

We'll come back to that in a moment, but as you can see it makes liberal use of fatalError(): if the file can’t be found, loaded, or decoded the app will crash. As before, though, this will never actually happen unless you’ve made a mistake, for example if you forgot to copy the JSON file into your project.

Now, you might wonder why we used an extension here rather than a method, but the reason is about to become clear as we load that JSON into our content view. Add this property to the ContentView struct now:

let astronauts = Bundle.main.decode("astronauts.json")

Yes, that’s all it takes. Sure, all we’ve done is just moved code out of ContentView and into an extension, but there’s nothing wrong with that – anything we can do to help our views stay small and focused is a good thing.

If you want to double check that your JSON is loaded correctly, modify the default body property to this:

Text(String(astronauts.count))

That should display 32 rather than “Hello World”.

Before we're done, I want to go back to our little extension and look at it a little more closely. The code we have is perfectly fine for this app, but if you want to use it in the future I'd recommend adding some extra code to help you diagnose problems.

Replace the second part of the method with this:

let decoder = JSONDecoder()

do {
    return try decoder.decode([String: Astronaut].self, from: data)
} catch DecodingError.keyNotFound(let key, let context) {
    fatalError("Failed to decode \(file) from bundle due to missing key '\(key.stringValue)' – \(context.debugDescription)")
} catch DecodingError.typeMismatch(_, let context) {
    fatalError("Failed to decode \(file) from bundle due to type mismatch – \(context.debugDescription)")
} catch DecodingError.valueNotFound(let type, let context) {
    fatalError("Failed to decode \(file) from bundle due to missing \(type) value – \(context.debugDescription)")
} catch DecodingError.dataCorrupted(_) {
    fatalError("Failed to decode \(file) from bundle because it appears to be invalid JSON.")
} catch {
    fatalError("Failed to decode \(file) from bundle: \(error.localizedDescription)")
}

It's not a big change, but it means the method will now tell you what went wrong with decoding – it's great for times when your Swift code and JSON file don't quite match up!

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.8/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.