Tapping on a View to also change its sibling views - swift

Within a VStack, I have 3 views. A view's selection and colour are toggled when tapping on them. I want the previously selected View to be deselected when selecting the next view.
The tapGesture is implemented in each view. I am not sure what is the best way to achieve this.
Thanks.
Here is the code sample:
struct ContentView: View {
#State var tile1 = Tile()
#State var tile2 = Tile()
#State var tile3 = Tile()
var body: some View {
VStack {
TileView(tile: tile1 )
TileView(tile: tile2 )
TileView(tile:tile3 )
}
.padding()
}
}
struct Tile: Identifiable, Equatable{
var id:UUID = UUID()
var isSelected:Bool = false
}
struct TileView: View {
#State var tile:Tile
var body: some View {
RoundedRectangle(cornerRadius: 15)
.fill( tile.isSelected ? Color.red : Color.yellow )
.frame(height: 100)
.padding()
.onTapGesture {
tile.isSelected.toggle()
}
}
}

You need to relate the 3 tiles somehow. An Array is an option. Then once they are related you can change the selection at that level.
extension Array where Element == Tile{
///Marks the passed `tile` as selected and deselects other tiles.
mutating func select(_ tile: Tile) {
for (idx, t) in self.enumerated(){
if t.id == tile.id{
self[idx].isSelected.toggle()
}else{
self[idx].isSelected = false
}
}
}
}
Then you can change your views to use the new function.
struct MyTileListView: View {
#State var tiles: [Tile] = [Tile(), Tile(), Tile()]
var body: some View {
VStack {
ForEach(tiles) { tile in
TileView(tile: tile, onSelect: {
//Use the array to select the tile
tiles.select(tile)
})
}
}
.padding()
}
}
struct TileView: View {
//#State just create a copy of the tile `#Binding` is a two-way connection if needed
let tile:Tile
///Called when the tile is selected
let onSelect: () -> Void
var body: some View {
RoundedRectangle(cornerRadius: 15)
.fill(tile.isSelected ? Color.red : Color.yellow)
.frame(height: 100)
.padding()
.onTapGesture {
onSelect()
}
}
}

Related

Binding a button leads to "Missing argument for parameter in call" error

I'm trying to create a Binding in two views so I can change something on one side and have it reflected on the other.
I basically have:
a circle on both views
a button to change the other view's circle color
and one to go to the other view
It all works fine if I only have a Binding in the "ColorChange2"
view, but when I add a Binding in "ColorChange1" I get into trouble.
It tells me: Missing argument for parameter 'isOn2'.
But when I add isOn2 into ColorChange1() it wants a binding, but if I do ColorChange1(isOn2: $isOn2) it says it can't find '$isOn2' in scope.
I found one solution suggesting to add .constant(true)) into the preview but since it's a constant, it wont change the view like I wanted since it's a constant.
What can I do to make it work?
Code:
struct ColorChange1: View {
#State private var isOn = false
#Binding var isOn2 : Bool
var body: some View {
NavigationView {
VStack {
Circle()
.fill(isOn ? .green : .red)
.frame(width: 100)
Button(action: {
isOn2.toggle()
}, label: {
Text("Change button view 2")
.padding()
})
NavigationLink(destination: {
ColorChange2(isOn: $isOn)
}, label: {
Text("Go to view 2")
})
}
}
}
}
struct ColorChange2: View {
#Binding var isOn : Bool
#State private var isOn2 = false
#Environment(\.dismiss) var dismiss
var body: some View {
VStack {
Circle()
.fill(isOn2 ? .green : .red)
.frame(width: 100)
Button(action: {
isOn.toggle()
}, label: {
Text("Change button view 1")
.padding()
})
Button(action: {
dismiss.callAsFunction()
}, label: {
Text("Go to view 1")
})
}
.navigationBarBackButtonHidden(true)
}
}
struct ColorChange_Previews: PreviewProvider {
static var previews: some View {
// ColorChange(isOn2: .constant(true))
ColorChange1()
}
} ```
You don't need both #Binding value in both screen to connect between screen like that.
#Binding means that get the value in #State of the first view and make a connection in the second view. In this scenero, when you go back from second view, it was dismissed.
For your problem, make an ObservableObject to store value. 1 for present in first view and 1 for second view. Then add it to second view when ever you need to display.
Code will be like this
class ColorModel : ObservableObject {
#Published var isOnFirstView = false
#Published var isOnSecondView = false
func didTapChangeColor(atFirstView: Bool) {
if atFirstView {
isOnSecondView = !isOnSecondView
} else {
isOnFirstView = !isOnFirstView
}
}
}
struct ColorChange2: View {
// binding model
#ObservedObject var colorModel : ColorModel
#Environment(\.dismiss) var dismiss
var body: some View {
VStack {
Circle()
.fill(colorModel.isOnSecondView ? .green : .red)
.frame(width: 100)
Button(action: {
colorModel.didTapChangeColor(atFirstView: false)
}, label: {
Text("Change button view 1")
.padding()
})
Button(action: {
dismiss.callAsFunction()
}, label: {
Text("Go to view 1")
})
}
.navigationBarBackButtonHidden(true)
}
}
struct ColorChange1: View {
#StateObject private var colorModel = ColorModel()
var body: some View {
NavigationView {
VStack {
Circle()
.fill(colorModel.isOnFirstView ? .green : .red)
.frame(width: 100)
Button(action: {
colorModel.didTapChangeColor(atFirstView: true)
}, label: {
Text("Change button view 2")
.padding()
})
NavigationLink(destination: {
ColorChange2(colorModel: colorModel)
}, label: {
Text("Go to view 2")
})
}
}
}
}
struct ColorChange_Previews: PreviewProvider {
static var previews: some View {
ColorChange1()
}
}

Pass view as parameter to Button triggering it as a Modal

I'd like to have a custom button struct that receives a view as a parameter that will be shown as modal when the button is clicked. However, the view parameter is always empty, and I can't find the mistake I'm doing. My button struct looks like that:
struct InfoButton<Content:View>: View {
#State private var showingInfoPage: Bool
private var infoPage: Content
init(infoPage: Content, showingInfoPage: Bool) {
self.infoPage = infoPage
_showingInfoPage = State(initialValue: showingInfoPage)
}
var body: some View {
return
Button(action: {
self.showingInfoPage.toggle()
}) {
Image(systemName: "info.circle")
.resizable()
.frame(width: 25, height: 25)
.foregroundColor(.white)
.padding()
}.sheet(isPresented: self.$showingInfoPage) {
self.infoPage
}.frame(minWidth: 0, maxWidth: .infinity, alignment: .topTrailing)
}
}
This button is placed in a navigation bar from a template I'm creating for multiple other views.
I think the most relevant parts of that template are these:
protocol TrainingView {
var title: String { get }
var subheadline: String { get }
var asAnyView: AnyView { get }
var hasInfoPage: Bool { get }
var infoPage: AnyView { get }
}
extension TrainingView where Self: View {
var asAnyView: AnyView {
AnyView(self)
}
var hasInfoPage: Bool {
false
}
var infoPage: AnyView {
AnyView(EmptyView())
}
}
struct TrainingViewTemplate: View {
#State var showInfoPage: Bool = false
#State var viewIndex: Int = 0
var body: some View {
//the views that conform to the template
let views: [TrainingView] = [
ExerciseView(),
TrainingSessionSummaryView()
]
return NavigationView {
ViewIterator(views, self.$viewIndex) { exerciseView in
VStack {
VStack {
Text(exerciseView.title)
.font(.title)
.fontWeight(.semibold).zIndex(1)
Text(exerciseView.subheadline)
.font(.subheadline)
Spacer()
exerciseView.asAnyView.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}.navigationBarItems(trailing: (exerciseView.hasInfoPage == true ? InfoButton(infoPage: exerciseView.infoPage, showingInfoPage: self.showInfoPage) : nil))
}
}
}
I debugged to the point, where the navigationBarItems are initialized. At that point, the exercise view has content for "hasInfoPage" and "infoPage" itself.
One exemplary Exercise View has a header like that:
struct ExerciseView: View, TrainingView {
var title: String = "Strength Session"
var subheadline: String = "Pushups"
var numberOfExercise: Int = 1
#State var ratingValue: Double = 0
#Environment(\.presentationMode) var presentationMode
var hasInfoPage: Bool = true
var infoPage = ExerciseDetailView()
So in this view, the infoPage gets initialized with the ExercieDetailView() which I receive in the TemplateView, but as soon as the InfoButton is clicked, the debugger shows an empty infoPage, even though the "showingInfoPage" variable contains the right value.
You don't confirm to protocol, so default infoPage from extension TrainingView is shown.
The solution is
struct ExerciseView: View, TrainingView {
// .. other code here
var infoPage = AnyView(ExerciseDetailView()) // << here !!
``

SwiftUI List exposed white background in landscape orientation

I have a viewController that allows the user to select their choice of notification sounds. The list is presented by the SwiftUI with the name of each sound and a preview button. All is fine in portrait mode:
But in landscape the edges of the screen show white:
Here's my SwiftUI code:
import SwiftUI
struct SoundItem: Hashable {
var name: String
var pdSelection: Bool = false
var pESelection: Bool = false
var advanceAlertSelection: Bool = false
}
struct SoundListItem: View {
var item: SoundItem
var delegate: NotificationsSoundControllerViewController?
var body: some View {
HStack {
Image(item.pdSelection ? "post-dose-select" : "post-dose-unselect")
.onTapGesture {
if let d = self.delegate {
d.selectPDSound(name: self.item.name) // defined in the protocol of the preferences view controller for updating the user prefs.
}
}
Spacer()
Text(item.name.localizedCapitalized).font(Font.custom("Exo2-SemiBold", size: 20.0)).foregroundColor(item.advanceAlertSelection ? Color.green : Color.white)
.onTapGesture {
if let d = self.delegate {
d.selectAdvanceSound(name: self.item.name) // defined in the protocol of the preferences view controller for updating the user prefs.
}
}
Spacer()
Image(systemName: "play.fill")
.foregroundColor(Color.white)
.onTapGesture {
if let d = self.delegate {
d.playPreviewSound(name: self.item.name)
}
}.padding(.trailing, 40.0)
Image(item.preExpirySelection ? "pre-expiry-select" : "pre-expiry-unselect")
.onTapGesture {
if let d = self.delegate {
d.selectPESound(name: self.item.name) // defined in the protocol of the preferences view controller for updating the user prefs.
}
}
}
}
}
struct ListHeading: View {
let headingFont: Font = Font.custom("Exo2-SemiBold", size: 12.0)
var body: some View {
HStack{
Text("Post").font(headingFont).foregroundColor(Color.gray)
Spacer()
Text("Click name for advance alert").font(headingFont).foregroundColor(Color.gray)
Spacer()
Spacer()
Text("Pre").font(headingFont).foregroundColor(Color.gray)
}
}
}
struct NotificationSoundsSUIView: View {
#ObservedObject var noteController: NotificationsSoundControllerViewController
init(){
UITableView.appearance().backgroundColor = .black
noteController = NotificationsSoundControllerViewController()
}
var body: some View {
Section(header: ListHeading()) {
List {
ForEach (self.noteController.sounds, id: \.self) { sound in
SoundListItem(item: sound, delegate: self.noteController).listRowBackground(Color.black)
}.background(Color.black)
}.listRowBackground(Color.black)
}
}
}
struct NotificationSoundsSUIView_Previews: PreviewProvider {
static var previews: some View {
NotificationSoundsSUIView()
}
}
And it's presented in the viewController with this:
var listView = NotificationSoundsSUIView() // See above code
listView.noteController = self //pass over data for display by referencing this viewController
let childView = UIHostingController(rootView: listView)
childView.view.backgroundColor = UIColor.black
childView.view.frame = view.frame
view.addSubview(childView.view)
view.sendSubviewToBack(childView.view)
view.backgroundColor = UIColor.black
childView.view.translatesAutoresizingMaskIntoConstraints = false
How can I force the entire background of the screen to remain black?
Assuming your constraints in UIViewController set correctly here is to be done in SwiftUI part
Section(header: ListHeading()) {
List {
ForEach (self.noteController.sounds, id: \.self) { sound in
SoundListItem(item: sound, delegate: self.noteController).listRowBackground(Color.black)
}.background(Color.black)
}.listRowBackground(Color.black)
}.edgesIgnoringSafeArea([.leading, .trailing]) // << here !!
I stumbled on an answer that seems to solve the problem and also maintain the safe area clearence of the list. I changed the SwiftUI view's body to:
var body: some View {
HStack {
Spacer() // < Inserted a spacer
VStack { // < Using a HStack causes the list headings to appear to the left of the list itself
Section(header: ListHeading()) {
List {
ForEach (self.noteController.sounds, id: \.self) { sound in
SoundListItem(item: sound, delegate: self.noteController).listRowBackground(Color.black)
}.background(Color.black)
}.listRowBackground(Color.black)
}
}
Spacer() // < Inserted a spacer
}
}
}

How can I stop all my SWIFTUI buttons toggling together?

I want to create a grid of square buttons that I can click/tap to toggle between black and white.
As a half-way point I am creating a row of buttons that do this - see code below.
But when I click on one of them all the buttons toggle together.
I can't see why this is because I have a state variable for each Cell?
struct ContentView: View {
var body: some View {
ZStack {
Color.green
.edgesIgnoringSafeArea(.all)
RowOfCellsView(string: "X.X.X.X")
}
}
}
struct Cell: Identifiable {
var id: Int
var state: Bool
}
struct RowOfCellsView: View {
var string: String
var cells: [Cell] {
string.map { Cell(id: 1, state: $0 == ".") }
}
var body: some View {
HStack {
ForEach(cells) { cell in
CellView(isBlack: cell.state, symbol: "Q")
}
}
}
}
struct CellView: View {
#State var isBlack: Bool
#State var symbol: String
var body: some View {
Button(action: { self.isBlack.toggle() }) {
Text("")
.font(.largeTitle)
.frame(width: 40, height: 40)
.aspectRatio(1, contentMode: .fill)
}
.background(isBlack ? Color.black : Color.white)
}
}
Aha - just noticed that I have the id set to 1 for all Cells - that's the problem!
So needs to have a way to have a different id for each Cell.
For example could do:
string.enumerated().map { Cell(id: $0.0, state: $0.1 == ".") }

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