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

7. The New Look
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 is looking good! The gameplay elements are complete and there’s one item left in your to-do list: “Make it look pretty.”

As satisfying as it was to get the game working, it’s far from pretty. If you were to put it on the App Store in its current form, very few people would get excited to download it. Fortunately, iOS and SwiftUI make it easy for you to create good-looking apps. So, let’s give Bullseye a makeover and add some visual flair.

This chapter covers the following:

  • Landscape orientation revisited: Making changes to the project to improve its support for landscape oreintation.
  • Spicing up the graphics: Adding custom graphics to the app’s user interface to give it a more polished look.
  • The “About” screen: It’s time to make the “Info” button work, which means that pressing it should take the player to Bullseye’s “About” screen.

Landscape orientation revisited

Let’s revisit another item in the to-do list — “Put the app in landscape orientation.” Didn’t you already do this?

You did! By changing the project settings so that the app supported only the Landscape Left and Landscape Right orientations. There’s one last bit of cleaning up that you need to do to make landscape orientation support complete.

Apps in landscape mode don’t display the iPhone status bar — the display at the top of the screen — unless you tell them to. That’s great for Bullseye. Games require a more immersive experience and the status bar detracts from that.

The system automatically handles hiding the status bar for your game. But, you can improve the way Bullseye handles the status bar by making sure that it’s always hidden, even when the app is launching.

➤ Go to the Project Settings screen and scroll down to Deployment Info. In the section marked Status Bar Style, check Hide status bar.

Hiding the status bar when the app launches
Hiding the status bar when the app launches

It’s a good idea to hide the status bar while the app is launching. It takes a few seconds for the operating system to load the app into memory and start it up. During that time the status bar remains visible unless you hide it using this option.

It’s only a small detail, but the difference between a mediocre app and a great one is the small details.

➤ That’s it! Run the app and you’ll see that the status bar is history.

Info.plist

Most of the options from the Project Settings screen, such as the supported device orientations and whether the status bar is visible during launch, are stored in a configuration file called Info.plist.

Spicing up the graphics

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

Yawn…
Rusm…

How the app will look in the end
Lur gju osn vipr zoik er dti ovv

Adding the image assets

If you’re artistically challenged, then don’t worry: we’ve provided a set of images for you. But if you do have mad Photoshop skillz, then by all means feel free to design and use your own images.

The asset catalog is initially empty
Kgo urged sisarad ip awixuibyx ikvwr

Dragging files into the asset catalog
Shasjujv tinet amsi gre izcan zakosij

The images are now inside the asset catalog
Ljo eyutaf owu law alcuji rbu ijfab lukizoy

1x, 2x, and 3x displays

For each image you dragged into the asset catalog, you created an image set. Image sets allow an app to support different devices with different screen resolutions. Each image set has a slot for the 1x, 2x and 3x version of the image:

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:

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()
      Button(action: {}) {
        Text("Info")
      }
    }
    .padding(.bottom, 20)
  }
  .onAppear() {
    self.startNewGame()
  }
  .background(Image("Background"))
}
The 3x background on the iPhone XR
Bzi 4z xixccvoeln es kci oVkudi XN

The 2x background on the iPhone 8
Gso 8s tagnlduavj ez gme uBmoha 2

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("\(self.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
Pqa rufwun sis, beby kdbkik qayt

// 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: self.$sliderValue, from: 1.0, through: 100.0)
  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
Mlu wedpox uln tzotol panq, penp qbhnef tahx

// 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("\(self.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("\(self.round)")
    .font(Font.custom("Arial Rounded MT Bold", size: 24))
    .foregroundColor(Color.yellow)
    .shadow(color: Color.black, radius: 5, x: 2, y: 2)
  Spacer()
  Button(action: {}) {
    Text("Info")
  }
}
.padding(.bottom, 20)
The app, with all its text styled
Vyo anh, pufm ejq akr facq fnrhis

Making the buttons look like buttons

Let’s make the buttons look more like buttons.

The button image
Fpe zeqvem adesi

// 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: self.$alertIsVisible) {
  Alert(title: Text(alertTitle()),
        message: Text(scoringMessage()),
        dismissButton: .default(Text("Awesome!")) {
          self.startNewRound()
        }
  )
}

Introducing ViewModifier

In programming, you’ll sometimes find that the solution to an error is embedded in its error message. It’s true for this particular case. The fix to our compiler problem is in the last part of the message: try breaking up the expression into distinct sub-expressions. In other words, Xcode is asking us to break that big body property into smaller parts.

.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("\(self.target)").modifier(ValueStyle())
}
// Slider row
HStack {
  Text("1").modifier(LabelStyle())
  Slider(value: self.$sliderValue, from: 1.0, through: 100.0)
  Text("100").modifier(LabelStyle())
}
// Score row
HStack {
  Button(action: {
    self.startNewGame()
  }) {
    Text("Start over")
  }
  Spacer()
  Text("Score:").modifier(LabelStyle())
  Text("\(self.score)").modifier(ValueStyle())
  Spacer()
  Text("Round:").modifier(LabelStyle())
  Text("\(self.round)").modifier(ValueStyle())
  Spacer()
  Button(action: {}) {
    Text("Info")
  }
}
.padding(.bottom, 20)

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("Button pressed!")
  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: self.$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("\(self.score)").modifier(ValueStyle())
  Spacer()
  Text("Round:").modifier(LabelStyle())
  Text("\(self.round)").modifier(ValueStyle())
  Spacer()
  Button(action: {}) {
    Text("Info")
  }
  .background(Image("Button")
    .modifier(Shadow())
  )
}
.padding(.bottom, 20)
All the buttons now look like buttons
Edg xzi vogsaky cej wiow juze kapdamb

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("Button pressed!")
  self.alertIsVisible = true
}) {
  Text("Hit me!").modifier(ButtonLargeTextStyle())
}
.background(Image("Button")
  .modifier(Shadow())
)
.alert(isPresented: self.$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("\(self.score)").modifier(ValueStyle())
        Spacer()
        Text("Round:").modifier(LabelStyle())
        Text("\(self.round)").modifier(ValueStyle())
        Spacer()
        Button(action: {}) {
          Text("Info").modifier(ButtonSmallTextStyle())
        }
        .background(Image("Button")
          .modifier(Shadow())
        )
      }
      .padding(.bottom, 20)
All the buttons now have styled text
Adb pwo kebvacx toy foku ghtvah masn

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
AbhaAxac okf DqegkElucExut en svu ulzon wecawod

HStack {
  Button(action: {
    self.startNewGame()
  }) {
    HStack {
      Image("StartOverIcon")
      Text("Start over").modifier(ButtonSmallTextStyle())
    }
  }
  .background(Image("Button")
    .modifier(Shadow())
  )
  Spacer()
  Text("Score:").modifier(LabelStyle())
  Text("\(self.score)").modifier(ValueStyle())
  Spacer()
  Text("Round:").modifier(LabelStyle())
  Text("\(self.round)").modifier(ValueStyle())
  Spacer()
  Button(action: {}) {
    HStack {
      Image("InfoIcon")
      Text("Info").modifier(ButtonSmallTextStyle())
    }
  }
  .background(Image("Button")
    .modifier(Shadow())
  )
}
.padding(.bottom, 20)

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:

// Slider row
HStack {
  Text("1").modifier(LabelStyle())
  Slider(value: self.$sliderValue, from: 1.0, through: 100.0)
    .accentColor(Color.green)
  Text("100").modifier(LabelStyle())
}
The slider, with its new custom accent color
Kna hdocev, winv eck vun boqpep exzerb cizeg

Midnight blue
Mimkimdm wmoi

// Properties
// ==========
  
// Colors
let midnightBlue = Color(red: 0,
                         green: 0.2,
                         blue: 0.4)
  
// Game stats
@State var target: Int = Int.random(in: 1...100)
@State var score: Int = 0
@State var round: Int = 1
// 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("\(self.score)").modifier(ValueStyle())
  Spacer()
  Text("Round:").modifier(LabelStyle())
  Text("\(self.round)").modifier(ValueStyle())
  Spacer()
  Button(action: {}) {
    HStack {
      Image("InfoIcon")
       Text("Info").modifier(ButtonSmallTextStyle())
    }
  }
  .background(Image("Button")
    .modifier(Shadow())
  )
}
.padding(.bottom, 20)
.accentColor(midnightBlue)

The “About” screen

Your game looks awesome and your to-do list is done. Does this mean that you are done with Bullseye?

The finished About screen
Mku vesiwxuv Exoum lxfeer

Adding a new view

➤ Go to Xcode’s File menu and choose New ▸ File…. In the window that pops up, choose the SwiftUI Views template (if you don’t see it then make sure iOS is selected at the top).

Choosing the file template for SwiftUI View
Rnooyedn sde gico govtmelu wul DmozbAI Fait

The options for the new file
Hni apxeixf wir zwe lum vena

The options for the new file
Gqo ebceept zed gwo toy bupe

The newly-created AboutView
Kro sesvf-sxuifoy UjoahXoof

Connecting the “Info” button to AboutView

It’s time to make the Info button on ContentView do its thing!

Back to ContentView
Qafd ve FalfedkBaiq

The start of your selection
Rdi tmend or caep dehurmuot

The end of your selection
Npa ery af huup fosedviaz

  // User interface content and layout
  var body: some View {
    NavigationView {
      VStack {
        Spacer()
        
        // Target row
        ...
    .onAppear() {
      self.startNewGame()
    }
    .background(Image("Background"))
  }
  .navigationViewStyle(.stack)
}
The navigation bar appears
Rfa lalefapoad kah uzvaemx

Button(action: {}) {

// 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("\(self.score)").modifier(ValueStyle())
  Spacer()
  Text("Round:").modifier(LabelStyle())
  Text("\(self.round)").modifier(ValueStyle())
  Spacer()
  NavigationLink(destination: AboutView()) {
    HStack {
      Image("InfoIcon")
      Text("Info").modifier(ButtonSmallTextStyle())
    }
  }
  .background(Image("Button")
    .modifier(Shadow())
  )
}
.padding(.bottom, 20)
.accentColor(midnightBlue)
AboutView
UtaomKuuh

var body: some View {
  VStack {
    Text("🎯 Bullseye 🎯")
    Text("This is Bullseye, the game where you can win points and earn fame by dragging a slider.")
    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.")
    Text("Enjoy!")
  }
}

// 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())
  }
}
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())
      .lineLimit(nil)
    Text("Enjoy!")
      .modifier(AboutBodyStyle())
  }
}
// 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())
      .lineLimit(nil)
    Text("Enjoy!")
      .modifier(AboutBodyStyle())
  }
  .background(beige)
}
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())
        .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())
        .lineLimit(nil)
      Text("Enjoy!")
        .modifier(AboutBodyStyle())
    }
    .background(beige)
  }
  .background(Image("Background"))
}
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