DEV Community

Cover image for Animated iOS Splash Screens: The Illusion Apple Actually Allows
Sagnik Saha
Sagnik Saha

Posted on

Animated iOS Splash Screens: The Illusion Apple Actually Allows

You've seen it a hundred times.

You tap an app icon. A logo fades in. Something animates. For a brief moment, you think, "Nice. This app feels polished."

Then you try to build the same thing in iOS… and the splash screen refuses to animate. Or worse, Apple refuses your App Store submission.

Welcome to the confusing world of iOS splash screens—where what users see, what developers call it, and what Apple actually allows are three completely different things.

Apps like Uber, Swiggy, and Indigo appear to pull off beautifully animated splash screens. But here's the uncomfortable truth: those animations are not happening on the launch screen at all. And once you understand why, a lot of Apple's "unreasonable" restrictions suddenly start to make sense.

This article breaks down what a splash screen really is on iOS, why it's intentionally boring, and how to build a dynamic splash experience without fighting the system—or App Review.

First, let's clear up a very common misunderstanding

On iOS, what developers casually call a "splash screen" is officially the Launch Screen.

And here's the catch: The launch screen is not a view controller.

It's a static launch asset, loaded before your app process is fully alive. Because of that, Apple places some strict limitations on it:

  • No Swift or Objective-C code
  • No API calls
  • No animations
  • No timers
  • No conditional logic (dark mode checks, user state, etc.)

If you've ever wondered why your animated launch screen idea didn't work—this is why. The system simply isn't ready to run your code yet.

So how do apps like Uber pull off animated splash screens?

Short answer:

They don't animate the launch screen.

They animate the first screen of the app.

We'll get to that later. First, let's set up the static part properly.

Note: We will be dealing with the integration in an UIKit project.

Setting up the static Launch Screen (the right way)

We'll start by creating a clean, minimal launch screen using LaunchScreen.storyboard. No hacks. No Info.plist gymnastics. Just UIKit doing UIKit things.

Step 1: Open LaunchScreen.storyboard

Once you open your project in Xcode, you'll find a file named LaunchScreen.storyboard in the project navigator on the left.

Open it, and you'll see a very boring, very empty layout. This is expected. Apple wants your launch screen to be predictable, not exciting.

Step 2: Add an Image View

If you're using Xcode 16+, open the Add Library panel and search for Image View.

Drop the UIImageView onto the canvas and position it wherever your design demands—usually centered, unless you're feeling rebellious.

Step 3: Verify the hierarchy

Expand the View Controller Scene, then expand the View.

You should see your image view neatly sitting there, behaving itself.

This step isn't strictly required, but it's a good habit—especially when things don't show up and Xcode gaslights you into thinking they do.

Step 4: Add your logo to Assets

Now add your app logo to the asset catalog.

A few recommendations:

  • Use a simple, flat image
  • Prefer vector formats
  • Name it something sensible like splashscreen

Your future self will thank you.

Step 5: Assign the image

Select the image view, open the Attributes Inspector, and set the Image property to the asset name you just added.

At this point, you should finally see something on screen. Small win. Celebrate quietly.

Step 6: Set the background color

Select the root View of the view controller.

In the Attributes Inspector, set the Background color to match your brand.

That's it. You now have a proper, Apple-approved static launch screen. While setting up the color, set it for both dark mode and light mode.

No code. No animation. No drama.

What's next?

This launch screen exists only to bridge the gap between tapping the app icon and your app becoming ready to render UI.

If you want:

  • animations
  • loaders
  • Lottie files
  • API-driven routing
  • user-specific logic

That all belongs in the first real screen of your app, not here.

The Real Dynamic Splash Screen (a.k.a. The Illusion That Everyone Uses)

In the previous section, we established an uncomfortable truth: iOS launch screens are intentionally boring.

So how do apps still manage to show animated splash screens?

Simple. They cheat. Respectfully.

If you want a dynamic splash screen on iOS, you don't animate the launch screen. You replace it immediately with something that looks identical—but is actually a real view controller where Swift is finally allowed to exist.

This works because users don't care what is animating. They only care that something is.

The "Bait and Switch" Technique

This pattern is so common that if your app doesn't do it, you're probably overthinking things.

1. The Bait — Static Launch Screen

The system shows LaunchScreen.storyboard.

This happens instantly, before your app code runs.

2. The Switch — Dynamic Overlay

As soon as your app finishes launching, you present a normal UIViewController that visually matches the launch screen.

3. The Animation

Because this is now a real view controller, you can run:

  • UIView animations
  • Lottie animations
  • Video playback
  • Async logic

Apple approves. Swift approves. Life is good.

4. The Transition

Once the animation finishes (and any startup logic completes), you swap this splash view out for your real app—home screen, login screen, or wherever the user belongs.

Step-by-Step: Building the Dynamic Splash Screen

1. Creating the SplashViewController

This view controller is the star of the illusion. Its only job is to look exactly like the launch screen, then gracefully get out of the way.

import UIKit

class SplashViewController: UIViewController {

    private let logoImageView: UIImageView = {
        let imageView = UIImageView(image: UIImage(named: "splashscreen"))
        imageView.contentMode = .scaleAspectFit
        return imageView
    }()
Enter fullscreen mode Exit fullscreen mode

Here:

  • We use the same image asset as the launch screen.
  • scaleAspectFit ensures the logo behaves nicely across device sizes.
  • Everything is created programmatically to avoid storyboard dependency.
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor(named: "SplashBackground")

        view.addSubview(logoImageView)
        logoImageView.translatesAutoresizingMaskIntoConstraints = false
Enter fullscreen mode Exit fullscreen mode

This is where the illusion begins:

  • The background color matches the launch screen.
  • The logo is added and centered using Auto Layout.
  • If this matches the launch screen visually, the transition feels seamless.
        NSLayoutConstraint.activate([
            logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            logoImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            logoImageView.widthAnchor.constraint(equalToConstant: 200),
            logoImageView.heightAnchor.constraint(equalToConstant: 200)
        ])
    }
Enter fullscreen mode Exit fullscreen mode

Nothing fancy here—just ensuring the logo lands exactly where users expect it.

2. Starting the Animation

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        animate()
    }
Enter fullscreen mode Exit fullscreen mode

We trigger animations in viewDidAppear, not viewDidLoad, because:

  • The view is guaranteed to be on screen
  • Animations feel smoother
  • You avoid half-rendered transitions
    private func animate() {
        UIView.animate(withDuration: 5, animations: {
            self.logoImageView.transform = CGAffineTransform(scaleX: 3.0, y: 3.0)
            self.logoImageView.alpha = 0
        }) { _ in
            (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?
                .changeRootViewController()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What's happening here:

  • The logo scales up and fades out
  • After the animation completes, control is handed back to SceneDelegate
  • This keeps navigation logic centralized and predictable

(Yes, 5 seconds is long—this is just for demonstration. Please don't do this in production.)

Wiring It Up in SceneDelegate

Now we tell the app: "Show the splash first. Decide everything else later."

func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
) {
    guard let windowScene = (scene as? UIWindowScene) else { return }
Enter fullscreen mode Exit fullscreen mode

Standard scene setup. Nothing exciting yet.

    let window = UIWindow(windowScene: windowScene)
    window.backgroundColor = UIColor(named: "SplashBackground") ?? .systemBackground
    window.rootViewController = SplashViewController()
    self.window = window
    window.makeKeyAndVisible()
}
Enter fullscreen mode Exit fullscreen mode

Important details:

  • The window background matches the launch screen color (prevents flashing)
  • SplashViewController becomes the first real screen
  • The illusion remains intact

Swapping to the Real App

Now comes the final act: deciding where the user actually goes.

func changeRootViewController() {
    let destinationVC: UIViewController
Enter fullscreen mode Exit fullscreen mode

We choose the destination dynamically.

    var isUserLoggedIn: Bool = false

    if isUserLoggedIn {
        destinationVC = MainViewController()
    } else {
        destinationVC = LoginViewController()
    }
Enter fullscreen mode Exit fullscreen mode

This mirrors real-world apps:

  • Logged-in users → Home
  • Logged-out users → Login
    guard let window = self.window else { return }
    destinationVC.loadViewIfNeeded()
Enter fullscreen mode Exit fullscreen mode

Forces the destination view to load early, ensuring a smooth transition.

    let currentView = window.snapshotView(afterScreenUpdates: false)
    window.rootViewController = destinationVC
Enter fullscreen mode Exit fullscreen mode

We take a snapshot of the splash screen and then immediately replace the root controller. This avoids sudden visual jumps.

    if let snapshot = currentView {
        window.addSubview(snapshot)

        UIView.animate(withDuration: 0.5, animations: {
            snapshot.alpha = 0
        }, completion: { _ in
            snapshot.removeFromSuperview()
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

This final fade-out is what makes everything feel deliberate and polished. No flicker. No hard cut. Just a clean exit.

What You've Actually Built

Despite appearances, you didn't animate the launch screen at all.

You:

  • Let iOS do its static launch thing
  • Immediately replaced it with a lookalike
  • Ran real animations legally
  • Transitioned smoothly into the app

And that's exactly how production iOS apps do it.

Optional follow-ups you could add later

  • Using Lottie instead of UIView.animate
  • Handling async startup tasks (API, auth, remote config)
  • SwiftUI version of the same pattern
  • iOS 17+ Scene lifecycle edge cases

Top comments (0)