An Approach to Handling App Launch States in SwiftUI

ยท

4 min read

Let's explore how we can show specific Views for different launch states while your SwiftUI app is opening & loading up. Giving users an awesome experience every time, and making your code super easy to maintain and follow. I have a deep love for enums, so we'll base this approach all on an enum ๐Ÿ˜. Let's go!

We can start by creating an @Observable class as a place to ultimately hold & update the current state of the app as it launches and as we're preparing to show the correct screen to the user. For this demo, let's name the class AppLauncher (I dunno, naming is tricky. Go with it. ๐Ÿ˜†)

import SwiftUI

@Observable
class AppLauncher {

}

Cool. Now, let's make use of my best friend, enum, to describe the different launch states we need to handle.

import SwiftUI

@Observable
class AppLauncher {
    enum LaunchState {
        case loading
        case pendingAuth
        case doneWithNothingPending
    }

    var launchState = LaunchState.loading

    // We'll do stuff right here in a minute...keep reading
}

Great! This is shaping up nicely! And since our class is @Observable we're already prepared to hook up the launchState variable to our @mainApp struct and start observing its changes! So let's do that now by, first, heading over to the App struct and adding a @State instance of AppLauncher. (And I'll name our demo app Milkshakes because I love milkshakes.)

import SwiftUI

@main
struct Milkshakes: App {

    @State private var launcher = AppLauncher()

    var body: some Scene {
        WindowGroup {
            Text("We'll do something right here next!")
        }
    }
}

Real quick, in order to keep the body property as concise as possible, we'll move the Text into a new bodyContentView method (passing in the launchState for later) and return it from there instead.

import SwiftUI

@main
struct Milkshakes: App {

    @State private var launcher = AppLauncher()

    var body: some Scene {
        WindowGroup {
            bodyContentView(launchState: launcher.launchState)
        }
    }

    @ViewBuilder 
    private func bodyContentView(launchState: AppLauncher.LaunchState) -> some View {
        // Get ready to rock!
        Text("This is where things get really exciting!")
    }
}

Alright, we're finally ready to rock! Let's spread the butter on this launchState bread using enum's partner in crime, the switch statement ๐ŸŽ‰ (my other best friend). We'll delete the Text in the method and start typing switch launchState ๏ผ and it's in this exact moment where we leverage Xcode's awesome auto-complete to get this output:

switch launchState {
case .loading:

case .pendingAuth:

case .doneWithNothingPending:

}

Love it! Exhaustivity for the win! Oh, and did you notice that I put @ViewBuilder on the bodyContentView(launchState:) method earlier? That sets us up perfectly to return a uniqueView in each case of the enum! ๐Ÿ˜ So what are we waiting for?!

@ViewBuilder 
private func bodyContentView(launchState: AppLauncher.LaunchState) -> some View {
    switch launchState {
    case .loading:
        AppLaunchLoadingScreen()

    case .pendingAuth:
        AppIntroScreen()

    case .doneWithNothingPending:
        AppRootTabView()
    }
}

Boom, baby! With all this in place, our Milkshakes app successfully observes any changes to launchState and shows the appropriate View for each state! ๐Ÿฅณ

Hold on though..! ๐Ÿ˜… We still need to tell AppLauncher to start doing stuff so it can actually change the value of launchState from the default .loading state to something else. One way to do that is to create a method in AppLauncher to call from the App struct in an .onAppear modifier. Let's see what that looks like real quick.

import SwiftUI

@Observable
class AppLauncher {
    enum LaunchState {
        case loading
        case pendingAuth
        case doneWithNothingPending
    }

    var launchState = LaunchState.loading

    func load() {
        // Do whatever setup code you want
        ...

        // Update the launch state accordingly
        launchState = .doneWithNothingPending
    }
}

Meanwhile, in the App struct, we call the new load method in .onAppear:

WindowGroup {
    bodyContentView(launchState: launcher.launchState)
        .onAppear { launcher.load() }
}

And that's it! We're officially done!

If you're thirsty for a bonus addition to this code, keep reading. Otherwise, thank you for sticking around and I hope you found some value here!
K love you bye ๐Ÿ‘‹๐Ÿฝ

Animate the State Changes

Consider slightly animating the launchState changes by adding an .animation modifier right under .onAppear. ๐Ÿ‘Œ๐Ÿผ

.onAppear { launcher.load() }
.animation(.default, value: launcher.launchState)

Did you find this article valuable?

Support Scott Smith by becoming a sponsor. Any amount is appreciated!

ย