Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI sheet infinitely shows and hides

Tags:

ios

swift

swiftui

I've found a solution to this (see below), but I figured it was worth posting in case anyone else runs into it.

Xcode 13.2.1, iOS 15

This only occurs with sheet(item:onDismiss:content:), not sheet(isPresented:onDismiss:content:).

Essentially, if you try to show a sheet again while it is in the process of dismissing, you can create a state where the item that shows the sheet is not nil, but the sheet is actually dismissed.

You also can create a situation where a sheet infinitely shows and hides. Do this by changing a variable in the sheet's onDismiss that causes the view to redraw. This seems to kick SwiftUI into gear and try to resolve the inconsistency with the sheet variable by dismissing the sheet. However, it doesn't actually set the sheet variable to nil, so the sheet immediately attempts to show again.

This repeats indefinitely, creating a situation where the sheet is constantly showing and hiding, and there is no way to stop it other than by force closing the app.

Below is the minimum amount of code needed to reproduce this:

import SwiftUI

struct SheetItem: Equatable, Identifiable {
    let id = UUID().uuidString
    var value: Int
}

struct ContentView: View {

    @State private var anything = 1.0
    @State private var sheetItem: SheetItem?

    var body: some View {

        // Bug #1: sheetItem can be non-nil with a dismissed sheet (should not be possible)
        //
        // 1. Tap button to set sheetItem, which shows the sheet
        // 2. Swipe down to dismiss the sheet
        // 3. Before animation completes, tap the button again
        //      (this sets the item to a new value, interrupting it before sheetValue can become nil)
        // 4. onDismiss fires
        //      (see that sheetItem is not nil, even though it should be, since the sheet is dismissed)

        // Bug #2 (even worse): infinite loop of sheet dismissing and re-appearing
        //     (cannot escape other than by force closing the app)
        //
        // 1. Tap button to set sheetItem, which shows the sheet
        // 2. Swipe down to dismiss the sheet
        // 3. Before animation completes, tap the button again
        //      (this sets the item to a new value, interrupting it before sheetValue can become nil)
        // 4. onDismiss edits a variable that causes the view to redraw
        //      (this seems to cause SwiftUI to try dismissing the sheet again)
        // 5. sheet tries to present again, since sheetItem is still not nil
        // 6. something (???) causes the sheet to dismiss again
        // 7. sheet infinitely shows and hides, with no way to interrupt it

        // Solution: only allow setting of sheetItem if it is currently nil
        //   (this prevents setting of sheetItem while the sheet is still dismissing)

        Button("Show sheet") {
            print("show sheet")
            sheetItem = .init(value: 3)
        }
        .foregroundColor(Color(
            red: anything,
            green: anything,
            blue: 1
        ))
        .sheet(
            item: $sheetItem,
            onDismiss: {
                print("onDismiss, sheetItem is \(sheetItem.debugDescription)")

                // Cause the view to redraw by changing a variable in its dependency tree
                // comment this out to see Bug #1
                // leave it as-is to show Bug #2
                anything = Double.random(in: 0...1)
            }
        ) { item in
            Text("Sheet")
        }
        .preferredColorScheme(.dark)
    }
}
like image 379
wazawoo Avatar asked Oct 23 '25 19:10

wazawoo


1 Answers

SOLUTION: only allow setting of sheetItem (which shows the sheet) when sheetItem is already nil. This prevent's sheetItem from being set while the sheet is dismissing. It appears that the isPresented version of .sheet has logic like this under the hood, as it won't let you show those sheets until they finish dismissing.

Replace sheetItem = .init(value: 3) with:

// ensure sheetItem is nil before setting it
if sheetItem == nil {
    sheetItem = .init(value: 3)
}

EDIT: I have since realized that this nil check may not be sufficient. If other sheets (instances of .sheet) can be presented at the same level, you will need to check that those also aren't presented, otherwise you get the same issue if you quickly dismiss one sheet and cause the other to appear. Not ideal, but it is what it is...

like image 83
wazawoo Avatar answered Oct 26 '25 09:10

wazawoo



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!