Chapters

Hide chapters

SwiftUI Apprentice

Third Edition · iOS 18 · Swift 5.9 · Xcode 16.2

Section I: Your First App: HIITFit

Section 1: 12 chapters
Show chapters Hide chapters

Section II: Your Second App: Cards

Section 2: 9 chapters
Show chapters Hide chapters

9. Refining Your App
Written by Caroline Begbie

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

While you’ve been toiling making your app functional, your designer has been busy coming up with a stunning eye-catching design. One of the strengths of SwiftUI is that, as long as you’ve been encapsulating views and separating them out along the way, it’s easy to restyle the UI without upsetting the main functionality.

In this chapter, you’ll style some of the views for iPhone, making sure that they work on all iPhone devices.

App design
App design

Creating individual reusable elements is a good place to start. Looking at the design, you’ll have to style:

  1. A raised button for Get Started and Start Exercise.
  2. An embossed button for History and the exercise rating. The History button is a capsule shape, while the rating is round.
  3. A shaped gray background view with a gradient behind.

The starter app contains the colors and images that you’ll need in the asset catalog. There’s also some code for creating the welcome image and text in WelcomeImages.swift.

Neumorphism

Skills you’ll learn in this section: neumorphism

The style of design used in HIITFit, where the background and controls are one single color, is called neumorphism. You achieve the look with shading rather than with colors.

When iPhone was first released, peak design was skeuomorphic interfaces with realistic surfaces, so you had wood and fabric textures with dials that looked real throughout your UI. iOS 7 went in the opposite direction focussing on content with minimalistic flat design. Since then design trends include gradients and depth.

The name Neumorphism comes from New + Skeuomorphism and refers to minimalism combined with realistic shadows.

Neumorphism
Neumorphism

Essentially, you choose a theme color. You then choose a lighter tint and a darker shade of that theme color for the highlight and shadow. You can define colors with either red, green, blue (RGB) or hue, saturation and lightness (HSL). When shifting tones within one color, HSL is the easier model to use as you keep the same hue. The base color in the picture above is Hue: 166, Saturation: 54, Lightness: 59. The lighter highlight color has the same Hue and Saturation, but a Lightness: 71. Similarly, the darker shadow color has a Lightness: 30.

Creating a Neumorphic Button

The first button you’ll create is the Get Started raised button.

Get Started button
Cuz Rgufciy kocqaf

struct RaisedButton: View {
  var body: some View {
    Button(action: {}, label: {
      Text("Get Started")
    })
  }
}

#Preview(traits: .sizeThatFitsLayout) {
  ZStack {
    RaisedButton()
      .padding(20)
   }
  .background(Color.background)
}
Plain button
Sfoav fuhmac

extension Text {
  func raisedButtonTextStyle() -> some View {
    self
    .font(.body)
    .fontWeight(.bold)
  }
}
.raisedButtonTextStyle()
Styled text
Dzrcad vanp

Styles

Skills you’ll learn in this section: view styles; button style; shadows

struct RaisedButtonStyle: ButtonStyle {
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .background(Color.red)
  }
}
extension ButtonStyle where Self == RaisedButtonStyle {
  static var raised: RaisedButtonStyle {
    .init()
  }
}
.buttonStyle(.raised)
Buttons with styled red background
Yadjogv hedj qscyun tar vazhgjuupq

.buttonStyle(.raised)
func makeBody(configuration: Configuration) -> some View {
  configuration.label
    .frame(maxWidth: .infinity)
    .padding([.top, .bottom], 12)
    .background(
      Capsule()
    )
}
Initial button
Ojeraux jursut

Shadows

You have two choices when adding shadows. You can choose a simple all round shadow, with a radius. The radius is how many pixels to blur out to. A default shadow with radius of zero places a faint gray line around the object, which can be attractive.

Shadows
Plisebz

.foregroundStyle(Color.background)
.shadow(color: Color.dropShadow, radius: 4, x: 6, y: 6)
.shadow(color: Color.dropHighlight, radius: 4, x: -6, y: -6)
Button styling
Gisfeq ndwqabt

Abstracting Your Button

Skills you’ll learn in this section: passing closures to views

Button(action: { selectedTab = 0 }) {
  Text("Get Started")
    .raisedButtonTextStyle()
}
.buttonStyle(.raised)
.padding()
New Get Started
Sen Feq Dyacraw

struct RaisedButton: View {
  let buttonText: String
  let action: () -> Void

  var body: some View {
    Button(action: {
      action()
    }, label: {
      Text(buttonText)
        .raisedButtonTextStyle()
    })
    .buttonStyle(.raised)
  }
}
RaisedButton(
  buttonText: "Get Started",
  action: {
    print("Hello World")
  })
RaisedButton(buttonText: "Get Started") {
  print("Hello World")
}
var getStartedButton: some View {
  RaisedButton(buttonText: "Get Started") {
    selectedTab = 0
  }
  .padding()
}
getStartedButton
var startButton: some View {
  RaisedButton(buttonText: "Start Exercise") {
    showTimer.toggle()
  }
}
Start Exercise button
Ryizd Uboglika yuvkoj

The Embossed Button

Skills you’ll learn in this section: stroking a shape

#Preview(traits: .sizeThatFitsLayout) {
  Button("History") {}
    .fontWeight(.bold)
    .buttonStyle(EmbossedButtonStyle())
    .padding(40)
}
History Button before embossed styling
Fannidp Foypeq bopide ovpibbih xsrnazh

func makeBody(configuration: Configuration) -> some View {
  let shadow = Color.dropShadow
  let highlight = Color.dropHighlight
  return configuration.label
    .padding(10)
    .background(
      Capsule()
        .stroke(Color.background, lineWidth: 2)
        .foregroundStyle(Color.background)
        .shadow(color: shadow, radius: 1, x: 2, y: 2)
        .shadow(color: highlight, radius: 1, x: -2, y: -2)
        .offset(x: -1, y: -1))
}
Embossed History Button
Alzozgaw Muyrecb Wulroh

enum EmbossedButtonShape {
  case circle, capsule
}
func shape() -> some View {
  Capsule()
}
shape()
var buttonShape = EmbossedButtonShape.capsule
func shape() -> some View {
  switch buttonShape {
  case .circle:
    Circle()
      .stroke(Color.background, lineWidth: 2)
  case .capsule:
    Capsule()
      .stroke(Color.background, lineWidth: 2)
  }
}

@ViewBuilder

Skills you’ll learn in this section: view builder attribute

@ViewBuilder
.buttonStyle(EmbossedButtonStyle(buttonShape: .circle))
Initial round button
Ibexoin xualq datzaj

Size of the button
Zole ix cga rufdux

.background(
  GeometryReader { geometry in
    shape(size: geometry.size)
      .foregroundStyle(Color.background)
      .shadow(color: shadow, radius: 1, x: 2, y: 2)
      .shadow(color: highlight, radius: 1, x: -2, y: -2)
      .offset(x: -1, y: -1)
  })
func shape(size: CGSize) -> some View {
.frame(
  width: max(size.width, size.height),
  height: max(size.width, size.height))
Correct diameter of the button
Zeshuqh doehicoj uy vme jinlog

.offset(x: -1)
.offset(y: -max(size.width, size.height) / 2 +
  min(size.width, size.height) / 2)
Completed round button
Podmrerid geoxn zirfec

var historyButton: some View {
  Button(
    action: {
      showHistory = true
    }, label: {
      Text("History")
        .fontWeight(.bold)
        .padding([.leading, .trailing], 5)
    })
    .padding(.bottom, 10)
    .buttonStyle(EmbossedButtonStyle())
}
Button("History") {
  showHistory.toggle()
}
.sheet(isPresented: $showHistory) {
  HistoryView(showHistory: $showHistory)
}
.padding(.bottom)
historyButton
  .sheet(isPresented: $showHistory) {
    HistoryView(showHistory: $showHistory)
  }
Button("History") {
  showHistory.toggle()
}
historyButton
Button(action: {
  updateRating(index: index)
}, label: {
  Image(systemName: "waveform.path.ecg")
    .foregroundStyle(
      index > rating ? offColor : onColor)
    .font(.body)
})
.buttonStyle(EmbossedButtonStyle(buttonShape: .circle))
.onChange(of: ratings) {
  convertRating()
}
.onAppear {
  convertRating()
}
New buttons
Nom jufgipg

ViewBuilder Container View

Skills you’ll learn in this section: container views

struct ContainerView<Content: View>: View {
  var content: Content
init(@ViewBuilder content: () -> Content) {
  self.content = content()
}
var body: some View {
  content
}
#Preview(traits: .sizeThatFitsLayout) {
  ContainerView {
    VStack {
      RaisedButton(buttonText: "Hello World") {}
        .padding(50)
      Button("Tap me!") {}
        .buttonStyle(EmbossedButtonStyle(buttonShape: .circle))
    }
  }
  .padding(50)
}
Preview of ContainerView
Rjumeut ed RolkiawojCeax

var body: some View {
  ZStack {
    RoundedRectangle(cornerRadius: 25.0)
      .foregroundStyle(Color.background)
    VStack {
      Spacer()
      Rectangle()
        .frame(height: 25)
        .foregroundStyle(Color.background)
    }
    content
  }
}
Finished ContainerView
Dafutvuw GiqjaayehLauz

Designing WelcomeView

Skills you’ll learn in this section: refactoring with view properties; the safe area

Welcome images and text
Tuqtoso atuquy oth buyv

var body: some View {
  VStack {
    HeaderView(
      selectedTab: $selectedTab,
      titleText: "Welcome")
    Spacer()
    // container view
    VStack {
      WelcomeView.images
      WelcomeView.welcomeText
      getStartedButton
      Spacer()
      historyButton
    }
  }
  .sheet(isPresented: $showHistory) {
    HistoryView(showHistory: $showHistory)
  }
}
// container view
ContainerView {
  VStack {
    ...
  }
}
Using the container view
Iyukn dwu rukdaimej juet

Gradients

Skills you’ll learn in this section: gradient views

var gradient: Gradient {
  Gradient(colors: [
    Color.gradientTop,
    Color.gradientBottom
  ])
}
var body: some View {
  LinearGradient(
    gradient: gradient,
    startPoint: .top,
    endPoint: .bottom)
}
Initial gradient
Oxegeeg ccucoork

.background(GradientBackground())
Gradient background
Rseziupv qawcvgoofm

The Safe Area

A safe area on a device, as its name suggests, is an area where you should never place interactive views. This area might be covered by the dynamic island, a navigation bar or a toolbar.

The safe area
Lme fugo eheo

.ignoresSafeArea()
Covering the safe area
Lodepuns ddo wozi apua

Gradient(colors: [
  Color.gradientTop,
  Color.gradientBottom,
  Color.background
])
Gray added to gradient
Hqaj uqjij ba pweseedw

var gradient: Gradient {
  let color1 = Color.gradientTop
  let color2 = Color.gradientBottom
  let background = Color.background
  return Gradient(
    stops: [
      Gradient.Stop(color: color1, location: 0),
      Gradient.Stop(color: color2, location: 0.9),
      Gradient.Stop(color: background, location: 0.9),
      Gradient.Stop(color: background, location: 1)
    ])
}
Gradient with stops
Npabauxw zimk djulc

containerRelativeFrame

You could embed the whole view hierarchy in GeometryReader and use GeometryReader.size to calculate the frames of the views. However, SwiftUI provides a view modifier containerRelativeFrame(_:alignment:_:) for relative sizing of views.

.containerRelativeFrame(.vertical) { length, _ in
  length * 0.8
}
.containerRelativeFrame(.vertical) { length, _ in
  length * 0.2
}
CaifacYail ZafhoemixLouw BJyedy
.waghuubaqDuduficuFpava

Dynamic Type Variants
Kjfuxen Nqne Yuhiuxqc

ViewThatFits

Using ViewThatFits, you can present alternative layouts. Work out what is important for interaction with your app. For the larger size text variants, you could dispense with the images.

ViewThatFits {
  VStack {
    WelcomeView.images
    WelcomeView.welcomeText
    getStartedButton
    Spacer()
    historyButton
  }
  VStack {
    WelcomeView.welcomeText
    getStartedButton
    Spacer()
    historyButton
  }
}
ViewThatFits
PuejZsehWujz

Multiple devices
Pidlivlo yunaven

Challenge

Your challenge is to continue styling. With ContentView pinned, style HeaderView.

Finished HeaderView
Rogatkoh PiumabHaer

Before and after styling
Pehoze ork ixhay vftpecz

Key Points

  • It’s not always possible to spend money on hiring a designer, but you should definitely spend time making your app as attractive and friendly as possible. Try various designs out and offer them to your testers for their opinions.
  • Neumorphism is a simple style that works well. Keep up with designer trends at https://dribbble.com.
  • Style protocols allow you to customize various view types to fit in with your desired design.
  • Using @ViewBuilder, you can return varying types of views from methods and properties. It’s easy to create custom container views that have added styling or functionality.
  • You can layer background colors in the safe area, but don’t place any of your user interface there.
  • Gradients are an easy way to create a stand-out design. You can find interesting gradients at https://uigradients.com.
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.
© 2025 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now