100 Days of SwiftUI Day 40

100 Days of SwiftUI – Day 40

We’ve arrived at day 40 of the 100 Days of SwiftUI! Yesterday, we had our introduction to the Moonshot project and learned about GeometryReader, ScrollView, NavigationLink and more in the progress. Today, we’re implementing the first part of our Moonshot app. Let’s take a look!

Working with Codable in SwiftUI

The theme of this project is are the Apollo missions and the astronauts who participated in them. This project features two JSON files that need to be decoded. One contains data about the astronauts and one about the missions.

To decode them, we’ve made two new Swift files, both containing a struct that’s designed to work with the data in the JSON files.

//
//  Astronaut.swift
//  Moonshot
//

import Foundation

struct Astronaut: Codable, Identifiable { // Struct formatted to store the data from the astronauts.json file
    let id: String
    let name: String
    let description: String
}
//
//  Mission.swift
//  Moonshot
//

import Foundation

struct Mission: Codable, Identifiable { // Struct formatted to store the data from the missions.json file
    struct CrewRole: Codable {
        let name: String
        let role: String
    }
    
    let id: Int
    let launchDate: Date? // Since there is a mission without a launchdate, this has to be an optional
    let crew: [CrewRole] // an array of CrewRole
    let description: String
    
    var displayName: String { // Used to call the name for the Apollo mission in the ContentView
        "Apollo \(id)"
    }
    
    var image: String { // Used to set the Image for the Apollo mission in the ContentView
        "apollo\(id)"
    }
    
    var formattedLaunchDate: String {
        launchDate?.formatted(date: .abbreviated, time: .omitted) ?? "N/A" // Formats the launchDate to what is appropriate for the users locale and nil coalesces to "N/A" if no launchDate is available.
    }
    
}

Using generics with the JSONDecoder

The way we used the JSONDecoder up until was with a specific type of data, for example, a String. However, this time, we will be using a generic type. A generic type allows us to write code that works with a wide variety of types. In this case, that means we can set up a JSONDecoder that accepts any type of Codable data.

//
//  Bundle-Decodable.swift
//  Moonshot
//

import Foundation

extension Bundle {
    func decode<T: Codable>(_ file: String) -> T { // placeholder T allows us to return any decodable type
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("We failed to locate \(file) in bundle.")
        }
        
        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }
        
        let decoder = JSONDecoder()
        let formatter = DateFormatter()
        formatter.dateFormat = "y-MM-dd" // Tells Swift how to format our dates
        decoder.dateDecodingStrategy = .formatted(formatter)
        
        guard let loaded = try? decoder.decode(T.self, from: data) else {
            fatalError("Failed t o decode \(file) from bundle.")
        }
        
        return loaded
    }
}

Designing the layout

The layout is already quite extensive, but not difficult in terms of the code we’ve used. We’ve pretty much only used a handful of new pieces of code.

//
//  ContentView.swift
//  Moonshot
//

import SwiftUI

struct ContentView: View {
    
    let astronauts: [String: Astronaut] = Bundle.main.decode("astronauts.json") // decodes the astronauts.json file and puts it in an array Strings containing Astronauts
    let missions: [Mission] = Bundle.main.decode("missions.json") // deocdes the missions.json file and puts it in an array of Missions
    
    let columns = [
        GridItem(.adaptive(minimum: 150)) // creates an adaptive, column based grid
    ]
    
    var body: some View {
        NavigationView {
            ScrollView {
                LazyVGrid(columns: columns) { // sets our specified columns grid as columns
                    ForEach(missions) { mission in
                        NavigationLink {
                            Text("Detail view")
                        } label: {
                            VStack {
                                Image(mission.image)
                                    .resizable()
                                    .scaledToFit()
                                    .frame(width: 100, height: 100)
                                    .padding()
                                
                                VStack {
                                    Text(mission.displayName) // displays the mission name
                                        .font(.headline)
                                        .foregroundColor(.white)
                                    
                                    Text(mission.formattedLaunchDate) // displays the mission date
                                        .font(.caption)
                                        .foregroundColor(.white.opacity(0.5))
                                }
                                .padding(.vertical)
                                .frame(maxWidth: .infinity)
                                .background(.lightBackground) // sets the background color as specified in the Color-Theme.swift file
                            }
                            .clipShape(RoundedRectangle(cornerRadius: 10))
                            .overlay(
                                RoundedRectangle(cornerRadius: 10)
                                    .stroke(.lightBackground)
                            )
                        }
                    }
                }
                .padding([.horizontal, .bottom])
            }
            .navigationTitle("Moonshot")
            .background(.darkBackground) // sets the background color as specified in the Color-Theme.swift file
            .preferredColorScheme(.dark) // sets the view to display as if the device is in Dark mode, even if it is not
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Wrap up

And that’s it for day 40! Tomorrow, we’ll be finishing our Moonshot app. Time to recharge and stay tuned for the finished app!

Darryl

Hi! My name is Darryl and this is my personal blog where I write about my journey as I learn programming! You'll also find articles about other things that interest me including games, tech and anime.

Post navigation

Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

100 Days of SwiftUI – Day 65

100 Days of SwiftUI – Day 57

100 Days of SwiftUI – Day 49

100 Days of SwiftUI – Day 92