Search code examples
iosswiftswiftui

How to prevent list row animation while Alert is shown


I am trying to prevent the 'swipe-back' animation after an alert is presented. Below is the reproducible code:

struct ContentView: View {
    @State private var showingAlert = false

    var body: some View {
        List {
            Text("Archive")
                .swipeActions(edge: .trailing, allowsFullSwipe: false) {

                    Button {
                        showingAlert = true
                    } label: {
                        Image(systemName: "archivebox.fill")
                    }
                    .tint(.yellow)
                }
                .alert("Archive?", isPresented: $showingAlert) {

                    Button("Archive") {
                       showingAlert = true
                    }
                    Button("Cancel", role: .cancel) {
                        showingAlert = false
                    }
                } message: {
                    Text("Some message")
                }
        }
    }
}

Here is how it looks:

gif example

But I want the row to stay in place so that the archive button is visible, and to animate back upon user interaction (if they choose cancel or archive). I guess this is possible?


Solution

  • One way to show the alert while the swipe action is showing is to cover the action button with an overlay and use this for triggering the alert.

    • A GeometryReader can be used to detect, when the row has been offset to the left. This is quite a reliable way of detecting, when the swipe actions are showing.
    • When the swipe actions are showing, a placeholder for the overlay is added to the background of the row. This placeholder is used as the source for .matchedGeometryEffect.
    • The overlay is shown above the placeholder. By setting an opacity of 0.001 it is effectively invisible, but still receptive to tap gestures. (If you want to see the overlay, change the opacity to 0.1.)
    • The overlay intercepts tap gestures to the underlying button. The alert is shown if the overlay is tapped.
    • Almost the hardest part is to find a way to get the swipe actions to disappear again when the alert is dismissed. A crude technique that works is to change the id of the row. This causes it to be replaced, without the swipe actions showing. An opacity transition is possible, but (unfortunately) the action buttons do not close again in an animated way.
    struct ContentView: View {
        @State private var showingAlert = false
        @State private var rowWithSwipeActions: UUID?
        @State private var rowId = UUID()
        @Namespace private var ns
        var body: some View {
            ZStack {
                List {
                    Text("Archive")
                        .id(rowId)
                        .swipeActions(edge: .trailing, allowsFullSwipe: false) {
                            Button {} label: {
                                Image(systemName: "archivebox.fill")
                            }
                            .tint(.yellow)
                        }
                        .listRowBackground(
                            GeometryReader { proxy in
                                let minX = proxy.frame(in: .global).minX
                                HStack(spacing: 0) {
                                    Color(.secondarySystemGroupedBackground)
                                        .frame(width: proxy.size.width)
                                    if minX < 0 {
                                        Color.clear
                                            .frame(minWidth: 74)
                                            .matchedGeometryEffect(id: rowId, in: ns, isSource: true)
                                            .onAppear { rowWithSwipeActions = rowId }
                                            .onDisappear { rowWithSwipeActions = nil }
                                    }
                                }
                            }
                        )
                }
                .alert("Archive?", isPresented: $showingAlert) {
                    Button("Archive") {
                        withAnimation { rowId = UUID() }
                    }
                    Button("Cancel", role: .cancel) {
                        withAnimation { rowId = UUID() }
                    }
                } message: {
                    Text("Some message")
                }
                if let rowWithSwipeActions {
                    Color.black
                        .opacity(0.001)
                        .onTapGesture {
                            showingAlert = true
                        }
                        .matchedGeometryEffect(id: rowWithSwipeActions, in: ns, isSource: false)
                }
            }
        }
    }
    

    Animation