Use #EnvironmentObject with PresentationButton in SwiftUI - ios

I am trying to pass data through an #EnvironmentObject, but it works only if I go to the next view through NavigationButton, however, I want to present the next view modally (PresentationButton)
struct ContentView : View {
#EnvironmentObject var settings: UserSettings
var body: some View {
NavigationView {
VStack {
// A button that writes to the environment settings
Button(action: {
self.settings.score += 1
}) {
Text("Increase Score")
}
NavigationButton(destination: DetailView()) {
Text("Show Detail View")
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var settings: UserSettings
var body: some View {
// A text view that reads from the environment settings
VStack {
Text("Score: \(settings.score)")
}
}
}
What I am trying to use :
PresentationButton( Text("Show Detail View"), destination: DetailView())

Try supplying the bindable object to the DetailView using environmentObject:
PresentationButton(Text("Show Detail View"),
destination: DetailView().environmentObject(settings))

Related

SwiftUI - Attempt to present SwiftUI.PlatformAlertController... whose view is not in the window hierarchy

While this question has been asked previously, none of the threads deal with the latest SwiftUI and iOS15+.
I am seeing this error in console when navigating to a second level in a custom tab view, where the NavigationView is instantiated outside of the custom tab view.
The full error is:
[Presentation] Attempt to present <SwiftUI.PlatformAlertController: 0x7f8962874800> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_24NavigationColumnModifier__: 0x7f896080dac0> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_24NavigationColumnModifier__: 0x7f896080dac0>) whose view is not in the window hierarchy
The hierarchy is as follows:
Login Page (NavigationView instantiated here) -> Tap NavigationLink
from this view
Custom Tab View is displayed -> Tap any tab and see
the correct view
Tap a NavigationLink from within one of these views
and the alert works perfectly
Tap a NavigationLink from within that view and the error is shown and no alerts work
I know it's a lot of code, but I have been asked to provide code that will compile, and since this is quite a few different files to make this happen, I stripped everything down to the bare minimum so the issue could be recreated.
Here's the code:
App:
import SwiftUI
#main
struct TestApp: App {
#StateObject var viewRouter: ViewRouter
init() {
let viewRouter = ViewRouter()
_viewRouter = StateObject(wrappedValue: viewRouter)
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewRouter)
}
}
}
ViewRouter:
import Foundation
class ViewRouter: ObservableObject {
#Published var accessView: DisplayView = .home
}
enum DisplayView {
case settings
case home
}
ContentView:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: HomeView()
) {
Text("Go To Tab View")
}
.isDetailLink(false)
}
}
}
HomeView:
import SwiftUI
struct HomeView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
switch viewRouter.accessView {
case .home:
UserHome()
.navigationBarHidden(true)
case .settings:
UserSettings()
.navigationBarHidden(true)
}
}
}
struct HomeNavView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack(spacing: 0) {
VStack(spacing: 30) {
HStack(alignment: .bottom) {
Image(systemName: "House")
.onTapGesture {
viewRouter.accessView = .home
}
.frame(maxWidth: .infinity)
Image(systemName: "Person")
.onTapGesture {
viewRouter.accessView = .settings
}
.frame(maxWidth: .infinity)
}
}
}
.ignoresSafeArea()
.zIndex(1)
}
}
UserHome:
import SwiftUI
struct UserHome: View {
var body: some View {
Text("User Home View")
HomeNavView()
}
}
UserSettings:
import SwiftUI
struct UserSettings: View {
var body: some View {
NavigationLink(
destination: SectionView()
) {
Text("Go To Section View")
}
}
}
SectionView (if you try to display an alert here, it works just fine):
import SwiftUI
struct SectionView: View {
var body: some View {
NavigationLink(
destination: AlertView()
) {
Text("Go To Alert View")
}
}
}
AlertView (This is where you see the error in console and the alert does not work):
import SwiftUI
struct AlertView: View {
#State private var alertTest: Bool = false
var body: some View {
Button("Show Alert") {
alertTest = true
}
.alert(
"Alert",
isPresented: $alertTest,
actions: {
Button("OK", role: .cancel) {
}
}, message: {
Text("The alert is working!")
})
}
}

SwiftUI: detecting the NavigationView back button press

In SwiftUI I couldn't find a way to detect when the user taps on the default back button of the navigation view when I am inside DetailView1 in this code:
struct RootView: View {
#State private var showDetails: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView1(), isActive: $showDetails) {
Text("show DetailView1")
}
}
.navigationBarTitle("RootView")
}
}
}
struct DetailView1: View {
#State private var showDetails: Bool = false
var body: some View {
NavigationLink(destination: DetailView2(), isActive: $showDetails) {
Text("show DetailView2")
}
.navigationBarTitle("DetailView1")
}
}
struct DetailView2: View {
var body: some View {
Text("")
.navigationBarTitle("DetailView2")
}
}
Using .onDisappear doesn't solve the problem as its closure is called when the view is popped off or a new view is pushed.
The quick solution is to create a custom back button because right now the framework have not this possibility.
struct DetailView : View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body : some View {
Text("Detail View")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action : {
self.mode.wrappedValue.dismiss()
}){
Image(systemName: "arrow.left")
})
}
}
As soon as you press the back button, the view sets isPresented to false, so you can use an observer on that value to trigger code when the back button is pressed. Assume this view is presented inside a navigation controller:
struct MyView: View {
#Environment(\.isPresented) var isPresented
var body: some View {
Rectangle().onChange(of: isPresented) { newValue in
if !newValue {
print("detail view is dismissed")
}
}
}
}
An even nicer (SwiftUI-ier?) way of observing the published showDetails property:
struct RootView: View {
class ViewModel: ObservableObject {
#Published var showDetails = false
}
#ObservedObject var viewModel = ViewModel()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView1(), isActive: $viewModel.showDetails) {
Text("show DetailView1")
}
}
.navigationBarTitle("RootView")
.onReceive(self.viewModel.$showDetails) { isShowing in
debugPrint(isShowing)
// Maybe do something here?
}
}
}
}
Following up on my comment, I would react to changes in the state of showDetails. Unfortunately didSet doesn't appear to trigger with #State variables. Instead, we can use an observable view model to hold the state, which does allow us to do intercept changes with didSet.
struct RootView: View {
class ViewModel: ObservableObject {
#Published var showDetails = false {
didSet {
debugPrint(showDetails)
// Maybe do something here?
}
}
}
#ObservedObject var viewModel = ViewModel()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView1(), isActive: $viewModel.showDetails) {
Text("show DetailView1")
}
}
.navigationBarTitle("RootView")
}
}
}

onDisappear not called when a modal View is dismissed

I rely on the SwiftUI's .onDisappear to perform some logic but it is not being called when the user dismisses a modally presented view with the swipe gesture. To reproduce
Present a view modally a "ChildView 1"
On this view, push a "ChildView 2" as a navigation child
Swipe down to dismiss the modal view.
The .onDisappear of "ChildView 2" is not called.
Sample code to reproduce
import SwiftUI
struct ContentView: View {
#State var isShowingModal
var body: some View {
NavigationView {
Button(action: {
self.isShowingModal.toggle()
}) {
Text("Show Modal")
}
}
.sheet(isPresented: $isShowingModal) {
NavigationView {
ChildView(title: 1)
}
}
}
}
struct ChildView: View {
let title: Int
var body: some View {
NavigationLink(destination: ChildView(title: title + 1)) {
Text("Show Child")
}
.navigationBarTitle("View \(title)")
.onAppear {
print("onAppear ChildView \(self.title)")
}
.onDisappear {
print("onDisappear ChildView \(self.title)")
}
}
}
The output is:
onAppear ChildView 1
onAppear ChildView 2
onDisappear ChildView 1
If you're looking for logic to occur when the actual modal is dismissed, you're going to want to call that here, where I print out Modal Dismissed:
struct ContentView: View {
#State var isShowingModal = false
var body: some View {
NavigationView {
Button(action: {
self.isShowingModal.toggle()
}) {
Text("Show Modal")
}
}
.sheet(isPresented: $isShowingModal) {
NavigationView {
ChildView(title: 1)
}
.onDisappear {
print("Modal Dismissed")
}
}
}
}
struct ContentView: View {
#State var isShowingModal = false
var body: some View {
NavigationView {
Button(action: {
self.isShowingModal.toggle()
}) {
Text("Show Modal")
}
}
.sheet(isPresented: $isShowingModal) {
NavigationView {
ChildView(title: 1)
}
}
}
}
in this code, you have NavigationView, and when presenting sheet, you push there another NavigationView. This is the source of trouble
You don't need any NavigationView to present modals. If you like to present another modal from modal, you can use
import SwiftUI
struct ContentView: View {
var body: some View {
ChildView(title: 1)
}
}
struct ChildView: View {
#State var isShowingModal = false
let title: Int
var body: some View {
Button(action: {
self.isShowingModal.toggle()
}) {
Text("Show Modal \(title)").font(.largeTitle)
}
.sheet(isPresented: $isShowingModal) {
ChildView(title: self.title + 1)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
UPDATE
from Apple
Human Interface Guidelines
Modality is a design technique that presents content in a temporary mode that’s separate from the user's previous current context and requires an explicit action to exit

Dismiss a parent modal in SwiftUI from a NavigationView

I am aware of how to dismiss a modal from a child view using #Environment (\.presentationMode) var presentationMode / self.presentationMode.wrappedValue.dismiss() but this is a different issue.
When you present a multi-page NavigationView in a modal window, and have navigated through a couple of pages, the reference to presentationMode changes to be the NavigationView, so using self.presentationMode.wrappedValue.dismiss() simply pops the last NavigationView rather than dismissing the containing modal.
Is it possible - and if so how - to dismiss the containing modal from a page in a NavigationView tree?
Here's a simple example showing the problem. If you create an Xcode Single View app project using SwiftUI and replace the default ContentView code with this, it should work with no further changes.
import SwiftUI
struct ContentView: View {
#State var showModal: Bool = false
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Launch Modal")
}
.sheet(isPresented: self.$showModal, onDismiss: {
self.showModal = false
}) {
PageOneContent()
}
}
}
struct PageOneContent: View {
var body: some View {
NavigationView {
VStack {
Text("I am Page One")
}
.navigationBarTitle("Page One")
.navigationBarItems(
trailing: NavigationLink(destination: PageTwoContent()) {
Text("Next")
})
}
}
}
struct PageTwoContent: View {
#Environment (\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
Text("This should dismiss the modal. But it just pops the NavigationView")
.padding()
Button(action: {
// How to dismiss parent modal here instead
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Finish")
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
}
.navigationBarTitle("Page Two")
}
}
}
Here is possible approach based on usage own explicitly created environment key (actually I have feeling that it is not correct to use presentationMode for this use-case.. anyway).
Proposed approach is generic and works from any view in modal view hierarchy. Tested & works with Xcode 11.2 / iOS 13.2.
// define env key to store our modal mode values
struct ModalModeKey: EnvironmentKey {
static let defaultValue = Binding<Bool>.constant(false) // < required
}
// define modalMode value
extension EnvironmentValues {
var modalMode: Binding<Bool> {
get {
return self[ModalModeKey.self]
}
set {
self[ModalModeKey.self] = newValue
}
}
}
struct ParentModalTest: View {
#State var showModal: Bool = false
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Launch Modal")
}
.sheet(isPresented: self.$showModal, onDismiss: {
}) {
PageOneContent()
.environment(\.modalMode, self.$showModal) // < bind modalMode
}
}
}
struct PageOneContent: View {
var body: some View {
NavigationView {
VStack {
Text("I am Page One")
}
.navigationBarTitle("Page One")
.navigationBarItems(
trailing: NavigationLink(destination: PageTwoContent()) {
Text("Next")
})
}
}
}
struct PageTwoContent: View {
#Environment (\.modalMode) var modalMode // << extract modalMode
var body: some View {
NavigationView {
VStack {
Text("This should dismiss the modal. But it just pops the NavigationView")
.padding()
Button(action: {
self.modalMode.wrappedValue = false // << close modal
}) {
Text("Finish")
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
}
.navigationBarTitle("Page Two")
}
}
}
Another Approach would be to simply use a notification for this case and just reset the triggering flag for your modal.
It is not the most beautiful solution for me but it is the solution I am most likely to still understand in a few months.
import SwiftUI
struct ContentView: View {
#State var showModalNav: Bool = false
var body: some View {
Text("Present Modal")
.padding()
.onTapGesture {
showModalNav.toggle()
}.sheet(isPresented: $showModalNav, content: {
ModalNavView()
}).onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "PushedViewNotifciation"))) { _ in
showModalNav = false
}
}
}
struct ModalNavView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: PushedView(),
label: {
Text("Show Another View")
}
)
}
}
}
struct PushedView: View {
var body: some View {
Text("Pushed View").onTapGesture {
NotificationCenter.default.post(Notification.init(name: Notification.Name(rawValue: "PushedViewNotifciation")))
}
}
}
If you don't want to loosely couple the views through a notification you could also just use a binding for this like so:
struct ContentView: View {
#State var showModalNav: Bool = false
var body: some View {
Text("Present Modal")
.padding()
.onTapGesture {
showModalNav.toggle()
}.sheet(isPresented: $showModalNav, content: {
ModalNavView(parentShowModal: $showModalNav)
}).onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "PushedViewNotifciation"))) { _ in
showModalNav = false
}
}
}
struct ModalNavView: View {
#Binding var parentShowModal: Bool
var body: some View {
NavigationView {
NavigationLink(
destination: PushedView(parentShowModal: $parentShowModal),
label: {
Text("Show Another View")
}
)
}
}
}
struct PushedView: View {
#Binding var parentShowModal: Bool
var body: some View {
Text("Pushed View").onTapGesture {
parentShowModal = false
}
}
}
If it's only two levels, and especially if you can dismiss the sheet at multiple levels, you could include showModal as a binding variable in your navigation views below, and then toggling it anywhere would dismiss the entire sheet.
I would assume you could do something similar with showModal as an EnvironmentObject as Wei mentioned above - which might be better if there are more than two levels and you only want to dismiss the sheet at the most specific level.
I can't remember if there's some reason to move away from doing this as a binding variable, but it seems to be working for me.
import SwiftUI
struct ContentView: View {
#State var showModal: Bool = false
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Launch Modal")
}
.sheet(isPresented: self.$showModal, onDismiss: {
self.showModal = false
}) {
// Bind showModal to the corresponding property in PageOneContent
PageOneContent(showModal: $showModal)
}
}
}
Then you add showModal as a binding variable in PageOneContent, and it is bound to the state variable in ContentView.
struct PageOneContent: View {
// add a binding showModal var in here
#Binding var showModal: Bool
var body: some View {
NavigationView {
VStack {
Text("I am Page One")
}
.navigationBarTitle("Page One")
.navigationBarItems(
// bind showModal again to PageTwoContent
trailing: NavigationLink(destination: PageTwoContent(showModal: $showModal)) {
Text("Next")
})
}
}
}
Finally, in PageTwoContent, you can add showModal here (and in the NavigationLink in PageOneContent, you have bound PageTwoContent's showModal to PageOneContent). Then in your button, all you have to do is toggle it, and it will dismiss the sheet.
struct PageTwoContent: View {
// Add showModal as a binding var here too.
#Binding var showModal: Bool
var body: some View {
NavigationView {
VStack {
Text("This should dismiss the modal. But it just pops the NavigationView")
.padding()
Button(action: {
// This will set the showModal var back to false in all three views, and will dismiss the current sheet.
self.showModal.toggle()
}) {
Text("Finish")
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
}
.navigationBarTitle("Page Two")
}
}
}
I found out you can actually make showModal into an EnvironmentObject, then simplify toggle the showModal to false on PageTwoContent to dismiss both PageOneContent and PageTwoContent.

iOS SwiftUI: pop or dismiss view programmatically

I couldn't find any reference about any ways to make a pop or a dismiss programmatically of my presented view with SwiftUI.
Seems to me that the only way is to use the already integrated slide dow action for the modal(and what/how if I want to disable this feature?), and the back button for the navigation stack.
Does anyone know a solution?
Do you know if this is a bug or it will stays like this?
This example uses the new environment var documented in the Beta 5 Release Notes, which was using a value property. It was changed in a later beta to use a wrappedValue property. This example is now current for the GM version. This exact same concept works to dismiss Modal views presented with the .sheet modifier.
import SwiftUI
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(
"Here is Detail View. Tap to go back.",
action: { self.presentationMode.wrappedValue.dismiss() }
)
}
}
struct RootView: View {
var body: some View {
VStack {
NavigationLink(destination: DetailView())
{ Text("I am Root. Tap for Detail View.") }
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
RootView()
}
}
}
SwiftUI Xcode Beta 5
First, declare the #Environment which has a dismiss method which you can use anywhere to dismiss the view.
import SwiftUI
struct GameView: View {
#Environment(\.presentationMode) var presentation
var body: some View {
Button("Done") {
self.presentation.wrappedValue.dismiss()
}
}
}
iOS 15+
Starting from iOS 15 we can use a new #Environment(\.dismiss):
struct SheetView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
Text("Sheet")
.toolbar {
Button("Done") {
dismiss()
}
}
}
}
}
(There's no more need to use presentationMode.wrappedValue.dismiss().)
Useful links:
DismissAction
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.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
MasterView()
}
}
}
struct MasterView: View {
#State private 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
I recently created an open source project called swiftui-navigation-stack (https://github.com/biobeats/swiftui-navigation-stack) that contains the NavigationStackView, an alternative navigation stack for SwiftUI. It offers several features described in the readme of the repo. For example, you can easily push and pop views programmatically. I'll show you how to do that with a simple example:
First of all embed your hierarchy in a NavigationStackVew:
struct RootView: View {
var body: some View {
NavigationStackView {
View1()
}
}
}
NavigationStackView gives your hierarchy access to a useful environment object called NavigationStack. You can use it to, for instance, pop views programmatically as asked in the question above:
struct View1: View {
var body: some View {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
VStack {
Text("VIEW 1")
Spacer()
PushView(destination: View2()) {
Text("PUSH TO VIEW 2")
}
}
}
}
}
struct View2: View {
#EnvironmentObject var navStack: NavigationStack
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.all)
VStack {
Text("VIEW 2")
Spacer()
Button(action: {
self.navStack.pop()
}, label: {
Text("PROGRAMMATICALLY POP TO VIEW 1")
})
}
}
}
}
In this example I use the PushView to trigger the push navigation with a tap. Then, in the View2 I use the environment object to programmatically come back.
Here is the complete example:
import SwiftUI
import NavigationStack
struct RootView: View {
var body: some View {
NavigationStackView {
View1()
}
}
}
struct View1: View {
var body: some View {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
VStack {
Text("VIEW 1")
Spacer()
PushView(destination: View2()) {
Text("PUSH TO VIEW 2")
}
}
}
}
}
struct View2: View {
#EnvironmentObject var navStack: NavigationStack
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.all)
VStack {
Text("VIEW 2")
Spacer()
Button(action: {
self.navStack.pop()
}, label: {
Text("PROGRAMMATICALLY POP TO VIEW 1")
})
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
RootView()
}
}
the result is:
Alternatively, if you don't want to do it programatically from a button, you can emit from the view model whenever you need to pop.
Subscribe to a #Published that changes the value whenever the saving is done.
struct ContentView: View {
#ObservedObject var viewModel: ContentViewModel
#Environment(\.presentationMode) var presentationMode
init(viewModel: ContentViewModel) {
self.viewModel = viewModel
}
var body: some View {
Form {
TextField("Name", text: $viewModel.name)
.textContentType(.name)
}
.onAppear {
self.viewModel.cancellable = self.viewModel
.$saved
.sink(receiveValue: { saved in
guard saved else { return }
self.presentationMode.wrappedValue.dismiss()
}
)
}
}
}
class ContentViewModel: ObservableObject {
#Published var saved = false // This can store any value.
#Published var name = ""
var cancellable: AnyCancellable? // You can use a cancellable set if you have multiple observers.
func onSave() {
// Do the save.
// Emit the new value.
saved = true
}
}
Please check Following Code it's so simple.
FirstView
struct StartUpVC: View {
#State var selection: Int? = nil
var body: some View {
NavigationView{
NavigationLink(destination: LoginView().hiddenNavigationBarStyle(), tag: 1, selection: $selection) {
Button(action: {
print("Signup tapped")
self.selection = 1
}) {
HStack {
Spacer()
Text("Sign up")
Spacer()
}
}
}
}
}
SecondView
struct LoginView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView{
Button(action: {
print("Login tapped")
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image("Back")
.resizable()
.frame(width: 20, height: 20)
.padding(.leading, 20)
}
}
}
}
}
You can try using a custom view and a Transition.
Here's a custom modal.
struct ModalView<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
if (!self.isShowing) {
self.content()
}
if (self.isShowing) {
self.content()
.disabled(true)
.blur(radius: 3)
VStack {
Text("Modal")
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.transition(.moveAndFade) // associated transition to the modal view
}
}
}
}
}
I reused the Transition.moveAndFade from the Animation Views and Transition tutorial.
It is defined like this:
extension AnyTransition {
static var moveAndFade: AnyTransition {
let insertion = AnyTransition.move(edge: .trailing)
.combined(with: .opacity)
let removal = AnyTransition.scale()
.combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
}
You can test it - in the simulator, not in the preview - like this:
struct ContentView: View {
#State var isShowingModal: Bool = false
func toggleModal() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation {
self.isShowingModal = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation {
self.isShowingModal = false
}
}
}
}
var body: some View {
ModalView(isShowing: $isShowingModal) {
NavigationView {
List(["1", "2", "3", "4", "5"].identified(by: \.self)) { row in
Text(row)
}.navigationBarTitle(Text("A List"), displayMode: .large)
}.onAppear { self.toggleModal() }
}
}
}
Thanks to that transition, you will see the modal sliding in from the trailing edge, and the it will zoom and fade out when it is dismissed.
The core concept of SwiftUI is to watch over the data flow.
You have to use a #State variable and mutate the value of this variable to control popping and dismissal.
struct MyView: View {
#State
var showsUp = false
var body: some View {
Button(action: { self.showsUp.toggle() }) {
Text("Pop")
}
.presentation(
showsUp ? Modal(
Button(action: { self.showsUp.toggle() }) {
Text("Dismiss")
}
) : nil
)
}
}
I experienced a compiler issue trying to call value on the presentationMode binding. Changing the property to wrappedValue fixed the issue for me. I'm assuming value -> wrappedValue is a language update. I think this note would be more appropriate as a comment on Chuck H's answer but don't have enough rep points to comment, I also suggested this change as and edit but my edit was rejected as being more appropriate as a comment or answer.
This will also dismiss the view
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
window?.rootViewController?.dismiss(animated: true, completion: {
print("dismissed")
})

Resources