Due to API changes, part of this article has been invalid, please check the latest complete Chinese tutorial and codeGithub.com/WillieWangW…

Wechat Technology Group

SwiftUI represents the direction of building App in the future. Welcome to join us to exchange technology and solve problems.

Add group needs to apply now, you can add my wechat first, note “SwiftUI”, I will pull you into the group.

Build lists and navigation

After completing the basic landmark details view, we need to provide the user with a way to view the complete list of landmarks and see the details of each landmark.

In this article, we will create a view that displays any landmark information and dynamically generate a scrolling list that the user can click to see a detailed view of the landmark. In addition, we’ll use Xcode’s Canvas to display different device sizes to fine-tune the UI.

Download the project file and follow these steps.

  • Estimated completion time: 35 minutes
  • Initial project file: Download

1. Know the sample data

In the last tutorial, we hard-coded the data into all our custom views. In this article, we’ll learn how to pass data to a custom view and display it.

Download the initial project and familiarize yourself with the sample data.

1.1 In the Project Navigator, choose Models > Landmarant.Swift.

Landmark.swift declares a Landmark structure that stores all the Landmark data the app needs to display and imports a set of Landmark data from landmarkdata.json.

Landmark.swift

import SwiftUI
import CoreLocation

struct Landmark: Hashable, Codable {
    var id: Int
    var name: String
    fileprivate var imageName: String
    fileprivate var coordinates: Coordinates
    var state: String
    var park: String
    var category: Category

    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(
            latitude: coordinates.latitude,
            longitude: coordinates.longitude)
    }

    func image(forSize size: Int) -> Image {
        ImageStore.shared.image(name: imageName, size: size)
    }

    enum Category: String, CaseIterable, Codable, Hashable {
        case featured = "Featured"
        case lakes = "Lakes"
        case rivers = "Rivers"
    }
}

struct Coordinates: Hashable, Codable {
    var latitude: Double
    var longitude: Double
}
Copy the code

1.2 In Project Navigator, choose Resources > LandmarkData.json.

We will use this sample data throughout the rest of the tutorial and beyond.

landmarkData.json

[{"name": "Turtle Rock"."category": "Featured"."city": "Twentynine Palms"."state": "California"."id": 1001,
        "park": "Joshua Tree National Park"."coordinates": {
            "longitude": 116.166868,"latitude": 34.011286},"imageName": "turtlerock"
    },
    {
        "name": "Silver Salmon Creek"."category": "Lakes"."city": "Port Alsworth"."state": "Alaska"."id": 1002,
        "park": "Lake Clark National Park and Preserve"."coordinates": {
            "longitude": 152.665167,"latitude": 59.980167},"imageName": "silversalmoncreek"},... ]Copy the code

1.3 Note that the ContentView type from the previous tutorial is now renamed LandmarkDetail.

We’ll also create multiple View types.

LandmarkDetail.swift

import SwiftUI

struct LandmarkDetail: View {
    var body: some View {
        VStack {
            MapView()
                .frame(height: 300)

            CircleImage()
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                Text("Turtle Rock")
                    .font(.title)

                HStack(alignment: .top) {
                    Text("Joshua Tree National Park")
                        .font(.subheadline)
                    Spacer()
                    Text("California")
                        .font(.subheadline)
                }
            }
            .padding()

            Spacer()
        }
    }
}

struct LandmarkDetail_Preview: PreviewProvider {
    static var previews: some View {
        LandmarkDetail()
    }
}
Copy the code

2. Create Row View

The first view we built in this article was a row to display the details of each landmark. Row stores landmark data in the Landmark property so that a row can display any landmark. Later we will combine the rows into a list of landmarks.

2.1 Create a new SwiftUI View named landmarkrow.swift.

2.2 If the preview is not displayed, choose Editor > Editor and Canvas and click Get Started.

2.3 Add a storage attribute landmark to LandmarkRow.

The preview will stop working when you add the Landmark attribute because the LandmarkRow type requires a Landmark instance when initialized.

LandmarkRow.swift

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        Text("Hello World")
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow()
    }
}
Copy the code

To restore the preview, we need to modify the PreviewProvider.

2.4 In the static attribute previews of LandmarkRow_Previews, add landmark parameter to the initialization method of LandmarkRow, and assign the first element of landmarkData array to landmark parameter.

The preview will display the text Hello World.

LandmarkRow.swift

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        Text("Hello World")
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow(landmark: landmarkData[0])
    }
}
Copy the code

With the preview restored, we are ready to build the layout of the row.

2.5 Nested the existing Text View into an HStack.

LandmarkRow.swift

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            Text("Hello World")
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow(landmark: landmarkData[0])
    }
}
Copy the code

2.6 Change the content of the Text View to landmark.name.

LandmarkRow.swift

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            Text(landmark.name)
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow(landmark: landmarkData[0])
    }
}
Copy the code

2.7 Add an image before the Text View to complete the row.

LandmarkRow.swift

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image(forSize: 50)
            Text(landmark.name)
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow(landmark: landmarkData[0])
    }
}
Copy the code

3. Customize the Row preview

Xcode’s Canvas automatically recognizes and displays any types in the current editor that conform to the PreviewProvider protocol. The Preview Provider returns one or more views that contain options for configuring the size and device.

By customizing the return value of the Preview provider, we can make the preview show what we want.

3.1 In LandmarkRow_Previews, change the parameter of landmark to the second element of landmarkData array.

The preview immediately switches from the first element to the display of the second element.

LandmarkRow.swift

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image(forSize: 50)
            Text(landmark.name)
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow(landmark: landmarkData[1])
    }
}
Copy the code

3.2 use the previewLayout(_:) method to set the approximate size of the row in the list.

LandmarkRow.swift

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image(forSize: 50)
            Text(landmark.name)
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow(landmark: landmarkData[1])
            .previewLayout(.fixed(width: 300, height: 70))
    }
}
Copy the code

We can use groups in the Preview provider to return multiple previews.

Wrap the returned row in a Group and add the first row back.

A Group is a container that combines views. Xcode will render the Group’s child views as separate preview views in the Canvas.

LandmarkRow.swift

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image(forSize: 50)
            Text(landmark.name)
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarkData[0])
                .previewLayout(.fixed(width: 300, height: 70))
            LandmarkRow(landmark: landmarkData[1])
                .previewLayout(.fixed(width: 300, height: 70))
        }
    }
}
Copy the code

Move the previewLayout(_:) call outside the group declaration to simplify the code.

A view’s children inherit the view’s context Settings, such as the preview Settings here.

LandmarkRow.swift

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image(forSize: 50)
            Text(landmark.name)
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarkData[0])
            LandmarkRow(landmark: landmarkData[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}
Copy the code

Code written in the Preview provider only changes how Xcode looks in the Canvas. Because of the #if DEBUG directive, the compiler removes this code when the app is released.

Create a list of landmarks

SwiftUI’s List type is used to display platform-specific List views. The elements of the list can be static, like the child view of stacks we created; It can also be dynamically generated. You can even mix static and dynamically generated views together.

4.1 Create a new SwiftUI View named LandmarkList.swift.

4.2 Replace the default Text view with a List, and then pass in two LandmarkRow objects containing the first two landmarks as children of the List.

The preview shows the two landmarks in an ios-style list.

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    var body: some View {
        List {
            LandmarkRow(landmark: landmarkData[0])
            LandmarkRow(landmark: landmarkData[1])
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
Copy the code

5. Dynamic lists

Instead of specifying a single element to a list, we can also generate rows directly from the collection.

Make the List display the elements of the collection by passing in a collection of data and a closure that provides a view for each element. A list converts the elements in each collection into child views through a passed closure.

5.1 Remove the two existing static landmark rows, and then pass landmarkData to the List initializer.

List uses identifiable data, and we can use either of the following two methods to make that data identifiable: Call the identified(by:) method, using the key path attribute to uniquely identify each element, or to have the data type conform to the Identifiable agreement.

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    var body: some View {
        List(landmarkData.identified(by: \.id)) { landmark in

        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
Copy the code

5.2 By returning LandmarkRow in a closure, we have completed the auto-generated list of content.

This creates a LandmarkRow for each element in the landmarkData array.

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    var body: some View {
        List(landmarkData.identified(by: \.id)) { landmark in
            LandmarkRow(landmark: landmark)
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
Copy the code

Next, we simplified the code by adding an Identifiable declaration to the Landmark type.

5.3 Switch to Landmark. Swift, and declare compliance with the Identifiable agreement.

After the Landmark type declared the ID attribute required by the Identifiable agreement, we completed the modification of Landmark.

Landmark.swift

import SwiftUI
import CoreLocation

struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
    fileprivate var imageName: String
    fileprivate var coordinates: Coordinates
    var state: String
    var park: String
    var category: Category

    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(
            latitude: coordinates.latitude,
            longitude: coordinates.longitude)
    }

    func image(forSize size: Int) -> Image {
        ImageStore.shared.image(name: imageName, size: size)
    }

    enum Category: String, CaseIterable, Codable, Hashable {
        case featured = "Featured"
        case lakes = "Lakes"
        case rivers = "Rivers"
    }
}

struct Coordinates: Hashable, Codable {
    var latitude: Double
    var longitude: Double
}
Copy the code

5.4 switch back to LandmarkList and delete identified(by:) calls.

From now on, we can use the collection of Landmark elements directly.

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    var body: some View {
        List(landmarkData) { landmark in
            LandmarkRow(landmark: landmark)
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
Copy the code

6. Set the navigation between the list and details

Although the list is displayed, we can’t yet view the landmark details page by clicking on individual landmarks.

The list is navigated by embedding the List in a NavigationView and nested each row in a NavigationButton to set transitions to the target view.

6.1 Embed a list of automatically created landmarks in a NavigationView.

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                LandmarkRow(landmark: landmark)
            }
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
Copy the code

Call the navigationBarTitle(_:) method to set the navigationBarTitle when the list is displayed.

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                LandmarkRow(landmark: landmark)
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
Copy the code

6.3 In the closure of the List, wrap the returned row in a NavigationButton with the LandmarkDetail View as the target.

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                NavigationButton(destination: LandmarkDetail()) {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
Copy the code

6.4 After switching to real-time mode, you can directly try the navigation function in the preview. Click the Live Preview button, then click the landmark to access the details page.

7. Pass data to child Views

LandmarkDetail still uses hard-coded data to display landmarks. Like LandmarkRow, the LandmarkDetail type and the other views it combines require a landmark attribute as their data source.

When we start the content of the child view, we change the display of the CircleImage, MapView, and LandmarkDetail from hard-coded to incoming data.

7.1 In circleimage. swif, add the storage attribute image.

This is a common pattern when building views with SwiftUI. Our custom views often wrap and encapsulate modifiers for specific views.

CircleImage.swift

import SwiftUI

struct CircleImage: View {
    var image: Image

    var body: some View {
        image
            .clipShape(Circle())
            .overlay(Circle().stroke(Color.white, lineWidth: 4))
            .shadow(radius: 10)
    }
}

struct CircleImage_Preview: PreviewProvider {
    static var previews: some View {
        CircleImage()
    }
}
Copy the code

Update the Preview Provider to pass a Turtle Rock image.

CircleImage.swift

import SwiftUI

struct CircleImage: View {
    var image: Image

    var body: some View {
        image
            .clipShape(Circle())
            .overlay(Circle().stroke(Color.white, lineWidth: 4))
            .shadow(radius: 10)
    }
}

struct CircleImage_Preview: PreviewProvider {
    static var previews: some View {
        CircleImage(image: Image("turtlerock"))}}Copy the code

7.3 In MapView. Swift, add a coordinate attribute to MapView, and then change the hard-coding of longitude and latitude to use this attribute.

MapView.swift

import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
    var coordinate: CLLocationCoordinate2D

    func makeUIView(context: Context) -> MKMapView {
        MKMapView(frame: .zero)
    }

    func updateUIView(_ view: MKMapView, context: Context) {

        letSpan = MKCoordinateSpan(latitudeDelta: 0.02, longitude udedelta: 0.02)let region = MKCoordinateRegion(center: coordinate, span: span)
        view.setRegion(region, animated: true)
    }
}

struct MapView_Preview: PreviewProvider {
    static var previews: some View {
        MapView()
    }
}
Copy the code

7.4 Update the Preview Provider to pass the coordinates of the first landmark in the data array.

MapView.swift

import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
    var coordinate: CLLocationCoordinate2D

    func makeUIView(context: Context) -> MKMapView {
        MKMapView(frame: .zero)
    }

    func updateUIView(_ view: MKMapView, context: Context) {
        letSpan = MKCoordinateSpan(latitudeDelta: 0.02, longitude udedelta: 0.02)let region = MKCoordinateRegion(center: coordinate, span: span)
        view.setRegion(region, animated: true)
    }
}

struct MapView_Preview: PreviewProvider {
    static var previews: some View {
        MapView(coordinate: landmarkData[0].locationCoordinate)
    }
}
Copy the code

7.5 In LandmarkDetail. Swift, add the landmark attribute to the LandmarkDetail type.

LandmarkDetail.swift

import SwiftUI

struct LandmarkDetail: View {
    var landmark: Landmark

    var body: some View {
        VStack {
            MapView()
                .frame(height: 300)

            CircleImage()
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                Text("Turtle Rock")
                    .font(.title)

                HStack(alignment: .top) {
                    Text("Joshua Tree National Park")
                        .font(.subheadline)
                    Spacer()
                    Text("California")
                        .font(.subheadline)
                }
            }
            .padding()

            Spacer()
        }
    }
}

struct LandmarkDetail_Preview: PreviewProvider {
    static var previews: some View {
        LandmarkDetail()
    }
}
Copy the code

7.6 Update the Preview Provider to use the first landmark in landmarkData.

LandmarkDetail.swift

import SwiftUI

struct LandmarkDetail: View {
    var landmark: Landmark

    var body: some View {
        VStack {
            MapView()
                .frame(height: 300)

            CircleImage()
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                Text("Turtle Rock")
                    .font(.title)

                HStack(alignment: .top) {
                    Text("Joshua Tree National Park")
                        .font(.subheadline)
                    Spacer()
                    Text("California")
                        .font(.subheadline)
                }
            }
            .padding()

            Spacer()
        }
    }
}

struct LandmarkDetail_Preview: PreviewProvider {
    static var previews: some View {
        LandmarkDetail(landmark: landmarkData[0])
    }
}
Copy the code

7.7 Pass the required data to our custom type.

LandmarkDetail.swift

import SwiftUI

struct LandmarkDetail: View {
    var landmark: Landmark

    var body: some View {
        VStack {
            MapView(coordinate: landmark.locationCoordinate)
                .frame(height: 300)

            CircleImage(image: landmark.image(forSize: 250))
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)

                HStack(alignment: .top) {
                    Text(landmark.park)
                        .font(.subheadline)
                    Spacer()
                    Text(landmark.state)
                        .font(.subheadline)
                }
            }
            .padding()

            Spacer()
        }
    }
}

struct LandmarkDetail_Preview: PreviewProvider {
    static var previews: some View {
        LandmarkDetail(landmark: landmarkData[0])
    }
}
Copy the code

7.8 Finally, call navigationBarTitle(_:displayMode:) to add a title to the navigation bar to display the detailed view.

LandmarkDetail.swift

import SwiftUI

struct LandmarkDetail: View {
    var landmark: Landmark

    var body: some View {
        VStack {
            MapView(coordinate: landmark.locationCoordinate)
                .frame(height: 300)

            CircleImage(image: landmark.image(forSize: 250))
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)

                HStack(alignment: .top) {
                    Text(landmark.park)
                        .font(.subheadline)
                    Spacer()
                    Text(landmark.state)
                        .font(.subheadline)
                }
            }
            .padding()

            Spacer()
        }
        .navigationBarTitle(Text(landmark.name), displayMode: .inline)
    }
}

struct LandmarkDetail_Preview: PreviewProvider {
    static var previews: some View {
        LandmarkDetail(landmark: landmarkData[0])
    }
}
Copy the code

7.9 In scenedelegate. swift, change app rootView to LandmarkList.

When we run the app independently in the emulator instead of using the preview, the app will start showing with the rootView defined in SceneDelegate.

SceneDelegate.swift

import UIKit import SwiftUI class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided  UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). // Use a UIHostingController as window root view controllerlet window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = UIHostingController(rootView: LandmarkList())
        self.window = window
        window.makeKeyAndVisible()
    }

    // ...
}
Copy the code

7.10 In landmarkList. swift, pass the current landmark to the target LandmarkDetail.

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
Copy the code

7.11 Switch to Live Preview, you can see the view of navigation from the list to the correct landmark details.

8. Dynamically generate previews

Next, we’ll add code to LandmarkList_Previews to render the list at different device sizes. By default, the preview is rendered at the size of the device in the current Scheme. We can change the previewDevice by calling the previewDevice(_:) method.

8.1 First, change the preview of the current list to show the size of the iPhone SE.

We can enter the device name as shown in any Xcode Scheme menu.

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
            .previewDevice(PreviewDevice(rawValue: "iPhone SE"))}}Copy the code

8.2 Embed a LandmarkList into a ForEach instance using an array of device names as data in the List preview.

ForEach operates on collections in the same way as list so that we can use the subview wherever it is available, such as stacks, Lists, groups, etc. When the data element is a simple value type like the string used here, we can use \.self as the key path for the identifier.

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(["iPhone SE"."iPhone XS Max"].identified(by: \.self)) { deviceName in
            LandmarkList()
                .previewDevice(PreviewDevice(rawValue: deviceName))
        }
    }
}
Copy the code

8.3 use the previewDisplayName(_:) method to add device names as labels to the preview.

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(["iPhone SE"."iPhone XS Max"].identified(by: \.self)) { deviceName in
            LandmarkList()
                .previewDevice(PreviewDevice(rawValue: deviceName))
                .previewDisplayName(deviceName)
        }
    }
}
Copy the code

8.4 We can experience different devices in canvas and compare how they render the View.