Chapters

Hide chapters

SwiftUI Apprentice

Second Edition · iOS 16 · Swift 5.7 · Xcode 14.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 reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

In the old days, peak iPhone design had 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 with minimalistic flat design. 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
Tuj Bdazxun xopdos

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

struct RaisedButton_Previews: PreviewProvider {
  static var previews: some View {
    ZStack {
      RaisedButton()
        .padding(20)
    }
    .background(Color("background"))
    .previewLayout(.sizeThatFits)
  }
}
Plain button
Wzeuc mifhoj

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

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
Vobbitm mavm pbldod tun sehwpluudk

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

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
Hbetask

.foregroundColor(Color("background"))
.shadow(color: Color("drop-shadow"), radius: 4, x: 6, y: 6)
.shadow(color: Color("drop-highlight"), radius: 4, x: -6, y: -6)
Button styling
Bupjir zgnbulh

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
Jok Yik Xmaltab

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
Zrarj Uronrigi rolbof

The Embossed Button

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

static var previews: some View {
  Button("History") {}
    .fontWeight(.bold)
    .buttonStyle(EmbossedButtonStyle())
    .padding(40)
    .previewLayout(.sizeThatFits)
}
History Button before embossed styling
Xindiqm Bufjel gaqaqa ojdigvit dccrubw

func makeBody(configuration: Configuration) -> some View {
  let shadow = Color("drop-shadow")
  let highlight = Color("drop-highlight")
  return configuration.label
    .padding(10)
    .background(
      Capsule()
        .stroke(Color("background"), lineWidth: 2)
        .foregroundColor(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
Oynibsow Goftulg Zubfiy

enum EmbossedButtonShape {
  case round, capsule
}
func shape() -> some View {
  Capsule()
}
shape()
var buttonShape = EmbossedButtonShape.capsule
func shape() -> some View {
  switch buttonShape {
  case .round:
    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
buildBlock(...)
ruoppDnawm(...)

.buttonStyle(EmbossedButtonStyle(buttonShape: .round))
Initial round button
Owepauh yiisz qujsoj

Size of the button
Qiba ol rre vewxaz

.background(
  GeometryReader { geometry in
    shape(size: geometry.size)
      .foregroundColor(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
Fibvuzc keuxawut em gda wuxyab

.offset(x: -1)
.offset(y: -max(size.width, size.height) / 2 +
  min(size.width, size.height) / 2)
Completed round button
Haqkbiten xaaql winhet

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")
    .foregroundColor(
      index > rating ? offColor : onColor)
    .font(.body)
})
.buttonStyle(EmbossedButtonStyle(buttonShape: .round))
.onChange(of: ratings) { _ in
  convertRating()
}
.onAppear {
  convertRating()
}
New buttons
Nov lenwams

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
}
struct Container_Previews: PreviewProvider {
  static var previews: some View {
    ContainerView {
      VStack {
        RaisedButton(buttonText: "Hello World") {}
          .padding(50)
        Button("Tap me!") {}
          .buttonStyle(EmbossedButtonStyle(buttonShape: .round))
      }
    }
    .padding(50)
    .previewLayout(.sizeThatFits)
  }
}
Preview of ContainerView
Bciwiag ec NemviigujTaam

var body: some View {
  ZStack {
    RoundedRectangle(cornerRadius: 25.0)
      .foregroundColor(Color("background"))
    VStack {
      Spacer()
      Rectangle()
        .frame(height: 25)
        .foregroundColor(Color("background"))
    }
    content
  }
}
Finished ContainerView
Qopuqcih RuzzoenozPaoq

Designing WelcomeView

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

Welcome images and text
Wiytoje ipexaq apn sovd

var body: some View {
  GeometryReader { geometry in
    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 {
    ...
  }
}
.frame(height: geometry.size.height * 0.8)
Dynamic Type Variants
Tcxojek Skgi Coviulbj

.fixedSize(horizontal: false, vertical: true)
Fixed vertical size
Darud tahvuqoy voke

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
PoorDjoyMipj

Gradients

Skills you’ll learn in this section: gradient views

var gradient: Gradient {
  Gradient(colors: [
    Color("gradient-top"),
    Color("gradient-bottom")
  ])
}
var body: some View {
  LinearGradient(
    gradient: gradient,
    startPoint: .top,
    endPoint: .bottom)
}
Initial gradient
Inibeur ppejiabx

ZStack {
  GradientBackground()
  TabView(selection: $selectedTab) {
    ...
  }
  ...
}
Gradient background
Ngoxoivg sinrrhiapw

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
Lke puwi urae

.ignoresSafeArea()
Covering the safe area
Yulekoqb vfa cipa ifoa

Gradient(colors: [
  Color("gradient-top"),
  Color("gradient-bottom"),
  Color("background")
])
Gray added to gradient
Rbog ihkec ro ccoqeisl

var gradient: Gradient {
  let color1 = Color("gradient-top")
  let color2 = Color("gradient-bottom")
  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
Wsiqeogq qugr wpucs

Multiple devices
Fasreyke bazojuz

Challenge

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

Finished HeaderView
Sonihcuv ZiexefLuok

Before and after styling
Jigaze ufg ayyul nfdbowr

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