An easier way to manage full screen covers in SwiftUI

Oct 28, 2020 · Follow on Twitter and Mastodon

In this post, we’ll look at an easier way to manage full screen covers in SwiftUI, in a way that reduces state management and lets us present many covers with the same modifier.

TLDR;

If you find the post too long, I have added the source code to my SwiftUIKit library. You can find it here. Feel free to try it out and let me know what you think.

The basics

To present full-screen cover modals in SwiftUI, you can use the fullScreenCover modifier with an isPresented binding and a content builder (more options have been added since this was written):

struct MyView: View {
    
    @State private var isCoverActive = false
    
    var body: some View {
        Button("Show cover", action: showCover)
            .fullScreenCover(isPresented: $isCoverActive, content: cover)
    }
    
    func cover() -> some View {
        Text("Hello, world!")
    }

    func showCover() {
        isCoverActive = true
    }
}

This can become tricky when you have to present multiple covers from the same screen or reuse a modal across the app. You may end up duplicating code, state, view builders etc.

I therefore use a way to handle covers in a reusable way, that requires less code and less state, while still being flexible to support both global and screen-specific covers.

It all begins with a very simple state manager that I call FullScreenCoverContext.

Full screen cover context

Instead of setting up individual modal state in every view, I use a FullScreenCoverContext:

public class FullScreenCoverContext: PresentationContext<AnyView> {
    
    public override func content() -> AnyView {
        contentView ?? EmptyView().any()
    }
    
    public func present<Cover: View>(_ cover: Cover) {
        present(cover.any())
    }
    
    public func present(_ provider: FullScreenCoverProvider) {
        present(provider.cover)
    }
}

This context has code for presenting a Cover view or a cover provider. We’ll come back to the provider shortly.

This context inherits a PresentationContext. Let’s take a look at this context base class.

PresentationContext

Since I use the same approach for alerts, sheets etc. I have a PresentationContext, which is a small ObservableObject class with an isActive binding and a generic content view:

public class PresentationContext<Content>: ObservableObject {
    
    public init() {}
    
    @Published public var isActive = false
    
    public var isActiveBinding: Binding<Bool> {
        .init(get: { self.isActive },
              set: { self.isActive = $0 }
        )
    }
    
    open func content() -> Content { contentView! }
    
    public internal(set) var contentView: Content? {
        didSet { isActive = contentView != nil }
    }
    
    public func dismiss() {
        isActive = false
    }
    
    public func present(_ content: Content) {
        contentView = content
    }
}

The cover-specific functions in FullScreenCoverContext use these to update the context.

Cover provider

FullScreenCoverContext can present views and cover providers. Cover is just a view, while FullScreenCoverProvider is a protocol for anything that can provide modal views:

public protocol FullScreenCoverProvider {
    
    var cover: AnyView { get }
}

For instance, you can have an enum that represents various views that your app supports:

enum AppCover: FullScreenCoverProvider {
    
    case settings
    case tutorial
    
    var cover: AnyView {
        switch self {
        case .settings: SettingsScreen().any()
        case .tutorial: TutorialScreen().any()
        }
    }
}

You can then present the covers from this enum like this:

context.present(AppCover.settings)

This makes it possible to create plain cover views or app- and view-specific enums and present all of them in the same way, using the same context.

New fullScreenCover modifier

We can also add a context-based .fullScreenCover modifier to simplify using the context to present full screen cover modals:

public extension View {
    
    func fullScreenCover(
        _ context: FullScreenCoverContext
    ) -> some View {
        fullScreenCover(
            isPresented: context.isActiveBinding, 
            content: context.content
        )
    }
}

If you use this modifier instead of a native modifier, you can then use the provided context to present many different modals.

Presenting a cover

With these new tools at our disposal, we can present covers in a much easier way.

First, create a context property:

@StateObject 
private var cover = FullScreenCoverContext()

then add a fullScreenCover modifier to the view:

.fullScreenCover(cover)

You can now present any view or FullScreenCoverProvider with the context, for instance:

cover.present(Text("Hello, I'm a custom cover."))

You no longer need multiple @State properties for different covers or switch over an enum to determine which cover to show.

Conclusion

FullScreenCoverContext can be used to present many view with a single modifier. All you have to do is provide the context with the views to present.

Source Code

I have added these types to my SwiftUIKit library. You can find the source code here. Feel free to try it out and let me know what you think.