SwiftUI Views & Layouts

Jun 20 2024 · Swift 5.10, iOS 17.4, Xcode 15.3

Lesson 03: Building Views

Demo 2

Episode complete

Play next episode

Next
Transcript

ViewThatFits will choose the view that better fits the current available space. But before you start using it, you’ll refactor code and create custom views that ViewThatFits may choose from.

Start by creating a new SwiftUI file and name it PortraitColorPicker.swift. Next, add the following properties:

@Binding var color: Color
@Binding var red: Double
@Binding var green: Double
@Binding var blue: Double

Much like the view you created in the previous lesson, RGBSlidersStackView, you’ll use these properties to bind the values of the sliders and the color rectangle to the controls of the view.

Next, replace the contents of the body with the following:

VStack(spacing: 16) {
  ColorCardView(color: color)

  RGBSlidersStackView(
    color: $color,
    red: $red,
    green: $green,
    blue: $blue
  )
}

Finally, to fix the preview compilation error, replace the contents of the preview with the following code:

PortraitColorPicker(
  color: .constant(.red),
  red: .constant(50),
  green: .constant(50),
  blue: .constant(50)
)
.padding()

PortraitColorPicker uses a VStack to arrange its views in a vertical way. You’ll use this view when there’s more available vertical space, in other words, when the device is in portrait orientation.

Next, you’ll create the view you’ll use when the device is in landscape orientation.

Create another SwiftUI file and name it LandscapeColorPicker.swift. Once again, add the following code:

@Binding var color: Color
@Binding var red: Double
@Binding var green: Double
@Binding var blue: Double

Exactly like PortraitColorPicker, you’ll use these properties to bind the values to the view.

Then, add the following code inside the body of the view:

HStack(spacing: 16) {
  ColorCardView(color: color)

  RGBSlidersStackView(
    color: $color,
    red: $red,
    green: $green,
    blue: $blue
  )
}

Unlike PortraitColorPicker, LandscapeColorPicker uses a HStack to arrange the views in a horizontal way, making better use of the available horizontal space.

Before you move on back to ContentView.swift, you’ll change the preview code to preview LandscapeColorPicker in landscape orientation. Replace the preview code with the following:

#Preview(traits: .landscapeLeft) {
  LandscapeColorPicker(
    color: .constant(.red),
    red: .constant(50),
    green: .constant(50),
    blue: .constant(50)
  )
}

This preview uses the .landscapeLeft trait so that the canvas displays the iPhone in landscape orientation.

Now, back in ContentView.swift, find and remove the following code:

@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass

var landscapeIsCompact: Bool {
  horizontalSizeClass == .compact && verticalSizeClass == .compact ||
    horizontalSizeClass == .regular && verticalSizeClass == .compact
}

var layout: AnyLayout {
  landscapeIsCompact ?
    AnyLayout(HStackLayout(spacing: 16)) :
    AnyLayout(VStackLayout(spacing: 16))
}

ViewThatFits already decides for you which view fits best on screen, so you don’t need this code to decide which layout to use and when.

Finally, replace the contents of the body with the following code:

ViewThatFits {
  PortraitColorPicker(
    color: $color,
    red: $red,
    green: $green,
    blue: $blue
  )

  LandscapeColorPicker(
    color: $color,
    red: $red,
    green: $green,
    blue: $blue
  )
}
.padding()

You pass both views, PortraitColorPicker and LandscapeColorPicker to ViewThatFits. It will calculate which of the two views fits the screen best, given the available space.

Build and run the project. Switch between landscape orientation and portrait orientation.

ViewThatFits chooses PortraitColorPicker when the device is in portrait orientation. That’s when there’s more vertical space available. But when you rotate the device to landscape orientation, ViewThatFits chooses LandscapeColorPicker as there’s more available space on the horizontal axis.

Comparing the current code with the code you had at the beginning of this lesson, it might seem like you have more code to achieve the same behavior of AnyLayout. And while that might be true, you also have way more flexibility to improve and polish each UI to its context, adding or removing views depending on the device and if it is in portrait or landscape orientation.

Having separate custom view types also allows you to reuse those views in different layouts for bigger devices, like the iPad.

See forum comments
Cinema mode Download course materials from Github
Previous: Instruction 2 Next: Conclusion