Global view which blurry overlays other views and can be easily called - swift

A similar question is asked here: SwiftUI: Global Overlay That Can Be Triggered From Any View. The examples however are shown a Toast, rather than a 'blocking' view. The accepted answer changes the presenting view (the toolbar is moving up after the toast is shown). Something is wrong with the code, but I don't know what (I just started learning SwiftUI). The code below is a combined effort from some other answers.
Alright, ideally I want to call a function on e.g. an EnvironmentObject which will automatically present an overlaying View with a ProgressView, to show that something is being loaded. The user should not be able to interact with the application while it is loading. The loading screen should be a bit blurry, overlapping with the presenting view.
Below shows what I have, the Text is never shown, but my breakpoint is hit when I click on the Load button. Any ideas why the LoadingView is never shown?
import SwiftUI
import UIKit
#main
struct TextLeadingApp: App {
var body: some Scene {
WindowGroup {
ZStack {
LoadingView() // This view should always be hidden, unless it is loading
ContentView()
}
.environmentObject(Load())
}
}
}
class Load: ObservableObject {
#Published var loader = 0
func load() {
loader += 1
}
}
struct LoadingView: View {
#EnvironmentObject var load: Load
var body: some View {
if load.loader > 0 {
GeometryReader { geometry in
Text("LOADING")
.frame(
width: geometry.size.width,
height: geometry.size.height
)
.ignoresSafeArea(.all, edges: .all)
}
} else {
EmptyView().frame(width: 0, height: 0)
}
}
}
struct ContentView: View {
#EnvironmentObject var load: Load
var body: some View {
NavigationView {
Text("hi")
.toolbar {
Button("Load") {
load.load()
}
}
.navigationBarTitle(Text("A List"), displayMode: .large)
}
.navigationViewStyle(.stack)
}
}

There are a couple of things that could be adjusted with your code.
I'd make Load a #StateObject on a parent view so that the LoadingView can be conditionally displayed and not displayed all the time and just default to an EmptyView
In a ZStack, the topmost view should be last -- you have it first.
You can use .background(.ultraThinMaterial)
#main
struct TextLeadingApp: App {
var body: some Scene {
WindowGroup {
ParentView()
}
}
}
struct ParentView : View {
#StateObject private var load = Load()
var body: some View {
ZStack {
ContentView()
if load.loader > 0 {
LoadingView()
}
}.environmentObject(load)
}
}
class Load: ObservableObject {
#Published var loader = 0
func load() {
loader += 1
}
}
struct LoadingView: View {
#EnvironmentObject var load: Load
var body: some View {
VStack {
Text("LOADING")
}
.ignoresSafeArea()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.ultraThinMaterial)
}
}
struct ContentView: View {
#EnvironmentObject var load: Load
var body: some View {
NavigationView {
Text("hi")
.onTapGesture {
print("tapped")
}
.font(.system(size: 100, weight: .bold, design: .default))
.foregroundColor(.orange)
.toolbar {
Button("Load") {
load.load()
}
}
.navigationBarTitle(Text("A List"), displayMode: .large)
}
.navigationViewStyle(.stack)
}
}
I've adjusted ContentView a bit, just to make the blur effect more obvious.
Update, with OP's request that only LoadingView responds to a change in the state, and not the parent view:
#main
struct TextLeadingApp: App {
var body: some Scene {
WindowGroup {
ZStack {
ContentView()
LoadingView()
}
.environmentObject(Load())
}
}
}
class Load: ObservableObject {
#Published var loader = 0
func load() {
loader += 1
}
}
struct LoadingView: View {
#EnvironmentObject var load: Load
var body: some View {
if load.loader > 0 {
VStack {
Text("LOADING")
}
.ignoresSafeArea()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.ultraThinMaterial)
} else {
EmptyView()
}
}
}

Related

Navigation stack yellow warning triangle

I'm attempting to listen for a change in a boolean value & changing the view once it has been heard which it does successfully, however, results in a yellow triangle. I haven't managed to pinpoint the issue but it doesn't seem to have anything to do with the view that it's transitioning to as even when changed the error still persists.
My code is below
import SwiftUI
struct ConversationsView: View {
#State var isShowingNewMessageView = false
#State var showChat = false
#State var root = [Root]()
var body: some View {
NavigationStack(path: $root) {
ZStack(alignment: .bottomTrailing) {
ScrollView {
LazyVStack {
ForEach(0..<20) { _ in
Text("Test")
}
}
}.padding()
}
Button {
self.isShowingNewMessageView.toggle()
} label: {
Image(systemName: "plus.message.fill")
.resizable()
.renderingMode(.template)
.frame(width: 48, height: 48)
.padding()
.foregroundColor(Color.blue)
.sheet(isPresented: $isShowingNewMessageView, content: {
NewMessageView(show: $isShowingNewMessageView, startChat: $showChat)
})
}
}
.onChange(of: showChat) { newValue in
guard newValue else {return}
root.append(.profile)
}.navigationDestination(for: Root.self) { navigation in
switch navigation {
case .profile:
ChatView()
}
}
}
enum Root {
case profile
}
}
ChatView() Code:
import SwiftUI
struct ChatView: View {
#State var messageText: String = ""
var body: some View {
VStack {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
ForEach(MOCK_MESSAGES) { message in
MessageView(message: message)
}
}
}.padding(.top)
MessageInputView(messageText: $messageText)
.padding()
}
}
}
Any support is much appreciated.
You should use navigationDestination modifier inside your NavigationStack component, just move it.
NavigationStack(path: $root) {
ZStack(alignment: .bottomTrailing) {
ScrollView {
LazyVStack {
ForEach(0..<20) { _ in
Text("Test")
}
}
}.padding()
}.navigationDestination(for: Root.self) { navigation in
switch navigation {
case .profile:
ChatView()
}
}
//...
}
Basically this yellow triangle means NavigationStack can't find suitable component for path. And when you using navigationDestination directly on NavigationStack View or somewhere outside it is ignored
You must set .environmentObject(root) to NavigationStack in order to provide the NavigationPath to the view subhierarchy (ChatView in your case). Also you must have a #EnvironmentObject property of type Root in your ChatView so that it can read the path.

Swiftui navigationLink macOS default/selected state

I build a macOS app in swiftui
i try to create a listview where the first item is preselected. i tried it with the 'selected' state of the navigationLink but it didn't work.
Im pretty much clueless and hope you guys can help me.
The code for creating this list view looks like this.
//personList
struct PersonList: View {
var body: some View {
NavigationView
{
List(personData) { person in
NavigationLink(destination: PersonDetail(person: person))
{
PersonRow(person: person)
}
}.frame(minWidth: 300, maxWidth: 300)
}
}
}
(Other views at the bottom)
This is the normal View when i open the app.
When i click on an item its open like this. Thats the state i want as default opening state when i render this view.
The Code for this view looks like this:
//PersonRow
struct PersonRow: View {
//variables definied
var person: Person
var body: some View {
HStack
{
person.image.resizable().frame(width:50, height:50)
.cornerRadius(25)
.padding(5)
VStack (alignment: .leading)
{
Text(person.firstName + " " + person.lastName)
.fontWeight(.bold)
.padding(5)
Text(person.nickname)
.padding(5)
}
Spacer()
}
}
}
//personDetail
struct PersonDetail: View {
var person : Person
var body: some View {
VStack
{
HStack
{
VStack
{
CircleImage(image: person.image)
Text(person.firstName + " " + person.lastName)
.font(.title)
Text("Turtle Rock")
.font(.subheadline)
}
Spacer()
Text("Subtitle")
.font(.subheadline)
}
Spacer()
}
.padding()
}
}
Thanks in advance!
working example. See how selection is initialized
import SwiftUI
struct Detail: View {
let i: Int
var body: some View {
Text("\(self.i)").font(.system(size: 150)).frame(maxWidth: .infinity)
}
}
struct ContentView: View {
#State var selection: Int?
var body: some View {
HStack(alignment: .top) {
NavigationView {
List {
ForEach(0 ..< 10) { (i) in
NavigationLink(destination: Detail(i: i), tag: i, selection: self.$selection) {
VStack {
Text("Row \(i)")
Divider()
}
}
}.onAppear {
if self.selection != nil {
self.selection = 0
}
}
}.frame(width: 100)
}
}.background(Color.init(NSColor.controlBackgroundColor))
}
}
screenshot
You can define a binding to the selected row and used a List reading this selection. You then initialise the selection to the first person in your person array.
Note that on macOS you do not use NavigationLink, instead you conditionally show the detail view with an if statement inside your NavigationView.
If person is not Identifiable you should add an id: \.self in the loop. This ressembles to:
struct PersonList: View {
#Binding var selectedPerson: Person?
var body: some View {
List(persons, id: \.self, selection: $selectedPerson) { person in // persons is an array of persons
PersonRow(person: person).tag(person)
}
}
}
Then in your main window:
struct ContentView: View {
// First cell will be highlighted and selected
#State private var selectedPerson: Person? = person[0]
var body: some View {
NavigationView {
PersonList(selectedPerson: $selectedPerson)
if selectedPerson != nil {
PersonDetail(person: person!)
}
}
}
}
Your struct person should be Hashable in order to be tagged in the list. If your type is simple enough, adding Hashable conformance should be sufficient:
struct Person: Hashable {
var name: String
// ...
}
There is a nice tutorial using the same principle here if you want a more complete example.
Thanks to this discussion, as a MacOS Beginner, I managed a very basic NavigationView with a list containing two NavigationLinks to choose between two views. I made it very basic to better understand. It might help other beginners.
At start up it will be the first view that will be displayed.
Just modify in ContentView.swift, self.selection = 0 by self.selection = 1 to start with the second view.
FirstView.swift
import SwiftUI
struct FirstView: View {
var body: some View {
Text("(1) Hello, I am the first view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
SecondView.swift
import SwiftUI
struct SecondView: View {
var body: some View {
Text("(2) Hello, I am the second View")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView()
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var selection: Int?
var body: some View {
HStack() {
NavigationView {
List () {
NavigationLink(destination: FirstView(), tag: 0, selection: self.$selection) {
Text("Click Me To Display The First View")
} // End Navigation Link
NavigationLink(destination: SecondView(), tag: 1, selection: self.$selection) {
Text("Click Me To Display The Second View")
} // End Navigation Link
} // End list
.frame(minWidth: 350, maxWidth: 350)
.onAppear {
self.selection = 0
}
} // End NavigationView
.listStyle(SidebarListStyle())
.frame(maxWidth: .infinity, maxHeight: .infinity)
} // End HStack
} // End some View
} // End ContentView
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Result:
import SwiftUI
struct User: Identifiable {
let id: Int
let name: String
}
struct ContentView: View {
#State private var users: [User] = (1...10).map { User(id: $0, name: "user \($0)")}
#State private var selection: User.ID?
var body: some View {
NavigationView {
List(users) { user in
NavigationLink(tag: user.id, selection: $selection) {
Text("\(user.name)'s DetailView")
} label: {
Text(user.name)
}
}
Text("Select one")
}
.onAppear {
if let selection = users.first?.ID {
self.selection = selection
}
}
}
}
You can use make the default selection using onAppear (see above).

SwiftUI Conditional View Transitions are not working

Consider the following code:
class ApplicationHostingView: ObservableObject {
#Published var value: Bool
}
struct ApplicationHostingView: View {
// view model env obj
var body: some View {
Group {
if applicationHostingViewModel.value {
LoginView()
.transition(.move(edge: .leading)) // <<<< Transition for Login View
} else {
IntroView()
}
}
}
}
struct IntroView: View {
// view model env obj
var body: some View {
Button(action: { applicationHostingViewModel.value = true }) {
Text("Continue")
}
}
}
struct LoginView: View {
var body: some View {
Text("Hello World")
}
}
ISSUE
In this case, I see my transition from IntroView to LoginView work fine except for any of the animations. Animations inside IntroView based on the conditionals seem to be working fine but transitions that change the entire screen don't seem to work.
change group to ZStack
add animation somewhere.
class ApplicationHostingViewModel: ObservableObject {
#Published var value: Bool = false
}
struct ApplicationHostingView: View {
// view model env obj
#ObservedObject var applicationHostingViewModel : ApplicationHostingViewModel
var body: some View {
ZStack {
if applicationHostingViewModel.value {
LoginView()
.transition(.move(edge: .leading))
} else {
IntroView(applicationHostingViewModel:applicationHostingViewModel)
}
}
}
}
struct IntroView: View {
// view model env obj
#ObservedObject var applicationHostingViewModel : ApplicationHostingViewModel
var body: some View {
Button(action: {
withAnimation(.default){
self.applicationHostingViewModel.value = true} }) {
Text("Continue")
}
}
}
struct LoginView: View {
var body: some View {
Text("Hello World").frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

SwiftUI conditional view will not animate/transition

I’m trying to get my views to animate/transition using .transition() on views. I use similar code from here and put .transition() to both conditional views.
struct Base: View {
#State private var isSignedIn = false
var body: some View {
Group {
if(isSignedIn){
Home().transition(.slide)
}else{
AuthSignin(isSignedIn: self.$isSignedIn).transition(.slide)
}
}
}
}
struct AuthSignin: View {
#Binding var isSignedIn: Bool
var body: some View {
VStack {
Button(action: {
self.isSignedIn = true
}) {
Text("Sign In")
.bold()
.frame(minWidth: CGFloat(0), maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(CGFloat(10))
}.padding()
}
}
}
However, whenever I click on the "Sign In" button (with or without .transition()), the app will freeze for a second and then the Home() view will suddenly appear without any animation/transition. I've also tried to wrap self.isSignedIn = true in withAnimation but it still won't work. Any ideas or is there a better way to do this?
Place your .transition on the container of the views that will switch, not each conditional view. Here's a trivial example from some code I have done (which works).
In the main View that needs to transition conditionally:
import SwiftUI
struct AppWrapperView: View {
#State var showFirstRun:Bool = true
var body: some View {
ZStack {
if (showFirstRun) {
FirstRunView(showFirstRun: $showFirstRun)
} else {
Text("Some other view")
}
}
.transition(.slide)
}
}
Then, somewhere in the view that triggers the change in condition:
import SwiftUI
struct FirstRunView: View {
#Binding var showFirstRun:Bool
var body: some View {
Button(action: {
withAnimation {
self.showFirstRun = false
}
}) {
Text("Done")
}
}
}
I had to put my if..else statement inside ZStack container instead of Group. Seems that Group was the main reason for broken animation in my case. Also, I applied .transition in combination with .animation to container instead of views.
ZStack {
if(isSignedIn){
Home()
} else {
AuthSignin(isSignedIn: self.$isSignedIn)
}
}
.transition(.slide)
.animation(.easeInOut)
Put
WithAnimation before self.isSignedIn = true

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