SwiftUI: dismiss sheet programmatically - ios

I'm using SwiftUI and I'm trying to implement the auth logic for my app.
I have a LoginView with a Register Button and if I click on it I use a sheet to present the RegisterView. Once the user is registered, the LoginView (on background) goes to HomeView and RegisterView should disappear. The problem is that RegisterView is not disappearing.
#ObservedObject var viewModel = RegisterViewModel()
#EnvironmentObject var authenticatedUser : AuthenticatedUser
#Environment(\.presentationMode) var presentationMode
ButtonWithLoadStateView(title: K.REGISTER, isLoading: self.$vm.isLoading) {
self.viewModel.isLoading = true
self.viewModel.register() { user in
self.authenticatedUser.setLogged(user) // without this IT WORKS!
self.presentationMode.wrappedValue.dismiss()
}
}
If I remove the authenticatedUser.setLogged row then it works but it just encapsulate the user and store a token..

The provided code is not testable, so just only idea - try the following
ButtonWithLoadStateView(title: K.REGISTER, isLoading: self.$vm.isLoading) {
self.viewModel.isLoading = true
self.viewModel.register() { user in
DispatchQueue.main.async {
self.authenticatedUser.setLogged(user)
}
self.presentationMode.wrappedValue.dismiss()
}
}

Related

Why does #FetchRequest not update and redraw Views on Deletion?

Context
I have a pretty basic SwiftUI setup containing of a ListView presenting all Entities and a DetailView presenting just a single of these Entities. The DetailView also has a Delete option.
The problem I have is, that when deleting the Entity, SwiftUI does not navigate back to the ListView, even though the #FetchRequest should update and redraw the View including ChildViews. Instead, it keeps presenting the DetailView but since it deleted the Entity, it only presents the custom-implemented Default ("N/A") Value for its name.
Code
struct ListView: View {
#FetchRequest(sortDescriptors: []) var entities: FetchedResults<Entity>
var body: some View {
NavigationStack {
ForEach(entities) { entity in
RowView(entity: entity)
}
}
}
}
struct RowView: View {
#ObservedObject var entity: Entity
var body: some View {
NavigationLink(destination: DetailView(entity: entity)) {
Text(entity.name)
}
}
}
struct DetailView: View {
#Environment(\.managedObjectContext) private var context
#ObservedObject var entity: Entity
var body: some View {
VStack {
Text(entity.name)
Button(action: { delete() }) { Text("Delete") }
}
}
private func delete() {
context.delete(entity)
do {
try context.save()
} catch let error as NSError {
print(error)
}
}
}
Question
Why does SwiftUI not navigate back to the ListView on Deletion even though the #FetchRequest should update? How can I achieve this goal?
The FetchRequest does update, but the navigation link isn't dependent upon the parent's fetch request - it's not like a sheet or fullScreenCover where the isPresented or item bound attributes determine the new view's visibility.
The easiest thing to do is to handle the dismissal yourself using the dismiss environment function:
struct DetailView: View {
#Environment(\.dismiss) private var dismiss
#Environment(\.managedObjectContext) private var context
// ...
private func delete() {
dismiss()
context.delete(entity)
// ...
}
}
While dismiss() is more commonly seen with modals like sheets and full screen covers, it also works in navigation stacks to pop the current view off the stack.

EnviromentObject trigger closes child view presented with NavigationLink

I got an EnvironmentObject that keeps track of the current user with a snapshotlistener connected to Firestore.
When the database get updated it triggers the EnvironmentObject as intended, but when in a child view presented with a NavigationLink the update dismisses the view, in this case PostView get dismiss when likePost() is called.
Should't the view be updated in the background?
Why is this happening, and what is the best way to avoid this?
class CurrentUser: ObservableObject {
#Published var user: User?
init() {
loadUser()
}
func loadUser() {
// firebase addSnapshotListener that sets the user property
}
}
MainView
struct MainView: View {
#StateObject var currentUser = CurrentUser()
var body some view {
TabView {
PostsView()
.enviromentObject(currentUser)
.tabItem {
Label("Posts", systemImage: "square.grid.2x2.fill")
}
}
}
}
Shows All Posts
struct PostsView: View {
#ObservableObject var viewModel = PostsViewModel()
#EnviromentObject var currentUser: CurrentUser
var body some view {
NavigationLink(destination: PostView()) {
HStack {
// Navigate to post item
}
}
}
}
Show Posts Detail
When im on this View and likes a post it's added to the document in Firestore, and triggers the snapshot listener. This causes the the PostView to be dismiss which is not what I want
struct PostView: View {
#ObservableObject var viewModel: PostViewModel
var body some view {
PostItem()
Button("Like Post") {
likePost()
// Saves the post into the current users "likedPosts" document field in Firestore
// This trigger the snapshotListener in currentUser and
}
}
}
It seems that PostsView is replaced, try to use StateObject in it, like
struct PostsView: View {
#StateObject var viewModel = PostsViewModel() // << here !!
...

How to Dismiss view from viewModel in SwiftUI?

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()
}

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
}
}
...

Why is it in SwiftUI, when presenting modal view with .sheet, init() is called twice

I am producing the situation on WatchOS with the following code
struct Modal : View {
#Binding var showingModal : Bool
init(showingModal : Binding<Bool>){
self._showingModal = showingModal
print("init modal")
}
var body: some View {
Button(action: {
self.showingModal.toggle()
}, label: {
Text("TTTT")
})
}
}
struct ContentView: View {
#State var showingModal = false
var body: some View {
Button(action: {
self.showingModal.toggle()
}, label: {
Text("AAAA")
}).sheet(isPresented: $showingModal, content: {Modal(showingModal: self.$showingModal)})
}
}
Every time I press the button in the master view to summon the modal with .sheet, Two instances of the modal view are created.
Could someone explain this phenomenon?
I tracked this down in my code to having the following line in my View:
#Environment(\.presentationMode) var presentation
I had been doing it due to https://stackoverflow.com/a/61311279/155186, but for some reason that problem seems to have disappeared for me so I guess I no longer need it.
I've filed Feedback FB7723767 with Apple about this.
It is probably a bug, as of Xcode 11.4.1 (11E503a).
Beware, that if for example initializing view models (or anything else for that matter) like so:
.sheet(isPresented: $isEditingModalPresented) {
LEditView(vm: LEditViewModel(), isPresented: self.$isEditingModalPresented)
}
the VM will be initialized twice.
Comment out/remove the init() method from Modal with everything else the same. You should be able to solve the issue of two instances of Modal being created, its because you are explicitly initializing the binding (showingModal) in the init() of Modal. Hope this makes sense.
private let uniqueId: String = "uniqueId"
Button(action: {
self.showingModal.toggle()
}, label: {
Text("AAAA")
})
.sheet(isPresented: $showingModal) {
Modal(showingModal: self.$showingModal)
.id("some-unique-id")
}
Ex:
.id(self.uniqueId)
Add unique id to your .sheet and not't worry :)
But, do not use UUID(), because sheet view will be represented on every touch event

Resources