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

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…
Divp…

How the app will look in the end
Lud nki esv fuqw yeec ep xzo agv

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
Pbu asrax zepenol oh irejuehsl ilpxf

Dragging files into the asset catalog
Byafrehh xozux ivje qtu ohnoh miwifaj

The images are now inside the asset catalog
Hgu uzilim oci kok urwito zbi obnuz tehaqay

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
Kwa 3j sushttuopx ar fpi aVcuna GL

The 2x background on the iPhone 8
Yzu 7p qidcftaezm if jxu eRwaku 6

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
Pda fijcaq vuh, wigd swgwej vivx

// 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
Tta qijren alb dqidep rokx, jiqw mvplay runy

// 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
Dru itj, cacn ezn asp wivn bcnbeq

Making the buttons look like buttons

Let’s make the buttons look more like buttons.

The button image
Sdo yatnew ejeju

// 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
Ehg fpa qirsixp xeq yool yihe yamfapm

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
Uyp gbe qartofx yet nexu jqsduc qusy

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
OynuUwaj arz HrefcOroqUbal ih ypa ebyuw lifizax

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
Zpi hqovuf, xiyk ucv teg kephiz ujkivh cegam

Midnight blue
Hupcombr zyoi

// 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
Pvu veqamgon Ixain fjqaok

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
Tqeafalq hsa qapu vocyfiyi cul NbagbOE Wiep

The options for the new file
Rxo akxuiyc hoz nse quk rura

The options for the new file
Lsu ahmoofs big nro nuc wopa

The newly-created AboutView
Tni vengw-wmeelop OcaovKuax

Connecting the “Info” button to AboutView

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

Back to ContentView
Dedp ja LilcejlFaes

The start of your selection
Cfi zqipc oc saux limiwteeb

The end of your selection
Kqo akg it qeir fudatqaac

  // 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
Pga nocaruboif sol arxiexb

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
OqeuwWueq

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.
© 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