I have a .navigationBarItems button that I want to enable/disable depending on what my app is doing but I cannot for the life of me figure out how the heck to do this?!
My view is being pulled into my main app view via a NavigationView so I know the bar items are being shown correctly and whatnot. On my view that's being pulled in upon navigating to it I have this code at the bottom of the stack:
.navigationBarItems(
trailing:
Button("End Day") {
//do something here
scheduleEndDayNotificatons()
}
)
I'd like to be able to toggle the trailing "End Day" button to be enabled/disabled, probably based on some Bool state variable? Such as when the user enters into a mode of the app it's enabled, when they exit that mode it's disabled. My app is an exercise app, so when they're working out I'd want it to be enabled and when they finish working out it's disabled (which is the default).
Any help would be very much appreciated! Thank you!!
We can use modifier .disabled applied to Button and link it with some state variable, like
#State private var isDisabled = false
// ... other code
.navigationBarItems(
trailing:
Button("End Day") {
//do something here
scheduleEndDayNotificatons()
}
.disabled(isDisabled)
)
Related
I'm struggling with how to detect that a certain view is being displayed on the screen.
I have a notifications list in which new notification cells have blue backgrounds, and 2 seconds after you see the notification, the background changes to white, meaning you've seen that new notification, just like in Twitter's new notification screen.
First I thought about using the onAppear modifier to detect whether you've seen the new notification cell, but it gets triggered before the particular cell is being displayed on the screen.
So I thought up a workaround. When the notifications bottom tab is selected, i.e. when the user comes to the notifications view from another view, then 2 seconds later change the background of the new notification cell.
struct NotificationCell: View {
#Binding var tabSelection: TabBarItem
#Binding var notificationsDetail: NotificationsDetail
var body: some View {
HStack {
//Views inside
}
.background(notificationsDetail.notificationsIsAlreadyRead ? .white : .blue)
}
}
However, I don't know where to write the following code because init() or onAppear gets triggered only once at the beginning. I need to make sure I run the code when the tabSelection property changes.
if tabSelection == .notifications {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
notificationsDetail.notificationsIsAlreadyRead = true
}
}
I've also studied onChange modifier but since I'm not using Texts or TexFields I thought onChange might not fit as a solution here.
Maybe my workaround is not a good approach in the first place to achieving what I want to do.
Any advice would be highly appreciated.
Thanks in advance.
onChange is what would work here.
.onChange(of: tabSelection) { selection in
if selection == .notifications {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
notificationsDetail.notificationsIsAlreadyRead = true
}
}
}
In a SwiftUI app I have a few buttons (let us say 3 as an example). One of them is highlighted.
When I tap on a non-highlighted button, the previously highlighted button toggles to non-highlighted and the tapped button becomes highlighted. If I tap the already highlighted button, nothing happens.
This scenario is simple and I have a highlighBtn variable, when a button is tapped highlighBtn takes the value of the tapped button. This variable is then used when the next tap happens to toggle off the previously highlighted button.
This cycle is OK, but the problem is when I do the first tap. For some reasons, things don't work.
This is how I handle the creation of the highlighBtn variable:
class ActiveButton: ObservableObject {
#Published var highlighBtn = Button(....)
}
#StateObject var box = ActiveButton()
Here is the relevant code, when the button is tapped:
#EnvironmentObject var box: ActiveButton
....
Button(action: {
// Toggle off the previously highlighted button.
box.highlighBtn.highLight = false
.... some useful processing ....
box.highlighBtn = self
})
One detail I should give: if I tap the highlighted button to start, then all works as it should.
I have tried various method to solve this apparently simple problem but failed.
The first method was to try to initialize the highlighBtn variable.
The second was to try to simulate a tap on the highlighted button.
I must be missing something simple.
Any tip would be welcome.
After further investigation .....
I have created a demo app to expose my problem.
Since I lack practice using GitHub, it took me some time, but it is now available here.
For that I created a SwiftUI app in Xcode.
In SceneDelegate.swift, the four lines of code right after this one have been customized for the needs of this app:
// Create the SwiftUI view that provides the window contents.
Beside that all I did resides inside the file ContentView.swift.
To save some time to anyone who is going to take a look; here is the way to get to the point (i.e. the issue I am facing).
Run the app (I did it on an iPhone 7). You will see seven buttons appear. One of them (at random) will be highlighted. Starting with the highlighted button, tap on a few buttons one after another as many times as you want. You will see how the app is supposed to work.
(After switching it off) Run the app a second time. This time you will also tap on a few buttons one after another as many times as you want; but start by tapping one of the non-highlighted button. You will see the problem appear at this point.
Here is a solution for the first part of your question: Three buttons where the last one tapped gets highlighted with a background:
import SwiftUI
struct ContentView: View {
enum HighlightTag {
case none, first, second, third
}
#State private var highlighted = HighlightTag.none
var body: some View {
VStack {
Button("First") {
highlighted = .first
}.background(
Color.accentColor.opacity(
highlighted == .first ? 0.2 : 0.0
)
)
Button("Second") {
highlighted = .second
}.background(
Color.accentColor.opacity(
highlighted == .second ? 0.2 : 0.0
)
)
Button("Third") {
highlighted = .third
}.background(
Color.accentColor.opacity(
highlighted == .third ? 0.2 : 0.0
)
)
}
}
}
Update:
After reviewing your sample code on GitHub, I tried to understand your code, I tried to make some simplifications and tried to find a working solution.
Here are some opinions:
The Attribute "#State" in front of "var butnsPool" is not needed and confusing.
The Attribute "#State" in front of "var initialHilight" is not needed and confusing.
Your ActiveButton stores a copy of the selected Button View because it is a struct which is probably the main reason for the strange behaviour.
The needInit in your ObservableObject smells bad at least. If you really need to initialize something, you may consider doing it with some .onAppear() modifier in you ContentView.
There is probably no need to use .environmentObject and #EnvironmentObject. You could consider using a parameter and #ObsservedObject
There is probably no need for the ActiveButton at all, if you only use it internally. You could consider using a #State with the selected utton name
Your BtnTxtView is fine, but consider replacing the conditional (func if) with some animatable properties, if you want to animate the transition.
Based on your code I created a much simpler and working solution.
I removed the ActiveButton class and also the BttnView struct.
And I replaced the ContentView with this:
struct ContentView: View {
var butnsPool: [String]
var initialHilight: Int
#State var selectedBox: String = ""
var body: some View {
ForEach(butnsPool, id: \.self) { buttonName in
Button(action: {
selectedBox = buttonName
})
{
BtnTxtView(theLabel: buttonName,
highLight: buttonName == selectedBox)
}
}.onAppear {
selectedBox = butnsPool[initialHilight]
}
}
}
the Form view seems to create troubles using the newest SwiftUI (I'm new to it and haven't tested it in older version tbh).
The simple code executes the onTapGesture function correctly when clicking on the button, although it disables all the user feedback (so not visual change in the button on pressing, hold and release) and also does not execute the button action anymore:
var body: some View
{
Form
{
Button(action: {
print("button action")
})
{
Text("Button")
}
.onTapGesture
{
print("tap button")
}
}
.onTapGesture
{
print("tap form")
}
}
When exchanging Form with HStack the visual feedback and console prints work. So it definitely seems to be an issue with the Form view.
When commenting the onTapGesture Form function the button feedback works again. So only the onTapGesture on the Form blocks all child interaction.
Does anyone know what this is - and is there a workaround?
I want to build a very simple iOS 14 sidebar using SwiftUI.
The setup is quite simple, I have three views HomeView, LibraryView and SettingsView and an enum representing each screen.
enum Screen: Hashable {
case home, library, settings
}
My end-goal is to automatically switch between a tab view and a sidebar depending on the size class but some things don't quite work as expected.
The global state is owned by the MainNavigationView, which is also the root view for my WindowGroup.
struct MainNavigationView: View {
#State var screen: Screen? = .home
var body: some View {
NavigationView {
SidebarView(state: $screen)
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
The SidebarView is a simple List containing three NavigationLink, one for each Screen.
struct SidebarView: View {
#Binding var state: Screen?
var body: some View {
List {
NavigationLink(
destination: HomeView(),
tag: Screen.home,
selection: $state,
label: {
Label("Home", systemImage: "house" )
})
NavigationLink(
destination: LibraryView(),
tag: Screen.library,
selection: $state,
label: {
Label("Library", systemImage: "book")
})
NavigationLink(
destination: SettingsView(),
tag: Screen.settings,
selection: $state,
label: {
Label("Settings", systemImage: "gearshape")
})
}
.listStyle(SidebarListStyle())
.navigationTitle("Sidebar")
}
}
I use the NavigationLink(destination:tag:selection:label) initializer so that the selected screen is set in my MainNavigationView so I can reuse that for my TabView later.
However, a lot of things don't quite work as expected.
First, when launching the app in a portrait-mode iPad (I used the iPad Pro 11-inch simulator), no screen is selected when launching the app. Only after I click Back in the navigation bar, the initial screen shows and my home view gets shown.
The second weird thing is that the binding seems to be set to nil whenever the sidebar gets hidden. In landscape mode the view works as expected, however when toggling the sidebar to hide and then shown again, the selection gets lost.
The content view stays correct, but the sidebar selection is lost.
Are these just SwiftUI bugs or is there a different way to create a sidebar with a Binding?
You need to include a default secondary view within the NavigationView { }, usually it would be a placeholder but you could use the HomeScreen, e.g.
struct MainNavigationView: View {
#State var screen: Screen? = .home
var body: some View {
NavigationView {
SidebarView(state: $screen)
HomeScreen()
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
Regarding the cell not re-selecting - as of iOS 14.2 there is no list selection binding (when not in editing mode) so selection is lost. Although the List API has a $selection param, it is only supported on macOS at the moment. You can see that info the header:
/// On iOS and tvOS, you must explicitly put the list into edit mode for
/// the selection to apply.
It's a bit convoluted but it means that selection binding that we need for a sidebar is only for macOS, on iOS it is only for multi-select (i.e. checkmarks) in edit mode. The reason could be since UITableView's selection is event driven, maybe it wasn't possible to translate into SwiftUI's state driven nature. If you've ever tried to do state restoration with a view already pushed on a nav controller and try to show the cell unhighlight animation when popping back and that table view wasn't loaded and cell was never highlighted in the first place you'll know what I mean. It was a nightmare to load the table synchronously, make the selected cell be drawn and then start the unhighlight animation. I expect that Apple will be reimplementing List, Sidebar and NavigationView in pure SwiftUI to overcome these issues so for now we just have to live with it.
Once this has been fixed it will be as simple as List(selection:$screen) { } like how it would work on macOS. As a workaround on iOS you could highlight the icon or text in your own way instead, e.g. try using bold text:
NavigationLink(
destination: HomeView(),
tag: Screen.home,
selection: $state,
label: {
Label("Home", systemImage: "house" )
})
.font(Font.headline.weight(state == Screen.home ? .bold : .regular))
It doesn't look very nice when in compact because after popping the main view, the bold is removed when the row is un-highlighted. There might be a way to disable using bold in that case.
There are 2 other bugs you should be aware of:
In portrait the sidebar only shows on the second tap of the Sidebar nav button.
In portrait if you show the sidebar and select the same item that is already showing, the sidebar does not dismiss.
I want to push a view programmatically instead of relying on the interface that NavigationLink provides (e.g. I want to use a button with no chevron). The correct way is to use NavigationLink with tag and selection, and an EmptyView.
When I attempt to use the following code to push a view, it works to push the view the first time:
struct PushExample: View {
#State private var pushedView: Int? = nil
var body: some View {
NavigationView {
VStack {
Form {
Button(action: { self.pushedView = 1 }) { Text("Push view") }
}
NavigationLink(destination: Text("Detail view"), tag: 1, selection: $pushedView) { EmptyView() }
}
}
}
}
However, if I tap the back button on the view, and try hitting the button again, it no longer pushes the view. This is because the value pushedView is being set to 1 again, but it is already at 1. Nothing is resetting it back to nil upon pop of the Detail view.
How do I get subsequent taps of the button to push the view again?
TL;DR: There is no need to reset the state variable, as SwiftUI will automatically handle it for you. If it's not, it may be a bug with the simulator.
This was a simulator bug on Xcode 11.3!
The way to check if it's a simulator bug is to run an even simpler example:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink("Push", destination: Text("Detail"))
}
}
}
On the Xcode 11.3 iPhone 11 Pro Max, this would only work the first time you tap the link.
This worked fine on both a 13.2 and a 13.3 device.
Therefore, when running into odd SwiftUI issues, test on device rather than the simulator.
Update: Restarting the computer didn't fix it either. Thus while SwiftUI is still new, may be better off to use a real device for testing rather than the simulator.