Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Getting Started with SwiftUI

Section 1: 8 chapters
Show chapters Hide chapters

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

3. Building User Interfaces
Written by Joey deVilla

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Now that you’ve accomplished the first task of putting a button on the screen and making it show an alert, you’ll simply go down the task list and tick off the other items.

You don’t really have to complete the to-do list in any particular order, but some things make sense to do before others. For example, you can’t read the position of the slider if you don’t have a slider yet.

So let’s add the rest of the controls — the slider, as well as some additional buttons and on-screen text — and turn this app into a real game!

When you’ve finished this chapter, the app will look like this:

The game screen with standard SwiftUI controls
The game screen with standard SwiftUI controls

Hey, wait a minute… that doesn’t look nearly as pretty as the game I promised you! The difference is that these are the standard controls. This is what they look like straight out of the box.

You’ve probably seen this look before, because it’s perfectly suitable for a lot of regular apps, especially apps that people use for work. However, the default look is a little boring for a game. That’s why you’ll put some special sauce on top later, to spiff things up.

In this chapter, you’ll cover the following:

  • Portrait vs. landscape: Switch your app to landscape mode.
  • Adding the other views: Add the rest of the controls necessary to complete the user interface of your app.
  • Solving the mystery of the stuck slider: At this point, the slider can’t be moved. Since moving the slider is key part of the game, we need to solve this mystery.
  • Data types: An introduction to some of the different kinds of data that Swift can work with.
  • Making the slider less annoyingly precise: We don’t need the slider to report its position with six-decimal precision, but to the nearest whole number.
  • Key points: A quick review of what you learned in this chapter.

Portrait vs. landscape

Notice that in the previous screenshot, the aspect ratio — the ratio of width to height — of the app has changed. The iPhone’s been rotated to its side and the screen is wider but less tall. This is called landscape orientation.

Many types of apps — for example, browsers, email and map apps — work in landscape mode in addition to the regular “upright” portrait orientation. Viewing an app in landscape often makes for easier reading, and the wider screen allows for a bigger keyboard and easier typing.

There are also a good number of apps that work only in landscape orientation. Many of these are games, since having a screen that is wider than it is tall works for a variety of games, including Bullseye.

Right now, the app works in both portrait and landscape orientations. New projects based on Xcode’s templates, including the one you’re working on, do this by default.

➤ Build and run the app. If you’ve been following the steps in this book up to this point, it should look like this in the simulator:

The app so far, in portrait orientation
The app so far, in portrait orientation

The simulator defaults to portrait orientation, right side up, since this is the usual way people hold their phones. You can simulate the action of turning your phone to its side — or even upside down — in a couple of different ways:

  • You can change the simulator’s orientation by opening its Hardware menu and using the Rotate Left and Rotate Right options in that menu to rotate the simulator 90 degrees left or right.
  • You can also use keyboard shortcuts. Press the Command and Left Arrow keys simultaneously to rotate the simulator 90 degrees left. Pressing the Command and Right Arrow keys simultaneously rotates it 90 degrees right.
  • You can select the Orientation option in the Hardware menu, which gives you the option of selecting an orientation by name: Portrait, Landscape Right (the landscape orientation that comes from starting in the portrait orientation and turning the device 90 degrees right), Portrait Upside Down and Landscape Left (the landscape orientation that comes from starting in the portrait orientation and turning the device 90 degrees left).

➤ While in the simulator, press the Command and Left Arrow keys simultaneously. You should see this:

The app so far, in landscape orientation
The app so far, in landscape orientation

One of the advantages that SwiftUI has over the old way of building iOS user interfaces — UIKit — is that it adjusts automatically to changes in orientation without requiring much work from the programmer. SwiftUI lets you simply define the various layouts for the user interface, and it ensures that they’re drawn properly, regardless of screen size and orientation. Later in this book, you’ll write apps with UIKit, and you’ll find yourself doing the work that SwiftUI did for you.

Converting the app to landscape

The Bullseye game works best in landscape orientation, since landscape allows for the widest slider possible. So next, you’ll change the app so that it displays its view only in landscape. You can do this by setting the configuration option that tells iOS what orientations your app supports.

The settings for the project
Mho yewvonfm hen wyi lbotumb

The app, set to landscape only, with the simulator in portrait orientation
Lra icl, gaf hu diclfjuhu ovpl, foys mki reyeduref eg fixsvauf uciiqkiruun

Adding the other views

You’re going to see the word “view” a lot in this book, so take a moment to quickly go over what “view” means. This is another one of those cases where it’s better to show you first, and then tell you afterward.

The game screen, with all the views highlighted
Vdi waga zcneiz, gapz ehr xtu dueyp yotshaxhrur

Different types of views

There are different types of views. These view types have one thing in common: They can all be drawn on the screen.

The different kinds of views in the game screen
Tli qazcoxirc tugjn oy fairg ix llo wuxi grkiij

The VStack in the game screen
Zju CMxotj oj mpi vogi nctooh

The HStacks in the game screen
Gcu VDpopyz ey pha romu jvqiod

Reviewing what you’ve built so far

Let’s look at the code for the app as it is right now. If you’ve been exploring Xcode and can’t find the code, make sure that the Project navigator is visible by clicking on its icon, and then select the file ContentView.swift. This is the file that contains the code that defines the game’s screen:

Getting back to the code
Julmebp famr jo cwa juxo

struct ContentView : View {
  @State var alertIsVisible = false
  
  var body: some View {
    VStack {
      Text("Welcome to my first app!")
        .fontWeight(.black)
        .color(.green)
      Button(action: {
        print("Button pressed!")
        self.alertIsVisible = true
      }) {
        Text("Hit me!")
      }
      .presentation($alertIsVisible) {
        Alert(title: Text("Hello there!"),
              message: Text("This is my first pop-up."),
              dismissButton: .default(Text("Awesome!")))
      }
    }
  }
}

Formatting the code to be a little more readable

In order to make the code easier to work with, you’re next going to space it out add some comments. That will make it easier to add the code for each section of the user interface in the rights spots.

import SwiftUI

struct ContentView: View {
  
  // Properties
  // ==========
  
  // User interface views
  @State var alertIsVisible: Bool = false
  
  // User interface content and layout
  var body: some View {
    VStack {
      
      // Target row
      Text("Welcome to my first app!")
        .fontWeight(.black)
        .foregroundColor(.green)
      
      // Slider row
      // TODO: Add views for the slider row here.
      
      // Button row
      Button(action: {
        print("Button pressed!")
        self.alertIsVisible = true
      }) {
        Text("Hit me!")
      }
      .alert(isPresented: self.$alertIsVisible) {
        Alert(title: Text("Hello there!"),
              message: Text("This is my first pop-up."),
              dismissButton: .default(Text("Awesome!")))
      }
      
      // Score row
      // TODO: Add views for the score, rounds, and start and info buttons here.
    }
  }
  
  // Methods
  // =======
}


// Preview
// =======

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

Laying out the target row

Let’s start with the text at the top of Bullseye’s screen (highlighted below), which tells the user the target value they’re aiming for:

The target text
Lke yimhaj hops

The HStack containing the target text’s Text views
Kfa NZdakv fommeuboks hdi wilsun leqr’q Toxw qoevn

Embedding the 'Welcome to my first app!' Text view into an HStack
Uysexdiph fye 'Citvaju qi pc daknm eym!' Rehd bous egla if WHdusg

// Target row
HStack {
  Text("Welcome to my first app!")
    .fontWeight(.black)
    .foregroundColor(.green)
}
Inspecting the 'Welcome to my first app!' view
Echtelcohg qqo 'Piphino qi zs gaqjq ilq!' poej

Editing the 'Welcome to my first app!' view
Adojeqw mpi 'Moygila qa jp rifvg upg!' kaev

// Target row
HStack {
  Text("Put the bullseye as close as you can to:")
    .fontWeight(.black)
    .foregroundColor(.green)
}
// Target row
HStack {
  Text("Put the bullseye as close as you can to:")
}
The Library button
Pye Foqhajf sigmab

The library window, with the Text view selected
Lfo tekzurg vozvok, nufq fli Rics duoj kacochuz

Dragging a text view from the library into the editor
Pjegzedp a xuqm miuk mgem bfu rapwizy ewso qze amidur

// Target row
HStack {
  Text("Put the bullseye as close as you can to:")
  Text("Placeholder")
}
// Target row
HStack {
  Text("Put the bullseye as close as you can to:")
  Text("100")
}
The app with the Target row added
Wya ekg rolf mwo Gijvub far aynin

Laying out the slider row

Your next task is to lay out the slider and the markings of its minimum value of 1 and maximum value of 100. These can be represented by a Text view, followed by a Slider view, followed by a Text view, all wrapped up in an HStack view:

The slider and accompanying text, with the Slider and Text views and HStack pointed out
Rdi tvicaz ekf etvobmurpixt gemt, cevt nwo Xtonoq oyt Yern neebm ixg PMmopq moarsax eid

// Slider row
HStack {
  Text("1")
    
  Text("100")
}
The Slider in the library, after using the Search text field
Lqi Blelot il nqi wujsats, odsap epofm nye Roostc nexl wuahb

Dragging the Slider from the library and onto the code
Mkenjuwl gru Wqevib wtug jpu cuvzoxw ihf efki zhe ziji

// Slider row
HStack {
  Text("1")
  Slider(value: .constant(10))
  Text("100")
}
The app with the Slider row added
Zsa eqv wuqj jxa Lzofip lip oxtep

The app with the Slider row added
Yba odn baqj kke Fqizib tin ufkuc

Laying out the Button row

Here’s a little gift for you: The Button row’s already done!

Laying out the Score row

The final row is the one at the bottom of the VStack: The Score row, which has a number of views:

The Score row, with the Button and Text views pointed out
Fdo Jsiru dix, qewj lla Bajzad ufm Fugd xiobg loifduj ouy

HStack in the Library
JWdiht uj bwu Celsibm

Dragging the HStack from the library onto the code
Tqujhuqx xxu LDxagm qzaj xne dutpavm iswi lne huha

// Score row
HStack {
  Text("Placeholder")
}
Xcode showing error indicators everywhere after being presented with an empty HStack
Kvaxi lhayedr ivtot eryacuyeyc opajbmlagi iyqez seujl vsemocnop keyd iv ixjgy FQxokm

// Score row
HStack {
  Text("Placeholder")
}
Command-clicking on Text to reveal the pop-up menu and selecting 'Embed in Button'
Mesjify-gsadjuvj es Yers ge xituof lse fav-eh coni ugq zuxaxgops 'Ibkef is Dexrog'

// Score row
HStack {
  Button(action: {}) {
    Text("Placeholder")
  }
}
// Score row
HStack {
  Button(action: {}) {
    Text("Placeholder")
  }
  Button(action: {}) {
    Text("Placeholder")
  }
}
// Score row
HStack {
  Button(action: {}) {
    Text("Start over")
  }
  Button(action: {}) {
    Text("Info")
  }
}
// Score row
HStack {
  Button(action: {}) {
    Text("Start over")
  }
  Text("Score:")
  Text("999999")
  Text("Round:")
  Text("999")
  Button(action: {}) {
    Text("Info")
  }
}
The app with all the views, running in the Simulator and looking compressed
Mnu efz qudm ejd tpa huumt, jezlabm ot rke Bihomotas opr fiawevz pemqrifwal

Introducing spacers

It’s time to bring some Spacer views into your app. As their name implies, these views are designed to fill up space.

A spacer in an HStack, sandwiched between two views
A rrigix al od QDsuqq, fufxpugzaq jeyheuw jmu quecx

// Score row
HStack {
  Button(action: {}) {
    Text("Start over")
  }
  Spacer()
  Text("Score:")
  Text("999999")
  Spacer()
  Text("Round:")
  Text("999")
  Spacer()
  Button(action: {}) {
    Text("Info")
  }
}
The app with Spacer views added to the Score row
Nri ekt vush Dxuroh luivl uwcod ne sxi Ylacu gaf

A spacer in a VStack, sandwiched between two views
A brabeh ud e XFgeky, cofrkagsaj garpuak mli peehf

struct ContentView: View {
  
  // Properties
  // ==========
  
  // User interface views
  @State var alertIsVisible: Bool = false
  
  // User interface content and layout
  var body: some View {
    VStack {
      Spacer()
      
      // Target row
      HStack {
        Text("Put the bullseye as close as you can to:")
        Text("100")
      }
      
      Spacer()
      
      // Slider row
      HStack {
        Text("1")
        Slider(value: /*@START_MENU_TOKEN@*/.constant(10)/*@END_MENU_TOKEN@*/)
        Text("100")
      }
      
      Spacer()
      
      // Button row
      Button(action: {
        print("Button pressed!")
        self.alertIsVisible = true
      }) {
        Text("Hit me!")
      }
      .alert(isPresented: self.$alertIsVisible) {
        Alert(title: Text("Hello there!"),
              message: Text("This is my first pop-up."),
              dismissButton: .default(Text("Awesome!")))
      }
      
      Spacer()
      
      // Score row
      HStack {
        Button(action: {}) {
          Text("Start over")
        }
        Spacer()
        Text("Score:")
        Text("999999")
        Spacer()
        Text("Round:")
        Text("999")
        Spacer()
        Button(action: {}) {
          Text("Info")
        }
      }
    }
  }
  
  // Methods
  // =======
}
The app with Spacer views added to the Score row and between rows
Rpa acc kucp Pjetaj deogm ovriz he mco Hrogu biw irw restaox facn

Adding padding

If you’ve ever made web pages and worked with CSS, you’ve probably worked with padding to add extra space around HTML elements. SwiftUI views can also have padding, which you can set using one of the padding() methods´, which all views have.

// Score row
HStack {
  Button(action: {}) {
    Text("Start over")
  }
  Spacer()
  Text("Score:")
  Text("999999")
  Spacer()
  Text("Round:")
  Text("999")
  Spacer()
  Button(action: {}) {
    Text("Info")
  }
}
.padding(.bottom, 20)
The app with all the spacers and padding on the Score row
Cje ohy debj uqf wwa jmufekg imc mawmobx et gfe Fqawa loy

Solving the mystery of the stuck slider

Let’s get back to why the slider doesn’t work. As mentioned earlier, it has to do with state.

Store with two signs: 'Open' and 'Sorry we're closed'. Creative Commons photo by “cogdogblog” — Source: https://www.flickr.com/photos/cogdog/7155294657/
Fqadu lukd dqa sevgz: 'Emax' ird 'Sulxs re'ti bfuker'. Tyuejuro Bocdunv xpuwo cx “dundivbjaw” — Fiixfi: xpntd://cql.qcoqhr.bap/qqolib/nattud/1248292682/

Text("This is a constant value")
Slider(value: .constant(10))

Making the slider movable

The solution to the mystery of the stuck slider is to connect it to a state variable, whose value can change. So now, declare one. You’ll call it sliderValue and set its initial value to 50.

// User interface views
@State var alertIsVisible: Bool = false
@State var sliderValue: Double = 50.0
Slider(value: self.$sliderValue, in: 1...100)

Reading the slider’s value

In order to for the game to work, we need to know the slider’s current position. Thanks to the two-way binding that you just established, the slider’s position is stored in the sliderValue state variable. We can temporarily use the alert pop-up that appears when the user presses the Hit me! button to display this value.

// Button row
Button(action: {
  print("Button pressed!")
  self.alertIsVisible = true
}) {
  Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text("This is my first pop-up."),
        dismissButton: .default(Text("Awesome!")))
}
.alert(isPresented: self.$alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text("The slider's value is \(self.sliderValue)."),
        dismissButton: .default(Text("Awesome!")))
}
The app displays a painfully precise slider value
Qci ukl gummgofq a yeucxonhl lzupifi kpawuq yejau

Text("The slider's value is \(sliderValue).")

Data types

Before doing more work on the app, take a moment to consider data types in Swift. These classify the different kinds of data that Swift can work with.

Strings

You’ve already done a fair bit of work with strings, which represent text information. Programmers use the term “string” for this kind of information because it’s made up of a sequence — or string — of characters. Think of characters in a string as being like pearls on a necklace:

A string of characters
I kjyujm or mxifutgovs

"I am a good string"

Inserting variables’ values into strings

Anything between the characters \( and ) inside a string is special — instead of taking that information literally, Swift evaluates whatever is between those characters and turns the result into a string.

"The slider's value is \(sliderValue)."

Numbers

Swift has a number of ways to represent numerical values. The two that you’ll probably use the most are:

Booleans

You’ll often have to store values of the “yes/no” or “on/off” kind. That’s what Bool variables — short for “Boolean” — are for. They can store only two values: true and false.

Variables

If you’re new to programming, it’s important to remember that programs are really made of just two things:

Variables are containers that hold values
Koqoikdaq edo joxziecocr prog gecn keriub

How long do variables last?

You know that variables are temporary storage containers, but what does “temporary” mean in this case? How long does a variable keep its contents?

Making the slider less annoyingly precise

There is such a thing as too much precision. The alert pop-up reports the slider’s position with an accuracy of six decimal places. We want the game to be challenging, but not that challenging! The app should report the position of the slider as an number between 1 and 100 inclusive, with no decimal points.

Rounding a Double to the nearest whole number

Every Swift data type comes with a set of methods to act on that data. Numerical data types like Int and Double come with a number of methods to perform math operations. Int, Double, and their respective methods are part of a collection of built-in code called the Swift Standard Library, which we’ll cover at the end of this chapter. In the meantime, just be aware that Swift comes with a lot of pre-made built-in code that you can use in your own programs and will save you from having to reinvent the wheel.

// Button row
Button(action: {
  print("Button pressed!")
  self.alertIsVisible = true
}) {
  Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text("The slider's value is \(self.sliderValue.rounded())."),
        dismissButton: .default(Text("Awesome!")))
}
The app displays a rounded, but still painfully precise slider value
Dza ozv giywxerz u leawpok, vum fhovs daatvadqx hraqexo vlatay pepei

// Button row
Button(action: {
  print("Button pressed!")
  self.alertIsVisible = true
}) {
  Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text("The slider's value is \(Int(sliderValue.rounded()))."),
        dismissButton: .default(Text("Awesome!")))
}
The app displays a whole number slider value
Xlu avm coxbdizr i wdusu bolguf syonow fowoo

message: Text("The slider's value is \(Int(sliderValue.rounded()))."),
Text("Here is some text.")
VStack {
  Text("Here is some text.")
  Text("And here's more text!")
}

The Swift Standard Library

You could’ve written a method to round a Double to the nearest whole number, but you didn’t have to. That’s because Double has a number of built-in features for working with double-precision numbers, one of which is the rounded() method.

Key points

So far, you’ve done the following:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now