How to Dismiss view from viewModel in SwiftUI? - ios

I have one view with MVVM architecture. in view on button click i am calling webService from viewModel class on success i want to dismiss view.
my viewmodel is ObservableObject
i am moving to other screen using navigation and not using sheet
I have tried this
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
`self.presentationMode.wrappedValue.dismiss()`
in view but it will call before API call.
is there any way to dismiss view once i receive API data in viewModel?
Thank you for help

I didn't compile my code but I think this approach should work. I used something like this in my pet-projects
import SwiftUI
import Combine
class ViewModel: ObservableObject {
var didSendRequest: AnyPublisher<Void, Never> {
subject.eraseToAnyPublisher()
}
private let subject = PassthroughSubject<Void, Never>()
}
struct ContentView: View {
var viewModel: ViewModel
#Environment(\.presentationMode)
var presentationMode
var body: some View {
VStack {
Text("Hello")
}
.onReceive(viewModel.didSendRequest) { _ in
presentationMode.wrappedValue.dismiss()
}
}
}
In your view model when you receive a response you should execute the next code:
subject.send()

In your ObservableObject, set an #Published var indicating that the API load is complete.
class APIController: ObservableObject {
#Published var apiLoaded = false;
func doTheApiLoad() {
...
//when you want to dismiss
DispatchQueue.main.async {
self.apiLoaded = true;
}
}
}
In your content view, watch that published value
#ObservedObject var apiController: APIController()
When it changes, dismiss the view
if (apiController.apiLoaded == true) {
presentationMode.wrappedValue.dismiss()
}

Related

Passed in published property not firing onReceive in iOS 14 but is in iOS 16 [duplicate]

I have a custom ViewModifier which simply returns the same content attached with a onReceive modifier, the onReceive is not triggered, here is a sample code that you can copy, paste and run in XCode:
import SwiftUI
import Combine
class MyViewModel: ObservableObject {
#Published var myProperty: Bool = false
}
struct ContentView: View {
#ObservedObject var viewModel: MyViewModel
var body: some View {
Text("Hello, World!")
.modifier(MyOnReceive(viewModel: viewModel))
.onTapGesture {
self.viewModel.myProperty = true
}
}
}
struct MyOnReceive: ViewModifier {
#ObservedObject var viewModel: MyViewModel
func body(content: Content) -> some View {
content
.onReceive(viewModel.$myProperty) { theValue in
print("The Value is \(theValue)") // <--- this is not executed
}
}
}
is SwiftUI designed to disallow onReceive to execute inside a ViewModifier or is it a bug ? I have a view in my real life project that gets bigger with some business logic put inside onReceive, so I need to clean that view by separating it from onReceive.
ok, this works for me:
func body(content: Content) -> some View {
content
.onAppear() // <--- this makes it work
.onReceive(viewModel.$myProperty) { theValue in
print("-----> The Value is \(theValue)") // <--- this will be executed
}
}
ObservableObject and #Published are part of the Combine framework if you aren't using Combine then you shouldn't be using a class for your view data. Instead, you should be using SwiftUI as designed and avoid heavy objects and either put the data in the efficient View data struct or make a custom struct as follows:
import SwiftUI
struct MyConfig {
var myProperty: Bool = false
mutating func myMethod() {
myProperty = !myProperty
}
}
struct ContentView: View {
#State var config = MyConfig()
var body: some View {
Text("Hello, World!")
.onTapGesture {
config.myMethod()
}
}
}
Old answer:
Try onChange instead
https://developer.apple.com/documentation/swiftui/scrollview/onchange(of:perform:)
.onChange(of: viewModel.myProperty) { newValue in
print("newValue \(newValue)")
}
But please don't use the View Model object pattern in SwiftUI, try to force yourself to use value types like structs for all your data as SwiftUI was designed. Property wrappers like #State will give you the reference semantics you are used to with objects.

StateObject doesn't deinit when PhotosPicker was presented

I just do some experiments in my pet project on iOS 16 Beta. I noticed one possible memory leak. I created a simple project with simple steps which reproduce this behavior. So I have ContentView and DetailView I use NavigationStack for navigation. I created DetailViewModel for DetailView and add it as #StateObject and also add it as environmentObject for DetailView subviews. All good as expected but if I present/show PhotosPicker and return back to ContentView I can see that DetailViewModel isn't deinit. Is it memory leak? How can we fix/avoid this? Thanks.
Below I added code from the simple project also for convenience you can have a have look on GitHub
ContentView:
struct ContentView: View {
var body: some View {
NavigationStack {
let route = Route.detail
NavigationLink("Show Detail", value: route)
.navigationDestination(for: Route.self) { route in
switch route {
case .detail:
DetailView()
}
}
}
}
}
Route:
enum Route: Hashable {
case detail
}
DetailView:
import SwiftUI
import PhotosUI
struct DetailView: View {
#StateObject private var viewModel = DetailViewModel()
// just for test
#State private var photoPickerPresented = false
#State private var selectedPickerItem: PhotosPickerItem?
var body: some View {
VStack {
Button {
photoPickerPresented.toggle()
} label: {
Text("Show Photo Picker")
}
}
.photosPicker(isPresented: $photoPickerPresented, selection: $selectedPickerItem)
.environmentObject(viewModel)
}
}
DetailViewModel:
class DetailViewModel: ObservableObject {
init() {
debugPrint("DetailViewModel init")
}
deinit {
debugPrint("DetailViewModel deinit")
}
}

SwiftUI: How to initialize a new StateObject in a parent view?

I have an app architecture similar to the below (simplified) code. I use a WorkoutManager StateObject which I initialize in the set up view, then pass down to its children via EnvironmentObject. The problem is that upon dismissing the .sheet there isn't any life cycle event which initializes a new WorkoutManager, which I need in order to be able to start new workouts consecutively. How in this example below can I give WorkoutView the ability to reinitialize WorkoutManager so that it is a clean object?
import SwiftUI
import HealthKit
class WorkoutManager: ObservableObject {
var workout: HKWorkout?
}
struct ContentView: View {
#StateObject var workoutManager = WorkoutManager()
#State var showingWorkoutView = false
var body: some View {
Button {
showingWorkoutView.toggle()
} label: {
Text("Start Workout")
}
.sheet(isPresented: $showingWorkoutView) {
WorkoutView(showingWorkoutView: $showingWorkoutView)
}
}
}
struct WorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#Binding var showingWorkoutView: Bool
var body: some View {
Text("Workout Started")
.padding()
Button {
showingWorkoutView.toggle()
//Here I want to initialize a new WorkoutManager to clear out the previous workout's state, how?
} label: {
Text("End Workout")
}
}
}
As mentioned in the comments already, the route you probably want to take is reseting the state within the same WorkoutManager. You wouldn't be able to assign a new object to a #StateObject anyway -- you'll end up with compiler errors because of the View's immutable self.
Secondly, I'd suggest that you probably don't want to rely on the Button in your WorkoutView to do this. For example, if the user dismissed the sheet by swiping, that wouldn't get called. Instead, you could listen for the sheet's state in onChange (another method would be using the onDismiss parameter of sheet):
class WorkoutManager: ObservableObject {
var workout: HKWorkout?
func resetState() {
//do whatever you need to do to reset the state
print("Reset state")
}
}
struct ContentView: View {
#StateObject var workoutManager = WorkoutManager()
#State var showingWorkoutView = false
var body: some View {
Button {
showingWorkoutView.toggle()
} label: {
Text("Start Workout")
}
.sheet(isPresented: $showingWorkoutView) {
WorkoutView(showingWorkoutView: $showingWorkoutView)
}
.onChange(of: showingWorkoutView) { newValue in
if !newValue {
workoutManager.resetState()
}
}
}
}
struct WorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#Binding var showingWorkoutView: Bool
var body: some View {
Text("Workout Started")
.padding()
Button {
showingWorkoutView.toggle()
} label: {
Text("End Workout")
}
}
}

SwiftUI: ViewModifier doesn't listen to onReceive events

I have a custom ViewModifier which simply returns the same content attached with a onReceive modifier, the onReceive is not triggered, here is a sample code that you can copy, paste and run in XCode:
import SwiftUI
import Combine
class MyViewModel: ObservableObject {
#Published var myProperty: Bool = false
}
struct ContentView: View {
#ObservedObject var viewModel: MyViewModel
var body: some View {
Text("Hello, World!")
.modifier(MyOnReceive(viewModel: viewModel))
.onTapGesture {
self.viewModel.myProperty = true
}
}
}
struct MyOnReceive: ViewModifier {
#ObservedObject var viewModel: MyViewModel
func body(content: Content) -> some View {
content
.onReceive(viewModel.$myProperty) { theValue in
print("The Value is \(theValue)") // <--- this is not executed
}
}
}
is SwiftUI designed to disallow onReceive to execute inside a ViewModifier or is it a bug ? I have a view in my real life project that gets bigger with some business logic put inside onReceive, so I need to clean that view by separating it from onReceive.
ok, this works for me:
func body(content: Content) -> some View {
content
.onAppear() // <--- this makes it work
.onReceive(viewModel.$myProperty) { theValue in
print("-----> The Value is \(theValue)") // <--- this will be executed
}
}
ObservableObject and #Published are part of the Combine framework if you aren't using Combine then you shouldn't be using a class for your view data. Instead, you should be using SwiftUI as designed and avoid heavy objects and either put the data in the efficient View data struct or make a custom struct as follows:
import SwiftUI
struct MyConfig {
var myProperty: Bool = false
mutating func myMethod() {
myProperty = !myProperty
}
}
struct ContentView: View {
#State var config = MyConfig()
var body: some View {
Text("Hello, World!")
.onTapGesture {
config.myMethod()
}
}
}
Old answer:
Try onChange instead
https://developer.apple.com/documentation/swiftui/scrollview/onchange(of:perform:)
.onChange(of: viewModel.myProperty) { newValue in
print("newValue \(newValue)")
}
But please don't use the View Model object pattern in SwiftUI, try to force yourself to use value types like structs for all your data as SwiftUI was designed. Property wrappers like #State will give you the reference semantics you are used to with objects.

SwiftUI, How to dismiss current presenting View when API call returns successfully

I am using SwiftUI and I am trying to achieve a simple logical action but unable to understand SwiftUI action hierarchy.
I have one API call something like this,
final class TaskData: ObservableObject {
#Published var updatedFields = false
#Published var updateMsg = ""
func updateFields()
{
//Some API Call
.whenSuccess { (response) in
DispatchQueue.main.async {
self.updatedFields = true
self.updateMsg = "Successfully updated fields"
//Send Request to dismiss current View ???
}
}
}
}
Now, I have a View something like this, and on a request I want to dismiss this View, but I am unable to find any method for that,
struct TaskView: View {
#Environment(\.presentationMode) var currentView: Binding<PresentationMode>
#EnvironmentObject var taskData: TaskData
var body : some View {
//Some Views here ////
//Need Some code here to dismiss currentView?????
.navigationBarItems(trailing: Button(action: {
}, label: {
Text("Done")
}).onTapGesture {
self.taskData.updateFields() // Method Call to Update fields
})
}
if someone can explain this thing in a little detail as I am newbie to SwiftUI, I have seen a lot tutorial but unable to understand this structure of swift.
It is not shown how TaskView is presented, but having presentationMode in give code snapshot let's assume that it is valid, so the approach might be as follows
#Environment(\.presentationMode) var presentationMode //better to name it same,
//type is extracted from Environment
#EnvironmentObject var taskData: TaskData
var body : some View {
//Some Views here ////
SomeView()
.onReceive(taskData.$updatedFields) { success in
if success {
self.presentationMode.wrappedValue.dismiss() // dismiss self
}
}
...

Resources