translation
Moonshot: Introduction


For more content, please follow the public account “Swift Garden”.


Like articles? How about 🔺💛➕ company 3? Follow this column, follow me 🚀🚀🚀

Moonshot, introduce

In this project, we will build an app that allows users to learn about NASA’s Apollo missions and astronauts. Not only will you become more proficient in Codable, but more importantly you’ll have access to scrolling views, navigation, and more interesting layouts.

Yes, you’ll still do some practice with List, Text, etc., but you’ll also start to solve some of the problems that are important in SwiftUI — how do you make an image fit in space correctly? How do I clean up code with computed properties? How do I combine smaller views into larger views to keep the project structure organized?

There’s a lot to do, so let’s get started: Create a new iOS App using the Single View App template and call it “Moonshot.” As usual, before starting a project, let’s take a closer look at some of the new skills you need to master…

translation
Resizing images to fit the screen using GeometryReader

Resize the image to fit the screen with the GeometryReader

When we create an Image view in SwiftUI, it automatically arranges itself according to the size of the content. Therefore, if the Image is 1000×500, then the Image view will also be 1000×500. This mechanism is sometimes exactly what you want, but most of the time you want the image to be displayed in a smaller size, and I’ll show you how to do that in this project, as well as how to use a new type called GeometryReader to help you fit the image to the width of the device’s screen.

First, add an image to your project. It doesn’t matter what the image is, as long as it’s wider than the screen. My image is called “Example”, obviously you can replace it with your own image name in the code.

Let’s draw the image to the screen:

struct ContentView: View {
    var body: some View {
        VStack {
            Image("Example")}}}Copy the code

Even in the preview, you can see that the image is too large for the available display space. Image has the same frame() modifier as any other view, so you can try zooming it out like this:

Image("Example")
    .frame(width: 300, height: 300)Copy the code

However, the above code won’t work — your image will still be displayed at full size. If you’re wondering why, take a closer look at the preview window: You’ll see that the image is full-size, but there’s a 300×300 blue box in the center of the image. That is, the frame of the image is set correctly, but the image content is still the original size.

Try changing the image code to the following:

Image("Example")
    .frame(width: 300, height: 300)
    .clipped()Copy the code

Now it seems more obvious: our image view is indeed 300×300, but that’s not what we wanted:

If you want the content of the image to also resize, we need to use resizable() modifier, like this:

Image("Example")
    .resizable()
    .frame(width: 300, height: 300)Copy the code

This is better, but only a little better. The image is the right size, but it’s probably squeezed. My image is not square, so when it is resized and tucked into a square, it looks distorted.

To solve this problem, we need to scale the image. To achieve this use aspectRatio modifier. It lets us provide a ratio to apply as a parameter, but if we ignore this parameter, SwiftUI will automatically use the original aspect ratio.

When it comes to the “how should I apply (scale)” part, SwiftUI calls it Content Mode and gives us two options:.fit means the image will load itself into the container, even if it leaves the view part blank. .fill means that the view will not be white, which means that part of the image will run out of the container.

Try it out for yourself and see the difference. The first is the app.fit mode:

Image("Example")
    .resizable()
    .aspectRatio(contentMode: .fit)
    .frame(width: 300, height: 300)Copy the code

Then apply.fill mode:

Image("Example")
    .resizable()
    .aspectRatio(contentMode: .fill)
    .frame(width: 300, height: 300)Copy the code

When we only want fixed size images, the above scheme can be well satisfied. But we often need images that automatically zoom in on one or both sides to fill the screen. That is, instead of hard-coding a width of 300, “let this image fill the screen width”.

SwiftUI has given us a special type called GeometryReader, which is very powerful. Yes, I know there are a lot of powerful things in SwiftUI, but let’s be honest: you’d be surprised what you can do with GeometryReader.

We’ll cover the GeometryReader in more detail in a later project, but for now we’ll use it to do one thing: make sure the image fills the full width of the container view.

Just like the other views we have used, GeometryReader is a view, except that it passes us an GeometryProxy object at build time. This object can be used to query environment information: How big is the container? Where is our view position? Are there safety zone insets? And so on.

We can use the geometry proxy to set the width of the image like this:

VStack {
    GeometryReader { geo in
        Image("Example")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: geo.size.width, height: 300)}}Copy the code

This way, no matter what device we are using, the image will fill the entire screen width.

As a final tip, we can remove the height setting from the image like this:

VStack {
    GeometryReader { geo in
        Image("Example")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: geo.size.width)
    }
}Copy the code

Because we’ve given SwiftUI enough information, it can automatically figure out the height: it knows the original width, it knows the target width, and it knows the Content Mode, so it understands how the target height should be set proportionally to the target width.

translation
How ScrollView lets us work with scrolling data

How ScrollView works

You already know that lists and Forms allow us to create scrollable tables of data, but when we want to scroll through arbitrary views — for example, some we’ve created manually — we need to turn to SwiftUI’s ScrollView.

ScrollView can scroll horizontally, vertically, or both, and you can control whether the system displays a scroll indicator — a small scroll bar that lets the user sense the size of the content. When we put a view into a scroll view, SwiftUI automatically calculates the size of the content to make it easier for users to scroll from one end to the other.

For example, we could create a scrolling list of 100 text views like this:

ScrollView(.vertical) {
    VStack(spacing: 10) {
        ForEach(0..<100) {
            Text("Item \ [$0)")
                .font(.title)
        }
    }
}Copy the code

Running in the emulator, you’ll see that you can freely drag and drop across the ScrollView to the bottom, and you’ll also see that the ScrollView handles the safe zone just like lists and forms — their contents will be below the horizontal line at the bottom of the screen, but because of the extra padding, The final view is fully visible.

You may also notice that you can only scroll through the middle area, which is a bit annoying — you usually want the whole area to scroll. To do this, we want to make the VStack take up more space, while keeping the default centered, like this:

ScrollView(.vertical) {
    VStack(spacing: 10) {
        ForEach(0..<100) {
            Text("Item \ [$0)")
                .font(.title)
        }
    }
    .frame(maxWidth: .infinity)
}Copy the code

Now you can click and drag and scroll from anywhere on the screen, making it a lot easier to use.

Seems simple enough, right? ScrollView is significantly easier to use than the original UIScrollView. However, you need to realize a key point: When we add views to a scroll view, they are created immediately.

To demonstrate this, we put a wrapper around a regular text view like this:

struct CustomText: View {
    var text: String

    var body: some View {
        Text(text)
    }

    init(_ text: String) {
        print("Creating a new CustomText")
        self.text = text
    }
}Copy the code

Then use it in ForEach:

ForEach(0..<100) {
    CustomText("Item \ [$0)")
        .font(.title)
}Copy the code

The result looks the same, but when you run the app, you’ll see 100 lines of “Creating a new CustomText” printed in Xcode’s log — SwiftUI doesn’t create views when you scroll down to see them, it just creates them immediately.

You can try the same experiment on the List, like this:

List {
    ForEach(0..<100) {
        CustomText("Item \ [$0)")
            .font(.title)
    }
}Copy the code

When you run this code, you’ll see that it’s lazy to load: you create the CustomText instance only when you really need it.


My official account here Swift and computer programming related articles, as well as excellent translation of foreign articles, welcome to pay attention to ~

                                                                Â