I noticed that Navigationbar- / Toolbar Buttons are not working properly when there is at least one @State variable refreshing the View very often.
I created a simple app to test it.
With below Code example you have 3 options to trigger a modal sheet. One Button in the main View, one in the toolbar and one in the navigationbar.
When my timer doesn't update "number" all 3 buttons are working properly. When i start the Timer which will refresh the view every 0,1 second only the button in the main view will work every time. The buttons in toolbar / navigationbar do not work most of the time. (The shorter the TimeInterval of my timer the less the buttons are working)
import SwiftUI
struct ContentView: View {
@State private var number = 0
@State private var showModal = false
func startTimer() {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (_) in
number += 1
}
}
var body: some View {
NavigationView {
VStack {
Text("Count \(number)")
Button(action: startTimer, label: {
Text("Start Timer")
})
Button(action: { showModal.toggle() }, label: {
Text("Open Modal")
})
}
.navigationBarTitle("Home", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
showModal.toggle()
}, label: {
Text("Open Modal")
}))
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button(action: {
showModal.toggle()
}, label: {
Text("Open Modal")
})
}
}
}
.sheet(isPresented: $showModal, content: {
Text("Hello, World!")
})
}
}
Does anyone have an idea if there is a way to make the 2 buttons work properly?
This issue occurs with Xcode 12 beta 5 and Xcode Xcode 11.6 (without toolbar as it's not available there)
The number
state in provided variant refreshes entire view, that is a result of issue, and not very optimal for UI.
The solution for navigation bar, and in general good style, is to separate refreshing part into standalone view, so SwiftUI rendering engine rebuild only it.
Tested with Xcode 12b3 / iOS 14
struct TestOftenUpdate: View {
@State private var showModal = false
var body: some View {
NavigationView {
VStack {
QuickTimerView()
Button(action: { showModal.toggle() }, label: {
Text("Open Modal")
})
}
.navigationBarTitle("Home", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
showModal.toggle()
}, label: {
Text("Open Modal")
}))
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button(action: {
showModal.toggle()
}, label: {
Text("Open Modal")
})
}
}
}
.sheet(isPresented: $showModal, content: {
Text("Hello, World!")
})
}
}
struct QuickTimerView: View {
@State private var number = 0
var body: some View {
VStack {
Text("Count \(number)")
Button(action: startTimer, label: {
Text("Start Timer")
})
}
}
func startTimer() {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (_) in
number += 1
}
}
}
With toolbar
is different issue - it does not work after first sheet open even without timer view, and here is why
as it is seen the button layout has corrupted and outside of toolbar area, that is why hit testing fails - and this is Apple defect that should be reported.
Workaround 1:
Place toolbar outside navigation view (visually toolbar is smaller, but sometimes might be appropriate, because button works)
NavigationView {
// ... other code
}
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button(action: {
showModal.toggle()
}, label: {
Text("Open Modal")
})
}
}
Xcode 13.1, iPhone 13 simulator target (and canvas preview).
None of the other solutions worked for me, but I did discover that this only affects top navigation buttons AND if there is a large nav bar title.
So the things that worked seem to be:
.navigationBarTitleDisplayMode(.inline)
If the problem did manifest, I could pull down on the list contained within the view (no need for refreshable to be enabled) and this would also restore the button functionality.
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