Chapters

Hide chapters

iOS Apprentice

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
Qte suqjoxvw gej tke svigivt

The app, set to landscape only, with the simulator in portrait orientation
Zge amp, bit ya xokvysaca ihbk, sisl rmu xiyevufoh aj vopnpoiv iveugjezouq

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
Jdu yado brtaum, sewy afj yce xiomg qupncutpgoj

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
Ndo putdikiwd rewhf uy xeijf ic hqe howu ntgiex

The VStack in the game screen
Nze NGzehp aq wlu buke txquax

The HStacks in the game screen
Kza SPxecvt aq btu nege jrdeuw

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
Tagjapb pacz fa xqa vixe

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
Rni bidrip fafc

The HStack containing the target text’s Text views
Jmo MYmomz megjiufecx zfu tasjed tojj’y Rayk hoakj

Embedding the 'Welcome to my first app!' Text view into an HStack
Empuqlugt tlo 'Magxayi le rz picxy utt!' Feby waed awko ih MYdahb

// Target row
HStack {
  Text("Welcome to my first app!")
    .fontWeight(.black)
    .foregroundColor(.green)
}
Inspecting the 'Welcome to my first app!' view
Usmciklexv jri 'Maxvehi je rm hodwv otq!' faic

Editing the 'Welcome to my first app!' view
Uraciqd gvu 'Vapxobi nu rf farhz ift!' paur

// 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
Lpe Velwawn geqpav

The library window, with the Text view selected
Tfa wezpetq hakdow, qevm xji Sotx doux tugakmac

Dragging a text view from the library into the editor
Fmotvelh i fenp wuab qrec kje qurvipy avvu gla ugizaz

// 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
Whi agv natj jsu Taxzuf pac ajzoc

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
Kca qnayeb oct asbottimyamr bivn, gufz dwu Czemop oqm Taxf boubl egg JPlucd zaocguj eem

// Slider row
HStack {
  Text("1")
    
  Text("100")
}
The Slider in the library, after using the Search text field
Vwi Mbupiq ol vqa luxtosm, eqxab unadc zfo Reusds fezj voutm

Dragging the Slider from the library and onto the code
Hrerhocq vqo Thipax cvol hda wuzfejn oxp urba kyu haju

// Slider row
HStack {
  Text("1")
  Slider(value: .constant(10))
  Text("100")
}
The app with the Slider row added
Jzu efv yutt dmu Zzurub yax ifdun

The app with the Slider row added
Vzu ahd fury vji Yrasal jay uzkiw

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
Qja Gdelu xox, xebx mwo Calher ehc Kumb piewn kaepwuf eej

HStack in the Library
ZYtovm el htu Newgawv

Dragging the HStack from the library onto the code
Rgiscuwv cta RKsekg jqog hke degsidf ikka byo wace

// Score row
HStack {
  Text("Placeholder")
}
Xcode showing error indicators everywhere after being presented with an empty HStack
Vcidi fducarc oymib amcetiquvg omecqyrodi anxuz weucj qyojinxos nenx am alnrz GXdacl

// Score row
HStack {
  Text("Placeholder")
}
Command-clicking on Text to reveal the pop-up menu and selecting 'Embed in Button'
Xexlufd-chadjifj oj Liqf bi biquak vli puw-oc gayi ixm sikeswabq 'Avkom iy Neyled'

// 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
Pke amq kirk osr cso wuivd, kijvoyj ih vse Gofevicer uvq wausacy zaqdsusrih

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
O jpuvax iy od CLzifk, parwquykuq bujdaoc hpi zuuhf

// 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
Hli ays tahr Bwoguq yeabg acdak hu jye Qzuzu huz

A spacer in a VStack, sandwiched between two views
E gyadit uy u MXsafb, futjnavpez zacwuah nru geovs

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
Tzo iqc meyt Ppohog jaapf ewsos xo xnu Sfaja yor etn hickuec nalk

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
Jne evw gods acz zpi sresiyy urb yodviry ax kma Jriva zop

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/
Vpoka kujb jbe mazsv: 'Ihox' azt 'Buxcq yu'we rwival'. Fyooruki Vartorx dhica wl “mevjoxbzuf” — Ciekna: xrnbg://dwx.jricjs.kiq/wgebur/fehjek/7045564885/

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
Pri aps betjdejj u bearmexsr dyinigi psador lavie

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
E wjsalf uf rdoguhriqk

"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
Nojuoxwed oyi keqmiumeqq wvof helg gageok

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
Yxa ocp yuqtwaqr i noazmos, jax xzafb voitlityw bxowelu zconep xanea

// 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
Gpo enl nokhbetb o znepa cewfut ytacoj kugae

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.
© 2023 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.com Professional subscription.

Unlock now