UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Day 42 - Moonshot Wrap Up - Challenge

Forums > 100 Days of SwiftUI

I was able to do the first task:

  1. Add the launch date to MissionView, below the mission badge.
Text("Launch Date: \(self.mission.formattedLaunchDate)")
                        .font(.headline)

But a bit lost on the second bit:

  1. Modify AstronautView to show all the missions this astronaut flew on.

Any ideas? I'm guessing it's something to do with the way Paul compared the 2 json files to show crewMember.role

Regards, Tim.

3      

In the initializer of AstronautView, I added the following:

init(astronaut: Astronaut) {
        self.astronaut = astronaut
        let missions: [Mission] = Bundle.main.decode("missions.json")

        var matches = [String]()

        for mission in missions {
            for _ in mission.crew {
                if let match = mission.crew.first(where: {$0.name == astronaut.id}) {
                    matches.append("Apollo \(mission.id) - \(match.role)")
                    break
                }
            }
        }
        self.missionsFlown = matches
    }

"missionsFlown" is obviously a new [String] variable in AstronautView, each element being a mission that they participated in.

Edit: FYI, I seriously struggled with this for awhile, especially with getting the first(where: ) line to work... If there is a better way to accomplish this, I'd love to know.

3      

Hello Tim,
This is probably my favourite challenge and to I used a similar approach to @ZoydWheeler.

    init(astronaut: Astronaut, missionsFlown: [Mission]) {
        self.astronaut = astronaut

        var matches = [Mission]()

        for mission in missions {
            if mission.crew.contains(where: { $0.name == astronaut.id }) {
                matches.append(mission)
            }
        }
        self.missionsFlown = matches
    }

As you rightly said, it has everything to do with "linking" the astronaut with the Mission array to create a matches array of Mission. Good luck with it.

William

8      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Hi @ZoydWheeler,

Thank you so much, I just got your solution working. I too struggled with the .first(where: ) line, now I can see I was so close! At one stage I could print a single mission but not multiple missions. I hadn't thought of a for in loop with a break.

I'm going to try the other solution that William posted now, I didn't even know about .contains(where: ).

Also I liked the way you added the mission.json directly into the init, I didn't need to change the preview code.

Thanks again, Tim.

3      

Hi William,

Thank you for your help. I managed to get a modified version of your code working:

let astronaut: Astronaut
let missions: [Mission] = Bundle.main.decode("missions.json")
let missionsFlown: [String]

init(astronaut: Astronaut) {
    self.astronaut = astronaut

    var matches = [String]()

    for mission in missions {
        if mission.crew.contains(where: { $0.name == astronaut.id }) {
            matches.append("\(mission.displayName)")
        }
    }
    self.missionsFlown = matches
}

And displayed using:

ForEach(self.missionsFlown, id: \.self) { mission in
    VStack {
        Text(mission.description)
            .font(.headline)
    }
}

I'm not sure how to add the crew role like @ZoydWheeler did. I'm guessing something like:

mission.crew[index].role

where index is the astronaut ... but really it's beyond the requirements of the challenge.

I'll try the third part of the challenge tomorrow.

Thanks again, Tim.

3      

Hello Tim,

There is always another way to code a solution, that is one of the great things about coding. And there will always be a way to get the data you want out of whatever structure. It helps sometimes to create a playground concentrating on a specific part of the code where it maybe difficult to visualize how to acheive a particular result.

Good luck with it.

William

3      

Has anyone got an idea for solving the last part of the wrap up to toggle between date and crew. The actual toggle bit seems easy. I can toggle between date and say a VStack of fixed text. For creating the the crew list I was wondering if I overrode the init of content view and ran over all the missions etc. but hitting a dead end.

4      

I just used a first(where:) on the astronauts array in a separate function and then called that function whenever the toggle is set to show crewMembers

func fullName(_ name: String) -> String {
        if let match = astronauts.first(where: {$0.id == name}) {
            return match.name
        } else {
            fatalError("Missing name")
        }
    }
VStack(alignment: .leading) {
                        Text(mission.displayName)
                            .font(.headline)
                        if(self.showingLaunchDate) {
                            Text(mission.formattedLaunchDate)
                        } else {
                            ForEach(mission.crewNames, id: \.self) {
                                Text(self.fullName($0))
                            }
                        }
                    }

The Mission variable 'crewNames' is [String] that I ended up creating for some reason...it's been awhile since I've looked at this code. It just pulls the crew names out of the [CrewRole] into their own array.

It's possible you can find a more elegant way to iterate over the crewmember names.

Good luck.

3      

Hi, thanks for that. It make sense to me but I get the error against NavigationView of

The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions

Full code is:

struct ContentView: View {

    let astronauts: [Astronaut] = Bundle.main.decode("astronauts.json")
    let missions: [Mission] = Bundle.main.decode("missions.json")

    @State private var showingLaunchDate = false

    var body: some View {

        NavigationView {
            List(missions) { mission in
                NavigationLink(destination: MissionView(mission: mission, astronauts: self.astronauts)) { //the mission and all the astronauts 
                    Image(mission.image)
                        .resizable()
                        .scaledToFit()
                        .frame(width: 44, height: 44)

                    VStack(alignment: .leading) {

                        Text(mission.displayName)
                            .font(.headline)

                        if(self.showingLaunchDate) {
                            Text(mission.formattedLaunchDate)
                        } else {
                            ForEach(mission.crewNames, id: \.self) {
                                Text(self.fullName($0))
                            }
                        }
                    }
                }
            }
            .navigationBarTitle("Moonshot")
            .navigationBarItems(trailing:
            Button(self.showCrew ? "Date" : "Crew" ) {
                self.showCrew.toggle()

                }
            )
        }
    }

    func fullName(_ name: String) -> String {
        if let match = astronauts.first(where: {$0.id == name}) {
            return match.name
        } else {
            fatalError("Missing name")
        }
    }
}

4      

This was my solution to the second problem, which is similar to what's already been proposed with a few slight modifications that uses code that is more common.

    init(astronaut: Astronaut) {
        self.astronaut = astronaut

        let missions: [Mission] = Bundle.main.decode("missions.json")

        var matches = [Int]()

        for mission in missions {
            for crew in mission.crew {
                if (astronaut.id == crew.name) {
                    matches.append(mission.id)
                }
            }
        }

        self.missions = matches

    }

This is my solution to the third problem, and I'm quite proud of it.

    var formattedNames: String {
        var crewNames = [String]()
        for crew in crew {
            crewNames.append(crew.name.capitalizingFirstLetter())
        }

        return crewNames.joined(separator: ", ")
    }

I put this code in my Missions.swift file. It is a variable like formattedLaunchDate, but it creates a string that will format all of the crew into one string. I looked up different String operators before finding joined, which fit my needs perfectly! Hopefully you tried and just need inspiration, because that's always the best way to learn. Good luck!

6      

Thanks @nickel-dime for your solver 3 task! It's very simple )


 struct: Mission{
 .....
 ....
 ....

 var formattedNames: String {
        var crewNames = [String]()
        for crew in crew {
            crewNames.append(crew.name.capitalizingFirstLetter())
        }

        return crewNames.joined(separator: ", ")
    }

4      

Tim, Your code works. Thank you.

3      

@financce doesn't that solution just display the name from mission strruct and not the full name from astronaut?

3      

This is my solution to exercise 3 - took me a while

import SwiftUI

struct CrewList: View {

    var astronauts = [CrewMember]()

    var body: some View {
        VStack(alignment: .leading) {
            ForEach(self.astronauts, id: \.role) { crewMember in
                Text(crewMember.astronaut.name)
            }
        }
    }

    init(mission: Mission, astronauts: [Astronaut]) {

        var matches = [CrewMember]()

        for member in mission.crew {
            if let match = astronauts.first(where: { $0.id == member.name }) {
                matches.append(CrewMember(role: member.role, astronaut: match))
            } else {
                fatalError("Missing \(member)")
            }
        }

        self.astronauts = matches
    }
}

struct ContentView: View {

    let astronauts: [Astronaut] = Bundle.main.decode("astronauts.json")
    let missions: [Mission] = Bundle.main.decode("missions.json")
    @State private var showingCrew = false

    @State var crewNames = [String]()

    var body: some View {
        NavigationView {
            List(missions) { mission in
                NavigationLink(destination: MissionView(mission: mission, astronauts: self.astronauts, missions: self.missions)) {

                    Image(mission.image)
                        .resizable()
                        .scaledToFit()
                        .frame(width: 44, height: 44)

                    VStack(alignment: .leading) {
                        Text(mission.displayName)
                            .font(.headline)
                        if self.showingCrew {
                            CrewList(mission: mission, astronauts: self.astronauts)

                        } else {
                            Text(mission.formattedLaunchDate)
                        }
                    }

                }
            }
            .navigationBarTitle("Moonshot")
            .navigationBarItems(leading:
                Button(action: {
                    self.showingCrew.toggle()
                }) {
                    Image(systemName: self.showingCrew ? "calendar" : "person.3")
                }
            )
        }
    }
}

3      

I was trying to do this with builtin array functions which are more efficient/faster than building a new array one element at a time. Given how many times the json files are decoded in this project, I hope that Swift makes the redundant decoding more efficient behind the scenes. Perhaps the json files should be decoded in ContentView and made Environment variables? I wouldn't expect multiple decodes of the json file to scale up very well.

Here's what I added to AstronautView to get all missions for an astronaut:

    init (astronaut: Astronaut, missions: [Mission]) {
        self.astronaut = astronaut

        let missions: [Mission] = Bundle.main.decode("missions.json")

        let filteredArray = missions.filter({mission in
            return mission.crew.contains(where: {$0.name == astronaut.id })
        }).filter({ $0.crew.count > 0 })

        self.missions = filteredArray
    }

Here's what I added to the Mission Struct to get a crew list

 var crewList: String {
        return crew.map { $0.name.capitalizingFirstLetter() }.joined(separator: ", ")
    }

And then at the bottom of Mission.swift I added this code which came from one of Paul's examples:

 extension String {
    func capitalizingFirstLetter() -> String {
        return prefix(1).capitalized + dropFirst()
    }

    mutating func capitalizeFirstLetter() {
        self = self.capitalizingFirstLetter()
    }
}

3      

solution for second problem:

import SwiftUI

struct AstronautView: View {
    let astronaut:Astronaut
    let missions: [Mission] = Bundle.main.decode("missions.json")

    var astronautMissions:[String]{
        var temp = [String]()
        for mis in missions{
            for cr in mis.crew {
                if astronaut.id == cr.name{
                    temp.append(mis.displayName)
                }
            }
        }
        return temp
    }

    var body: some View {

        GeometryReader{ geometry in
            ScrollView(.vertical){
                VStack{
                    Image(astronaut.id)
                        .resizable()
                        .scaledToFit()
                        .frame(width: geometry.size.width)
                    Text(astronaut.description)
                        .padding()

                    Text("Missions participated")
                        .font(.headline)
                    ForEach(astronautMissions,id:\.self){ mis in
                            Text(mis)
                                .font(.title)
                                .fontWeight(.heavy)
                    }
                }
            }

        }
        .navigationBarTitle(Text(astronaut.name),displayMode: .inline)
    }
}

solution for third problem:

struct ContentView: View {
    let astronauts:[Astronaut] = Bundle.main.decode("astronauts.json")
    let missions:[Mission] = Bundle.main.decode("missions.json")
    @State private var showingDates = false

    var body: some View {

        NavigationView{
            List(missions){mission in
                NavigationLink(
                    destination: MissionView(mission: mission, astronauts: astronauts),
                    label: {
                        Image(mission.displayImage)
                            .resizable()
                            .scaledToFit()
                            .frame(width:40, height: 40)
                        VStack(alignment: .leading){
                            Text(mission.displayName)
                                .font(.headline)

                            Text(showingDates ? mission.formattedLaunchDate : crewNames(actualMission: mission))
                        }

                    })

            }
            .navigationBarItems(trailing: Button(action: {
                showingDates.toggle()
            }, label: {
                Text(showingDates ? "Crew" : "Dates")
            }))

            .navigationBarTitle("MoonShot")

        }

    }

    func crewNames(actualMission: Mission) -> String{
        var temp = [String]()
        for crewName in actualMission.crew{
            temp.append(crewName.name.capitalized)
        }

        return temp.joined(separator: ", ")
    }
}

3      

Anyone else finding SwiftUI's layout prioritization way too trigger happy about truncating, shrinking, and/or hiding your views?

In the second challenge (adding flown missions to AstronautView), I can't seem to convince SwiftUI to let me have all three view requirements at once:

  1. the astronaut image at full width
  2. description text that's untruncated
  3. a list of the flown missions appearing while 1 & 2 are visible

After solving the data part of the challenge, my first pass at laying out the view was:

GeometryReader { geo in
  ScrollView(.vertical) {
    VStack {
      Image(self.astronaut.id)
        .resizable()
        .scaledToFit()
        .frame(width: geo.size.width)

      Text(self.astronaut.description)
        .padding()

      List(missionsFlown) { mission in
        HStack {
          Image(mission.imageName)
            .resizable()
            .scaledToFit()
            .frame(width: 44, height: 44)

          VStack(alignment: .leading) {
            Text(mission.displayName)
              .font(.headline)
            Text(mission.formattedLaunchDate)
          }
        }
      }
    }
  }
}
.navigationBarTitle(Text(self.astronaut.name), displayMode: .inline)

Everything appeared, but the text was always truncated and the image was ever slightly shrunk from full size.

So I came across a fun little modifier meant to prevent SwiftUI from truncating the text:

Text(self.astronaut.description)
  .padding
  .fixedSize(horizontal: false, vertical: true)

With that, both the text and list appeared unharmed by SwiftUI, but bafflingly the image shrunk to 50% its width.

So I tried adding .layoutPriority(1) to the image and, finally, its width extends edge to edge AND the text wasn't truncated.

But the List of missions completely disappeared!

In the end, I tried every combination of .layoutPriority() modifiers on the three views, and no matter what I try, I still can't get all three view requirements met at the same time.

Is it possible in SwiftUI? Can anyone else wrangle SwiftUI into presenting these three views unmolested?

3      

I am finding it works perfectly when you first run it. Then if you add a new item and dismiss the sheet, the label toggles between Edit and Done and the list bounces sideways. If you then swipe an item sideways to reveal the delete button and then swipe it back, the Edit button starts functioning normally again, putting the list into edit mode and showing the red circle - buttons again!

3      

I see your .sheet(isPresented: ) is attached to the list - which happens to be false here because it's initialized to false. Since you want your sheet for AddView to be presented when the button is pressed, I think it belongs in the action of the button.

3      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.