How to Create a Neumorphic Design With SwiftUI

In this neumorphic design tutorial, you’ll learn how to use SwiftUI’s powerful modifiers to craft beautiful custom elements. By Yono Mittlefehldt.

Leave a rating/review
Download materials
Save for later
Share

Don’t deny it. You feel the longing, that absence to make hearts grow fonder. You’re heart’s been fond to near bursting since the release of iOS 7. You miss your dear skeuomorphism.

When you look at your screens today, they look so flat and boring. Then again, when you look at those pre-iOS 7 designs, they’re old and outdated. If only there was a skeuomorphic design language that looked new and fresh.

You’ve come to the right place!

In late 2019, a tweet made the rounds that included designs from Alexandar Plyuto. And they are fabulous:

Modern skeuomorphic design

In this tutorial, you’ll learn how to recreate some of these design elements in SwiftUI. You will discover how to:

  • Use linear gradients to give depth to your views.
  • Create complex effects by combining simple ones such as shadows.
  • Master the art of the inverse mask, an effect that does not exist natively in SwiftUI.

Get ready for a whole lot of design fun!

Getting Started

Smart homes are all the rage these days. Not to be outdone, super villains and mad scientists are now super into smart lairs. Hey, you don’t build your HQ in an active caldera without wiring some mad tech throughout.

Here’s the little-known secret: The app super villains use to control their lairs — called SmartLair — is as flat and boring as the Home app on your iPhone. Maybe more so.

In this exercise, you’ve retrieved a copy of SmartLair’s source code through illicit back channels. You’ve been threatened, erm, hired to snazz it up.

To get started, click the Download Materials button at the top or bottom of this tutorial. Open the begin project and explore its contents.

Remember: This is a big opportunity for you. If the villains like your work, it could lead to more contracts in the future. But if they don’t, well, laser sharks aren’t known to be picky eaters.

Introducing Linear Gradient

Before you begin, you need to familiarize yourself with LinearGradient. Skeuomorphic design leans heavily on linear gradients. They’re kind of a big deal.

In SwiftUI, you define a linear gradient like this:

LinearGradient(
  gradient: Gradient(colors: [.white, .lairLightGray]),
  startPoint: UnitPoint(x: 0.2, y: 0.2),
  endPoint: .bottomTrailing
)

If used as a view to cover the whole screen, it looks like this:

LinearGradient whole screen example

Here, you define that the gradient will go from white to lairLightGray, a Color you will add to the project later. You can have more than two colors if you want the gradient to pass through several:

LinearGradient using multiple colors

startPoint and endPoint are coordinates relative to the unit square, which has a coordinate of (0, 0) in the top left and (1, 1) in the bottom right. However, they aren’t required to be within this range. For instance, a negative coordinate value would start the gradient outside of the view.

In the code above, there are also some predefined constants for typical start and end points, such as .leading, .trailing, .top, .bottom and combinations of those.

Customizing Your First Element

Start by attacking the boring looking AccessoryView. It represents the large rectangles in the middle of the screen with labels such as Control Room Lights and Dungeon.

AccessoryView starter

Drag the Extensions folder from the downloaded materials to your Xcode project. Place it above the Views group. Make sure Copy items if needed and Create groups are selected, then click Finish.

These three files define some UIColor constants, the SwiftUI Color equivalents, and some LinearGradient definitions. You already saw how to create a LinearGradient, but skeuomorphic design uses a lot of gradients. It takes too long to go through them one by one, and super villains aren’t the patient type, so you’ll get straight to it.

Including the Image Gradient

In AccessoryView.swift under the definition for body, find the line that begins with image in the VStack. Replace that line with the code below, but don’t remove the modifiers frame, padding and font that are there:

LinearGradient.lairHorizontalDark
  .mask(image.resizable().scaledToFit())

You just turned the SFSymbol image into a mask for the gradient. The layer with the gradient will be cut out in the shape of the opaque pixels from the image. Cool! You can build and run to see the changes, or turn on the previewing canvas in Xcode to see changes immediately:

AccessoryView gradient symbol

Adding Highlight and Shadow

Add this code after font:

// 1
.shadow(color: .white, radius: 2, x: -3, y: -3)

// 2
.shadow(color: .lairShadowGray, radius: 2, x: 3, y: 3)

With these two lines, you have:

  1. Added a white shadow that is offset relative to the top left of the image.
  2. Added a dark shadow that is offset relative to the bottom right.

This contrast provides the illusion of depth in all directions. You can use this shadow trick to get a raised effect.

AccessoryView final symbol

The code for the element with all modifiers should now look like this:

LinearGradient.lairHorizontalDark
  .mask(image.resizable().scaledToFit())
  .frame(width: 150, height: 236)
  .padding(40)
  .font(.system(size: 150, weight: .thin))
  .shadow(color: .white, radius: 2, x: -3, y: -3)
  .shadow(color: .lairShadowGray, radius: 2, x: 3, y: 3)

Changing the Text Gradient

The text clearly can’t stay black. Add the following modifier to Text within the HStack:

.foregroundColor(.lairDarkGray)

Now, your text is an attractive shade of lair dark gray.

AccessoryView text color

The complete HStack should look like this now:

HStack {
  Text(title)
    .foregroundColor(.lairDarkGray)
    .bold()
    .padding(.leading)
    .padding(.bottom)
  Spacer()
}

Note that if you put foregroundColor in a different location among Text, it still works. You can order some modifiers any which way, but as you’ll see, the order matters for others.

Rounding the Corners

Your next step is to round off the border corners. It’s not as straightforward as it sounds, though.

For example, try something like this:

.border(Color.gray, width: 1)
.cornerRadius(15)

And you see that the corners are cut off.

Cut off border corners

Swap the two modifiers like so:

.cornerRadius(15)
.border(Color.gray, width: 1)

And you see that the borders maintain sharp corners.

Sharp corner border corners

Fortunately, there is a workaround. You can use an overlay to obtain those sweet, sweet curved borders.

Delete border and, if you added it, cornerRadius. Replace them with:

.overlay(
  RoundedRectangle(cornerRadius: 15)
    .stroke(LinearGradient.lairDiagonalDarkBorder, lineWidth: 2)
)

This code lays another view over your view — i.e., an overlay — that draws a rounded rectangle with the desired corner radius.

For structs conforming to Shape, such as RoundedRectangle or Path, use stroke instead of border to draw a line around it.

With this change, you also add a gradient to the border by stroking it with LinearGradient.lairDiagonalDarkBorder instead of Color.gray. This addition gives the border a bright highlight in the top left of the element and a darker shadow in the bottom right. Simultaneously, you make the border heavier by increasing the width of the border/stroke to “2.”

AccessoryView rounded border

You may notice the top and bottom borders are thinner than the left and right. That’s because the view has no vertical padding and clips half of the stroke. No worries. You’ll fix this in a bit.