BLACK FRIDAY: Save 50% on all books and bundles! >>

Creating a secondary view for NavigationView

Paul Hudson    @twostraws   

Right now our NavigationLink directs the user to some sample text, which is fine for prototyping but obviously not good enough for our actual project. We’re going to replace that with a new ResortView that shows a picture from the resort, some description text, and a list of facilities.

Important: Like I said earlier, the content in my example JSON is mostly fictional, and this includes the photos – these are just generic ski photos taken from Unsplash. Unsplash photos can be used commercially or non-commercially without attribution, but I’ve included the photo credit in the JSON so you can add it later on. As for the text, this is taken from Wikipedia. If you intend to use the text in your own shipping projects, it’s important you give credit to Wikipedia and its authors and make it clear that the work is licensed under CC-BY-SA available from here: https://creativecommons.org/licenses/by-sa/3.0.

To start with, our ResortView layout is going to be pretty simple – not much more than a scroll view, a VStack, an Image, and some Text. The only interesting part is that we’re going to show the resort’s facilities as a single text view using resort.facilities.joined(separator: ", ") to get a single string.

Replace the default ResortView with this:

struct ResortView: View {
    let resort: Resort

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 0) {
                Image(decorative: resort.id)
                    .resizable()
                    .scaledToFit()

                Group {
                    Text(resort.description)
                        .padding(.vertical)

                    Text("Facilities")
                        .font(.headline)

                    Text(resort.facilities.joined(separator: ", "))
                        .padding(.vertical)
                }
                .padding(.horizontal)
            }
        }
        .navigationBarTitle(Text("\(resort.name), \(resort.country)"), displayMode: .inline)
    }
}

You’ll also need to update ResortView_Previews to pass in an example resort for Xcode’s preview window:

struct ResortView_Previews: PreviewProvider {
    static var previews: some View {
        ResortView(resort: Resort.example)
    }
}

And now we can update the navigation link in ContentView to point to our actual view, like this:

NavigationLink(destination: ResortView(resort: resort)) {

There’s nothing terribly interesting in our code so far, but that’s going to change now because I want to add more details to this screen – how big the resort is, roughly how much it costs, how high it is, and how deep the snow is.

We could just put all that into a single HStack in ResortView, but that restricts what we can do in the future. So instead we’re going to group them into two views: one for resort information (price and size) and one for ski information (elevation and snow depth).

The ski view is the easier of the two to implement, so we’ll start there: create a new SwiftUI view called SkiDetailsView and give it this code:

struct SkiDetailsView: View {
    let resort: Resort

    var body: some View {
        VStack {
            Text("Elevation: \(resort.elevation)m")
            Text("Snow: \(resort.snowDepth)cm")
        }
    }
}

struct SkiDetailsView_Previews: PreviewProvider {
    static var previews: some View {
        SkiDetailsView(resort: Resort.example)
    }
}

As for the resort details, this is a little trickier because of two things:

  1. The size of a resort is stored as a value from 1 to 3, but really we want to use “Small”, “Average”, and “Large” instead.
  2. The price is stored as a value from 1 to 3, but we’re going to replace that with $, $$, or $$$.

As always, it’s a good idea to get calculations out of your SwiftUI layouts so it’s nice and clear, so we’re going to create two computed properties: size and price.

Start by creating a new SwiftUI view called ResortDetailsView, and give it this property:

let resort: Resort

As with ResortView, you’ll need to update the preview struct to use some example data:

struct ResortDetailsView_Previews: PreviewProvider {
    static var previews: some View {
        ResortDetailsView(resort: Resort.example)
    }
}

When it comes to getting the size of the resort we could just add this property to ResortDetailsView:

var size: String {
    ["Small", "Average", "Large"][resort.size - 1]
}

That works, but it would cause a crash if an invalid value was used, and it’s also a bit too cryptic for my liking. Instead, it’s safer and clearer to use a switch block like this:

var size: String {
    switch resort.size {
    case 1:
        return "Small"
    case 2:
        return "Average"
    default:
        return "Large"
    }
}

As for the price property, we can leverage the same repeating/count initializer we used to create example cards in project 17: String(repeating:count:) creates a new string by repeating a substring a certain number of times.

So, please add this second computed property to ResortDetailsView:

var price: String {
    String(repeating: "$", count: resort.price)
}

Now what remains in the body property is simple, because we just use the two computed properties we wrote:

var body: some View {
    VStack {
        Text("Size: \(size)")
        Text("Price: \(price)")
    }
}

That completes our two mini views, so we can now drop them into ResortView with spacers on either side to make sure they are centered – put this into the group in ResortView, directly before the resort description:

HStack {
    Spacer()
    ResortDetailsView(resort: resort)
    SkiDetailsView(resort: resort)
    Spacer()
}
.font(.headline)
.foregroundColor(.secondary)
.padding(.top)

We’re going to add to that some more in a moment, but first I want to make one small tweak: using joined(separator:) does an OK job of converting a string array into a single string, but we’re not here to write OK code – we’re here to write great code.

Apple’s Foundation library comes with a better solution called ListFormatter, which only has one job: to convert an array of strings into a string. The difference is that rather than sending back “A, B, C” like we have right now, we get back “A, B, and C” – it’s more natural to read.

To use ListFormatter, replace the current facilities text view with this:

Text(ListFormatter.localizedString(byJoining: resort.facilities))
    .padding(.vertical)

Much better!

Save 50% in my Black Friday sale.

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.6/5

Link copied to your pasteboard.