SwiftUI and Structured Concurrency

Learn how to manage concurrency into your SwiftUI iOS app. By Andrew Tetlaw.

5 (6) · 4 Reviews

Download materials
Save for later
Share
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

Displaying the Rover Manifests

The solution is to drill down to photos by each Martian day — or sol — of a rover’s mission. The entries property on PhotoManifest contains only the data you need.

In the Views group, create a new SwiftUI View file called RoverManifestView.swift, and replace the default code with:

import SwiftUI
struct RoverManifestView: View {
  // 1
  let manifest: PhotoManifest
  var body: some View {
    // 2
    List(manifest.entries, id: \.sol) { entry in
      NavigationLink {
        Text("Fear my botany powers, Mars!")
      } label: {
        // 3
        HStack {
          Text("Sol \(entry.sol) 
      (\(Text.marsDateText(dateString:
            entry.earthDate)))")
          Spacer()
          Text("\(entry.totalPhotos) \(Image(systemName: "photo"))")
        }
      }
    }
    .navigationTitle(manifest.name)
  }
}

Here’s what’s going on, step by step:

  1. Your new view needs a PhotoManifest to display.
  2. You create a List by looping through each entry in the manifest.
  3. Each entry creates a NavigationLink and shows the sol, the corresponding Earth date and the total photos for that day.

For completeness, add a suitable SwiftUI preview below RoverManifestView:

struct RoverManifestView_Previews: PreviewProvider {
  static var previews: some View {
    RoverManifestView(manifest:
      PhotoManifest(
        name: "WALL-E",
        landingDate: "2021-12-31",
        launchDate: "2021-12-01",
        status: "active",
        maxSol: 31,
        maxDate: "2022-01-31",
        totalPhotos: 33,
        entries: [
          ManifestEntry(
            sol: 1,
            earthDate: "2022-01-01",
            totalPhotos: 33,
            cameras: []
          )
        ]
      )
    )
  }
}

This code will create a fictional manifest for the preview.

Finally, in RoversListView.swift, replace Text("I'm sorry Dave, I'm afraid I can't do that") with:

RoverManifestView(manifest: manifest)

Build and run, select the Rovers tab, and drill down to each manifest. These rovers certainly are snap-happy Mars tourists!

A list of days and photo counts for the Perseverance rover

So far, so good, but a step remains: displaying the photos.

Presenting the Photos

In the Views group, create another SwiftUI View file called RoverPhotosBySolListView.swift. Replace the default code with:

import SwiftUI
struct RoverPhotosBySolListView: View {
  let name: String
  let sol: Int
  private let marsData = MarsData()
  @State private var photos: [Photo] = []
  var body: some View {
    Text("Douglas Quaid, get to Mars!")
  }
}

Your view has two arguments: the rover name and the sol. You also prepared the MarsData and state properties you’ll need.

In RoverManifestView.swift, replace the Text view in the NavigationLink with:

RoverPhotosBySolListView(name: manifest.name, sol: entry.sol)

Next, in MarsData.swift, add a function to MarsData that’ll download the photos by rover and sol:

func fetchPhotos(roverName: String, sol: Int) async -> [Photo] {
  do {
    return try await marsRoverAPI.photos(roverName: roverName, sol: sol)
  } catch {
    log.error("Error fetching rover photos: \(String(describing: error))")
    return []
  }
}

This simple async function makes the appropriate API call and returns an array of Photo.

In RoverPhotosBySolListView.swift, replace the Text view with:

// 1
ZStack {
  // 2
  ScrollView {
    LazyVStack(spacing: 0) {
      ForEach(photos) { photo in
        MarsImageView(photo: photo)
          .padding(.horizontal, 20)
          .padding(.vertical, 8)
      }
    }
  }
  // 3
  if photos.isEmpty {
    MarsProgressView()
  }
}
// 4
.navigationTitle(name)
// 5
.task {
  photos = await marsData.fetchPhotos(roverName: name, sol: sol)
}

That’s a lot of code! Here’s a breakdown:

  1. You set up a ZStack so the MarsProgressView displays in the center while loading.
  2. Then you present all the photos as a scrollable LazyVStack, using MarsImageView.
  3. While photos is empty, you display the progress view.
  4. name is a convenient navigation title.
  5. Finally, your view’s task calls fetchPhotos(roverName:sol:).

Build and run your app. Take some time to drill down and explore those photos. Consider how amazing it is to browse a photo album of pictures from the surface of another planet!

A display of photos from Spirit and Opportunity

A display of photos from Curiosity and Perseverance and Opportunity

Browse by Earth Date

If you’re looking for an extra challenge, SwiftUI has an alternative to task(priority:_:) called task(id:priority:_:). This alternative takes an Equatable value representing an ID. When the ID value changes, the view cancels and restarts the task.

You could store the currently selected date in a state variable and use it for the ID. When the user selects a different date, the task will fetch the next photos. And yes, there’s an API endpoint for photos taken by a rover on a specific Earth date.

Where to Go From Here?

You can download the completed version of the project using Download Materials at the top or bottom of this tutorial.

In this tutorial, you learned how to manage concurrent tasks with async, await and TaskGroup. Here are some links to help learn more about Swift Concurrency:

We hope you’ve enjoyed this tutorial about structured concurrency and AsyncImage in SwiftUI.

If you have any questions or comments, join the discussion below!