SwiftUI having #Environment(\.presentationMode) for dismissing view misbehaving - ios

I have a view with #Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> for a custom dismiss action based on a published value around some logic like this:
.onAppear(perform: {
viewModel.cancellable = viewModel.$shouldPopBack.sink(receiveValue: { shouldPopBackToHome in
if shouldPopBackToHome {
presentationMode.wrappedValue.dismiss()
}
})
})
This view also has another option to present a sheet
Button(action: {
shouldNavigateToQRCodeScanner = true
}, label: {
Text("Scan QR code")
.padding(.horizontal)
}).sheet(isPresented: $shouldNavigateToQRCodeScanner, content: {
QRCodeScannerView()
})
The problem here is when I have #Environment(\.presentationMode) the QRCodeScannerView is been initialized twice, when I remove the presentationMode it works fine.
Update
I've tried the same behaviour in a test project and it's the same
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State var isPresentd: Bool = false
var body: some View {
NavigationView {
Button(action: {
isPresentd = true
}, label: {
Text("Navigate to Second View")
})
.sheet(isPresented: $isPresentd, content: {
SecondView()
})
}
}
}
struct SecondView: View {
init() {
print("Initialized")
}
var body: some View {
Text("Second View")
}
}
It has the same issue

Related

Refresh List After a New Entity is Added in Core Data for SwiftUI App

I am building a small app using SwiftUI and Core Data. I have a main view, which launches the sheet. The sheet allows me to add a new movie to the SQLite database through Core Data. But I am having a hard time to refresh the parent view once the sheet is dismissed.
ContentView
struct ContentView: View {
#State private var isPresented: Bool = false
#StateObject private var vm = MovieListViewModel()
var body: some View {
NavigationView {
VStack {
List(vm.movies) { movie in
Text(movie.title)
}
}
.navigationTitle("Movies")
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add Movie") {
isPresented = true
}
}
})
.sheet(isPresented: $isPresented, content: {
AddMovieView()
})
.onAppear {
try? vm.populateMovies()
}.padding()
}
}
}
AddMovieView
struct AddMovieView: View {
#Environment(\.dismiss) private var dismiss
#StateObject private var vm = AddMovieViewModel()
var body: some View {
Form {
TextField("Title", text: $vm.title)
Button("Save") {
do {
try vm.saveMovie()
dismiss()
} catch {
print(error.localizedDescription)
}
}
}
}
}
Do I need to call vm.populateMovies() on the onDismiss function of the sheet from the ContentView?
You can use a #FetchRequest as follows:
struct ContentView: View {
#FetchRequest(sortDescriptors: []) var movies: FetchedResults<Movie>
#State private var isPresented: Bool = false
#StateObject private var vm = MovieListViewModel()
var body: some View {
NavigationView {
VStack {
List(movies) { movie in
Text(movie.title)
}
}
.navigationTitle("Movies")
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add Movie") {
isPresented = true
}
}
})
.sheet(isPresented: $isPresented, content: {
AddMovieView()
})
.padding()
}
}
}
You won't need a populateMovies() as the FetchRequest result is automatically populated.

Swift UI updating #EnvironmentObject prevent other update on #StateObject

I have the following view ModalView opened by by a parent view ParentView using a button in the toolbar
** Parent View **
import SwiftUI
struct ParentView: View {
#EnvironmentObject var environmentObject: MainStore
#State private var showCreationView = false
var body: some View {
NavigationView {
Text("ENV_OBJ Count: \(environmentObject.shoppingChartFullList.count)")
.navigationBarTitle(Text("Navigation Bar Title"))
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button(action: { showCreationView = true }) {
Image(systemName: "plus")
}
.sheet(isPresented: $showCreationView, content: {
ModalView(showModelView: $showCreationView)
})
}
}
}
}
}
struct ParentView_Previews: PreviewProvider {
static var previews: some View {
ParentView().environmentObject(MainStore())
}
}
** Modal View **
import SwiftUI
struct ModalView: View {
#EnvironmentObject var environmentObject: MainStore
#Binding var showModelView: Bool
#State var newItem = ShoppingChartModel()
var body: some View {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
Button(action: {
environmentObject.shoppingChartFullList.append(newItem)
self.showModelView = false
}) {Text("Salva")}
}
}
struct ModalView_Previews: PreviewProvider {
static var previews: some View {
ModalView(showModelView: .constant(true)).environmentObject(MainStore())
}
}
** Main Store **
import Foundation
import SwiftUI
import Combine
final class MainStore: ObservableObject {
//An observable object needs to publish any changes to its data, so that its subscribers can pick up the change.
#Published var shoppingChartFullList: [ShoppingChartModel] = load()
}
The problem is that, action executed by the Save Button in the Modal View doesn't dismiss the modal (even if it correctly updates both the Boolean variable and the Env_Obj).
Seems to be related with the toolbar... in fact if I remove the Navigation View and the toolbar, putting the button directly in a stack ... it works...
In the Parent2View I remove the NavigationView and just configured the button directly in the view after the Text property. And it is working as expected.
import SwiftUI
struct ParentView2: View {
#EnvironmentObject var environmentObject: MainStore
#State private var showCreationView = false
var body: some View {
VStack {
Text("ENV_OBJ Count: \(environmentObject.shoppingChartFullList.count)")
Button(action: { showCreationView = true }) {
Image(systemName: "plus")
}
.sheet(isPresented: $showCreationView, content: {
ModalView(showModelView: $showCreationView)
})
}
}
}
struct ParentView2_Previews: PreviewProvider {
static var previews: some View {
ParentView2().environmentObject(MainStore())
}
}
UPDATE
Ok I finally found the solution. The issue is the toolbar. If the ParentView is modified as follow, all start working well
struct ParentView: View {
#EnvironmentObject var environmentObject: MainStore
#State private var showCreationView = false
var body: some View {
NavigationView {
Text("ENV_OBJ Count: \(environmentObject.shoppingChartFullList.count)")
.navigationBarTitle("Nav View Title")
.navigationBarItems(trailing:
Button(action: {
self.showCreationView.toggle()
})
{
Image(systemName: "plus")
.font(Font.system(.title))
}
)
}
.sheet(isPresented: $showCreationView) {
ModalView(showModelView: $showCreationView)
}
}
}
Thanks!
Cristian

SwiftUI: How to display the second Sheet when first sheet is closed

I want to make a side menu with .fullScreenCover, when a user is logged in and press MyGarage button. the .fullScreenCover will dismiss and the main view will navigate to MyGarage View. But if user is not logged in the .fullScreenCover will dismiss and a loginView with .fullScreenCover will appear. My problem is, the .fullScreenCover will not work if I put 2 same .fullScreenCover inside the main view. Is there any way to solve this? I'm sorry it's a little bit difficult for me to explain.
Here's the code
SideMenuView
struct SideMenuView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var showMyGarage: Bool
#Binding var showSignIn: Bool
var user = 0 //If user is 1, it is logged in
var body: some View {
NavigationView{
VStack{
Button(action: {
presentationMode.wrappedValue.dismiss()
if user == 1 {
self.showMyGarage = true
}else{
self.showSignIn = true
}
}, label: {
Text("My Garage")
})
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading:
HStack(spacing: 20){
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("X")
})
Text("Main Menu")
}
)
}.navigationViewStyle(StackNavigationViewStyle())
}
}
MainView
struct HomeView: View {
#State var showSideMenu = false
#State private var showMyGarage = false
#State var showSignIn = false
var body: some View {
VStack{
Text("Home")
NavigationLink(destination: MyGarageView(showMyGarage: $showMyGarage), isActive: $showMyGarage){
EmptyView()
}
}
.navigationBarItems(leading:
Button(action: {
self.showSideMenu.toggle()
}, label: {
Text("Menu")
})
)
.fullScreenCover(isPresented: $showSideMenu, content: {
SideMenuView(showMyGarage: $showMyGarage, showSignIn: $showSignIn)
})
.fullScreenCover(isPresented: $showSignIn, content: {
SignInView()
})
}
}
struct MyGarageView: View {
#Binding var showMyGarage: Bool
var body: some View {
Text("MyGarage")
}
}
struct SignInView: View {
var body: some View {
Text("Sign In")
}
}
Try to attach them to different views, like
var body: some View {
VStack{
Text("Home")
.fullScreenCover(isPresented: $showSideMenu, content: {
SideMenuView(showMyGarage: $showMyGarage, showSignIn: $showSignIn)
})
NavigationLink(destination: MyGarageView(showMyGarage: $showMyGarage), isActive: $showMyGarage){
EmptyView()
}
.fullScreenCover(isPresented: $showSignIn, content: {
SignInView()
})
}
.navigationBarItems(leading:
Button(action: {
self.showSideMenu.toggle()
}, label: {
Text("Menu")
})
)
}

How to push many views into a NavigationView in SwiftUI [duplicate]

In my navigation, I want to be able to go from ContentView -> ModelListView -> ModelEditView OR ModelAddView.
Got this working, my issue now being that when I hit the Back button from ModelAddView, the intermediate view is omitted and it pops back to ContentView; a behaviour that
ModelEditView does not have.
There's a reason for that I guess – how can I get back to ModelListView when dismissing ModelAddView?
Here's the code:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List{
NavigationLink(
destination: ModelListView(),
label: {
Text("1. Model")
})
Text("2. Model")
Text("3. Model")
}
.padding()
.navigationTitle("Test App")
}
}
}
struct ModelListView: View {
#State var modelViewModel = ModelViewModel()
var body: some View {
List(modelViewModel.modelValues.indices) { index in
NavigationLink(
destination: ModelEditView(model: $modelViewModel.modelValues[index]),
label: {
Text(modelViewModel.modelValues[index].titel)
})
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
trailing:
NavigationLink(
destination: ModelAddView(modelViewModel: $modelViewModel), label: {
Image(systemName: "plus")
})
)
}
}
struct ModelEditView: View {
#Binding var model: Model
var body: some View {
TextField("Titel", text: $model.titel)
}
}
struct ModelAddView: View {
#Binding var modelViewModel: ModelViewModel
#State var model = Model(id: UUID(), titel: "")
var body: some View {
TextField("Titel", text: $model.titel)
}
}
struct ModelViewModel {
var modelValues: [Model]
init() {
self.modelValues = [ //mock data
Model(id: UUID(), titel: "Foo"),
Model(id: UUID(), titel: "Bar"),
Model(id: UUID(), titel: "Buzz")
]
}
}
struct Model: Identifiable, Equatable {
let id: UUID
var titel: String
}
Currently placing a NavigationLink in the .navigationBarItems may cause some issues.
A possible solution is to move the NavigationLink to the view body and only toggle a variable in the navigation bar button:
struct ModelListView: View {
#State var modelViewModel = ModelViewModel()
#State var isAddLinkActive = false // add a `#State` variable
var body: some View {
List(modelViewModel.modelValues.indices) { index in
NavigationLink(
destination: ModelEditView(model: $modelViewModel.modelValues[index]),
label: {
Text(modelViewModel.modelValues[index].titel)
}
)
}
.background( // move the `NavigationLink` to the `body`
NavigationLink(destination: ModelAddView(modelViewModel: $modelViewModel), isActive: $isAddLinkActive) {
EmptyView()
}
.hidden()
)
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: trailingButton)
}
// use a Button to activate the `NavigationLink`
var trailingButton: some View {
Button(action: {
self.isAddLinkActive = true
}) {
Image(systemName: "plus")
}
}
}

Dismiss NavigationView when Hidden SwiftUI [duplicate]

I was playing around with SwiftUI and want to be able to come back to the previous view when tapping a button, the same we use popViewController inside a UINavigationController.
Is there a provided way to do it so far ?
I've also tried to use NavigationDestinationLink to do so without success.
struct AView: View {
var body: some View {
NavigationView {
NavigationButton(destination: BView()) {
Text("Go to B")
}
}
}
}
struct BView: View {
var body: some View {
Button(action: {
// Trying to go back to the previous view
// previously: navigationController.popViewController(animated: true)
}) {
Text("Come back to A")
}
}
}
Modify your BView struct as follows. The button will perform just as popViewController did in UIKit.
struct BView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body: some View {
Button(action: { self.mode.wrappedValue.dismiss() })
{ Text("Come back to A") }
}
}
Use #Environment(\.presentationMode) var presentationMode to go back previous view. Check below code for more understanding.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.gray.opacity(0.2)
NavigationLink(destination: NextView(), label: {Text("Go to Next View").font(.largeTitle)})
}.navigationBarTitle(Text("This is Navigation"), displayMode: .large)
.edgesIgnoringSafeArea(.bottom)
}
}
}
struct NextView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
Color.gray.opacity(0.2)
}.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: { Image(systemName: "arrow.left") }))
.navigationBarTitle("", displayMode: .inline)
}
}
struct NameRow: View {
var name: String
var body: some View {
HStack {
Image(systemName: "circle.fill").foregroundColor(Color.green)
Text(name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
With State Variables. Try that.
struct ContentViewRoot: View {
#State var pushed: Bool = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination:ContentViewFirst(pushed: self.$pushed), isActive: self.$pushed) { EmptyView() }
.navigationBarTitle("Root")
Button("push"){
self.pushed = true
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentViewFirst: View {
#Binding var pushed: Bool
#State var secondPushed: Bool = false
var body: some View {
VStack{
NavigationLink(destination: ContentViewSecond(pushed: self.$pushed, secondPushed: self.$secondPushed), isActive: self.$secondPushed) { EmptyView() }
.navigationBarTitle("1st")
Button("push"){
self.secondPushed = true;
}
}
}
}
struct ContentViewSecond: View {
#Binding var pushed: Bool
#Binding var secondPushed: Bool
var body: some View {
VStack{
Spacer()
Button("PopToRoot"){
self.pushed = false
} .navigationBarTitle("2st")
Spacer()
Button("Pop"){
self.secondPushed = false
} .navigationBarTitle("1st")
Spacer()
}
}
}
This seems to work for me on watchOS (haven't tried on iOS):
#Environment(\.presentationMode) var presentationMode
And then when you need to pop
self.presentationMode.wrappedValue.dismiss()
There is now a way to programmatically pop in a NavigationView, if you would like. This is in beta 5.
Notice that you don't need the back button. You could programmatically trigger the showSelf property in the DetailView any way you like. And you don't have to display the "Push" text in the master. That could be an EmptyView(), thereby creating an invisible segue.
(The new NavigationLink functionality takes over the deprecated NavigationDestinationLink)
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
MasterView()
}
}
}
struct MasterView: View {
#State var showDetail = false
var body: some View {
VStack {
NavigationLink(destination: DetailView(showSelf: $showDetail), isActive: $showDetail) {
Text("Push")
}
}
}
}
struct DetailView: View {
#Binding var showSelf: Bool
var body: some View {
Button(action: {
self.showSelf = false
}) {
Text("Pop")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
It seems that a ton of basic navigation functionality is super buggy, which is disappointing and may be worth walking away from for now to save hours of frustration. For me, PresentationButton is the only one that works. TabbedView tabs don't work properly, and NavigationButton doesn't work for me at all. Sounds like YMMV if NavigationButton works for you.
I'm hoping that they fix it at the same time they fix autocomplete, which would give us much better insight as to what is available to us. In the meantime, I'm reluctantly coding around it and keeping notes for when fixes come out. It sucks to have to figure out if we're doing something wrong or if it just doesn't work, but that's beta for you!
Update: the NavigationDestinationLink API in this solution has been deprecated as of iOS 13 Beta 5. It is now recommended to use NavigationLink with an isActive binding.
I figured out a solution for programmatic pushing/popping of views in a NavigationView using NavigationDestinationLink.
Here's a simple example:
import Combine
import SwiftUI
struct DetailView: View {
var onDismiss: () -> Void
var body: some View {
Button(
"Here are details. Tap to go back.",
action: self.onDismiss
)
}
}
struct MainView: View {
var link: NavigationDestinationLink<DetailView>
var publisher: AnyPublisher<Void, Never>
init() {
let publisher = PassthroughSubject<Void, Never>()
self.link = NavigationDestinationLink(
DetailView(onDismiss: { publisher.send() }),
isDetail: false
)
self.publisher = publisher.eraseToAnyPublisher()
}
var body: some View {
VStack {
Button("I am root. Tap for more details.", action: {
self.link.presented?.value = true
})
}
.onReceive(publisher, perform: { _ in
self.link.presented?.value = false
})
}
}
struct RootView: View {
var body: some View {
NavigationView {
MainView()
}
}
}
I wrote about this in a blog post here.
You can also do it with .sheet
.navigationBarItems(trailing: Button(action: {
self.presentingEditView.toggle()
}) {
Image(systemName: "square.and.pencil")
}.sheet(isPresented: $presentingEditView) {
EditItemView()
})
In my case I use it from a right navigation bar item, then you have to create the view (EditItemView() in my case) that you are going to display in that modal view.
https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)
EDIT: This answer over here is better than mine, but both work: SwiftUI dismiss modal
What you really want (or should want) is a modal presentation, which several people have mentioned here. If you go that path, you definitely will need to be able to programmatically dismiss the modal, and Erica Sadun has a great example of how to do that here: https://ericasadun.com/2019/06/16/swiftui-modal-presentation/
Given the difference between declarative coding and imperative coding, the solution there may be non-obvious (toggling a bool to false to dismiss the modal, for example), but it makes sense if your model state is the source of truth, rather than the state of the UI itself.
Here's my quick take on Erica's example, using a binding passed into the TestModal so that it can dismiss itself without having to be a member of the ContentView itself (as Erica's is, for simplicity).
struct TestModal: View {
#State var isPresented: Binding<Bool>
var body: some View {
Button(action: { self.isPresented.value = false }, label: { Text("Done") })
}
}
struct ContentView : View {
#State var modalPresented = false
var body: some View {
NavigationView {
Text("Hello World")
.navigationBarTitle(Text("View"))
.navigationBarItems(trailing:
Button(action: { self.modalPresented = true }) { Text("Show Modal") })
}
.presentation(self.modalPresented ? Modal(TestModal(isPresented: $modalPresented)) {
self.modalPresented.toggle()
} : nil)
}
}
Below works for me in XCode11 GM
self.myPresentationMode.wrappedValue.dismiss()
instead of NavigationButton use Navigation DestinationLink
but You should import Combine
struct AView: View {
var link: NavigationDestinationLink<BView>
var publisher: AnyPublisher<Void, Never>
init() {
let publisher = PassthroughSubject<Void, Never>()
self.link = NavigationDestinationLink(
BView(onDismiss: { publisher.send() }),
isDetail: false
)
self.publisher = publisher.eraseToAnyPublisher()
}
var body: some View {
NavigationView {
Button(action:{
self.link.presented?.value = true
}) {
Text("Go to B")
}.onReceive(publisher, perform: { _ in
self.link.presented?.value = false
})
}
}
}
struct BView: View {
var onDismiss: () -> Void
var body: some View {
Button(action: self.onDismiss) {
Text("Come back to A")
}
}
}
In the destination pass the view you want to redirect, and inside block pass data you to pass in another view.
NavigationLink(destination: "Pass the particuter View") {
Text("Push")
}

Resources