It’s day 39 of the 100 Days of SwiftUI! Yesterday, we wrapped up our iExpense app and completed a few challenges. Today, we’re diving headfirst into project 8, which will be an app called Moonshot. Let’s dive in!
SwiftUI app: Moonshot introduction
In this project we’re going to build an app that lets users learn about the missions and astronauts that formed NASA’s Apollo space program. We’ll get more experience with Codable
, but more importantly we’ll also work with scroll views, navigation, and much more interesting layouts.
Resizing images to fit the screen using GeometryReader
We’ve seen how we can add images to our apps in past projects. However, we have not yet seen how to properly align and resize images. That’s where the GeometryReader
comes in. It allows us to specify what width of the users screen an image should cover and also how to resize it properly, without warping the aspect ratio of the image.
Hacking with Swift, Paul Hudson (@twostraws)
GeometryReader
is a view just like the others we’ve used, except when we create it we’ll be handed aGeometryProxy
object to use. This lets us query the environment: how big is the container? What position is our view? Are there any safe area insets? And so on.
struct ContentView: View {
var body: some View {
VStack {
GeometryReader { geo in
Image("black-sands")
.resizable()
.scaledToFit()
.frame(width: geo.size.width * 0.8) // the multiplication stands for the percentage, in this case 80 percent. 1.0 would be 100 percent.
.frame(width: geo.size.width, height: geo.size.height) // this frame will perfectly align the image to the center
}
}
}
}
Using ScrollView to work with scrolling data
You’ve seen how
List
andForm
let us create scrolling tables of data, but for times when we want to scroll arbitrary data – i.e., just some views we’ve created by hand – we need to turn to SwiftUI’sScrollView
.Scroll views can scroll horizontally, vertically, or in both directions, and you can also control whether the system should show scroll indicators next to them – those are the little scroll bars that appear to give users a sense of how big the content is. When we place views inside scroll views, they automatically figure out the size of that content so users can scroll from one edge to the other
Hacking with Swift, Paul Hudson (@twostraws)
struct CustomText: View { // used to print "Creating a new CustomText" for each row in our ScrollView, to show how a LazyVStack/LazyHStack works.
let text: String
var body: some View {
Text(text)
}
init(_ text: String) {
print("Creating a new CustomText")
self.text = text
}
}
struct ContentView: View {
var body: some View {
ScrollView(.vertical) { // we can specicy horizon or vertical for the direction we scroll
LazyVStack(spacing: 10) { // Lazy loads views as they're scrolled into the screen. It always takes all full available space, to ensure all incoming content fits.
ForEach(0..<100) {
CustomText("Item \($0)")
.font(.title)
}
}
.frame(maxWidth: .infinity)
}
}
}
Pushing new views onto the stack using NavigationLink
SwiftUI’s
Hacking with Swift, Paul Hudson (@twostraws)NavigationView
shows a navigation bar at the top of our views, but also does something else: it lets us push views onto a view stack. In fact, this is really the most fundamental form of iOS navigation – you can see it in Settings when you tap Wi-Fi or General, or in Messages whenever you tap someone’s name.
struct ContentView: View {
var body: some View {
NavigationView {
List(0..<100) { row in
NavigationLink { // used to show a new view with details that relates to what the user selected. A sheet, for example, is used to show unrelated content, like settings or a compose window.
Text("Detail \(row)")
} label: {
Text("Row \(row)")
}
.navigationTitle("SwiftUI")
}
}
}
}
Working with hierarchical Codable data
The
Codable
protocol makes it trivial to decode flat data: if you’re decoding a single instance of a type, or an array or dictionary of those instances, then things just work. However, in this project we’re going to be decoding slightly more complex JSON: there will be an array inside another array, using different data types.If you want to decode this kind of hierarchical data, the key is to create separate types for each level you have. As long as the data matches the hierarchy you’ve asked for,
Hacking with Swift, Paul Hudson (@twostraws)Codable
is capable of decoding everything with no further work from us.
If you want to decode this kind of hierarchical data, the key is to create separate types for each level you have. As long as the data matches the hierarchy you’ve asked for, Codable
is capable of decoding everything with no further work from us.
struct User: Codable { // Used to show how the JSON in the ContentView translates to a Swift struct. The structs must match the JSON string.
let name: String
let address: Address
}
struct Address: Codable { // Used to show how the JSON in the ContentView translates to a Swift struct. The structs must match the JSON string.
let street: String
let city: String
}
struct ContentView: View {
var body: some View {
Button("Decode JSON") { // multi line String with example JSON, containing a name and a dictionary for Address
let input = """
{
"name": "Taylor Swift",
"address": {
"street": "555, Taylor Swift Avenue",
"city": "Nashville"
}
}
"""
let data = Data(input.utf8)
if let user = try? JSONDecoder().decode(User.self, from: data) {
print(user.address.street)
}
}
}
}
How to lay out views in a scrolling grid
SwiftUI’s
List
view is a great way to show scrolling rows of data, but sometimes you also want columns of data – a grid of information, that is able to adapt to show more data on larger screens.In SwiftUI this is accomplished with two views:
LazyHGrid
for showing horizontal data, andLazyVGrid
for showing vertical data. Just like with lazy stacks, the “lazy” part of the name is there because SwiftUI will automatically delay loading the views it contains until the moment they are needed, meaning that we can display more data without chewing through a lot of system resources.Creating a grid is done in two steps. First, we need to define the rows or columns we want – we only define one of the two, depending on which kind of grid we want.
Hacking with Swift, Paul Hudson (@twostraws)
struct ContentView: View {
let layout = [ // create layout for the LazyVGrid or LazyHGrid
GridItem(.adaptive(minimum: 80, maximum: 120)) // Sets an adaptive grid with minimum and maximum size specified. This is great as it scales depending on the device and the available screenspace on that device.
// GridItem(.fixed(80)),
// GridItem(.fixed(80)),
// GridItem(.fixed(80))
]
var body: some View {
ScrollView(.horizontal) { // you can specify horizontal or vertical and change Lazy Grid and colums/rows depending on your choice
LazyHGrid(rows: layout) { // LazyVGrid and LazyHGrid lazy load views as they're scrolled in, so as to not use unnecessary system resources.
ForEach(0..<1000) {
Text("Item \($0)")
}
}
}
}
}
Wrap up
And that’s it for day 39! Tomorrow we’ll be back with the implementation of the first part of our Moonshot app. Stay tuned for that!
100 Days of SwiftUI – Day 39 – Moonshot