This page looks best with JavaScript enabled

How to build lists and navigation in SwiftUI

 ·  ☕ 10 min read  ·  ✍️ Adesh

In this blog, we will learn about how to build lists and navigation in SwiftUI.

This tutorial follows the Apple Developer demo app for creating and combining views in SwiftUI. Let’s play with SwiftUI. In the first part, we have learned about creating and combining views in SwiftUI. We will continue the same project and will modify it as per our requirements.

With the basic landmark detail view set up, you need to provide a way for users to see the full list of landmarks, and to view the details about each location.

You’ll create views that can show information about any landmark, and dynamically generate a scrolling list that a user can tap to see a detail view for a landmark. To fine-tune the UI, you’ll use Xcode’s canvas to render multiple previews at different device sizes.

Create a landmark JSON sample data

Copy this landmarkData.json file and put this under the Resources folder of your project. This file represents each landmark data including the following information.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
[
  {
    "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"
  },
  {
    "name": "Chilkoot Trail",
    "category": "Rivers",
    "city": "Skagway",
    "state": "Alaska",
    "id": 1003,
    "park": "Klondike Gold Rush National Historical Park",
    "coordinates": {
      "longitude": -135.334571,
      "latitude": 59.560551
    },
    "imageName": "chilkoottrail"
  },
  {
    "name": "St. Mary Lake",
    "category": "Lakes",
    "city": "Browning",
    "state": "Montana",
    "id": 1004,
    "park": "Glacier National Park",
    "coordinates": {
      "longitude": -113.536248,
      "latitude": 48.69423
    },
    "imageName": "stmarylake"
  },
  {
    "name": "Twin Lake",
    "category": "Lakes",
    "city": "Twin Lakes",
    "state": "Alaska",
    "id": 1005,
    "park": "Lake Clark National Park and Preserve",
    "coordinates": {
      "longitude": -153.849883,
      "latitude": 60.641684
    },
    "imageName": "twinlake"
  },
  {
    "name": "Lake McDonald",
    "category": "Lakes",
    "city": "West Glacier",
    "state": "Montana",
    "id": 1006,
    "park": "Glacier National Park",
    "coordinates": {
      "longitude": -113.934831,
      "latitude": 48.56002
    },
    "imageName": "lakemcdonald"
  },
  {
    "name": "Charley Rivers",
    "category": "Rivers",
    "city": "Eaking",
    "state": "Alaska",
    "id": 1007,
    "park": "Charley Rivers National Preserve",
    "coordinates": {
      "longitude": -143.122586,
      "latitude": 65.350021
    },
    "imageName": "yukon_charleyrivers"
  },
  {
    "name": "Icy Bay",
    "category": "Lakes",
    "city": "Icy Bay",
    "state": "Alaska",
    "id": 1008,
    "park": "Wrangell-St. Elias National Park and Preserve",
    "coordinates": {
      "longitude": -141.518167,
      "latitude": 60.089917
    },
    "imageName": "icybay"
  },
  {
    "name": "Rainbow Lake",
    "category": "Lakes",
    "city": "Willow",
    "state": "Alaska",
    "id": 1009,
    "park": "State Recreation Area",
    "coordinates": {
      "longitude": -150.086103,
      "latitude": 61.694334
    },
    "imageName": "rainbowlake"
  },
  {
    "name": "Hidden Lake",
    "category": "Lakes",
    "city": "Newhalem",
    "state": "Washington",
    "id": 1010,
    "park": "North Cascades National Park",
    "coordinates": {
      "longitude": -121.17799,
      "latitude": 48.495442
    },
    "imageName": "hiddenlake"
  },
  {
    "name": "Chincoteague",
    "category": "Rivers",
    "city": "Chincoteague",
    "state": "Virginia",
    "id": 1011,
    "park": "Chincoteague National Wildlife Refuge",
    "coordinates": {
      "longitude": -75.383212,
      "latitude": 37.91531
    },
    "imageName": "chincoteague"
  },
  {
    "name": "Lake Umbagog",
    "category": "Lakes",
    "city": "Errol",
    "state": "New Hampshire",
    "id": 1012,
    "park": "Umbagog National Wildlife Refuge",
    "coordinates": {
      "longitude": -71.056816,
      "latitude": 44.747408
    },
    "imageName": "umbagog"
  }
]

Rename ContentView.swift to LandmarkDetail.swift

Open ContentView.swift file and refactor its name to LandmarkDetail.swift.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import SwiftUI

struct LandmarkDetail: View {
    var body: some View {
        VStack {
            MapView()
                .edgesIgnoringSafeArea(.top)
                .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()
    }
}

Create Landmark Row view

The first view you’ll build in this tutorial is a row for displaying details about each landmark. This row view stores information in a property for the landmark it displays, so that one view can display any landmark. Later, you’ll combine multiple rows into a list of landmarks.

Create a new SwiftUI view, named LandmarkRow.swift.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
            Text(landmark.name)
            Spacer()
        }
    }
}

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

Customize the Row Preview

Xcode’s canvas automatically recognizes and displays any type in the current editor that conforms to the PreviewProvider protocol. A preview provider returns one or more views, with options to configure the size and device.

You can customize the returned content from a preview provider to render exactly the previews that are most helpful to you.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
            Text(landmark.name)
            Spacer()
        }
    }
}

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

Create the List of Landmarks

When you use SwiftUI’s List type, you can display a platform-specific list of views. The elements of the list can be static, like the child views of the stacks you’ve created so far, or dynamically generated. You can even mix static and dynamically generated views.

Create a new SwiftUI view, named LandmarkList.swift.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17

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()
    }
}

Make the List Dynamic

Instead of specifying a list’s elements individually, you can generate rows directly from a collection.

You can create a list that displays the elements of collection by passing your collection of data and a closure that provides a view for each element in the collection. The list transforms each element in the collection into a child view by using the supplied closure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

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()
    }
}

Switch to Landmark.swift and declare conformance to the Identifiable protocol.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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)
    }

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

extension Landmark {
    var image: Image {
        ImageStore.shared.image(name: imageName)
    }
}


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

Set Up Navigation Between List and Detail

The list renders properly, but you can’t tap an individual landmark to see that landmark’s detail page yet.

You add navigation capabilities to a list by embedding it in a NavigationView, and then nesting each row in a NavigationLink to set up a transtition to a destination view.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

import SwiftUI

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

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

Pass Data into Child Views

The LandmarkDetail view still uses hard-coded details to show its landmark. Just like LandmarkRow, the LandmarkDetail type and the views it comprises need to use a landmark property as the source for their data.

Starting with the child views, you’ll convert CircleImageMapView, and then LandmarkDetail to display data that’s passed in, rather than hard-coding each row.

In CircleImage.swift, add a stored imageproperty to CircleImage.

This is a common pattern when building views using SwiftUI. Your custom views will often wrap and encapsulate a series of modifiers for a particular view.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

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"))
    }
}

In MapView.swift, add a coordinateproperty to MapView and convert the code to use that property instead of hard-coding the latitude and longitude.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
    var coordinate: CLLocationCoordinate2D

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

    func updateUIView(_ view: MKMapView, context: Context) {
        let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 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)
    }
}

In LandmarkDetail.swift, add a Landmark property to the LandmarkDetailtype.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import SwiftUI

struct LandmarkDetail: View {
    var landmark: Landmark

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

            CircleImage(image: landmark.image)
                .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])
    }
}

In SceneDelegate.swift, switch the root view of the app to be LandmarkList.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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).

        // Create the SwiftUI view that provides the window contents.
        let contentView = LandmarkList()

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

navigation list in swift

child view in swiftui

Reference

https://developer.apple.com/tutorials/swiftui/building-lists-and-navigation

Further Reading

How to create and combine views in SwiftUI

Share on

Adesh
WRITTEN BY
Adesh
Technical Architect