SwiftUI app for iOS 15

Design a SwiftUI app with custom layouts, animations and gestures using Xcode 13, Concurrency, Searchable and a whole lot more.

Play Video

What's New in SwiftUI 3

This year, we can take on more challenging apps thanks to SwiftUI’s maturity, with new features like concurrency with async / await, SF Symbols 3 with new styles, Markdown support using Text, button styles, swipe actions, blur materials, drawing graphics with canvas and much more. Apple is committed to SwiftUI and used it to build apps like Photos, Maps, Shortcuts, Weather, Photos, Find My and system interfaces like Apple Pay.

SF Symbols 3

With 3,100 free icons now, SF Symbols 3 is a huge time-saver for both designers and developers. You can customize more than ever before with the new rendering modes: Hierarchical and Palette on top of the existing Monochrome and Multicolor. Additionally, it’s now easier to add variants like fill and circle by using modifiers while keeping icon names as short as possible.

SF Symbols are now simpler to use and variants can be customized using a symbolVariant modifier. If not specified, fill and outline will be platform specific.

Image(systemName: "person") .symbolVariant(.circle.fill)

You can change the rendering mode by using the symbolRenderingMode modifier.


Finally, using the new foregroundStyle, you can set up to 3 colors for your symbol depending on the rendering mode.

.foregroundStyle(.blue, .blue.opacity(0.3), .red)

Blur Material

Material sheets, also known as background blur, is an excellent way to make your text more readable. If you have a multi-layered design with background + card + content, the material is a great alternative to just using opacity. iOS and macOS uses it for their sidebar, tab bar, modals, control center and even apps like Weather. The good news is that it’s now available with a single modifier.


Graphics in Canvas

When you’re drawing vector graphics that don’t need tracking or validation, Canvas is a great option. You can create performant animations by using the TimelineView. In this project, we’ll learn how to draw icons from SF Symbols 3, turn SVG code to Path and animate a blob in a loop. It’s really fun!


Working with functions that need to wait for tasks to complete such as an API call is now much easier with async / await. Before, you had to use completion handlers which can make the code difficult to read and debug. With concurrency, your code will be shorter, safer and you will have access to the new AsyncImage and Pull to Refresh features that leverage on the new API.


AsyncImage allows you to load an image from a URL without relying on a third-party library. It offers great features like placeholder while loading and phases for error handling. On top of that, you can easily apply transitions and animations.

Let’s load an image from Lorem Picsum with placeholder that shows a loading indicator

AsyncImage(url: URL(string: "https://picsum.photos/200")) { image.resizable() } placeholder: { ProgressView() }

You can also load in phases such as empty, success, failure and default. Additionally, you can customize the transition and animation timing.

AsyncImage(url: URL(string: "https://picsum.photos/200"), transaction: .init(animation: .easeOut)) { phase in switch phase { case .empty: Color.white case .success(let image): image.resizable().transition(.scale) case .failure(_): Color.gray @unknown default: Color.gray } }


We can create an async function that gets data from an API without using completion handlers. Before decoding the JSON data, we have to wait for the API call to complete. Also, we’re using do / catch to handle errors.

func fetchAddress() async { do { let url = URL(string: "https://random-data-api.com/api/address/random_address")! let (data, _) = try await URLSession.shared.data(from: url) address = try JSONDecoder().decode(Address.self, from: data) } catch { print("Error fetching") } }

We can call the async function using the new task modifier.

.task { await fetchAddress() }


If you need to refresh the data, you can use the new refreshable modifier, which automatically creates the Pull to Refresh mechanism on a List.

.refreshable { await fetchAddress() }

Markdown Support

Nowadays, a lot of content is written using Markdown. It supports links, images, and basic text styling like headings, bold, italic, bullet points, etc. It’s a friendlier alternative to HTML, which can be daunting to implement. Best of all, SwiftUI 3 now allows you to support Markdown by simply using the Text.

Text("By clicking on **Sign up**, you agree to our [Terms of service](https://designcode.io/terms)")

Safe Area Inset

You can now place content on top of a Scrollable View. This is especially great for setting the scroll area when you have a custom tab bar or nav bar.

.safeAreaInset(edge: .bottom) { VStack {}.frame(height: 44) }

Preview Orientation

The live preview is no longer limited to portrait mode. You can now add a modifier to show your design in landscape as well.



Apple has always prioritized accessibility in their platforms. This year, they’ve improved it massively for SwiftUI.


The new Accessibility inspector panels will make it easy to spot the elements and its properties.


While labels will automatically give the description for VoiceOver, images will default to the local image name. For async images, you’re out of luck. That’s why you should add an accessibilityLabel modifier when necessary.

Button(action: {}) { // Image }

Sometimes, your element won’t be picked up, so you’ll need to be explicit.


For collection items like list rows and cards, you should also wrap everything inside a Button. Make sure to set the buttonStyle to plain. Otherwise, you’ll gain some unwanted tint color and alignment.


If you absolutely want to use onTapGesture and want to avoid Button, you can set the container and combine the inner elements.

.accessibilityElement(children: .combine)


I know It is tempting to use onTapGesture on labels and images, but you will not only lose the tap feedback but also might get an inconsistent tappable area regardless of the frame or padding you apply. As a result, it’s best to wrap your visual elements inside a Button.

// Don't
Image(systemName: "magnifyingglass") .font(.system(size: 17, weight: .bold)) .frame(width: 36, height: 36) .onTapGesture { show.toggle() }

// Do
Button { showSheet.toggle() } label: { Image(systemName: "magnifyingglass") .font(.system(size: 17, weight: .bold)) .frame(width: 36, height: 36) }


Some elements like background layers don’t need to be picked up by VoiceOver, so you can hide them.

.accessibility(hidden: true)


It’s never been easier to create a search experience with text field focus and suggestions. In this course, we’ll learn to customize the looks while building a search experience with data and search completion.

.searchable(text: $text) { ForEach(suggestions) { suggestion in Button { text = suggestion.text } label: { Text(suggestion.text) } .searchCompletion(suggestion.text) } }

Swipe Actions

With a few lines of code, you can add a fairly complex swipe interaction that can pin, delete or favorite items in a List.

.swipeActions(edge: .leading) { Button { withAnimation { isPinned.toggle() } } label: { if isPinned { Label("Unpin", systemImage: "pin.slash") } else { Label("Pin", systemImage: "pin") } } }

This Course

First, we won’t use a single third-party library for creating trivial things such as translucent material sheets. Second, we’re going to build a fully tab bar and navigation bar. There will be great lessons for beginners and experts alike, especially if you want to use SwiftUI for both prototyping and building real apps.


Compared to many frameworks, SwiftUI is easy to learn and has a ton of resources made by Apple and the community. Since my teaching style is friendly, focused on visuals and we’ll code together step-by-step from a designer’s perspective, you can learn with virtually zero experience. However, it is recommended that you are familiar with a computer and have light experience with HTML and CSS.

Developing an app for iOS 15 requires a Mac with Big Sur or later, and Xcode 13.

If you’ve never touched SwiftUI before, it is recommended to take my SwiftUI for iOS 13 course beforehand as it will go more in-depth with the basics. Beginners can take this course, but minimum experience with HTML and CSS (or coding equivalent) recommended.

Learn Design While Coding

Most coding courses consider design as an afterthought. They use stock UIs and don’t teach how to properly create custom layouts. They don’t dive deep into design techniques such as typography, colors, animations, shadows, outlines, blur, etc. Whether you are prototyping or building an app that needs to stand out, alone or with a solid design and engineering team, learning advanced layout techniques will help you reach that extra mile that will push you ahead of the competition.

While implementing the design, I will explain my decisions and show you techniques that I personally use as a designer.

All the design source files will be shared, layered and with components, so you can inspect and try to replicate in your own projects.

Basic Layout and Styling

We’ll start the course by getting familiar with Xcode 13’s layout, importing assets a design tool and creating a basic layout with Text, Stacks, Images, Spacers and its modifiers without writing code. That’s right, by using Xcode’s Inspector and Library, you can drag and drop UI elements and edit the properties. It’s perfect for beginners and designers. As you get used to the syntax of SwiftUI, you’ll want to edit your code directly instead of using the Inspector. Just like CSS, knowing the code is far more efficient and powerful, but that can come later.

Then, we’ll learn how to use SF Symbols 3 and effects such as Colors, Materials, Blur and Transform. You’ll pick up more advanced techniques like maxWidth, alignment, overlay, offset, etc.

Animations, Transitions and Gestures

Unlike most courses, a big focus on this course is about giving life to your user interface with animations that enhance and not over-the-top. Transitions are easily done by using states and applying withAnimation when the states change.

@State var show = false

.offset(y: show ? 10 : 0) .onTapGesture { withAnimation { show.toggle() } }

Custom Tab Bar

One of the things I wanted to teach in this course is how to create a fully custom user interface and all the challenges that come with creating a custom tab bar and navigation bar. Understanding this can not only help with building an app that stand out but also a great way to prototyping techniques on a real platform that developers use.

Custom Navigation Bar

When you have a custom tab bar, it makes sense to pair it with a custom navigation bar. While it is recommended to use the built-in Navigation View and Tab View for best adaptability across platforms and accessibility, learning how to build and style your own will take your skills to the next level. Popular apps like Instagram, TikTok, Clubhouse actually build their very own bars. For designers, this is fantastic for prototyping since you will have less contraints and are not forced to use the stock user interface for everything.

Adaptive Layouts

Even if you don’t plan on supporting all platforms, it is important to have a solid foundation for adaptive layouts. Like this, you’ll be prepared for all sorts of resolutions and layout switch. You’ll learn when it is appropriate to use Spacer, maxWidth, GeometryReader, Environment, Size Classes and detecting devices.

Custom Modifiers

When you repeat the same modifiers or collection of styles over and over, it is a good idea to start your custom modifiers. A huge advantage with this technique is that you can isolate the styles, just like a component, and apply conditions based on resolutions and devices. Also, when you apply changes, it’ll apply everywhere at once.

struct BackgroundStyle: ViewModifier { var cornerRadius: CGFloat = 20 var opacity: Double = 0.6 @AppStorage("isLiteMode") var isLiteMode = true func body(content: Content) -> some View { content .backgroundColor(opacity: opacity) .cornerRadius(cornerRadius) .shadow(color: Color("Shadow").opacity(isLiteMode ? 0 : 0.3), radius: 20, x: 0, y: 10) } }

Omitting Types

You can now omit the value types, making the code shorter and easier to read. This might not seem like a big deal at first, but it’s actually a huge improvement in term of syntax for better auto-completion and readability.

// Before
// Now

// Before
.shadow(color: Color.black.opacity(0.2), radius: 20, x: 0, y: 10)
// Now
.shadow(color: .black.opacity(0.2), radius: 20, x: 0, y: 10)

Objects can also be expressed as properties. Like this, there is less confusion between the two.

// Before
.background(LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing))
// Now
.background(.linearGradient(colors: [.red, .blue], startPoint: .top, endPoint: .bottom))

Swift Playgrounds for iPad

For the first time, you can build entire apps and deploy to the App Store using the iPad exclusively. Playgrounds is a great way for beginners to learn Swift without requiring an expensive Mac machine. You can’t have a simpler setup — you just need to download from the App Store and you have access to a bunch of beginner-friendly template apps where you can start toying with the code.

Note: Swift Playgrounds 4 will be available in late Fall 2021.

What you'll build

In this course, we’ll build an iOS 15 app from scratch focusing on custom layout techniques, animations and interactions. We’ll build our own custom Tab Bar, Navigation Bar and interactions. While SwiftUI hasn’t changed significantly over the past 3 years, the framework is far more comprehensive, with more concise code for state management, animation and now fully supporting effects like blur material.

Download SF Fonts

If you’re designing for iOS, it is essential to download the SF Fonts from Apple. There are 4 fonts:

  • SF Pro is the main font that is consistent across Apple’s platforms, specifically for iOS, iPadOS and tvOS.
  • SF Compact is for watchOS design, optimized to be readable at smaller sizes.
  • SF Mono (optional) for evenly-spaced characters, which is useful for showing code.
  • New York is a serif typeface, typically for more traditional designs such as Books and News.

Figma Template

As I was building this app, I followed the design that I made in Figma. From the Figma file, you’ll be able to inspect the typography, sizes, colors and assets. All the components are neatly organized and each screen has been adapted to both light and dark modes.

Download Xcode 13

Xcode is the development tool that we’ll use to create our iOS app. You can download it and install it directly from the App Store. Unless you’re a seasoned developer, I don’t recommend using the beta version and stick to the public releases.

Create a New Xcode Project

Once Xcode is installed, open the app and you’ll see a welcome screen. Click on Create a New Xcode Project.

In this course, we’ll learn to create custom UI components such as the Tab Bar, Nav Bar and Modals on top of the stock UI. As a result, we’ll focus on iOS to keep things simple. Select App inside the iOS tab, then click Next.

Time to fill the project details:

  • Product Name: this is the name of your app. E.G. News, Reminders, Mail. You don’t need to include App at the end.
  • Team: this is useful if you wish to test on your device or publish to the App Store. If you have a developer account with Apple, you can set here, but otherwise you can do this later in Preferences under Accounts Tab with your Apple account. You can set None for now.
  • Bundle Identifier: this is your domain name in reverse. It’s a unique ID for all your apps. E.G. com.twitter, com.instagram, com.figma.
  • Make sure to select SwiftUI as the interface and language to Swift.

Finally, click Next. Select a place to save your Xcode project, such as Downloads.

Congratulations, you’ve just created your Xcode Project! The first time you land, you’ll see a fully open project with the project files on the left, the code editor in the middle with the Preview next to it and the Inspector in the right. Click on Resume in the top right to start seeing a Preview of your code.

Make sure to have your a device selected in top bar. A good default is the iPhone 13.

Xcode Layout

Before we start, it is helpful to know your way around Xcode. These are the essential parts of working with SwiftUI inside Xcode 13.


The Navigator is where you’ll navigate all the files, commits, issues for your project. By default, you’ll have the Project Navigator tab selected. That’s where you’ll spend most of your time on, so it’s important that you know how to get back to it, especially when Xcode sometimes auto-switches to other tabs when showing issues.


Whenever you have something selected in your code or assets, the inspector will give options based on that. The Attributes Inspector tab is the default and most relevant tab most of the time.