Chapters

Hide chapters

SwiftUI by Tutorials

Fifth Edition · iOS 16, macOS 13 · Swift 5.8 · Xcode 14.2

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

3. Diving Deeper Into SwiftUI
Written by Audrey Tam

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

SwiftUI’s declarative style makes it easy to implement eye-catching designs. In this chapter, you’ll use SwiftUI modifiers to give RGBullsEye a design makeover with neumorphism, the latest design trend.

Neumorphism

Neumorphism gained popularity in 2020 as the new skeuomorphism, a pushback against super-flat minimal UI. It can cause accessibility issues and is best used for non-functional elements like your app’s color circles or in designs with obvious UI elements.

A neumorphic UI element appears to push up from below its background, producing a flat 3D effect. Imagine the element protrudes a little from the screen, and the sun is setting northwest of the element. This produces a highlight on the upper-left edge and a shadow at the lower-right edge. Or the sun rises southeast of the element, so the highlight is on the lower-right edge and the shadow is at the upper left edge:

Northwest and southeast highlights and shadows
Northwest and southeast highlights and shadows

You need three colors to create these highlights and shadows:

  • A neutral color for the background and element surface.
  • A lighter color for the highlight.
  • A darker color for the shadow.

This example uses colors that create high contrast, just to make it really visible. In your project, you’ll use colors that create a more subtle effect.

You’ll add highlights and shadows to the color circles, labels and button in RGBullsEye to implement this Figma design:

Figma design
Figma design

This design was laid out for a 375x812-point screen (iPhone 11 Pro or 13 mini), although most of the screenshots use an iPhone 14 Pro (393 x 852). You’ll set up your design with the size values from the Figma design, then change these to screen-size-dependent values.

Note: Many developers skip the Figma/Sketch design step and just design directly in SwiftUI — it’s that easy!

Views and Modifiers

Open the project in this chapter’s starter folder. It’s the same as the final challenge project from the previous chapter, but ColorCircle is in its own file with a size parameter, and Assets.xcassets contains colors for the neumorphic design — more about these soon.

Library of primitive views and modifiers
Ralrelp ex hsicuguxe toibw irg femufoevv

Implementing Neumorphism

Assets.xcassets contains Element, Highlight and Shadow colors for both light and dark mode:

static let element = Color("Element")
static let highlight = Color("Highlight")
static let shadow = Color("Shadow")

Shadows for Neumorphism

First, you’ll create custom modifiers for northwest and southeast shadows.

import SwiftUI

extension View {
  func northWestShadow(
    radius: CGFloat = 16,
    offset: CGFloat = 6
  ) -> some View {
    return self
      .shadow(
        color: .highlight, radius: radius, x: -offset,
          y: -offset)
      .shadow(
        color: .shadow, radius: radius, x: offset, y: offset)
  }

  func southEastShadow(
    radius: CGFloat = 16,
    offset: CGFloat = 6
  ) -> some View {
    return self
      .shadow(
        color: .shadow, radius: radius, x: -offset, y: -offset)
      .shadow(
        color: .highlight, radius: radius, x: offset, y: offset)
  }
}

Setting the Background Color

For these shadows to work, the view background must be the same color as the UI elements. Head back to ContentView to set this up.

ZStack {
  Color.element
  VStack {...}
}
.ignoresSafeArea()

Creating a Neumorphic Border

The easiest way to create a border is to layer the RGB-colored circle on top of an element-colored circle using — you guessed it — a ZStack.

ZStack {
  Circle()
    .fill(Color.element)
    .northWestShadow()
  Circle()
    .fill(Color(red: rgb.red, green: rgb.green, blue: rgb.blue))
    .padding(20)
}
.frame(width: size, height: size)
Circle()
  .padding(20)
  .fill(Color(red: rgb.red, green: rgb.green, blue: rgb.blue))
Neumorphic color circle on white background
Miuxoyrwuy caxim yabkge ev blire ribqcjaarq

ZStack {
  Color.element
  ColorCircle(rgb: RGB(), size: 200)
}
.frame(width: 300, height: 300)
.previewLayout(.sizeThatFits)
Neumorphic color circle on element-colored background
Xiusawbkat kifef dexdsu am uwodicz-hefutek xomjrniopc

Neumorphic target color circle
Nauqavjvif fexmil rixoc renzdo

Order of Modifiers

When you apply more than one modifier to a view, sometimes the order matters.

.padding()
.border(Color.purple)
Border around padded text
Qulxov unuijv jofcuc wamp

.border(Color.purple)
.padding()
Padding around bordered text
Kitwonz ikiucy xurtejoq luhg

Text modifiers
Quhh rayokiomq

Text(guess.intString)
  .lineLimit(0)
  .bold()
Text(guess.intString)
  .bold()
  .lineLimit(0)

Creating a Neumorphic Button

Next, still in ContentView, let’s make your Hit Me! button pop!

.frame(width: 327, height: 48)
.background(Capsule())
.fill(Color.element)
.northWestShadow()
Neumorphic button
Bialixjvup rukped

Creating a Custom Button Style

When you start customizing a button, it’s a good idea to create a custom button style. Even if you’re not planning to reuse it in this app, your code will be less cluttered. Especially if you decide to add more options to this button style.

import SwiftUI

struct NeuButtonStyle: ButtonStyle {
  let width: CGFloat
  let height: CGFloat

  func makeBody(configuration: Self.Configuration)
  -> some View {
    configuration.label
      // Move frame and background modifiers here
  }
}
struct NeuButtonStyle: ButtonStyle {
  let width: CGFloat
  let height: CGFloat

  func makeBody(configuration: Self.Configuration)
  -> some View {
    configuration.label
      .frame(width: width, height: height)
      .background(
        Capsule()
          .fill(Color.element)
          .northWestShadow()
      )
  }
}
.buttonStyle(NeuButtonStyle(width: 327, height: 48))
Neumorphic button using NeuButtonStyle
Seocedqyox xeqzip awuwv NiuFikloyBsxma

Fixing Button Style Issues

When you create a custom button style, you lose the default label color and the default visual feedback when the user taps the button.

.foregroundColor(Color(UIColor.systemBlue))
Pin the ContentView preview
Nur tvu DivxiwtTuac jbafuej

.opacity(configuration.isPressed ? 0.2 : 1)
Group {
  if configuration.isPressed {
    Capsule()
      .fill(Color.element)
  } else {
    Capsule()
      .fill(Color.element)
      .northWestShadow()
  }
}
Group {
  if configuration.isPressed {
    Capsule()
      .fill(Color.element)
      .southEastShadow()   // Add this line
  } else {
    Capsule()
      .fill(Color.element)
      .northWestShadow()
  }
}

Creating a Beveled Edge

Next, you’ll create a new look for the color circles’ labels. You’ll use Capsule again, to unify the design. But you’ll create a bevel edge effect, to differentiate it from the button.

let text: String
let width: CGFloat
let height: CGFloat

var body: some View {
  Text(text)
}
if !showScore {
  BevelText(
    text: "R: ??? G: ??? B: ???", width: 200, height: 48)
} else {
  BevelText(
    text: game.target.intString(), width: 200, height: 48)
}
ColorCircle(rgb: guess, size: 200)
BevelText(text: guess.intString(), width: 200, height: 48)
ZStack {
  Color.element
  BevelText(
    text: "R: ??? G: ??? B: ???", width: 200, height: 48)
}
.frame(width: 300, height: 100)
.previewLayout(.sizeThatFits)
BevelText: Getting started
XavixLuhv: Nemsejx mdulzuh

.frame(width: width, height: height)
.background(
  Capsule()
    .fill(Color.element)
    .northWestShadow(radius: 3, offset: 1)
)
Outer Capsule with northwest shadow
Aoxul Vojmaro lupp ciplqrebd kdamiq

ZStack {
  Capsule()
    .fill(Color.element)
    .northWestShadow(radius: 3, offset: 1)
  Capsule()
    .inset(by: 3)
    .fill(Color.element)
    .southEastShadow(radius: 1, offset: 1)
}
BevelText: Finished
YofivKeds: Hapegpul

Neumorphism accomplished!
Zoowunrcolc oqqucbxopsix!

“Debugging” Dark Mode

Remember that the color sets in Assets have dark mode values. What does this design look like in dark mode?

Set preview's color scheme to Dark appearance.
Hin pvopoak'f gadal kdcava se Kuxj egyeedebde.

Neumorphism: Dark mode
Zuivipqfuvb: Hocl yigo

Alert's color scheme isn't dark?!
Agumq'd moxar pnjode ujg'p nisg?!

Override color scheme while running in a simulator.
Eduddeku bubij ktnigi fqoje rewcifr ow e latomucug.

Simulator: Alert's color scheme is dark.
Xawifahej: Uqajz's satob fjxevu ur bucr.

Modifying Font

You need one more thing to put the finishing touch on the Figma design: All the text needs to be a little bigger and a little bolder.

.font(.headline)
Headline font size applies to all the text.
Veupnohu caqt jivi axdvoel nu ory czu nimx.

.font(.subheadline)
Slider labels use subheadline font size.
Rdozod yovefk ofi yaldoepmule dixk kena.

Adapting to the Device Screen Size

OK, time to see how this design looks on a smaller screen. By default, the preview uses the currently active scheme’s run destination. To check how your design fits in a different screen, you can select a different run destination, or you can specify a previewDevice for previews.

Changing the Run Destination

Change the run destination to iPhone 14 Pro Max. Xcode takes a while to start up this simulator in the background, and eventually the preview updates to use this simulator:

Run destination: iPhone 14 Pro Max
Lal latlahufeap: aWgopu 30 Kyo Piw

Specifying a previewDevice

When you install Xcode 14, there might be only a few iPhone simulators in the destination run menu — mine lists only the iPhone 14 sizes and the SE 3rd generation. But Apple’s support page lists all the iPhone models compatible with iOS 16.

Add additional simulator: iPhone 8
Uxc ezfozaopet limumiwif: iPquto 9

.previewDevice(
  PreviewDevice(
    rawValue: "iPhone 8"))
Preview device: iPhone 8
Xmefeax xofiko: iPluke 5

Calculating Size Proportions

In ContentView, add these properties below the @State properties:

let circleSize: CGFloat = 0.275
let labelHeight: CGFloat = 0.06
let labelWidth: CGFloat = 0.53
let buttonWidth: CGFloat = 0.87

Getting Screen Size From GeometryReader

This is what you’ll do: Embed the ZStack in a GeometryReader to access its size and frame values.

Embed ZStack in ... some container.
Ujdus PRtutd ay ... nopu lofvoenow.

GeometryReader { proxy in
  ZStack {
size: proxy.size.height * circleSize
width: proxy.size.width * labelWidth,
height: proxy.size.height * labelHeight
(NeuButtonStyle(
  width: proxy.size.width * buttonWidth,
  height: proxy.size.height * labelHeight))

Previewing Different Devices

To see all three screen sizes at once, you could build and run the app on two simulators. Instead, you’ll add previews to ContentView_Previews.

Embed ContentView in Group.
Izqec KemburnVoad ag Cgoef.

Group {
  ContentView(guess: RGB())
  ContentView(guess: RGB())
    .previewDevice(
      PreviewDevice(
        rawValue: "iPhone 8"))
  ContentView(guess: RGB())
    .previewDevice(
      PreviewDevice(
        rawValue: "iPhone 8"))
}
Three preview buttons
Sqsia bfugaij bergudr

ContentView(guess: RGB())
  .previewDevice(
    PreviewDevice(
      rawValue: "iPhone 14 Pro"))
Previews of 14 Pro Max, 14 Pro and 8
Fkafauvt eb 41 Mwa Suw, 42 Cme ilx 9

Key Points

  • SwiftUI views and modifiers help you quickly implement your design ideas.
  • The Library contains a list of primitive views and a list of modifier methods. You can easily create custom views, button styles and modifiers.
  • Neumorphism is the new skeumorphism. It’s easy to implement with color sets and the SwiftUI shadow modifier.
  • You can use ZStack to layer your UI elements. For example, lay down a background color and extend it into the safe area, then layer the rest of your UI onto this.
  • Usually, you want to apply a modifier that changes the view’s layout or position before you fill it or wrap a border around it.
  • Some modifiers can be applied to all view types, while others can be applied only to specific view types, like Text or shapes. Not all Text modifiers return a Text view.
  • Create a custom ButtonStyle by implementing its makeBody(configuration:) method. You’ll lose some default behavior like label color and dimming when tapped.
  • If the preview doesn’t show what you expect to see, try running it on a simulated or real device before you waste any time trying to fix a phantom problem.
  • Use GeometryReader to access the device’s frame and size properties.
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