Reputation: 744
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.
Upvotes: 19
Views: 8258
Reputation: 126
If you want to have a single method that can be executed either from the View
or from the ViewModel
, you can achieve it like this (even simpler without Combine):
View
:
class MyView: View {
// MARK: - Private Properties
@Environment(\.dismiss) private var dismiss
@ObservedObject private var viewModel: ViewModel
// MARK: - Init
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
// MARK: - Body
var body: some View {
Text("My View")
.onChange(of: viewModel.shouldDismiss) { dismiss() }
}
}
ViewModel
:
extension MyView {
// MARK: - ViewModel
@MainActor class ViewModel: ObservableObject {
// MARK: - Internal Properties
@Published var shouldDismiss: Bool = false
// MARK: - Internal Methods
func dismiss() {
shouldDismiss = true
// Track analytics event, etc.
}
}
}
Then:
ViewModel
, you can call dismiss()
method directly.View
itself, you can do so by calling viewModel.dismiss()
in the same way.P.S. This is not the ideal option, as MVVM doesn't always perfectly fit into SwiftUI. However, if you want to have one method, for example, to close the screen and also send analytics events or anything else, my example above will allow you to achieve this result.
Upvotes: 2
Reputation: 144
The solution can be further simplified. Just use a Void publisher in your model. E.g.:
class MyModel: ObservableObject {
private(set) var actionExecuted = PassthroughSubject<Void,Never>()
func execute() {
actionExecuted.send()
}
}
class MyView: View {
@Environment(\.dismiss) var dismiss
@StateObject var model = MyModel()
var body: some View {
Text("Hi")
.onReceive(model.actionExecuted) {
dismiss()
}
}
}
Upvotes: 4
Reputation: 329
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()
}
}
}
}
Upvotes: 19
Reputation: 7585
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.
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()
}
}
}
}
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()
}
}
}
}
Upvotes: 31