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!
100 Days of SwiftUI – Day 40