100 Days of SwiftUI Day 41

100 Days of SwiftUI – Day 41

It’s day 41 of the 100 Days of SwiftUI! Yesterday, we started coding our Moonshot app. Today, we’re finishing up the app by adding more views and functionality to read from our JSON files. Let’s dive in!

Showing mission details

When the user selects a mission from our list, they should get an overview of all the relevant mission details, meaning the name, logo, description and the crew. To accomplish this, we create a new view called MissionView. You’ll find the code a bit further down below.

Merging codable structs

Below our mission description we want to show the pictures, names, and roles of each crew member, which means matching up data that came from two different JSON files.

what we need to do is make our MissionView accept the mission that got tapped, along with our full astronauts dictionary, then have it figure out which astronauts actually took part in the launch. Here’s the complete MissionView, showing how we accomplished this.

//
//  MissionView.swift
//  Moonshot
//

import SwiftUI

struct MissionView: View {
    struct CrewMember {
        let role: String
        let astronaut: Astronaut
    }
    
    let mission: Mission
    let crew: [CrewMember]
    
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                VStack {
                    Image(mission.image)
                        .resizable()
                        .scaledToFit()
                        .frame(maxWidth: geometry.size.width * 0.6)
                        .padding(.top)
                    
                    VStack(alignment: .leading) {
                        Rectangle() // Used as a custom divider to divide sectons in the app
                            .frame(height: 2)
                            .foregroundColor(.lightBackground)
                            .padding(.vertical)
                        
                        Text("Mission Highlights")
                            .font(.title.bold())
                            .padding(.bottom, 5)
                        
                        Text(mission.description)
                        
                        Rectangle() // Used as a custom divider to divide sectons in the app
                            .frame(height: 2)
                            .foregroundColor(.lightBackground)
                            .padding(.vertical)
                        
                        Text("Crew")
                            .font(.title.bold())
                            .padding(.bottom, 5)
                    }
                    .padding(.horizontal)
                    
                    ScrollView(.horizontal, showsIndicators: false) {
                        HStack {
                            ForEach(crew, id: \.role) { crewMember in
                                NavigationLink {
                                    AstronautView(astronaut: crewMember.astronaut)
                                } label: {
                                    HStack {
                                        Image(crewMember.astronaut.id)
                                            .resizable()
                                            .frame(width: 104, height: 72)
                                            .clipShape(Capsule())
                                            .overlay(
                                                Capsule()
                                                    .strokeBorder(.white, lineWidth: 1)
                                            )
                                        
                                        VStack(alignment: .leading) {
                                            Text(crewMember.astronaut.name)
                                                .foregroundColor(.white)
                                                .font(.headline)
                                            
                                            Text(crewMember.role)
                                                .foregroundColor(crewMember.role == "Commander" ? .red : .secondary )
                                                .font(crewMember.role == "Commander" ? .title3.bold() : nil)
                                        }
                                    }
                                    .padding(.horizontal)
                                }
                            }
                        }
                    }
                }
                .padding(.bottom)
            }
        }
        .navigationTitle(mission.displayName)
        .navigationBarTitleDisplayMode(.inline)
        .background(.darkBackground)
    }
    
    init(mission: Mission, astronauts: [String: Astronaut]) {
        self.mission = mission
        
        self.crew = mission.crew.map { member in
            if let astronaut = astronauts[member.name] { // if the astronaut's name is found, return the role.
                return CrewMember(role: member.role, astronaut: astronaut)
            } else {
                fatalError("Missing \(member.name)")
            }
        }
    }
    
}

struct MissionView_Previews: PreviewProvider {
    static let missions: [Mission] = Bundle.main.decode("missions.json") // Create an array of Missions and put the decoded JSON in the array
    static let astronauts: [String: Astronaut] = Bundle.main.decode("astronauts.json") // Create a dictionary of Strings with the Astronaut as key.
    
    static var previews: some View {
        MissionView(mission: missions[0], astronauts: astronauts) // Expects an isntance of mission, so we pass in the first item in our Missions array for preview purposes
            .preferredColorScheme(.dark) // The preview doesn't know that we force the app in dark mode, so we have to set it here so the preview is correct.
    }
}

Finishing the app with the AstronautView

Now that the MissionView is completed, we have to create one final view. When in the MissionView, selecting an astronaut should lead to a new view where the astronaut’s picture, name and description are displayed.

import SwiftUI

struct AstronautView: View {
    let astronaut: Astronaut
    
    var body: some View {
        ScrollView {
            VStack {
                Image(astronaut.id)
                    .resizable()
                    .scaledToFit()
                
                Text(astronaut.description)
                    .padding()
            }
        }
        .background(.darkBackground)
        .navigationTitle(astronaut.name)
        .navigationBarTitleDisplayMode(.inline)
    }
}

struct AstronautView_Previews: PreviewProvider {
    static let astronauts: [String: Astronaut] = Bundle.main.decode("astronauts.json")

    static var previews: some View {
        AstronautView(astronaut: astronauts["armstrong"]!)
            .preferredColorScheme(.dark)
    }
}

Screenshots of the finished SwiftUI Moonshot app

Wrap up

And that’s it for the Moonshot app! We’ve made great strides and delivered an app that works smoothly while looking good. It’s time to recharge as tomorrow, we’ll be reviewing the project as well as completing a few challenges. Until then!

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 88

100 Days of SwiftUI – Day 5 – Conditional Statements

100 Days of SwiftUI – Day 42

100 Days of SwiftUI – Day 7 – Functions