100 Days of SwiftUI Day 39

100 Days of SwiftUI – Day 39 – Moonshot

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.

GeometryReader is a view just like the others we’ve used, except when we create it we’ll be handed a GeometryProxy 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.

Hacking with Swift, Paul Hudson (@twostraws)
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 and Form 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’s ScrollView.

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 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.

Hacking with Swift, Paul Hudson (@twostraws)
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, Codable is capable of decoding everything with no further work from us.

Hacking with Swift, Paul Hudson (@twostraws)

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, and LazyVGrid 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!

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 44

100 Days of SwiftUI – Day 15 – Consolidation

100 Days of SwiftUI – Day 67

100 Days of SwiftUI – Day 94