reCAPTCHA WAF Session Token
Programming Languages

SwiftUI Tutorial: Navigation | Kodeco

Update note: Fabrizio Brancati updated this tutorial for Xcode 14 and iOS 16. Audrey Tam wrote the original.

Also, this tutorial assumes you’re comfortable with using Xcode to develop iOS apps. You need Xcode 14. Some familiarity with UIKit and SwiftUI will be helpful.

Getting Started

Use the Download Materials button at the top or bottom of this tutorial to download the starter project. Open the PublicArt project in the Starter folder. You’ll build a master-detail app using the Artwork.swift file already included in this project.

SwiftUI Basics in a Nutshell

SwiftUI lets you ignore Interface Builder and storyboards without having to write step-by-step instructions for laying out your UI. You can preview a SwiftUI view side-by-side with its code — a change to one side will update the other side, so they’re always in sync. There aren’t any identifier strings to get wrong. And it’s code, but a lot less than you’d write for UIKit, so it’s easier to understand, edit and debug. What’s not to love?

The canvas preview means you don’t need a storyboard. The subviews keep themselves updated, so you also don’t need a view controller. And live preview means you rarely need to launch the simulator.

Note: Check out SwiftUI: Getting Started to learn more about the mechanics of developing a single-view SwiftUI app in Xcode.

SwiftUI doesn’t replace UIKit. Like Swift and Objective-C, you can use both in the same app. At the end of this tutorial, you’ll see how easy it is to use a UIKit view in a SwiftUI app.

Declarative App Development

SwiftUI enables you to do declarative app development: You declare both how you want the views in your UI to look and also what data they depend on. The SwiftUI framework takes care of creating views when they should appear and updating them whenever data they depend on changes. It recomputes the view and all its children, then renders what has changed.

A view’s state depends on its data, so you declare the possible states for your view and how the view appears for each state — how the view reacts to data changes or how data affect the view. Yes, there’s a definite reactive feeling to SwiftUI! If you’re already using one of the reactive programming frameworks, you’ll have an easier time picking up SwiftUI.

Declaring Views

A SwiftUI view is a piece of your UI: You combine small views to build larger views. There are lots of primitive views like Text and Color, which you can use as building blocks for your custom views.

Open ContentView.swift, and ensure its canvas is open (Option-Command-Return). Then click the + button or press Command-Shift-L to open the Library:

The first tab lists primitive views for layout and control, plus Layouts, Other Views and Paints. Many of these, especially the control views, are familiar to you as UIKit elements, but some are unique to SwiftUI.

Library of primitive modifiers

The second tab lists modifiers for layout, effects, text, events and other purposes, including presentation, environment and accessibility. A modifier is a method that creates a new view from the existing view. You can chain modifiers like a pipeline to customize any view.

SwiftUI encourages you to create small reusable views, then customize them with modifiers for the specific context where you use them. Don’t worry. SwiftUI collapses the modified view into an efficient data structure, so you get all this convenience with no visible performance hit.

Creating a Basic List

Start by creating a basic list for the master view of your master-detail app. In a UIKit app, this would be a UITableViewController.

Edit ContentView to look like this:

struct ContentView: View {
  let disciplines = ["statue", "mural", "plaque"]
  var body: some View {
    List(disciplines, id: \.self) { discipline in
      Text(discipline)
    }
  }
}

You create a static array of strings and display them in a List view, which iterates over the array, displaying whatever you specify for each item. And the result looks like a UITableView!

Ensure your canvas is open, then refresh the preview (click the Resume button or press Option-Command-P):

A basic list of strings

There’s your list, like you expected to see. How easy was that? No UITableViewDataSource methods to implement, no UITableViewCell to configure, and no UITableViewCell identifier to misspell in tableView(_:cellForRowAt:)!

The List id Parameter

The parameters of List are the array, which is obvious, and id, which is less obvious. List expects each item to have an identifier, so it knows how many unique items there are (instead of tableView(_:numberOfRowsInSection:)). The argument \.self tells List that each item is identified by itself. This is OK as long as the item’s type conforms to the Hashable protocol, which all the built-in types do.

Take a closer look at how id works: Add another "statue" to disciplines:

let disciplines = ["statue", "mural", "plaque", "statue"]

Refresh the preview: all four items appear. But, according to id: \.self, there are only three unique items. A breakpoint might shed some light.

Add a breakpoint at Text(discipline).

Starting Debug

Run the simulator, and the app execution stops at your breakpoint, and the Variables View displays discipline:

First stop at breakpoint: discipline = statue

Click the Continue program execution button: Now discipline = "statue" again.

Click Continue again to see discipline = "mural". After tapping on Continue, you see the same value, mural, again. Same happens in the next two clicks on the Continue as well with discipline = "plaque". Then one final Continue displays the list of four items. So no — execution doesn’t stop for the fourth list item.

What you’ve seen is: execution visited each of the three unique items twice. So List does see only three unique items. Later, you’ll learn a better way to handle the id parameter. But first, you’ll see how easy it is to navigate to a detail view.

Stop the simulator execution and remove the breakpoint.

Navigating to the Detail View

You’ve seen how easy it is to display the master view. It’s about as easy to navigate to the detail view.

First, embed List in a NavigationView, like this:

NavigationStack {
  List(disciplines, id: \.self) { discipline in
    Text(discipline)
  }
  .navigationBarTitle("Disciplines")
}

This is like embedding a view controller in a navigation controller: You can now access all the navigation items such as the navigation bar title. Notice .navigationBarTitle modifies List, not NavigationView. You can declare more than one view in a NavigationView, and each can have its own .navigationBarTitle.

Refresh the preview to see how this looks:

List in NavigationView with navigationBarTitle

Nice! You get a large title by default. That’s fine for the master list, but you’ll do something different for the detail view’s title.

Creating a Navigation Link

NavigationView also enables NavigationLink, which needs a destination view and a label — like creating a segue in a storyboard, but without those pesky segue identifiers.

First, create your DetailView. For now, declare it in ContentView.swift, below the ContentView struct:

struct DetailView: View {
  let discipline: String
  var body: some View {
    Text(discipline)
  }
}

This has a single property and, like any Swift struct, it has a default initializer — in this case, DetailView(discipline: String). The view is the String itself, presented in a Text view.

Now, inside the List closure in ContentView, make the row view Text(discipline) into a NavigationLink button, and add the .navigationDestination(for:destination:) destination modifier:

List(disciplines, id: \.self) { discipline in
  NavigationLink(value: discipline) {
    Text(discipline)
  }
}
.navigationDestination(for: String.self, destination: { discipline in
  DetailView(discipline: discipline)
})
.navigationBarTitle("Disciplines")

There’s no prepare(for:sender:) rigmarole — you pass the current list item to DetailView to initialize its discipline property.

Refresh the preview to see a disclosure arrow at the trailing edge of each row:

NavigationLink disclosure arrow on each row

Tap a row to show its detail view:

NavigationLink to DetailView

And zap, it works! Notice you get the usual back button, too.

But the view looks so plain — it doesn’t even have a title.

Add a title to the DetailView:

var body: some View {
  Text(discipline)
    .navigationBarTitle(Text(discipline), displayMode: .inline)
}

This view is presented by a NavigationLink, so it doesn’t need its own NavigationView to display a navigationBarTitle. But this version of navigationBarTitle requires a Text view for its title parameter — you’ll get peculiarly meaningless error messages if you try it with just the discipline string. Option-click the two navigationBarTitle modifiers to see the difference in the title and titleKey parameter types.

The displayMode: .inline argument displays a normal-size title.

Start Live Preview again, and tap a row to see the title:

Inline navigation bar title in DetailView

Now you know how to create a basic master-detail app. You used String objects, to avoid clutter that might obscure how lists and navigation work. But list items are usually instances of a model type you define. It’s time to use some real data.

Revisiting Honolulu Public Artworks

The starter project contains the Artwork.swift file. Artwork is a struct with eight properties, all constants except for the last, which the user can set:

struct Artwork {
  let artist: String
  let description: String
  let locationName: String
  let discipline: String
  let title: String
  let imageName: String
  let coordinate: CLLocationCoordinate2D
  var reaction: String
}

Below the struct is artData, an array of Artwork objects. It’s a subset of the data used in our MapKit Tutorial: Getting Started — public artworks in Honolulu.

The reaction property of some of the artData items is 💕, 🙏 or 🌟 but, for most items, it’s an empty String. The idea is when users visit an artwork, they set a reaction to it in the app. So an empty-string reaction means the user hasn’t visited this artwork yet.

Now start updating your project to use Artwork and artData:

In Artwork.swift file add the following:

extension Artwork: Hashable {
  static func == (lhs: Artwork, rhs: Artwork) -> Bool {
    lhs.id == rhs.id
  }
  
  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
}

This will let you use Artwork inside a List, because all items must be Hashable.

Creating Unique id Values With UUID()

The argument of the id parameter can use any combination of the list item’s Hashable properties. But, like choosing a primary key for a database, it’s easy to get it wrong, then find out the hard way that your identifier isn’t as unique as you thought.

Add an id property to your model type, and use UUID() to generate a unique identifier for every new object.

In Artwork.swift, add this property at the top of the Artwork property list:

let id = UUID()

You use UUID() to let the system generate a unique ID value, because you don’t care about the actual value of id. This unique ID will be useful later!

Conforming to Identifiable

But there’s an even better way: Go back to Artwork.swift, and add this extension, outside the Artwork struct:

extension Artwork: Identifiable { }

The id property is all you need to make Artwork conform to Identifiable, and you’ve already added that.

Now you can avoid specifying id parameter entirely:

List(artworks) { artwork in

Looks much neater now! Because Artwork conforms to Identifiable, List knows it has an id property and automatically uses this property for its id argument.

Then, in ContentView, add this property:

let artworks = artData

Delete the disciplines array.

Then replace disciplines, discipline and “Disciplines” with artworks, artwork and “Artworks”:

List(artworks) { artwork in
  NavigationLink(value: artwork) {
    Text(artwork.title)
  }
}
.navigationDestination(for: Artwork.self, destination: { artwork in
  DetailView(artwork: artwork)
})
.navigationBarTitle("Artworks")

Also, edit DetailView to use Artwork:

struct DetailView: View {
  let artwork: Artwork
 
  var body: some View {
  Text(artwork.title)
    .navigationBarTitle(Text(artwork.title), displayMode: .inline)
  }
}

You’ll soon create a separate file for DetailView, but this will do for now.

Showing More Detail

Artwork objects have lots of information you can display, so update your DetailView to show more details.

First, create a new SwiftUI View file: Command-N ▸ iOS ▸ User Interface ▸ SwiftUI View. Name it DetailView.swift.

Replace import Foundation with import SwiftUI.

Delete DetailView completely from ContentView.swift. You’ll replace it with a whole new view.

Add the following to DetailView.swift:

struct DetailView: View {
  let artwork: Artwork
  
  var body: some View {
    VStack {
      Image(artwork.imageName)
        .resizable()
        .frame(maxWidth: 300, maxHeight: 600)
        .aspectRatio(contentMode: .fit)
      Text("\(artwork.reaction) \(artwork.title)")
        .font(.headline)
        .multilineTextAlignment(.center)
        .lineLimit(3)
      Text(artwork.locationName)
        .font(.subheadline)
      Text("Artist: \(artwork.artist)")
        .font(.subheadline)
      Divider()
      Text(artwork.description)
        .multilineTextAlignment(.leading)
        .lineLimit(20)
    }
    .padding()
    .navigationBarTitle(Text(artwork.title), displayMode: .inline)
  }
}

You’re displaying several views in a vertical layout, so everything is in a VStack.

First is the Image: The artData images are all different sizes and aspect ratios, so you specify aspect-fit, and constrain the frame to at most 300 points wide by 600 points high. However, these modifiers won’t take effect unless you first modify the Image to be resizable.

You modify the Text views to specify font size and multilineTextAlignment, because some of the titles and descriptions are too long for a single line.

Finally, you add some padding around the stack.

You also need a preview, so add it:

struct DetailView_Previews: PreviewProvider {
  static var previews: some View {
    DetailView(artwork: artData[0])
  }
}

Refresh the preview:

Artwork detail view

There’s Prince Jonah! In case you’re curious, Kalanianaole has seven syllables, four of them in the last six letters ;].

The navigation bar doesn’t appear when you preview or even live-preview DetailView, because it doesn’t know it’s in a navigation stack.

Go back to ContentView.swift and tap a row to see the complete detail view:

Artwork detail view with navigation bar title

Declaring Data Dependencies

You’ve seen how easy it is to declare your UI. Now it’s time to learn about the other big feature of SwiftUI: declarative data dependencies.

Guiding Principles

SwiftUI has two guiding principles for managing how data flows through your app:

  • Data access = dependency: Reading a piece of data in your view creates a dependency for that data in that view. Every view is a function of its data dependencies — its inputs or state.
  • Single source of truth: Every piece of data that a view reads has a source of truth, which is either owned by the view or external to the view. Regardless of where the source of truth lies, you should always have a single source of truth. You give read-write access to a source of truth by passing a binding to it.

In UIKit, the view controller keeps the model and view in sync. In SwiftUI, the declarative view hierarchy plus this single source of truth means you no longer need the view controller.

Tools for Data Flow

SwiftUI provides several tools to help you manage the flow of data in your app.

Property wrappers augment the behavior of variables. SwiftUI-specific wrappers — @State, @Binding, @ObservedObject and @EnvironmentObject — declare a view’s dependency on the data represented by the variable.

Each wrapper indicates a different source of data:

  • @State variables are owned by the view. @State var allocates persistent storage, so you must initialize its value. Apple advises you to mark these private to emphasize that a @State variable is owned and managed by that view specifically.
  • @Binding declares dependency on a @State var owned by another view, which uses the $ prefix to pass a binding to this state variable to another view. In the receiving view, @Binding var is a reference to the data, so it doesn’t need initialization. This reference enables the view to edit the state of any view that depends on this data.
  • @ObservedObject declares dependency on a reference type that conforms to the ObservableObject protocol: It implements an objectWillChange property to publish changes to its data.
  • @EnvironmentObject declares dependency on some shared data — data that’s visible to all views in the app. It’s a convenient way to pass data indirectly, instead of passing data from parent view to child to grandchild, especially if the child view doesn’t need it.

Now move on to practice using @State and @Binding for navigation.

Adding a Navigation Bar Button

If an Artwork has 💕, 🙏 or 🌟 as its reaction value, it indicates the user has visited this artwork. A useful feature would let users hide their visited artworks so they can choose one of the others to visit next.

In this section, you’ll add a button to the navigation bar to show only artworks the user hasn’t visited yet.

Start by displaying the reaction value in the list row, next to the artwork title: Change Text(artwork.title) to the following:

Text("\(artwork.reaction) \(artwork.title)")

Refresh the preview to see which items have a nonempty reaction:

List of reactions and artworks

Now, add these properties at the top of ContentView:

@State private var hideVisited = false

var showArt: [Artwork] {
  hideVisited ? artworks.filter { $0.reaction.isEmpty } : artworks
}

The @State property wrapper declares a data dependency: Changing the value of this hideVisited property triggers an update to this view. In this case, changing the value of hideVisited will hide or show the already-visited artworks. You initialize this to false, so the list displays all of the artworks when the app launches.

The computed property showArt is all of artworks if hideVisited is false; otherwise, it’s a sub-array of artworks, containing only those items in artworks that have an empty-string reaction.

Now, replace the first line of the List declaration with:

List(showArt) { artwork in

Now add a navigationBarItems modifier to List after .navigationBarTitle("Artworks"):

.navigationBarItems(
  trailing: Toggle(isOn: $hideVisited) { Text("Hide Visited") })

You’re adding a navigation bar item on the right side (trailing edge) of the navigation bar. This item is a Toggle view with label “Hide Visited”.

You pass the binding $hideVisited to Toggle. A binding allows read-write access, so Toggle will be able to change the value of hideVisited whenever the user taps it. This change will flow through to update the List view.

Start Live-Preview to see this working:

Navigation bar with title and toggle

Tap the toggle to see the visited artworks disappear: Only the artworks with empty-string reactions remain. Tap again to see the visited artworks reappear.

Reacting to Artwork

One feature that’s missing from this app is a way for users to set a reaction to an artwork. In this section, you’ll add a context menu to the list row to let users set their reaction for that artwork.

Adding a Context Menu

Still in ContentView.swift, make artworks a @State variable:

@State var artworks = artData

The ContentView struct is immutable, so you need this @State property wrapper to be able to assign a value to an Artwork property.

Next, add the contextMenu modifier to the list row Text view:

Text("\(artwork.reaction) \(artwork.title)")
  .contextMenu {
    Button("Love it: 💕") {
      self.setReaction("💕", for: artwork)
    }
     Button("Thoughtful: 🙏") {
       self.setReaction("🙏", for: artwork)
    }
     Button("Wow!: 🌟") {
       self.setReaction("🌟", for: artwork)
    }
  }

The context menu shows three buttons, one for each reaction. Each button calls setReaction(_:for:) with the appropriate emoji.

Finally, implement the setReaction(_:for:) helper method:

private func setReaction(_ reaction: String, for item: Artwork) {
  self.artworks = artworks.map { artwork in
    guard artwork.id == item.id else { return artwork }
    let updateArtwork = Artwork(
      artist: item.artist,
      description: item.description,
      locationName: item.locationName,
      discipline: item.discipline,
      title: item.title,
      imageName: item.imageName,
      coordinate: item.coordinate,
      reaction: reaction
    )
    return updateArtwork
  }
}

Here’s where the unique ID values do their stuff! You compare id values to find the index of this item in the artworks array, then set that item’s reaction value.

Note: You might think it’d be easier to set artwork.reaction = "💕" directly. Unfortunately, the artwork list iterator is a let constant.

Refresh the live preview (Option-Command-P), then touch and hold an item to display the context menu. Tap a context menu button to select a reaction or tap outside the menu to close it.

Select a reaction to an artwork

How does that make you feel? 💕 🙏 🌟!

Bonus Section: Eager Evaluation

A curious thing happens when a SwiftUI app starts up: It initializes every object that appears in ContentView. For example, it initializes DetailView before the user taps anything that navigates to that view. It initializes every item in List, regardles of whether the item is visible in the window.

This is a form of eager evaluation, and it’s a common strategy for programming languages. Is it a problem? Well, if your app has many items, and each item downloads a large media file, you might not want your initializer to start the download.

To simulate what’s happening, add an init() method to Artwork, so you can include a print statement:

init(
  artist: String, 
  description: String, 
  locationName: String, 
  discipline: String,
  title: String, 
  imageName: String, 
  coordinate: CLLocationCoordinate2D, 
  reaction: String
) {
  print(">>>>> Downloading \(imageName) <<<<<")
  self.artist = artist
  self.description = description
  self.locationName = locationName
  self.discipline = discipline
  self.title = title
  self.imageName = imageName
  self.coordinate = coordinate
  self.reaction = reaction
}

Now, run the app in simulator, and watch the debug console:

>>>>> Downloading 002_200105 <<<<< >>>>> Downloading 19300102 <<<<< >>>>> Downloading 193701 <<<<< >>>>> Downloading 193901-5 <<<<< >>>>> Downloading 195801 <<<<< >>>>> Downloading 198912 <<<<< >>>>> Downloading 196001 <<<<< >>>>> Downloading 193301-2 <<<<< >>>>> Downloading 193101 <<<<< >>>>> Downloading 199909 <<<<< >>>>> Downloading 199103-3 <<<<< >>>>> Downloading 197613-5 <<<<< >>>>> Downloading 199802 <<<<< >>>>> Downloading 198803 <<<<< >>>>> Downloading 199303-2 <<<<< >>>>> Downloading 19350202a <<<<< >>>>> Downloading 200304 <<<<<

It initialized all of the Artwork items. If there were 1,000 items, and each downloaded a large image or video file, it could be a problem for a mobile app.

Here’s a possible solution: Move the download activity to a helper method, and call this method only when the item appears on the screen.

In Artwork.swift, comment out init() and add this method:

func load() {
  print(">>>>> Downloading \(self.imageName) <<<<<")
}

Back in ContentView.swift, modify the List row:

Text("\(artwork.reaction) \(artwork.title)")
  .onAppear { artwork.load() }

This calls load() only when the row of this Artwork is on the screen.

Run the app in simulator again:

>>>>> Downloading 002_200105 <<<<< >>>>> Downloading 19300102 <<<<< >>>>> Downloading 193701 <<<<< >>>>> Downloading 193901-5 <<<<< >>>>> Downloading 195801 <<<<< >>>>> Downloading 198912 <<<<< >>>>> Downloading 196001 <<<<< >>>>> Downloading 193301-2 <<<<< >>>>> Downloading 193101 <<<<< >>>>> Downloading 199909 <<<<< >>>>> Downloading 199103-3 <<<<< >>>>> Downloading 197613-5 <<<<< >>>>> Downloading 199802 <<<<<

This time, the last four items — the ones that aren’t visible — haven’t “downloaded”. Scroll the list to see their message appear in the console.

Where to Go From Here?

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

In this tutorial, you used SwiftUI to implement the navigation of a master-detail app. You implemented a navigation stack, a navigation bar button, and a context menu, as well as a tab view. And you picked up one technique to prevent too-eager evaluation of your data items.

Apple’s WWDC sessions and SwiftUI tutorials are the source of everything, but you’ll also find the most up-to-date code in our book SwiftUI by Tutorials.

We hope you enjoyed this tutorial, and if you have any questions or comments, please join the forum discussion below!


Source link

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button
WP Twitter Auto Publish Powered By : XYZScripts.com
SiteLock