TakeOne
TakeOne

Reputation: 213

How to dismiss multiple view with SwiftUI

import SwiftUI

struct ContentView: View {
    @State var showSecond = false
    @State var showThird = false

    var body: some View {
        VStack(spacing: 50) {
            Text("FirstView")
            Button("to SecondView") {
                self.showSecond = true
            }
            .sheet(isPresented: $showSecond) {
                VStack(spacing: 50) {
                    Text("SecondView")
                    Button("to ThirdView") {
                        self.showThird = true
                    }
                    .sheet(isPresented: self.$showThird) {
                        VStack(spacing: 50) {
                            Text("ThirdView")
                            Button("back") {
                                self.showThird = false
                            }
                            Button("back to FirstView") {
                                self.showThird = false
                                self.showSecond = false
                            }
                        }
                    }
                    Button("back") {
                        self.showSecond = false
                    }
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The above code transitions from FirstView to SecondView, and transitions from SecondView to ThirdView. And the "back" button in SecondView and ThirdView returns to the previous screen normally.

However, if you tap the "back to FirstView" button in the ThirdView, SecondView will be displayed without returning to FirstView. And after this operation, when you tap the "back" button of SecondView, it does not return to FirstView.

How can I return directly from ThirdView to FirstView?


Added February 19, 2020

I have added the solution code based on answers.

Solution1: based on Asperi's plan A.

struct ContentView: View {
    @State var showSecond = false
    @State var showThird = false

    var body: some View {
        VStack(spacing: 50) {
            Text("FirstView")
            Button("to SecondView") {
                self.showSecond = true
            }
            .sheet(isPresented: $showSecond) {
                VStack(spacing: 50) {
                    Text("SecondView")
                    Button("to ThirdView") {
                        self.showThird = true
                    }
                    .sheet(isPresented: self.$showThird) {
                        VStack(spacing: 50) {
                            Text("ThirdView")
                            Button("back") {
                                self.showThird = false
                            }
                            Button("back to FirstView") {
                                DispatchQueue.main.async {
                                    self.showThird = false
                                    DispatchQueue.main.async {
                                        self.showSecond = false
                                    }
                                }
                            }
                        }
                    }
                    Button("back") {
                        self.showSecond = false
                    }
                }
            }
        }
    }
}

Solution2: based on Asperi's plan B.

struct ContentView: View {
    @State var showSecond = false
    @State var showThird = false
    @State var backToFirst = false

    var body: some View {
        VStack(spacing: 50) {
            Text("FirstView")
            Button("to SecondView") {
                self.showSecond = true
            }
            .sheet(isPresented: $showSecond) {
                VStack(spacing: 50) {
                    Text("SecondView")
                    Button("to ThirdView") {
                        self.showThird = true
                    }
                    .sheet(isPresented: self.$showThird, onDismiss: {
                        if self.backToFirst {
                            self.showSecond = false
                        }
                    }) {
                        VStack(spacing: 50) {
                            Text("ThirdView")
                            Button("back") {
                                self.showThird = false
                                self.backToFirst = false
                            }
                            Button("back to FirstView") {
                                self.showThird = false
                                self.backToFirst = true
                            }
                        }
                    }
                    Button("back") {
                        self.showSecond = false
                    }
                }
            }
        }
    }
}

Solution3: based on Joseph's advice.

struct ContentView: View {
    @State var showSecond = false
    @State var showThird = false

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                VStack(spacing: 50) {
                    Text("FirstView")
                    Button("to SecondView") {
                        self.showSecond = true
                    }
                }
                .frame(width: geometry.size.width, height: geometry.size.height)
                .background(Rectangle().foregroundColor(.white))
                if self.showSecond {
                    VStack(spacing: 50) {
                        Text("SecondView")
                        Button("to ThirdView") {
                            self.showThird = true
                        }
                        Button("back") {
                            self.showSecond = false
                        }
                    }
                    .frame(width: geometry.size.width, height: geometry.size.height)
                    .background(Rectangle().foregroundColor(.white))
                    if self.showThird {
                        VStack(spacing: 50) {
                            Text("ThirdView")
                            Button("back") {
                                self.showThird = false
                            }
                            Button("back to FirstView") {
                                self.showThird = false
                                self.showSecond = false
                            }
                        }
                        .frame(width: geometry.size.width, height: geometry.size.height)
                        .background(Rectangle().foregroundColor(.white))
                    }
                }
            }
        }
    }
}

Upvotes: 7

Views: 5433

Answers (4)

Joony
Joony

Reputation: 4698

It's not pretty, but you could fall-back to UIKit like this (n is the number of modal views you would like to dismiss at same time):

private func dismiss(_ n: Int) {
    let rootViewController = UIApplication.shared.connectedScenes
        .filter { $0.activationState == .foregroundActive }
        .map {$0 as? UIWindowScene }
        .compactMap { $0 }
        .first?.windows
        .filter({ $0.isKeyWindow }).first?.rootViewController
    guard let rootViewController = rootViewController else { return }

    var leafFlound = false
    var viewStack: [UIViewController] = [rootViewController]
    while(!leafFlound) {
        if let presentedViewController = viewStack.last?.presentedViewController {
            viewStack.append(presentedViewController)
        } else {
            leafFlound = true
        }
    }
    let presentingViewController = viewStack[max(0, viewStack.count - n - 1)]
    presentingViewController.dismiss(animated: true)
}

Upvotes: 0

Ihor Vovk
Ihor Vovk

Reputation: 611

Asperi's solutions work for dismissing two screens at once, but don't work for more screens. In such cases, we need to add DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) in both approaches like this:

self.showFourth = false
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
    self.showThird = false
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
        self.showSecond = false
    }
}

.milliseconds(200) is not enought.

Upvotes: 1

Joseph Beuys' Mum
Joseph Beuys' Mum

Reputation: 2264

Another /cough/ "solution" - possibly to a slightly different scenario - but anyway:

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: SecondView()) {
                Text("Show Second View")
                    .font(.largeTitle)
            }
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
}

struct SecondView: View {
    @Environment(\.presentationMode) var presentationMode

    @State private var showModal = false

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Rectangle()
                    .foregroundColor(.gray)
                    .frame(width: geometry.size.width, height: geometry.size.height)
                Button(action: {
                    withAnimation {
                        self.showModal.toggle()
                    }
                }) {
                    Text("Show Third View")
                        .font(.largeTitle)
                        .foregroundColor(.white)
                }

                if self.showModal {
                    VStack {
                        Button(action: {
                            self.presentationMode.wrappedValue.dismiss()
                        } ) {
                            Text("Show First View")
                                .font(.largeTitle)
                                .foregroundColor(.white)
                        }
                    }
                        .frame(width: geometry.size.width, height: geometry.size.height)
                        .background(Rectangle().foregroundColor(.orange))
                        .transition(.move(edge: .trailing))
                }
            }
            .frame(width: geometry.size.width, height: geometry.size.height)
        }
    }
}

Upvotes: 1

Asperi
Asperi

Reputation: 257523

Directly no way for now (and IMO will never be - it is two modal sessions)... I found two possible approaches that maybe worth considering:

A. sequential close from one place

Button("back to FirstView") {
    DispatchQueue.main.async {
        self.showThird = false
        DispatchQueue.main.async {
            self.showSecond = false
        }
    }
}

B. sequential close from different places

.sheet(isPresented: self.$showThird, onDismiss: {
    self.showSecond = false // with some additional condition for forced back
})...

...

Button("back to FirstView") {
    self.showThird = false
}

Upvotes: 2

Related Questions