Right now our NavigationLink
doesn't send the user anywhere, which is fine for prototyping but obviously not good enough for our actual project. So, in this step we're going to add 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.
Create a new SwiftUI view called ResortView
, and give it this code to start with:
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)
}
}
.navigationTitle("\(resort.name), \(resort.country)")
.navigationBarTitleDisplayMode(.inline)
}
}
You’ll also need to update your preview code to pass in an example resort for Xcode’s preview window:
#Preview
ResortView(resort: .example)
}
And now we can update ContentView
to point to our actual view – add this modifier after navigationTitle()
there:
.navigationDestination(for: Resort.self) { resort in
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 {
Group {
VStack {
Text("Elevation")
.font(.caption.bold())
Text("\(resort.elevation)m")
.font(.title3)
}
VStack {
Text("Snow")
.font(.caption.bold())
Text("\(resort.snowDepth)cm")
.font(.title3)
}
}
.frame(maxWidth: .infinity)
}
}
#Preview {
SkiDetailsView(resort: .example)
}
Giving the Group
view a maximum frame width of .infinity
doesn’t actually affect the group itself, because it has no impact on layout. However, it does get passed down to its child views, which means they will automatically spread out horizontally.
As for the resort details, this is a little trickier because of two things:
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:
#Preview {
ResortDetailsView(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: "Small"
case 2: "Average"
default: "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 {
Group {
VStack {
Text("Size")
.font(.caption.bold())
Text(size)
.font(.title3)
}
VStack {
Text("Price")
.font(.caption.bold())
Text(price)
.font(.title3)
}
}
.frame(maxWidth: .infinity)
}
Again, giving the whole Group
an infinite maximum width means these views will spread out horizontally just like those from the previous view.
That completes our two mini views, so we can now drop them into ResortView
– put this just before the group in ResortView
:
HStack {
ResortDetailsView(resort: resort)
SkiDetailsView(resort: resort)
}
.padding(.vertical)
.background(.primary.opacity(0.1))
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 okay job of converting a string array into a single string, but we’re not here to write okay code – we’re here to write great code.
Previously we’ve used the format
parameter of Text
to control the way numbers are formatted, but there’s a format for string arrays too. This is similar to using joined(separator:)
, but 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.
Replace the current facilities text view with this:
Text(resort.facilities, format: .list(type: .and))
.padding(.vertical)
Notice how the .and
type is there? That’s because you can also use .or
to get “A, B, or C” if that’s what you want.
Anyway, it’s a tiny change but I think it’s much better!
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's all new Paywall Editor allow you to remotely configure your paywall view without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.