Show bottom sheet after button press swiftui - swift

I'm trying to add to my app bottom sheet with responsive height which I can set programmatically. For this purpose I'm trying to use this video. Here is code of my view controller:
struct SecondView: View {
#State var cardShown = false
#State var cardDismissal = false
var body: some View {
Button {
cardShown.toggle()
cardDismissal.toggle()
} label: {
Text("Show card")
.bold()
.foregroundColor(Color.white)
.background(Color.red)
.frame(width: 200, height: 50)
}
BottomCard(cardShown: $cardShown, cardDismissal: $cardDismissal) {
CardContent()
}
}
}
struct CardContent:View{
var body: some View{
Text("some text")
}
}
struct BottomCard<Content:View>:View{
#Binding var cardShown:Bool
#Binding var cardDismissal:Bool
let content:Content
init(cardShown:Binding<Bool> , cardDismissal:Binding<Bool>, #ViewBuilder content: () -> Content){
_cardShown = cardShown
_cardDismissal = cardDismissal
self.content = content()
}
var body: some View{
ZStack{
//Dimmed
GeometryReader{ _ in
EmptyView()
}
.background(Color.red.opacity(0.2))
.opacity(cardShown ? 1 : 0)
.animation(.easeIn)
.onTapGesture {
cardShown.toggle()
}
// Card
VStack{
Spacer()
VStack{
content
}
}
.edgesIgnoringSafeArea(.all)
}
}
}
but after pressing the button I don't see any pushed bottom menu. I checked and it seems that I have similar code to this video but on the video bottom sheet appears. Maybe I missed something important for menu showing. The main purpose is to show bottom menu with responsive height which will wrap elements and will be able to change menu height. I tried to use .sheet() but this element has stable height as I see. I know that from the ios 15+ we will have some solutions for this problem but I would like to create something more stable and convenient :)

iOS 16
We can have native SwiftUI resizable sheet (like UIKit). This is possible with the new .presentationDetents() modifier.
.sheet(isPresented: $showBudget) {
BudgetView()
.presentationDetents([.height(250), .medium])
.presentationDragIndicator(.visible)
}
Demo:

This is what I got when running your code
I got this after some adjustments to bottom card
struct BottomCard<Content:View>:View{
#Binding var cardShown:Bool
#Binding var cardDismissal:Bool
let content:Content
init(cardShown:Binding<Bool> , cardDismissal:Binding<Bool>, #ViewBuilder content: () -> Content){
_cardShown = cardShown
_cardDismissal = cardDismissal
self.content = content()
}
var body: some View{
ZStack{
//Dimmed
GeometryReader{ _ in
EmptyView()
}
.background(Color.red.opacity(0.2))
.animation(.easeIn)
.onTapGesture {
cardShown.toggle()
}
// Card
VStack{
Spacer()
VStack{
content
}
Spacer()
}
}.edgesIgnoringSafeArea(.all)
.opacity(cardShown ? 1 : 0)
}
}
So you just need to set the height!

what you want to do is to have a card that only exists when there is a certain standard met.
If you want to push up a card from the bottom then you can make a view of a card and put it at the bottom of a Zstack view using a geometry reader and then make a button that only allows for that card to exist when the button is pressed INSTEAD of trying to hire it by changing its opacity. Also, make sure you move the dismissal button to the inside of the cad you have.
Heres an example you can try :
struct SecondView: View {
#State var cardShown = false
var body: some View {
GeometryReader{
ZStack {
ZStack{
// I would also suggest getting used to physically making your
//button and then giving them functionality using a "Gesture"
Text("Show Button")
.background(Rectangle())
.onTapGesture{
let animation = Animation.spring()
withAnimation(animation){
self.cardShown.toggle
}
}
}
ZStack {
if cardShown == true{
BottomCard(cardShown: $cardShown) {
CardContent()
}
}
// here you can change how far up the card comes after the button
//is pushed by changing the "0"
.offset(cardShown == false ? geometry.size.height : 0)
}
}
}
}
}
Also, you don't need to have a variable for the card being shown and a variable for the card being dismissed. Just have one "cardShown" variable and make it so that when it is TRUE the card is shown and when it is FALSE (after hitting the button on the card or hitting the initial button again.) the card goes away.

iOS 16.0+
iPadOS 16.0+
macOS 13.0+
Mac Catalyst 16.0+
tvOS 16.0+
watchOS 9.0+
Use presentationDetents(_:)
struct ContentView: View {
#State private var isBottomSheetVisible = false
var body: some View {
Button("View Settings") {
isBottomSheetVisible = true
}
.sheet(isPresented: $isBottomSheetVisible) {
Text("Bottom Sheet")
.presentationDetents([.height(250), .medium])
.presentationDragIndicator(.visible)
}
}
}

Related

SwiftUI sheet not animating dismissal on macOS Big Sur

I would like the sheet dismissal animated just like the appearance but reversed. I think this is also the standard behavior. You can see it in Xcode for example when you create a new file.
But as you can see it just disappears without animation
Here's my code:
struct ContentView: View {
#State var isAnotherViewPresented: Bool = false
var body: some View {
HStack {
Button(action: {
isAnotherViewPresented.toggle()
}, label: {
Text("Button")
}).sheet(isPresented: $isAnotherViewPresented, content: {
AnotherView()
})
}
.frame(width: 500, height: 300, alignment: .center)
}
}
struct AnotherView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Close")
})
}.padding()
}
}
I'm on
Mac mini (M1, 2020)
macOS Big Sur 11.1 (20C69)
Xcode 12.3 (12C33)
But I can reproduce this on a
Mac mini (2018)
macOS Big Sur 11.0.1 (20B29)
Xcode 12.2 (12B45b)
I finally figured out how to do it, in my SwiftUI app it works if I do this while closing the sheet:
isSheetVisible = false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!)
Example:
struct SheetView: View {
#Binding var isSheetVisible: Bool
var body: some View {
Button("Close") {
isSheetVisible = false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!)
}
}
}
Disclaimer: I had/have the same problem where if I try to dismiss a sheet through a binding, it just disappears instead of having an animation. The below solution worked for me but I am unclear as to why its working.
Solution
Apparently the view you "attach" a modal to has an impact on how it transitions from being presented to not. For instance, in your code the sheet is attached to the button view:
Button(action: {
isAnotherViewPresented.toggle()
}, label: {
Text("Button")
// sheet is attached here
}).sheet(isPresented: $isAnotherViewPresented, content: {
AnotherView()
})
When you call presentationMode.wrappedValue.dismiss() in the second view the modal jolts and disappears instead of sliding away. However, if you attach the sheet to the outer HStack view, then it works and it slides away as expected:
var body: some View {
HStack {
Button(action: {
isAnotherViewPresented.toggle()
}, label: {
Text("Button")
})
}
.frame(width: 500, height: 300, alignment: .center)
.sheet(isPresented: $isAnotherViewPresented, content: {
AnotherView()
})
// sheet is now here
}
For me as long as the sheet wasn't attached to the button the animation worked. I don't know why this works but it did for me and hopefully it will for you as well.
I like this mix:
assuming you have:
#Environment(\.presentationMode) var presentationMode
then:
presentationMode.wrappedValue.dismiss() // this updates the binding from .sheet(isPresented: ...) to false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!) // this runs the animation

SwiftUI: NavigationLink is always activated when in a List

I can't prevent SwiftUI's NavigationLink from being activated when in a List, I have this simple piece of code in which I need to do some kind of business check before deciding to show the details page or not (in a real world app, there might be some business logic happens inside the button's action):
struct ContentView: View {
#State var showDetail = false
var body: some View {
NavigationView {
List {
Text("Text 1")
Text("Text 2")
Text("Text 3")
NavigationLink(destination: DetailView(), isActive: $showDetail) {
LinkView(showDetails: $showDetail)
}
}
}
}
}
struct LinkView: View {
#Binding var showDetails: Bool
var body: some View {
Button(action: {
self.showDetails = false
}) {
Text("Open Details")
}
}
}
struct DetailView: View {
var body: some View {
Text("Detail View")
}
}
how can I prevent navigation link from opening the details page in this case ? and is this a bug in the SDK ?
p.s. XCode version: 13.3.1 and iOS version (real device): 13.3.1
Edit
I can't replace List with ScrollView because I have a ForEach list of items in my real app, so don't post an answer considering using ScrollView.
in a real world app, there might be some business logic happens inside
the button's action
seems to be a little bit alogical.You can simply conditionally disable the link (and inform the user, that the link is unavailable by visual appearance)
NavigationLink(...).disabled(onCondition)
where
func disabled(_ disabled: Bool) -> some View
Parameters
disabled
A Boolean value that determines whether users can interact with this view.
Return Value
A view that controls whether users can interact with this view.
Discussion
The higher views in a view hierarchy can override the value you set on this view. In the following example, the button isn’t interactive because the outer disabled(_:) modifier overrides the inner one:
HStack {
Button(Text("Press")) {}
.disabled(false)
}
.disabled(true)
If I correctly understood your goal, it can be as follows
List {
Text("Text 1")
Text("Text 2")
Text("Text 3")
LinkView(showDetails: $showDetail)
.background(
NavigationLink(destination: DetailView(), isActive: $showDetail) { EmptyView() })
}
and
struct LinkView: View {
#Binding var showDetails: Bool
var body: some View {
Button(action: {
self.showDetails = true // < activate by some logic
}) {
Text("Open Details")
}
}
}
If you use .disable(true) it will reduce your list item opacity like a disabled button, to prevent this. use below code style. Use Navigation Link in backGround and check your navigation condition on Tap Gesture of your view.
VStack{
List(0..<yourListArray.count, id: \.self) { index in
{
Text("\(yourListArr[index].firstName)")
}().onTapGesture{
let jobType = getFlags(jobsArr: yourListArray, index:index)
if jobType.isCancelledFlag == true{
self.shouldNavigate = false
}else{
self.shouldNavigate = true
}
}//Tap Gesture End
.background(NavigationLink(destination: YourDestinationView(),isActive: self.$shouldNavigate) {
}.hidden())}}//vStack

SwiftUI - Navigation bar button not clickable after sheet has been presented

I have just started using SwiftUI a couple of weeks ago and i'm learning. Today I ran into a into an issue.
When I present a sheet with a navigationBarItems-button and then dismiss the ModalView and return to the ContentView I find myself unable to click on the navigationBarItems-button again.
My code is as follows:
struct ContentView: View {
#State var showSheet = false
var body: some View {
NavigationView {
VStack {
Text("Test")
}.sheet(isPresented: self.$showSheet) {
ModalView()
}.navigationBarItems(trailing:
Button(action: {
self.showSheet = true
}) {
Text("SecondView")
}
)
}
}
}
struct ModalView: View {
#Environment(\.presentationMode) var presentation
var body: some View {
VStack {
Button(action: {
self.presentation.wrappedValue.dismiss()
}) {
Text("Dismiss")
}
}
}
}
I think this happens because the presentationMode is not inherited from the presenter view, so the presenter didn't know that the modal is already closed. You can fix this by adding presentationMode to presenter, in this case to ContentView.
struct ContentView: View {
#Environment(\.presentationMode) var presentation
#State var showSheet = false
var body: some View {
NavigationView {
VStack {
Text("Test")
}.sheet(isPresented: self.$showSheet) {
ModalView()
}.navigationBarItems(trailing:
Button(action: {
self.showSheet = true
}) {
Text("SecondView")
}
)
}
}
}
Tested on Xcode 12.5.
Here is the full working
example.
This seems to be a bug in SwiftUI. I am also still seeing this issue with Xcode 11.5 / iOS 13.5.1. The navigationBarMode didn't make a difference.
I filed an issue with Apple:
FB7641003 - Taps on a navigationBarItem Button presenting a sheet sometimes not recognized
You can use the attached example project SwiftUISheet (also available via https://github.com/ralfebert/SwiftUISheet) to reproduce the issue. It just presents a sheet from a navigation bar button.
Run the app and tap repeatedly on the 'plus' button in the nav bar. When the sheet pops up, dismiss it by sliding it down. Only some taps to the button will be handled, often a tap is ignored.
Tested on Xcode 11.4 (11E146) with iOS 13.4 (17E255).
Very hacky but this worked for me:
Button(action: {
self.showSheet = true
}) {
Text("SecondView")
.frame(height: 96, alignment: .trailing)
}
I'm still seeing this issue with Xcode 13 RC and iOS 15. Unfortunately the solutions above didn't work for me. What I ended up doing is adding a small Text view to the toolbar whose content changes depending on the value of the .showingSheet property.
struct ContentView: View {
#State private var showingSheet = false
var body: some View {
NavigationView {
VStack {
Text("Content view")
Text("Swift UI")
}
.sheet(isPresented: $showingSheet) {
Text("This is a sheet")
}
.navigationTitle("Example")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
// Text view workaround for SwiftUI bug
// Keep toolbar items tappable after dismissing sheet
Text(showingSheet ? " " : "").hidden()
Button(action: {
self.showingSheet = true
}) {
Label("Show Sheet", systemImage: "plus.square")
}
}
}
}
}
}
I realize it's not ideal but it's the first thing that worked for me. My guess is that having the Text view's content change depending on the .showingSheet property forces SwiftUI to fully refresh the toolbar group.
So far, I can still observe the disorder of navi buttons right after dismissing its presented sheet.
FYI, I am using a UINavigationController wrapper instead as workaround. It works well.
Unfortunately, I sure that the more that kind of bugs, the farther away the time of using SwiftUI widely by the ios dev guys. Because those are too basic to ignore.
Very hacky but this worked for me:
I had the same problem. this solution worked for me.
struct ContentView: View {
#State var showSheet = false
var body: some View {
NavigationView {
VStack {
Text("Test")
}.sheet(isPresented: self.$showSheet) {
ModalView()
}.navigationBarItems(trailing:
Button(action: {
self.showSheet = true
}) {
Text("SecondView")
// this is a workaround
.frame(height: 96, alignment: .trailing)
}
)
}
}
}
Only #adamwjohnson5's answer worked for me. I don't like doing it but it's the only solution that works as of Xcode 13.1 and iOS 15.0. Here is my code for anyone interested in seeing iOS 15.0 targeted code:
var body: some View {
NavigationView {
mainContentView
.navigationTitle(viewModel.navigationTitle)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
PlusButton {
viewModel.showAddDialog.toggle()
}
.frame(height: 96, alignment: .trailing) // Workaroud, credit: https://stackoverflow.com/a/62209223/5421557
.confirmationDialog("CatalogView.Add.DialogTitle", isPresented: $viewModel.showAddDialog, titleVisibility: .visible) {
Button("Program") {
viewModel.navigateToAddProgramView.toggle()
}
Button("Exercise") {
viewModel.navigateToAddExerciseView.toggle()
}
}
}
}
.sheet(isPresented: $viewModel.navigateToAddProgramView, onDismiss: nil) {
Text("Add Program View")
}
.sheet(isPresented: $viewModel.navigateToAddExerciseView, onDismiss: nil) {
AddEditExerciseView(viewModel: AddEditExerciseViewModel())
}
}
.navigationViewStyle(.stack)
}

Why is ScollView content size not adjusting properly for this SwiftUI view?

I am using Xcode beta7 and the following is the code.
This is for a MacOs app.
here is my code:
import SwiftUI
struct ContentView: View {
#State var isClicked = false
var body: some View {
ScrollView {
Text("Click me").onTapGesture {
self.isClicked.toggle()
}.border(Color.red)
V1(isClicked: $isClicked)
}
}
}
struct V1: View {
#Binding var isClicked: Bool
var body: some View {
VStack(alignment: .leading) {
if isClicked {
ForEach(0...100, id: \.self) { index in
Text("value \(index)")
}
}
}.padding()
}
}
Run this code and click on the Click me button.
You will see that the scrollView's contnent size does not update and stay's squished.
If i try to resize the frame of the application by using the mouse to resize the screen, then instantly, the ScrollView's content size snaps to the correct size.
Do i need to do something to get the ScrollView to do this automatically (instead of me having to manually inscrease the frame of the app with the mouse?
I got the same issue recently on iOS, watchOS. The only way, I solve it was to move the content of the scrollView as a func in your struct having the ScrollView. As shown below.
struct ContentView: View {
#State var isClicked = false
var body: some View {
ScrollView {
Text("Click me").onTapGesture {
self.isClicked.toggle()
}.border(Color.red)
otherView()
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
func otherView() -> some View{
return VStack(alignment: .leading) {
if isClicked {
ForEach(0...100, id: \.self) { index in
Text("value \(index)")
}
}
}.padding()
}
}
It looks like a bug.

SwiftUI: Global Overlay That Can Be Triggered From Any View

I'm quite new to the SwiftUI framework and I haven't wrapped my head around all of it yet so please bear with me.
Is there a way to trigger an "overlay view" from inside "another view" when its binding changes? See illustration below:
I figure this "overlay view" would wrap all my views. I'm not sure how to do this yet - maybe using ZIndex. I also guess I'd need some sort of callback when the binding changes, but I'm also not sure how to do that either.
This is what I've got so far:
ContentView
struct ContentView : View {
#State private var liked: Bool = false
var body: some View {
VStack {
LikeButton(liked: $liked)
}
}
}
LikeButton
struct LikeButton : View {
#Binding var liked: Bool
var body: some View {
Button(action: { self.toggleLiked() }) {
Image(systemName: liked ? "heart" : "heart.fill")
}
}
private func toggleLiked() {
self.liked = !self.liked
// NEED SOME SORT OF TOAST CALLBACK HERE
}
}
I feel like I need some sort of callback inside my LikeButton, but I'm not sure how this all works in Swift.
Any help with this would be appreciated. Thanks in advance!
It's quite easy - and entertaining - to build a "toast" in SwiftUI!
Let's do it!
struct Toast<Presenting>: View where Presenting: View {
/// The binding that decides the appropriate drawing in the body.
#Binding var isShowing: Bool
/// The view that will be "presenting" this toast
let presenting: () -> Presenting
/// The text to show
let text: Text
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.presenting()
.blur(radius: self.isShowing ? 1 : 0)
VStack {
self.text
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.transition(.slide)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
Explanation of the body:
GeometryReader gives us the preferred size of the superview , thus allowing the perfect sizing for our Toast.
ZStack stacks views on top of each other.
The logic is trivial: if the toast is not supposed to be seen (isShowing == false), then we render the presenting view. If the toast has to be presented (isShowing == true), then we render the presenting view with a little bit of blur - because we can - and we create our toast next.
The toast is just a VStack with a Text, with custom frame sizing, some design bells and whistles (colors and corner radius), and a default slide transition.
I added this method on View to make the Toast creation easier:
extension View {
func toast(isShowing: Binding<Bool>, text: Text) -> some View {
Toast(isShowing: isShowing,
presenting: { self },
text: text)
}
}
And a little demo on how to use it:
struct ContentView: View {
#State var showToast: Bool = false
var body: some View {
NavigationView {
List(0..<100) { item in
Text("\(item)")
}
.navigationBarTitle(Text("A List"), displayMode: .large)
.navigationBarItems(trailing: Button(action: {
withAnimation {
self.showToast.toggle()
}
}){
Text("Toggle toast")
})
}
.toast(isShowing: $showToast, text: Text("Hello toast!"))
}
}
I used a NavigationView to make sure the view fills the entire screen, so the Toast is sized and positioned correctly.
The withAnimation block ensures the Toast transition is applied.
How it looks:
It's easy to extend the Toast with the power of SwiftUI DSL.
The Text property can easily become a #ViewBuilder closure to accomodate the most extravagant of the layouts.
To add it to your content view:
struct ContentView : View {
#State private var liked: Bool = false
var body: some View {
VStack {
LikeButton(liked: $liked)
}
// make it bigger by using "frame" or wrapping it in "NavigationView"
.toast(isShowing: $liked, text: Text("Hello toast!"))
}
}
How to hide the toast afte 2 seconds (as requested):
Append this code after .transition(.slide) in the toast VStack.
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
self.isShowing = false
}
}
}
Tested on Xcode 11.1
I modified Matteo Pacini's great answer, above, incorporating comments to have the Toast fade in and fade out after a delay. I also modified the View extension to be a bit more generic, and to accept a trailing closure similar to the way .sheet works.
ContentView.swift:
struct ContentView: View {
#State private var lightsOn: Bool = false
#State private var showToast: Bool = false
var body: some View {
VStack {
Button(action: {
if (!self.showToast) {
self.lightsOn.toggle()
withAnimation {
self.showToast = true
}
}
}){
Text("switch")
} //Button
.padding(.top)
Image(systemName: self.lightsOn ? "lightbulb" : "lightbulb.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(.all)
.toast(isPresented: self.$showToast) {
HStack {
Text("Lights: \(self.lightsOn ? "ON" : "OFF")")
Image(systemName: self.lightsOn ? "lightbulb" : "lightbulb.fill")
} //HStack
} //toast
} //VStack
} //body
} //ContentView
View+Toast.swift:
extension View {
func toast<Content>(isPresented: Binding<Bool>, content: #escaping () -> Content) -> some View where Content: View {
Toast(
isPresented: isPresented,
presenter: { self },
content: content
)
}
}
Toast.swift:
struct Toast<Presenting, Content>: View where Presenting: View, Content: View {
#Binding var isPresented: Bool
let presenter: () -> Presenting
let content: () -> Content
let delay: TimeInterval = 2
var body: some View {
if self.isPresented {
DispatchQueue.main.asyncAfter(deadline: .now() + self.delay) {
withAnimation {
self.isPresented = false
}
}
}
return GeometryReader { geometry in
ZStack(alignment: .bottom) {
self.presenter()
ZStack {
Capsule()
.fill(Color.gray)
self.content()
} //ZStack (inner)
.frame(width: geometry.size.width / 1.25, height: geometry.size.height / 10)
.opacity(self.isPresented ? 1 : 0)
} //ZStack (outer)
.padding(.bottom)
} //GeometryReader
} //body
} //Toast
With this you could toast Text, or an Image (or both, as shown below), or any other View.
here is the how to overlay on all of your views including NavigationView!
create a class model to store your views!
class ParentView:ObservableObject {
#Published var view:AnyView = AnyView(EmptyView())
}
create the model in your parrent view and call it in your view hierarchy
pass this class to your environment object of your parent view
struct Example: View {
#StateObject var parentView = ParentView()
var body: some View {
ZStack{
NavigationView{
ChildView()
.environmentObject(parentView)
.navigationTitle("dynamic parent view")
}
parentView.view
}
}
}
from now on you can call parentview in your child view by
#EnvironmentObject var parentView:ParentView
then for example in your tap gesture, you can change the parent view and show a pop up that covers everything including your navigationviews
#StateObject var parentView = ParentView()
here is the full solution copy and play with it in your preview!
import SwiftUI
class ParentView:ObservableObject {
#Published var view:AnyView = AnyView(EmptyView())
}
struct example: View {
#StateObject var parentView = ParentView()
var body: some View {
ZStack{
NavigationView{
ChildView()
.environmentObject(parentView)
.navigationTitle("dynamic parent view")
}
parentView.view
}
}
}
struct ChildView: View {
#EnvironmentObject var parentView:ParentView
var body: some View {
ZStack{
Text("hello")
.onTapGesture {
parentView.view = AnyView(Color.red.opacity(0.4).ignoresSafeArea())
}
}
}
}
struct example_Previews: PreviewProvider {
static var previews: some View {
example()
}
}
also you can improve this dramatically like this...!
struct ParentViewModifire:ViewModifier {
#EnvironmentObject var parentView:ParentView
#Binding var presented:Bool
let anyView:AnyView
func body(content: Content) -> some View {
content
.onChange(of: presented, perform: { value in
if value {
parentView.view = anyView
}
})
}
}
extension View {
func overlayAll<Overlay>(_ overlay: Overlay, presented: Binding<Bool>) -> some View where Overlay : View {
self
.modifier(ParentViewModifire(presented: presented, anyView: AnyView(overlay)))
}
}
now in your child view you can call this modifier on your view
struct ChildView: View {
#State var newItemPopUp:Bool = false
var body: some View {
ZStack{
Text("hello")
.overlayAll(newCardPopup, presented: $newItemPopUp)
}
}
}
App-wide View
If you want it to be app-wide, put in somewhere app-wide! For example, you can add it to the MyProjectApp.swift (or in sceneDelegate for UIKit/AppDelegate projects) file like this:
Note that the button and the State are just for more explanation and you may consider changing them in the way you like
#main
struct SwiftUIAppPlaygroundApp: App { // <- Note that where we are!
#State var showToast = false
var body: some Scene {
WindowGroup {
Button("App-Wide Button") { showToast.toggle() }
ZStack {
ContentView() // <- The app flow
if showToast {
MyCustomToastView().ignoresSafeArea(.all, edges: .all) // <- App-wide overlays
}
}
}
}
}
See? now you can add any sort of view on anywhere of the screen, without blocking animations. Just convert that #State to some sort of AppState like Observables or Environments and boom! 💥 you did it!
Note that it is a demo, you should use an environment variable or smt to be able for changing it from outside of this view's body
Apple does not currently provide any APIs that allow you to make global views similar to their own alert pop-ups.
In fact these views are actually still using UIKit under the hood.
If you want your own global pop-ups you can sort of hack your own (note this isn't tested, but something very similar should work for global presentation of toasts):
import SwiftUI
import Foundation
/// Global class that will manage toasts
class ToastPresenter: ObservableObject {
// This static property probably isn't even needed as you can inject via #EnvironmentObject
static let shared: ToastPresenter = ToastPresenter()
private init() {}
#Published private(set) var isPresented: Bool = false
private(set) var text: String?
private var timer: Timer?
/// Call this function to present toasts
func presentToast(text: String, duration: TimeInterval = 5) {
// reset the toast if one is currently being presented.
isPresented = false
self.text = nil
timer?.invalidate()
self.text = text
isPresented = true
timer = Timer(timeInterval: duration, repeats: false) { [weak self] _ in
self?.isPresented = false
}
}
}
/// The UI for a toast
struct Toast: View {
var text: String
var body: some View {
Text(text)
.padding()
.background(Capsule().fill(Color.gray))
.shadow(radius: 6)
.transition(AnyTransition.opacity.animation(.default))
}
}
extension View {
/// ViewModifier that will present a toast when its binding changes
#ViewBuilder func toast(presented: Binding<Bool>, text: String) -> some View {
ZStack {
self
if presented.wrappedValue {
Toast(text: text)
}
}
.ignoresSafeArea(.all, edges: .all)
}
}
/// The first view in your app's view hierarchy
struct RootView: View {
#StateObject var toastPresenter = ToastPresenter.shared
var body: some View {
MyAppMainView()
.toast(presented: $toastPresenter.isPresented, text: toastPresenter.text)
// Inject the toast presenter into the view hierarchy
.environmentObject(toastPresenter)
}
}
/// Some view later on in the app
struct SomeViewDeepInTheHierarchy: View {
#EnvironmentObject var toastPresenter: ToastPresenter
var body: some View {
Button {
toastPresenter.presentToast(text: "Hello World")
} label: {
Text("Show Toast")
}
}
}
Use .presentation() to show an alert when the button is tapped.
In LikeButton:
#Binding var liked: Bool
var body: some View {
Button(action: {self.liked = !self.liked}, label: {
Image(systemName: liked ? "heart.fill" : "heart")
}).presentation($liked) { () -> Alert in
Alert.init(title: Text("Thanks for liking!"))
}
}
You can also use .presentation() to present other Modal views, like a Popover or ActionSheet. See here and the "See Also" section on that page in Apple's SwiftUI documentation for info on the different .presentation() options.
Edit: Example of what you want with a custom view using Popover:
#State var liked = false
let popover = Popover(content: Text("Thanks for liking!").frame(width: 200, height: 100).background(Color.white), dismissHandler: {})
var body: some View {
Button(action: {self.liked = !self.liked}, label: {
Image(systemName: liked ? "heart.fill" : "heart")
}).presentation(liked ? popover : nil)
}
I am using this open source: https://github.com/huynguyencong/ToastSwiftUI . It is very simple to use.
struct ContentView: View {
#State private var isShowingToast = false
var body: some View {
VStack(spacing: 20) {
Button("Show toast") {
self.isShowingToast = true
}
Spacer()
}
.padding()
// Just add a modifier to show a toast, with binding variable to control
.toast(isPresenting: $isShowingToast, dismissType: .after(3)) {
ToastView(message: "Hello world!", icon: .info)
}
}
}