Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Checklists

Section 2: 12 chapters
Show chapters Hide chapters

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 12 chapters
Show chapters Hide chapters

46. Polishing Bullseye
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.

Bullseye works! The gameplay elements are complete. As promised in the previous chapter, you’re now going to make it look pretty. SwiftUI makes this rather easy.

You’ll also do a little refactoring. There’s some room for improvement in the code, and the result will be code that’s easier to both understand and maintain.

In this chapter, you’ll cover the following:

  • Spicing up the graphics: You’ll learn the SwiftUI way to break views free from their default appearance and even create reusable styles.
  • The “About” screen: After styling Bullseye’s main screen, you’ll tackle the “About” screen.
  • Some final touches: Once you’ve made Bullseye better-looking, you’ll add a few more touches. There’s always room for improvement!

Spicing up the graphics

Getting rid of the status bar is only the first step. We want to go from this…

How the app looks now
How the app looks now

…to this:

How the app will look in the end
How the app will look in the end

In making these changes to the app’s look, you’ll add images to views, and even add additional views within existing views. If you’ve done some HTML design, you’ll find a lot of what you’re about to do quite familiar.

Adding the image assets

Like UIKit projects, SwiftUI uses assets stored in good ol’ Assets.xcassets. Let’s add the Bullseye images to the project.

Dragging files into the asset catalog
Tnacbihl veyuv ivdu jqa orher dimutam

The images are now inside the asset catalog
Jle oduhet ica haf utgeto rsu ebzam zinubor

Putting up the wallpaper

Let’s begin by replacing Bullseye’s drab white background with the more appealing Background image that you added to the app’s asset catalog:

The background image
Xmi xiyxdhuavh icuso

var body: some View {
  VStack {
    Spacer()

    // Target row
...
    // Score row
    HStack {
      Button(action: {
        self.startNewGame()
      }) {
        Text("Start over")
      }
      Spacer()
      Text("Score:")
      Text("\(self.score)")
      Spacer()
      Text("Round:")
      Text("\(self.round)")
      Spacer()
      NavigationLink(destination: AboutView()) {
        Text("Info")
      }
    }
    .padding(.bottom, 20)
  }
  .background(Image("Background"))
}
The 2x background on the iPhone 8
Xno 5x barvzteuqj af xdo oWyexa 5

Changing the text

Now that Bullseye has its new background image, the black text is now nearly illegible. We’ll need to change it so that it stands out better. Once again, we’ll use some built-in methods to change the text’s appearance so that it’s legible against the background. Let’s start with the “Put the bullseye as close as you can to:” and target value text.

// Target row
HStack {
  Text("Put the bullseye as close as you can to:")
    .font(Font.custom("Arial Rounded MT Bold", size: 18))
    .foregroundColor(Color.white)
    .shadow(color: Color.black, radius: 5, x: 2, y: 2)
  Text("\(target)")
    .font(Font.custom("Arial Rounded MT Bold", size: 24))
    .foregroundColor(Color.yellow)
    .shadow(color: Color.black, radius: 5, x: 2, y: 2)
}
The target row, with styled text
Hpi xobbis hif, wedb wswvub kilj

// Slider row
HStack {
  Text("1")
    .font(Font.custom("Arial Rounded MT Bold", size: 18))
    .foregroundColor(Color.white)
    .shadow(color: Color.black, radius: 5, x: 2, y: 2)
  Slider(value: $sliderValue, in: 1...100)
  Text("100")
    .font(Font.custom("Arial Rounded MT Bold", size: 18))
    .foregroundColor(Color.white)
    .shadow(color: Color.black, radius: 5, x: 2, y: 2)
}
The target and slider rows, with styled text
Tfu yoyril ulz hkayib gubr, gupd rkhbit geyh

// Score row
HStack {
  Button(action: {
    self.startNewGame()
  }) {
    Text("Start over")
  }
  Spacer()
  Text("Score:")
    .font(Font.custom("Arial Rounded MT Bold", size: 18))
    .foregroundColor(Color.white)
    .shadow(color: Color.black, radius: 5, x: 2, y: 2)
  Text("\(score)")
    .font(Font.custom("Arial Rounded MT Bold", size: 24))
    .foregroundColor(Color.yellow)
    .shadow(color: Color.black, radius: 5, x: 2, y: 2)
  Spacer()
  Text("Round")
    .font(Font.custom("Arial Rounded MT Bold", size: 18))
    .foregroundColor(Color.white)
    .shadow(color: Color.black, radius: 5, x: 2, y: 2)
  Text("\(round)")
    .font(Font.custom("Arial Rounded MT Bold", size: 24))
    .foregroundColor(Color.yellow)
    .shadow(color: Color.black, radius: 5, x: 2, y: 2)
  Spacer()
  NavigationLink(destination: AboutView()) {
    Text("Info")
  }
}
.padding(.bottom, 20)
The app, with all its text styled
Mro evn, lewm ewd odq qumf rddlim

Making the buttons look like buttons

Let’s make the buttons look more like buttons.

The button image
Vke zofxit aroqo

// Button row
Button(action: {
  print("Button pressed!")
  self.alertIsVisible = true
}) {
  Text("Hit me!")
  .font(Font.custom("Arial Rounded MT Bold", size: 18))
  .foregroundColor(Color.black)
}
.background(Image("Button")
  .shadow(color: Color.black, radius: 5, x: 2, y: 2)
)
.alert(isPresented: $alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text("The slider's value is \(sliderValueRounded).\n" +
                      "You earned \(pointsForCurrentRound()) points."),
        dismissButton: .default(Text("Awesome!")) {
          self.startNewRound()
        }
  )
}
The 'Hit me!' button’s new look
Bwo 'Giz hu!' nuvnaw’s cif waog

Introducing ViewModifier

If you look at body in its current state, you’ll see a lot of repetition. For starters, there are five instances where the following methods are called on a Text view:

.font(Font.custom("Arial Rounded MT Bold", size: 18))
.foregroundColor(Color.white)
.font(Font.custom("Arial Rounded MT Bold", size: 24))
.foregroundColor(Color.yellow)
.shadow(color: Color.black, radius: 5, x: 2, y: 2)
// View modifiers
// ==============

struct LabelStyle: ViewModifier {
  func body(content: Content) -> some View {
    content
      .font(Font.custom("Arial Rounded MT Bold", size: 18))
      .foregroundColor(Color.white)
      .shadow(color: Color.black, radius: 5, x: 2, y: 2)
  }
}
struct ValueStyle: ViewModifier {
  func body(content: Content) -> some View {
    content
      .font(Font.custom("Arial Rounded MT Bold", size: 24))
      .foregroundColor(Color.yellow)
      .shadow(color: Color.black, radius: 5, x: 2, y: 2)
  }
}
// Target row
HStack {
  Text("Put the bullseye as close as you can to:").modifier(LabelStyle())
  Text("\(target)").modifier(ValueStyle())
}
// Slider row
HStack {
  Text("1").modifier(LabelStyle())
  Slider(value: $sliderValue, in: 1...100)
  Text("100").modifier(LabelStyle())
}
// Score row
HStack {
  Button(action: {
    self.startNewGame()
  }) {
    Text("Start over")
  }
  Spacer()
  Text("Score:").modifier(LabelStyle())
  Text("\(score)").modifier(ValueStyle())
  Spacer()
  Text("Round").modifier(LabelStyle())
  Text("\(round)").modifier(ValueStyle())
  Spacer()
  NavigationLink(destination: AboutView()) {
    Text("Info")
  }
}
.padding(.bottom, 20)
The app, styled with ViewModifiers
Dli asp, cwtfip pasn JiuyNeteyuohd

Some refactoring and more styling

You may have noticed that both LabelStyle and ValueStyle have one line of code in common — the line that adds a shadow:

.shadow(color: Color.black, radius: 5, x: 2, y: 2)
struct Shadow: ViewModifier {
  func body(content: Content) -> some View {
    content
      .shadow(color: Color.black, radius: 5, x: 2, y: 2)
  }
}
func body(content: Content) -> some View
struct LabelStyle: ViewModifier {
  func body(content: Content) -> some View {
    content
      .font(Font.custom("Arial Rounded MT Bold", size: 18))
      .foregroundColor(Color.white)
      .modifier(Shadow())
  }
}

struct ValueStyle: ViewModifier {
  func body(content: Content) -> some View {
    content
      .font(Font.custom("Arial Rounded MT Bold", size: 24))
      .foregroundColor(Color.yellow)
      .modifier(Shadow())
  }
}
// Button row
Button(action: {
  print("Points awarded: \(self.pointsForCurrentRound())")
  self.alertIsVisible = true
}) {
  Text("Hit me!")
    .font(Font.custom("Arial Rounded MT Bold", size: 18))
    .foregroundColor(Color.black)
}
.background(Image("Button")
  .modifier(Shadow())
)
.alert(isPresented: $alertIsVisible) {
  Alert(title: Text(alertTitle()),
        message: Text(scoringMessage()),
        dismissButton: .default(Text("Awesome!")) {
          self.startNewRound()
    })
}
// Score row
HStack {
  Button(action: {
    self.startNewGame()
  }) {
    Text("Start over")
  }
  .background(Image("Button")
    .modifier(Shadow())
  )
  Spacer()
  Text("Score:").modifier(LabelStyle())
  Text("\(score)").modifier(ValueStyle())
  Spacer()
  Text("Round").modifier(LabelStyle())
  Text("\(round)").modifier(ValueStyle())
  Spacer()
  NavigationLink(destination: AboutView()) {
    Text("Info")
  }
  .background(Image("Button")
    .modifier(Shadow())
  )
}
.padding(.bottom, 20)
All the buttons now look like buttons
Ejp pqu tasnaln zir ruax vima jandurd

.padding(.bottom, 20)
.padding(.leading, 20)
.padding(.trailing, 40)
struct ButtonLargeTextStyle: ViewModifier {
  func body(content: Content) -> some View {
    content
      .font(Font.custom("Arial Rounded MT Bold", size: 18))
      .foregroundColor(Color.black)
  }
}

struct ButtonSmallTextStyle: ViewModifier {
  func body(content: Content) -> some View {
    content
      .font(Font.custom("Arial Rounded MT Bold", size: 12))
      .foregroundColor(Color.black)
  }
}
// Button row
Button(action: {
  print("Points awarded: \(self.pointsForCurrentRound())")
  self.alertIsVisible = true
}) {
  Text("Hit me!").modifier(ButtonLargeTextStyle())
}
.background(Image("Button")
  .modifier(Shadow())
)
.alert(isPresented: $alertIsVisible) {
  Alert(title: Text(alertTitle()),
        message: Text(scoringMessage()),
        dismissButton: .default(Text("Awesome!")) {
          self.startNewRound()
    })
}
// Score row
HStack {
  Button(action: {
    self.startNewGame()
  }) {
    Text("Start over").modifier(ButtonSmallTextStyle())
  }
  .background(Image("Button")
    .modifier(Shadow())
  )
  Spacer()
  Text("Score:").modifier(LabelStyle())
  Text("\(score)").modifier(ValueStyle())
  Spacer()
  Text("Round").modifier(LabelStyle())
  Text("\(round)").modifier(ValueStyle())
  Spacer()
  NavigationLink(destination: AboutView()) {
    Text("Info").modifier(ButtonSmallTextStyle())
  }
  .background(Image("Button")
    .modifier(Shadow())
  )
}
.padding(.bottom, 20)
.padding(.leading, 20)
.padding(.trailing, 40)
All the buttons now have styled text
Iqk nni jilradd vus nogu jjtyol yekp

Putting images inside buttons

Let’s add some more visual flair to Bullseye: icons for the Start over and Info buttons. They’re in the StartOverIcon and InfoIcon image sets in the asset catalog:

InfoIcon and StartOverIcon in the asset catalog
UstaUfag eyh VyaydAwipOxov iq pti upwuf tapukac

The subviews in the small buttons
Pya rucnoarh ib gwu ctunq lityagk

// Score row
HStack {
  Button(action: {
    self.startNewGame()
  }) {
    HStack {
      Image("StartOverIcon")
      Text("Start over").modifier(ButtonSmallTextStyle())
    }
  }
  .background(Image("Button")
    .modifier(Shadow())
  )
  Spacer()
  Text("Score:").modifier(LabelStyle())
  Text("\(score)").modifier(ValueStyle())
  Spacer()
  Text("Round").modifier(LabelStyle())
  Text("\(round)").modifier(ValueStyle())
  Spacer()
  NavigationLink(destination: AboutView()) {
    HStack {
      Image("InfoIcon")
      Text("Info").modifier(ButtonSmallTextStyle())
    }
  }
  .background(Image("Button")
    .modifier(Shadow())
  )
}
.padding(.bottom, 20)
.padding(.leading, 20)
.padding(.trailing, 40)
The app with button images
Lvi ejx xots lisqus epevoy

Adding accent colors

iOS subtly applies colors to user interface elements to give the user a hint that something is active, tappable, moveable or highlighted. These so-called accent colors are, by default, the same blue that we saw on many controls before we changed Bullseye’s user interface. Even with all the tweaks you’ve made, you can still see the default accent color on the slider, and in the button icons:

The default accent color
Hju teziojk ikcekp bilec

// Slider row
HStack {
  Text("1").modifier(LabelStyle())
  Slider(value: $sliderValue, in: 1...100)
    .accentColor(Color.green)
  Text("100").modifier(LabelStyle())
}
The slider, with its new custom accent color
Bqa nlivev, petz otj hus luvjif abhafz cacoc

Midnight blue
Jobpazzc fsou

// Colors
let midnightBlue = Color(red: 0,
                         green: 0.2,
                         blue: 0.4)
// Score row
HStack {
  Button(action: {
    self.startNewGame()
  }) {
    HStack {
      Image("StartOverIcon")
      Text("Start over").modifier(ButtonSmallTextStyle())
    }
  }
  .background(Image("Button")
    .modifier(Shadow())
  )
  Spacer()
  Text("Score:").modifier(LabelStyle())
  Text("\(score)").modifier(ValueStyle())
  Spacer()
  Text("Round").modifier(LabelStyle())
  Text("\(round)").modifier(ValueStyle())
  Spacer()
  NavigationLink(destination: AboutView()) {
    HStack {
      Image("InfoIcon")
      Text("Info").modifier(ButtonSmallTextStyle())
    }
  }
  .background(Image("Button")
    .modifier(Shadow())
  )
}
.padding(.bottom, 20)
.padding(.leading, 20)
.padding(.trailing, 40)
.accentColor(midnightBlue)

Some SwiftUI limitations

SwiftUI is still a new framework, and you should expect it to have limitations. It can’t (yet) do everything that UIKit can do.

The “About” screen

Now that you’ve styled the main screen, let’s do the same for the “About” screen with a similar treatment.

// View modifiers
// ==============

struct AboutHeadingStyle: ViewModifier {
  func body(content: Content) -> some View {
    content
      .font(Font.custom("Arial Rounded MT Bold", size: 30))
      .foregroundColor(Color.black)
      .padding(.top, 20)
      .padding(.bottom, 20)
  }
}

struct AboutBodyStyle: ViewModifier {
  func body(content: Content) -> some View {
    content
      .font(Font.custom("Arial Rounded MT Bold", size: 16))
      .foregroundColor(Color.black)
      .padding(.leading, 60)
      .padding(.trailing, 60)
      .padding(.bottom, 20)
  }
}
var body: some View {
  VStack {
    Text("🎯 Bullseye 🎯")
      .modifier(AboutHeadingStyle())
    Text("This is Bullseye, the game where you can win points and earn fame by dragging a slider.")
      .modifier(AboutBodyStyle())
    Text("Your goal is to place the slider as close as possible to the target value. The closer you are, the more points you score.")
      .modifier(AboutBodyStyle())
    Text("Enjoy!")
      .modifier(AboutBodyStyle())
  }
}
AboutView, with styled text
IpaarXeos, zogt tqxkay hagj

// Constants
let beige = Color(red: 1.0,
                  green: 0.84,
                  blue: 0.70)
var body: some View {
  VStack {
    Text("🎯 Bullseye 🎯")
      .modifier(AboutHeadingStyle())
    Text("This is Bullseye, the game where you can win points and earn fame by dragging a slider.")
      .modifier(AboutBodyStyle())
      .lineLimit(nil)
    Text("Your goal is to place the slider as close as possible to the target value. The closer you are, the more points you score.")
      .modifier(AboutBodyStyle())
    Text("Enjoy!")
      .modifier(AboutBodyStyle())
  }
  .background(beige)
}
AboutView, with the beige VStack
ImoajLuen, robz wta goaqa FTcihl

var body: some View {
  Group {
    VStack {
      Text("🎯 Bullseye 🎯")
        .modifier(AboutHeadingStyle())
      Text("This is Bullseye, the game where you can win points and earn fame by dragging a slider.")
        .modifier(AboutBodyStyle())
      Text("Your goal is to place the slider as close as possible to the target value. The closer you are, the more points you score.")
        .modifier(AboutBodyStyle())
      Text("Enjoy!")
        .modifier(AboutBodyStyle())
    }
    .background(beige)
  }
  .background(Image("Background"))
}
The final AboutView
Bje xiyit IxoizMiih

Some final touches

Let’s add some additional features to bring the SwiftUI version of Bullseye a little closer to the original UIKit version.

Randomizing the slider’s position at the start of each game and the start of each round

Let’s make the game a little more challenging by randomizing the slider’s position at the start of each round, including the round at the start of the game.

func startNewRound() {
  score = score + pointsForCurrentRound()
  sliderValue = Double.random(in: 1...100)
  target = Int.random(in: 1...100)
}

func startNewGame() {
  score = 0
  round = 1
  sliderValue = Double.random(in: 1...100)
  target = Int.random(in: 1...100)
}
sliderValue = Double.random(in: 1...100)
target = Int.random(in: 1...100)
func resetSliderAndTarget() {
  sliderValue = Double.random(in: 1...100)
  target = Int.random(in: 1...100)
}
func startNewRound() {
  score = score + pointsForCurrentRound()
  resetSliderAndTarget()
}

func startNewGame() {
  score = 0
  round = 1
  resetSliderAndTarget()
}
      .padding(.bottom, 20)
      .padding(.leading, 20)
      .padding(.trailing, 40)
      .accentColor(midnightBlue)
    }
    .background(Image("Background"))
    .onAppear() {
      self.startNewGame()
    }
  }
  .navigationViewStyle(StackNavigationViewStyle())
}
The app, when launched
Dlo ifl, qniy dauqlbum

Switching to the “About” screen
Tbubnxucl va msi “Iviaw” yhkeov

Returning back to the main screen
Yexuchiqt qidf ko bte duum lzlueh

@State var sliderValue = 50.0
@State var sliderValue = Double.random(in: 1...100)
      .padding(.bottom, 20)
      .padding(.leading, 20)
      .padding(.trailing, 40)
      .accentColor(midnightBlue)
    }
    .background(Image("Background"))
  }
  .navigationViewStyle(StackNavigationViewStyle())
}

Adding a title to the main screen’s navigation bar

On the main screen, the navigation bar looks like a white translucent strip that does nothing. Users might even think it’s a bug. Let’s spruce it up by displaying its title in the navigation bar.

var body: some View {
  NavigationView {
    VStack {
      Spacer().navigationBarTitle("🎯 Bullseye 🎯")
The app, with its title in the navigation bar
Yke aqw, puwq ikr zalxu ik qwe mikudegeuy wuq

Improving the alert messages

Let’s update the alert so that it shows a title that varies with the user’s accuracy. We’ll also add a method to generate the alert’s message to simplify the Alert initializer and make its code more readable.

func alertTitle() -> String {
  let title: String
  if sliderTargetDifference == 0 {
    title = "Perfect!"
  } else if sliderTargetDifference < 5 {
    title = "You almost had it!"
  } else if sliderTargetDifference <= 10 {
    title = "Not bad."
  } else {
    title = "Are you even trying?"
  }
  return title
}

func scoringMessage() -> String {
  return "The slider's value is \(sliderValueRounded).\n" +
         "The target value is \(target).\n" +
         "You scored \(pointsForCurrentRound()) points this round."
}
Alert(title: Text(alertTitle()),
      message: Text(scoringMessage()),
      dismissButton: .default(Text("Awesome!")) {
        self.startNewRound()
      }
)
The updated alert
Dpu oyciwas ipewz

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