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.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.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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With