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.

Making That Border Pop

Right now, you want the white sections of the border and the highlights to stand out. Time to change the background color of the element.

After the closing parenthesis for overlay, add the following lines:

.background(Color.lairBackgroundGray)
.cornerRadius(15)

Since a background can be any View and not just a color, you need to pass it Color.lairBackgroundGray. This is different from foregroundColor, which can only take a Color.

Background color

Changing the Border Shape

You may be asking yourself, “What’s up with the cornerRadius? Wasn’t that already taken care of with the border you created using the overlay?

Sort of. cornerRadius defined that the border would have a corner radius. However, the border is an overlay on top of the view. This means cornerRadius doesn’t change the shape of the view. You’ll still need to change the underlying shape yourself.

If you comment out cornerRadius, you’ll see your view still has sharp corners and your border is just an overlay.

Background corners visible

That’s ugly. But uncomment the modifier again, and everything is back to normal.

AccessoryView looks much better. But it doesn’t pop now, and the symbol in the middle lacks depth. To add that depth to the view, use the same technique you used with the symbol: Highlights and shadows.

Just below cornerRadius, add the following:

.shadow(
  color: Color(white: 1.0).opacity(0.9),
  radius: 18, 
  x: -18, 
  y: -18)
.shadow(
  color: Color.lairShadowGray.opacity(0.5),
  radius: 14, 
  x: 14, 
  y: 14)

Since the first shadow is white, it acts as a highlight. The second shadow is your, well, shadow.

Troubleshooting AccessoryView

If you run the app, you’ll encounter two problems that prevent you from viewing AccessoryView in its full glory.

AccessoryView problems

Here you see that:

  1. The background remains white, so the highlight cannot be seen against it.
  2. AccessoryViewRow has no vertical padding, so the shadow and highlight are cut off.

To fix the first problem, open LairView.swift and embed NavigationView‘s VStack in a ZStack. Command-click on VStack and select Embed in HStack (there’s no option to embed in a ZStack. Then change HStack to a ZStack.

LairView embed in ZStack

Add the following as the first element in the new ZStack right above the VStack:

Color.lairBackgroundGray.edgesIgnoringSafeArea(.all)

This adds Color in the desired background color, allowing it to fill the screen by ignoring all safe area edges.

To fix the second problem, open AccessoryRowView.swift. Then add the following two modifiers to the entire HStack in ScrollView:

.padding(.top, 32)
.padding(.bottom, 38)

This code adds padding to the top and the bottom of the view. Very cool.

You’re now done with AccessoryView. Build and run.

AccessoryView completed

That’s starting to look good!

Introducing Built-in Modifiers

You’ve used a couple of View modifiers to customize your first element. But SwiftUI sports a ton of built-in modifiers. For example:

  • animation: This applies an animation to the view.
  • clipShape: This sets a clipping shape for the view.
  • onAppear: This allows some code to run when the view appears.
  • rotationEffect: This rotates the view about a given point.

If you are interested in the full list, check out Apple’s documentation.

There’s a modifier for almost everything you could possibly want to do. Almost.

Discovering Inverse Masks

Before you can tackle the tab bar, you need to learn about inverse masks.

Since Apple included mask, you’d think it would also include inverseMask so everything opaque could cut a “hole” in the layer below. Well, Apple did not.

You will have to create your own modifier for this. Add a new Swift File to the Extensions group. Name it ViewExtension.swift.

Then replace the contents with the following code:

import SwiftUI

extension View {
  // 1
  func inverseMask<Mask>(_ mask: Mask) -> some View where Mask: View {
    // 2
    self.mask(mask
      // 3
      .foregroundColor(.black)
      // 4
      .background(Color.white)
      // 5
      .compositingGroup()
      // 6
      .luminanceToAlpha()
    )
  }
}

With this handful of lines, you have:

  1. Defined a new inverseMask that mimics mask.
  2. Returned the current view masked with the input mask and modified.
  3. Set the foreground color of the input mask to black.
  4. Ensured the background of the input mask is solid white.
  5. Wrapped the input mask in a compositing group.
  6. Converted the luminance to alpha, turning the black foreground transparent and keeping the light background opaque — i.e., an inverse mask!

It’s worth stressing that this would not work without compositingGroup. Before creating the compositing group, the background was a solid white layer, and the foreground was a black image with a transparent background sitting on top of the white background.

No compositing group

If you call luminaceToAlpha, the black foreground becomes transparent, and the entire solid white background becomes visible.

No compositing group luminaceToAlpha

But by using compositingGroup, you have a single-rendered layer composed of black and white pixels.

Compositing group

After running luminanceToAlpha, you get the dark foreground cut out from the view.

Compositing group luminaceToAlpha

Phew! Time to use this new effect on the tab bar buttons!

Tackling Tab Bar Buttons

This step is the most involved in the tutorial. Part of the problem is the use of inverse masks as required by the designer. But, of course, you already solved that part.

The other part is the limited options you have for customizing tab bar buttons. Luckily, the previous developers of SmartLair didn’t know how to properly use TabView, so they implemented it manually. This makes your job easier! As for the previous developers, may they rest in peace.

Even so, you still need to design both a selected and unselected look for the buttons.

To get started, open TabBarItemView.swift and add the following constant above the definition of body:

let size: CGFloat = 32

Yes, that’s a hard-coded size. Don’t worry. This is just for the tutorial. You can fix it in a point release later. ;]

Just below the constant, add a helper function:

func isSelected() -> Bool {
  return selectedItem == smartView
}

This function does exactly what it says on the tin: It asks whether the current tab bar item is selected. It does so by checking if the bound selectedItem matches its defined SmartView.

Because this is a toggle button, the function can help you determine how to present the tab bar button.

Next, add the following stubs to the bottom of TabBarItemView:

var buttonUp: some View {
  EmptyView()
}

var buttonDown: some View {
  EmptyView()
}

You’ll use these to better organize how the tab bar items look when they’re up and down. For now, the EmptyViews are placeholders to prevent Xcode from nagging you.

Now, update the Button in the body to look like this:

Button(action: {
  self.selectedItem = self.smartView
}) {
  // This is the new stuff!
  if isSelected() {
    buttonDown
  } else {
    buttonUp
  }
}

You replaced the Image with a conditional to decide which button state to present to the user.

All that’s left is to design how the buttons look.

Designing the Unselected Tab Bar Button

You will start with the unselected button and design it similarly to AccessoryView. There’s one difference, though. Instead of making the symbol look raised above the surface, you will make it look cut from the surface. It’s inverse mask time!

Replace buttonUp with:

var buttonUp: some View {
  // 1
  var buttonMask: some View {
    // 2
    ZStack {
      // 3
      Rectangle()
        .foregroundColor(.white)
        .frame(width: size * 2, height: size * 2)
      // 4
      Image(systemName: self.icon)
        .resizable()
        .scaledToFit()
        .frame(width: size, height: size)
    }
  }
    
  // 5
  return buttonMask
}

In this code, you have:

  1. Defined a property within a property to store the mask you’ll use for the button. This will keep the code a little more readable.
  2. Used a ZStack as the top-level view for the mask.
  3. Defined a white Rectangle to act as the background of the mask.
  4. Created an Image that is half the width and height of the background rectangle. This image will be what’s cut out when you turn this button into an inverse mask.
  5. Returned the mask, so you can see what it looks like. You’ll replace this line after checking to make sure it looks right.

You should see a very simple icon in the middle of the canvas preview.

Button mask preview

Note: Apple may call this icon pencil.tip, but just tell your clients its Volcano Lair.

Next, replace return buttonMask with the following:

// 1
var button: some View {
  // 2
  ZStack {
    // 3
    Rectangle()
      .inverseMask(buttonMask)
      .frame(width: size * 2, height: size * 2)
      .foregroundColor(.lairBackgroundGray)
    }
  }

// 4
return button

Here, you have:

  1. Defined another property for the actual button.
  2. Used a ZStack to contain all the elements. There will be more to come.
  3. Created a Rectangle that uses buttonMask as an inverse mask!
  4. Returned the button.

If you look at the canvas preview, you finally see the fruits of your inverse mask labor!

Button inverse mask

Adding Unselected Tab Bar Button Effects

A button with a hole isn’t spectacular on its own, so you’ll add some more effects to it!

Just above the button‘s Rectangle, but still within ZStack, add the following LinearGradient:

LinearGradient.lairHorizontalDarkReverse
  .frame(width: size, height: size)

This LinearGradient is just big enough to cover the symbol cutout in the button and be visible through the cutout.

LinearGradient visible through cutout

Next, add the following modifiers to the button‘s Rectangle just after foregroundColor:

.shadow(color: .lairShadowGray, radius: 3, x: 3, y: 3)
.shadow(color: .white, radius: 3, x: -3, y: -3)
.clipShape(RoundedRectangle(cornerRadius: size * 8 / 16))

Here, you added highlights and shadows but in the opposite direction from before. That’s because you want them to affect the cutout in the middle of the button. The clipShape not only rounds the corners of the button; it also contains the highlights and shadows within those bounds. If they leaked out, it wouldn’t look right.

Clip shape and inner shadows

Finally, add these effects to the entire ZStack:

.compositingGroup()
.shadow(
  color: Color.white.opacity(0.9),
  radius: 10, 
  x: -5, 
  y: -5)
.shadow(
  color: Color.lairShadowGray.opacity(0.5),
  radius: 10, 
  x: 5, 
  y: 5)

First, ensure all views within the ZStack are in a compositing group. Then add the typical highlight and shadow to give the button a raised look. Your unselected button now looks like this:

Unselected button with white background

And when you get the correct background color behind it, it will look like this:

Unselected button with correct background

Designing the Select Tab Bar Button

An unselected state for a button is not enough. You’ll need to add the selected state, too.

Before you get started, scroll down to the bottom of TabBarItemView.swift to TabBarItemView_Previews. In the parameter list for the preview TabBarItemView, change the selectedItem to be .constant(SmartView.lair):

struct TabBarItemView_Previews: PreviewProvider {
  static var previews: some View {
    TabBarItemView(
      selectedItem: .constant(SmartView.lair),
      smartView: .lair, 
      icon: "pencil.tip")
  }
}

This presents the button as selected in the preview canvas, so you can see the changes as you make them.

OK. Now, replace the current implementation of buttonDown with the following:

var buttonDown: some View {
  ZStack {
    Rectangle()
      .foregroundColor(.lairBackgroundGray)
      .frame(width: size * 2.25, height: size * 2.25)
      .cornerRadius(size * 8 / 16)
  }
}

Here, you defined the shape, color and size of the button when it is selected.

Selected button background

Unfortunately, it’s a bit larger than the unselected button. Here’s the cross-section effect you’re shooting for from a different angle:

Button cross section

As such, you need to make it slightly larger to account for the outer rim of the selected button. Previously, this would have been “hidden” in the highlights and shadows, but now it needs to be visible.

Add the following Rectangle below the one you just created:

Rectangle()
  .foregroundColor(.lairBackgroundGray)
  .frame(width: size * 2.25, height: size * 2.25)
  .cornerRadius(size * 8 / 16)
  .inverseMask(Rectangle()
    .cornerRadius(size * 6 / 16)
    .padding(size / 8)
  )

The preview looks exactly the same, but don’t adjust your screen. Instead, change foregroundColor of this Rectangle to .blue. See what happens.

Selected button blue border

You created a border around the button that’s invisible, but you didn’t use the same overlay trick from earlier. That’s because an inverse mask will allow you to create a shadow on the inside of the button, while the overlay will not.

Change the .blue back to .lairBackgroundGray.

Now, add these modifiers to the Rectangle after inverseMask‘s closing parenthesis:

.shadow(
  color: Color.lairShadowGray.opacity(0.7),
  radius: size * 0.1875,
  x: size * 0.1875, 
  y: size * 0.1875)
.shadow(
  color: Color(white: 1.0).opacity(0.9),
  radius: size * 0.1875,
  x: -size * 0.1875, 
  y: -size * 0.1875)
.clipShape(RoundedRectangle(cornerRadius: size * 8 / 16))

These add the typical inner shadow and highlight to the inverse mask and clip the outer shape of the button so that the shadows don’t bleed through to the other side.

Selected button without a logo

It’s starting to look like a button that’s been pressed!

Incorporating the Button Symbol

You’ll now add the button symbol. You’ll make it slightly heavier — that is, darker — to show the button has been selected. You’ll also skip the inverse mask this time.

Add the following below the last Rectangle and all of its modifiers:

LinearGradient.lairHorizontalDarkReverse
  .frame(width: size, height: size)
  .mask(Image(systemName: self.icon)
    .resizable()
    .scaledToFit()
  )
  .shadow(
    color: Color.lairShadowGray.opacity(0.5),
    radius: size * 0.1875,
    x: size * 0.1875, 
    y: size * 0.1875)
  .shadow(
    color: Color(white: 1.0).opacity(0.9),
    radius: size * 0.1875,
    x: -size * 0.1875, 
    y: -size * 0.1875)

With this view, you use the button icon to mask the LinearGradient at the appropriate size and then add highlights and shadows.

Selected button with logo

There’s one last effect to add: A nice gradient border around the button. After the closing brace of the ZStack, add the following overlay:

.overlay(
  RoundedRectangle(cornerRadius: size * 8 / 16)
    .stroke(LinearGradient.lairDiagonalLightBorder, lineWidth: 2)
  )

This overlay defines a border that’s a rounded rectangle with a width of two points and uses a diagonal linear gradient.

Final selected button with white background

Again, here’s how it will look with the proper background color:

Final selected button with correct background

Tidying up the Details

Before you build and run, you’ll make some small changes to the code in ContentView.swift.

First, find and remove the following Rectangle:

Rectangle()
  .frame(height: 1.0 / UIScreen.main.scale)
  .foregroundColor(Color(white: 0.698))

This defined a one-pixel line between the tab bar and the rest of the screen, but it’s no longer necessary.

Now, change the padding and the backgroundColor of the TabBarView to this:

.padding(.bottom, geometry.safeAreaInsets.bottom / 2)
.background(Color.lairBackgroundGray)

Here, you halved the amount of padding to lower the tab bar buttons and give the rest of the content room. You also matched TabBarView‘s background color to that of the NavigationView.

Build and run, and see out how far you’ve come.

Final tab bar

Not too shabby!

Crafting the Progress Bar

You’re in the home stretch. There’s one final UI element to tackle.

To get that villainous look, the progress bar will rely less on shadows and more on linear gradients. The bar’s long, thin nature allows you to make this substitution.

Open ProgressBarView.swift and take a look at what’s currently there.

It’s divided into two main sections: An HStack that contains all the labels and a ZStack to draw the progress bar. If you look at the ZStack, you see it’s made of two Capsules. One is for the total bar length, the other for the progress. Capsule is a shape included in SwiftUI. Score!

First, update the two Text so that your HStack looks like this:

HStack {
  Text(self.title)
    .foregroundColor(.lairDarkGray)
    .bold()
  Spacer()
  Text("\(Int(self.percent * 100))%")
    .foregroundColor(.lairDarkGray)
    .bold()
}

The bold text gives the bar a more pronounced look, while the dark gray color removes some harsh contrast that the default black color had.

Progress bar text update

Next, in the progress bar ZStack section, embed the first Capsule in another ZStack, change the frame height to 14 and the foreground color to .lairBackgroundGray.

The first capsule should now look like this:

ZStack {
  Capsule()
    .frame(height: 14)
    .foregroundColor(.lairBackgroundGray)
}

You’ve used a slightly lighter color for the capsule to match your current color scheme. The increase in the height will soon make the progress bar feel like it’s sitting in a groove cut out for it.

Under that same Capsule, but still within the new ZStack, add this LinearGradient:

LinearGradient.lairHorizontalDarkToLight
  .frame(height: 14)
  .mask(Capsule())
  .opacity(0.7)

The LinearGradient.lairHorizontalDarkToLight starts dark at the top, goes to 100 percent clear color in the middle and ends with white at the bottom. You use this to simulate shadow and highlight effects. The clear color in the middle of the gradient allows the color of the Capsule to shine through. It now looks like it’s a groove cut out of the iPhone.

You also set the frame height to match the previous Capsule and used a Capsule as a mask to ensure it has the same shape. Finally, the opacity lets more of the lairBackgroundGray color through.

Progress bar background

Modifying the Second Capsule

Next, check out the Capsule that draws the actual progress. It’s currently just a boring blue blob. Replace Capsule and its two modifiers with the following:

// 1
ZStack {
  // 2
  LinearGradient.lairHorizontalLight
    // 3
    .frame(
      width: (geometry.size.width - 32) * CGFloat(self.percent),
      height: 10)
    // 4
    .mask(
      Capsule()
        .padding(.horizontal, 2)
    )
}
  1. Created a ZStack to contain the LinearGradient and the second one you’ll add next.
  2. Added a horizontal gradient, which will showcase the importance of the length of the progress bar, going from light to dark.
  3. Used the same frame size as the Capsule you deleted. It uses the percent property and the size of the view to calculate how wide it should be.
  4. Masked the gradient with a Capsule since, by default, a LinearGradient is a rectangle. You also padded the Capsule to give it a pleasant offset from the groove it sits in.

Progress bar foreground with horizontal gradient

Add the next LinearGradient just below the previous one, but still within the ZStack:

LinearGradient.lairVerticalLightToDark
  .frame(
    width: (geometry.size.width - 32) * CGFloat(self.percent),
    height: 10)
  .mask(
    Capsule()
      .padding(.horizontal, 2)
  )
  .opacity(0.7)

This gradient is very similar. The only differences are that the gradient is vertical and the opacity is 0.7. This gradient simulates the shadows and highlights but in the opposite direction to the groove gradient. It makes it look like the progress bar is resting inside the groove.

Progress bar foreground with both gradients

Deepening the Progress Bar

To give the progress bar some depth, you want to include a shadow to the entire shape within the groove. Add the following shadow to the ZStack containing the two LinearGradients:

.shadow(
  color: Color.lairShadowGray.opacity(0.5),
  radius: 2, 
  x: 0, 
  y: 1)

Because this shadow bleeds outside of the groove as well, you need to clip the top-level ZStack (the one with leading alignment, to help you match braces):

.clipShape(Capsule())

Here’s how the progress bar should look on the screen:

Final progress bar

This was a complicated section, so if you’re not seeing the results you expect, check against the end project included in the materials.

Navigating the Navigation Bar

Two parts of the navigation view don’t yet fit your newly styled app: the navigation bar title and the profile icon.

For the navigation bar title, you need to use an appearance proxy. Open LairView.swift, and add this init to LairView:

init() {
  UINavigationBar.appearance().largeTitleTextAttributes =
    [.foregroundColor: UIColor.lairDarkGray]
}

Next is the profileView. This is your challenge! Change it to use a LinearGradient.lairHorizontalDark. After completing this tutorial, you know everything you need to do it yourself!

Hint: you’ll need to hard code the size to be 22×22 points.

[spoiler title=”Solution”]

var profileView: some View {
  LinearGradient.lairHorizontalDark
    .frame(width: 22, height: 22)
    .mask(
      Image(systemName: "person.crop.circle")
        .resizable()
        .scaledToFit()
    )
  .padding()
}

[/spoiler]

When you’re done, the finished app should be phenomenal!

Final app

Where to Go From Here?

Woohoo! You’ve reached the end of this tutorial. You clearly have a bright future ahead of you as the lead iOS engineer for all evil villains!

You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

While you’ve crafted a few elements in this modern skeuomorphic style, there are others to try. For example, you could tackle a switch, a slider or a search bar. You could also take this skeuomorphic design to the next level with haptics. That would really be something special!

If you’re interested in getting deeper into SwiftUI, check out How to Create a Splash Screen With SwiftUI, Getting Started With SwiftUI Animations or the SwiftUI by Tutorials book available on this site.

Thank you for trying this tutorial. If you have any questions or comments, please join the forum discussion below!