I have a custom navigation bar view that's used throughout my app. To make this reusable, I've created a ToolbarContent conformant struct that implements this custom navigation bar. This view contains some navigation bar buttons (ToolbarItems). I need to be able to enable/disable some of these buttons on demand after the view has been presented on screen.
Everything works fine on iOS 16, I can enable/disable the buttons as expected. However, on iOS 15, I ran into an issue, where I cannot change the enabled/disabled state of the ToolbarItems once they're displayed on the screen. Doesn't matter whether their initial state is enabled or disabled, toggling their state doesn't do anything.
Here's a minimal reproducible example, where the disabled state of the trailing navigation bar ToolbarItem is toggled via a button's action:
import SwiftUI
final class TopBarViewModel: ObservableObject {
@Published var isTrailingButtonDisabled: Bool = true
}
struct TopBar: ToolbarContent {
@ObservedObject private var viewModel: TopBarViewModel
init(viewModel: TopBarViewModel) {
self.viewModel = viewModel
}
var body: some ToolbarContent {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
print("Trailing navigation bar button pressed")
}, label: {
let state = viewModel.isTrailingButtonDisabled ? "disabled" : "enabled"
Text("Button is \(state)")
})
.disabled(viewModel.isTrailingButtonDisabled)
}
}
}
final class PlaygroundViewModel: ObservableObject {
let topBarViewModel: TopBarViewModel
init(topBarViewModel: TopBarViewModel) {
self.topBarViewModel = topBarViewModel
}
}
struct Playground: View {
@ObservedObject private var viewModel: PlaygroundViewModel
@ObservedObject private var topBarViewModel: TopBarViewModel
init(viewModel: PlaygroundViewModel) {
self.viewModel = viewModel
self.topBarViewModel = viewModel.topBarViewModel
}
var body: some View {
NavigationView {
Button(action: {
viewModel.topBarViewModel.isTrailingButtonDisabled.toggle()
}, label: {
let title = viewModel.topBarViewModel.isTrailingButtonDisabled ? "Enable" : "Disable"
Text("\(title) navigation bar trailing button")
})
.toolbar {
TopBar(viewModel: viewModel.topBarViewModel)
}
}
}
}
The Button that's part of the View is updated correctly when the @Published property on the ObservableObject conformant ViewModel is updated, however, the ToolbarContent isn't updated at all - the ToolbarItem doesn't update either its label or its disabled state on iOS 15. On iOS 16, everything is updated as expected.
I suspect this is a bug in SwiftUI on iOS 15 - how can I work around this bug so that I get the expected behaviour on both iOS 15 and 16?
The "usual" ways of forcing a view to update by manually calling
objectWillChange.send()on theObservableObjector by changing the View'siddoesn't seem to work onToolbarContentunfortunately.However, if you create a
@Statevariable that shadows the@Publishedon theObservableObject, you can force theToolbarContentto update correctly even on iOS 15.