Hallo handdoeken en wastafels, Leo hier. Today we will do an animated launch screen in SwiftUI.

Launch screens are controversial. Some argue that you shouldn’t have them and instead put a skeleton view or use the redacted view modifier to all your first screen views until the content is fully loaded. On the other hand, people are used to loading screens in the past, and creating a custom launch screen can be an engaging experience for the user, especially if you use that time to promote different features of your app.

One way or another, I think you should see what is the best fit for your product and context. This is the magic of iOS development, there’s no silver bullet. This article is for the ones that want to add a simple SwiftUI launch screen, also known as Splash Screens.

In this tutorial, we’ll use techniques that I describe in another article about refactoring massive SwiftUI views. If you are interested in that topic give it a look. And we covered a little more on animations in the Toast Views in SwiftUI.

No more talking, let’s code! But first…  


Painting of The Day

I chose today a 1902 painting called The Pool by the master Victor Borisov-Musatov.

Victor Elpidiforovich Borisov-Musatov, lived from April 1870 to November 1905. He was a Russian painter, prominent for his unique Post-Impressionistic style that blended with Symbolism, pure decorative style, and realism. Together with contemporary artist Mikhail Vrubel, he is often referred to as the creator and the root of the Russian Symbolism style.

I chose this painting because when we jump in water pools the sound that makes is: Splash! Got it?  


The Problem – Launch Screen in SwiftUI

You want to create a fully animated Launch Screen in SwiftUI.

In this article you will learn how to create a Launch screen in SwiftUI using modern approaches like environment variables, create a state machine to control the animations, and also async/await to mock an API call.

The API call part is very important because after that is completed it will trigger the dismissal of the Launch Screen, and this behavior is really close to or if not identical to what we have in a lot of apps. Keep in mind the responsibility of everything here. The act of call to dismiss doesn’t come from the view itself as an external agent, but the dismissal itself is the responsibility of the Launch Screen State Manager.

The end result is this moving hurricane and then the “Hello, Apple Script” view like the gif below:

Now we have to answer an important question…  


How to add a Launch Screen in SwiftUI?

There are several ways to do that. We will implement a straightforward one in just four steps. The steps are:

  1. Ignore the default launch screen from Info.plist.
  2. Create a Manager or State machine to control the appearance of the launch screen.
  3. Create your launch screen.
  4. Connect everything!

Let’s code our new launch screen feature.  


Step 1 – Implementing the Launch Screen in SwiftUI

Let’s follow the steps we said in the session above.

First of all, we will create a new SwiftUI project and we need to remove the default screen from Info.plist because we will customize the behavior. Creating a launch screen with default behavior is a subject for another article.

Now go to your project target, and in the Info tab, and check the Launch Screen key.

how to remove default launch screen in SwiftUI tutorial

You shouldn’t delete it, just ignore it.


Step 2 – Creating the Launch Screen State Machine

This will be a state machine with 3 steps, the first step, the second step, and the finished. In your case could have lot more steps, with other screens appearing.

This onboarding system can be as complex as you wish, the bright side of using a state to do this is to have fine control over what is happening at each step of the process.

Create a new Swift file called LaunchScreenStep.swift and copy-paste the enum below:

enum LaunchScreenStep {
    case firstStep
    case secondStep
    case finished

Those cases above will be our steps for the animations in the Launch Screen.

Now create a new file called LaunchScreenStateManager.swift  and copy-paste the code below:


import Foundation final class LaunchScreenStateManager: ObservableObject {

@MainActor @Published private(set) var state: LaunchScreenStep = .firstStep

    @MainActor func dismiss() {
        Task {
            state = .secondStep

            try? await Task.sleep(for: Duration.seconds(1))

            self.state = .finished 

Here we are creating two interesting capabilities.

The first one is the state variable that can be read throughout the app and the app can respond to changes, but it can’t be modified from outside because of the private(set) access modifier. The second one is the dismiss function which will control how the dismissing process will happen. And that is it for the Launch Screen State manager.

Let’s begin building the animated launch screen view in SwiftUI.  


Step 3 – Creating the Launch Screen View with animations in SwiftUI

Create now a new file called LaunchScreenView.swift and copy/paste the code below.  

import SwiftUI

struct LaunchScreenView: View {
    @EnvironmentObject private var launchScreenState: LaunchScreenStateManager // Mark 1

    @State private var firstAnimation = false  // Mark 2
    @State private var secondAnimation = false // Mark 2
    @State private var startFadeoutAnimation = false // Mark 2
    private var image: some View {  // Mark 3
        Image(systemName: "hurricane")
            .frame(width: 100, height: 100)
            .rotationEffect(firstAnimation ? Angle(degrees: 900) : Angle(degrees: 1800)) // Mark 4
            .scaleEffect(secondAnimation ? 0 : 1) // Mark 4
            .offset(y: secondAnimation ? 400 : 0) // Mark 4
    private var backgroundColor: some View {  // Mark 3
    private let animationTimer = Timer // Mark 5
        .publish(every: 0.5, on: .current, in: .common)
    var body: some View {
        ZStack {
            backgroundColor  // Mark 3
            image  // Mark 3
        }.onReceive(animationTimer) { timerValue in
            updateAnimation()  // Mark 5
        }.opacity(startFadeoutAnimation ? 0 : 1)
    private func updateAnimation() { // Mark 5
        switch launchScreenState.state {  
        case .firstStep:
            withAnimation(.easeInOut(duration: 0.9)) {
        case .secondStep:
            if secondAnimation == false {
                withAnimation(.linear) {
                    self.secondAnimation = true
                    startFadeoutAnimation = true
        case .finished: 
            // use this case to finish any work needed

struct LaunchScreenView_Previews: PreviewProvider {
    static var previews: some View {

In this view, we will use a technique with timers. As we need to update the launch screen from time to time, nothing better than a timer to help us with that.

Let’s explain each one of the marks:

  1. Mark 1 we are using the injected environment object to be the source of truth of the animation state. There we can access the state property that will tell us what to do on the launch screen.
  2. Mark 2 are all the boolean variables controlling the animations. Observe that in the first animation we keep toggling on and off, but in the second one, we toggle just one time.
  3. Mark 3 depicts all views extracted for easy comprehension.
  4. Mark 4 is where part of the animation occurs. When we change those properties in and withAnimation {}, the SwiftUI engine animates that for us automatically.
  5. Mark 5 is all the timer-handling stuff. We are creating a timer and each time the timer fires we respond with something in the updateAnimation function.

Now we just need to connect everything together in the ContentView.  


Step 4- Dismissing Launch Screen after getting data from the Server

This tale is old as time. Every time that we have a splash screen/launch screen we are trying to get all systems up to receive user input and/or show the right updated data to them.

This is exactly what we will reproduce here, we will make an API call using async/await, and then when it returns we call the animations. Remember API calls and other business rules should be separated from the view code, this is just sample code not intended to be an architecture proposal.

Copy/paste the code below into your ContentView.

import SwiftUI

struct ContentView: View {
    @EnvironmentObject private var launchScreenState: LaunchScreenStateManager
    var body: some View {
        VStack {
            Image(systemName: "applescript")
                .frame(width: 150, height: 150)
            Text("Hello, Apple Script!").font(.largeTitle)
        .task {
            try? await getDataFromApi()
            try? await Task.sleep(for: Duration.seconds(1))
    fileprivate func getDataFromApi() async throws {
        let googleURL = URL(string: "https://www.google.com")!
        let (_,response) = try await URLSession.shared.data(from: googleURL)
        print(response as? HTTPURLResponse)

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {

Here the most important part is the task modifier where we are calling the API in the function getDataFromApi and waiting for the response. Then only after making that call, do we use the dismiss function on the state manager.

I’m only using the “try? await Task.sleep” function here because the API call is so fast that you couldn’t see the animation result very well without it, ideally, you shouldn’t need to do that in production code.  


Showing the Launch Screen in the Root Scene of the App

The final step is to instantiate the LaunchScreenManager in the root of the app and add the launch screen to the view hierarchy only if the state of the launch screen is different from than completed. Copy/paste the code below into the root scene of your app:

import SwiftUI

struct LaunchScreenTutorialApp: App {
    @StateObject var launchScreenState = LaunchScreenStateManager()
    var body: some Scene {
        WindowGroup {
            ZStack {
                if launchScreenState.state != .finished {

As you can observe we are adding the launch screen to the view hierarchy only if the state of the splash screen is finished. And that, my friends, is one example of how to make animated launch screens in SwiftUI.  


Wrap up

The detailed steps are:

  1. The app begins and starts the ContentView and the LaunchScreen above it in a ZStack.
  2. At the same time, ContentView is making an API call to fetch data from APIs and the splash screen is animated.
  3. When the API returned values, we call dismiss in the launch screen state manager.
  4. The launch screen responds to the changes in the state manager with new animations.
  5. The root of the app responds to the changes of the state manager removing the Launch Screen when the state is finished.

And that’s it! The result is this.

Really cool, isn’t it? And we are done for today!  


Summary – Animated Splash Screen in SwiftUI

Today we did a lot. We covered animations in SwiftUI, and how to rotate views and move them around on the screen. During this article, we checked how to add a state machine to control your launch and splash screen state throughout your app.

We made a real API call using async/await and only after all the work was done did we initiate the dismissal of the launch screen. And finally, we added the launch screen to the app root hierarchy but only when it is needed.

This article is heavily inspired by this video go check it out if you want more content about this. There are a lot of examples of how to do launch screens in SwiftUI, and I liked a lot this way because it gives you full control over what is happening at every step. And finally, I want to thank my colleague Evgeny for the great suggestions that were applied to this article revision.

Fellow developers, that’s all. I hope you liked reading this article as much as I enjoyed writing it. If you want to support this blog you can Buy Me a Coffee or just say hello on Twitter. You can reach me on LinkedIn, or send me an e-mail through the contact page.

You can also sponsor this blog so I can get my blog free of random advertisers.

Thanks for the reading and… That’s all folks. Image Credit: Wikiart