It’s day 42 of the 100 Days of SwiftUI! Slowly but surely, we’re nearing the halfway point. Yesterday, we finished our Moonshot app and it turned out quite well. Today, we’re completing a few challenges to solidify what we’ve learned. Let’s take a look!
Moonshot SwiftUI challenge #1
The first challenge is to add the launch date to MissionView
, below the mission badge. This was quite simple, because we already have a formatted launch date.
Text(mission.formattedLaunchDate)
.foregroundColor(.secondary)
.padding(.top)Text(mission.formattedLaunchDate)
.foregroundColor(.secondary)
.padding(.top)
Moonshot SwiftUI challenge #2
The second challenge is to extract one or two views into their own new SwiftUI view. I took the easy way out due to time constraints and set up a new view for the divider.
//
// ViewDivider.swift
// Moonshot
//
import SwiftUI
struct ViewDivider: View {
var body: some View {
Rectangle() // Used as a custom divider to divide sectons in the app
.frame(height: 2)
.foregroundColor(.lightBackground)
.padding(.vertical)
}
}
struct ViewDivider_Previews: PreviewProvider {
static var previews: some View {
ViewDivider()
}
}
// some code...
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
Image(mission.image)
.resizable()
.scaledToFit()
.frame(maxWidth: geometry.size.width * 0.6)
.padding(.top)
Text(mission.formattedLaunchDate)
.foregroundColor(.secondary)
.padding(.top)
VStack(alignment: .leading) {
// Rectangle() // Used as a custom divider to divide sectons in the app
// .frame(height: 2)
// .foregroundColor(.lightBackground)
// .padding(.vertical)
ViewDivider()
// some more code...
Moonshot SwiftUI challenge #3
The third challenge is to add a toolbar item to ContentView
that toggles between showing missions as a grid and as a list.
To start, we move the entire ScrollView
from the ContentView
to a new view
called GridLayout
. For the list, we created a new view
called ListLayout
.
//
// GridLayout.swift
// Moonshot
//
import SwiftUI
struct GridLayout: View {
let missions: [Mission] // Creates an instance of the Mission array
let astronauts: [String: Astronaut] // Creates an instance of the String/Astronaut dictionary
let columns = [
GridItem(.adaptive(minimum: 150)) // creates an adaptive, column based grid
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns) { // sets our specified columns grid as columns
ForEach(missions) { mission in
NavigationLink {
MissionView(mission: mission, astronauts: astronauts)
} 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])
}
}
}
struct GridLayout_Previews: PreviewProvider {
static var previews: some View {
GridLayout(missions: Bundle.main.decode("missions.json"), astronauts: Bundle.main.decode("astronauts.json")) // decode the JSON files. We don't have to declare the type because SwiftUI already knows what types they are
.preferredColorScheme(.dark)
}
}
//
// ListLayout.swift
// Moonshot
//
import SwiftUI
struct ListLayout: View {
let missions: [Mission]
let astronauts: [String: Astronaut]
var body: some View {
List(missions) { mission in
NavigationLink {
MissionView(mission: mission, astronauts: astronauts)
} label: {
HStack {
Image(mission.image)
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.padding()
VStack(alignment: .leading) {
Text(mission.displayName)
.font(.headline)
Text(mission.formattedLaunchDate)
}
}
}
.listRowBackground(Color.darkBackground) // Sets our custom dark background color as background for the list
}
.listStyle(.plain) // Makes the list take the full width of the screen
}
}
struct ListLayout_Previews: PreviewProvider {
static var previews: some View {
ListLayout(missions: Bundle.main.decode("missions.json"), astronauts: Bundle.main.decode("astronauts.json"))
.preferredColorScheme(.dark)
}
}
Then, we update our ContentView
to implement the new views.
//
// 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") // decodes the missions.json file and puts it in an array of Missions
@AppStorage("showingGrid") private var showingGrid = true // Saves the users preference and remembers which type of grid to load
var body: some View {
NavigationView {
Group { // use a group to wrap your conditions in a view, otherwise they won't work, which you can then add modifiers to.
if showingGrid {
GridLayout(missions: missions, astronauts: astronauts) // if true, show the GridLayout view
} else {
ListLayout(missions: missions, astronauts: astronauts) // if false, show the ListLayout view
}
ListLayout(missions: missions, astronauts: astronauts)
}
.toolbar {
Button {
showingGrid.toggle() // Toggles showingGrid to false or true
} label: {
if showingGrid {
Label("Show as table", systemImage: "list.dash")
} else {
Label("Show as grid", systemImage: "square.grid.2x2")
}
}
}
.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 42 and the Moonshot project! We’ll be diving into project #9 tomorrow, to high time to rest and recharge. Until then!
Thank you so much for the solution.
Looks like with Xcode 14, you just need to use “GridLayout()” instead of “GridLayout(missions: missions, astronauts: astronauts)”.
I wish you could have a solution to split the horizontal scroll view in MissionView like Paul asked.