14

I'm using swiftUI and combine, I'have some business logic in my VM. Some results have to dismiss my view.

I'v used this one in some views :

@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

self.presentationMode.wrappedValue.dismiss()

I want to something similar in my view model.

theMouk
  • 595
  • 1
  • 7
  • 21
  • Please give more details about your problem and what you want to achieve. – Tobias Hesselink Nov 29 '19 at 10:51
  • 1
    You don't do the dismissal in _imperative_ way in `SwiftUI`. Instead you use a `.sheet` type view by binding it to a boolean property that will be mutated from that said view model. – nayem Nov 29 '19 at 10:54

2 Answers2

28

You don't do the dismissal in imperative way in SwiftUI. Instead you use a .sheet view by binding it to a boolean property that will be mutated from that said view model.

Edit:

After answering a follow-up question, I came up with a different approach. It plays nice if the dismissal is actually needed to be done from inside the modally presented View itself.

You can achieve this by implementing your custom Publisher which will use .send() method to allow you to send specific values to the subscriber (in this case, your View). You will use onReceive(_:perform:) method defined on the View protocol of SwiftUI to subscribe to the output stream of the custom Publisher you defined. Inside the perform action closure where you will have the access to the latest emitted value of your publisher, you will do the actual dismissal of your View.

Enough of the theory, you can look at the code, should not be very hard to follow, below:

import Foundation
import Combine

class ViewModel: ObservableObject {
    var viewDismissalModePublisher = PassthroughSubject<Bool, Never>()
    private var shouldDismissView = false {
        didSet {
            viewDismissalModePublisher.send(shouldDismissView)
        }
    }

    func performBusinessLogic() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.shouldDismissView = true
        }
    }
}

And the views counterparts is:

import SwiftUI

struct ContentView: View {
    @State private var isDetailShown = false
    var body: some View {
        VStack {
            Text("Hello, World!")
            Button(action: {
                self.isDetailShown.toggle()
            }) {
                Text("Present Detail")
            }
        }
        .sheet(isPresented: $isDetailShown) {
            DetailView()
        }
    }
}

struct DetailView: View {
    @ObservedObject var viewModel = ViewModel()
    @Environment(\.presentationMode) private var presentationMode
    var body: some View {
        Text("Detail")
        .navigationBarTitle("Detail", displayMode: .inline)
        .onAppear {
            self.viewModel.performBusinessLogic()
        }
        .onReceive(viewModel.viewDismissalModePublisher) { shouldDismiss in
            if shouldDismiss {
                self.presentationMode.wrappedValue.dismiss()
            }
        }
    }
}

Old Answer:

A very simple implementation of view dismissal with respect to business logic changes in View Model would be:

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel()
    var body: some View {
        Text("Hello, World!")

        // the animation() modifier is optional here
        .sheet(isPresented: $viewModel.isSheetShown.animation()) { 
            Text("Sheet Presented")
        }

        // From here - for illustration purpose
        .onAppear {
            self.viewModel.perform()
        }
        // To here

    }
}

class ViewModel: ObservableObject {
    @Published var isSheetShown = false

    func perform() {
        // this just an example. In real application, you will be responsible to
        // toggle between the states of the `Bool` property
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.isSheetShown.toggle()
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                self.isSheetShown.toggle()
            }
        }
    }
}
nayem
  • 7,285
  • 1
  • 33
  • 51
  • Well @theMouk, I think this answered your question already. If that is so, you should first mark this as accepted for this will help future readers with the similar issues. And the navigation view pop should be a separate question instead. – nayem Dec 02 '19 at 03:17
  • Thanks! it worked for me (newbie). Lots of lines to close the View from the ViewModel. I hope future versions of Swift offer a more streamlined approach. – Richard H Apr 09 '21 at 02:47
  • I would also like to add that this answer is better than using `@Published` on that particular property in the view model because if you use protocols to define the view model, you'll get a compile error since you can't use wrapped properties along with protocols and your view wouldn't know that the property is a publisher if it was defined in that protocol. Using the Combine syntax without the `@Publisher` fixes it. – Tarek Sep 14 '22 at 13:36
14

If you want to make it simple then just make a @Published variable in viewModel called goBack of type Bool and change it to true whenever you want and in the view just use the .onChange modifier if that bool is true on change then run presentationMode.wrappedValue.dismiss().

class ViewModel: ObservableObject {
  @Published var goBack: Bool = false
  
  fun itWillToggleGoBack() {
    goBack.toggle()
  }
}


struct MyView {
  @StateObject var vm = ViewModel()
  @Environment(\.presentationMode) var presentationMode

  var body: some View {
    Text("Any kind of view")
      .onChange(of: vm.goBack) { goBack in 
         if goBack {
           self.presentationMode.wrappedValue.dismiss()
         }
      }
  }
}
  • The disadvantage of this method is that if you use a protocol to define your view model's functionality, you'll get a compile error since protocols don't work well with wrapped properties. The view basically wouldn't know that the property has the `@Published` keyword; however using the Combine syntax fixes that since it would be clearly defined in the protocol that it's a `PassthroughSubject` – Tarek Sep 14 '22 at 13:39
  • @Tarek , yeah that's true, this is just for simple scenarios, or we can say a quick fix. May be for someone who don't know how to work with combine. – Mohammed Owais Khan Sep 15 '22 at 05:19
  • Yea I thought of leaving this comment in case someone gets stuck trying to get it to work with protocols. But thanks though! – Tarek Sep 15 '22 at 21:26