Home > Net >  Menu Command Buttons don't disable in reaction to Published variable changes
Menu Command Buttons don't disable in reaction to Published variable changes

Time:08-24

Situation: I need the menu bar to recognise the active tab in a TabView, even when multiple windows are open. I have found this solution (https://stackoverflow.com/a/68334001/2682035), which seems to work in principal, but not in practice.

Problem: Menu bar buttons do not disable immediately when their corresponding tab is changed. However, they will disable correctly if another State variable is modified.

Minimal example which now works: I made this without the code for managing multiple windows because the problem seemed to occur without that. Thanks to the suggestion from lorem ipsum, this will now function as expected.

import SwiftUI

enum TabType: String, Codable{
    case tab1 = "first tab"
    case tab2 = "second tab"
}

public class ViewModel: ObservableObject {
    @Published var activeTab: TabType = .tab1
}

struct MenuCommands: Commands {
    @ObservedObject var viewModel: ViewModel // CHANGED
    @Binding var someInformation: String
    
    var body: some Commands{
        CommandMenu("My menu"){
            Text(someInformation)
            Button("Go to tab 1"){
                viewModel.activeTab = .tab1
            }
            .disabled(viewModel.activeTab == .tab1) // this now works as expected
            Button("Go to tab 2"){
                viewModel.activeTab = .tab2
            }
            .disabled(viewModel.activeTab == .tab2) // this does too
            Button("Print active tab"){
                print(viewModel.activeTab) // this does too
            }
        }
    }
}

struct Tab: View{
    var tabText: String
    @Binding var someInformation: String
    
    var body: some View{
        VStack{
            Text("Inside tab "   tabText)
            TextField("Info", text: $someInformation)
        }
    }
}


struct ContentView: View {
    
    @EnvironmentObject var viewModel: ViewModel
    @Binding var someInformation: String
    
    var body: some View {
        TabView(selection: $viewModel.activeTab){
            Tab(tabText: "1", someInformation: $someInformation)
                .tabItem{
                    Label("Tab 1", systemImage: "circle")
                }
                .tag(TabType.tab1)
            Tab(tabText: "2", someInformation: $someInformation)
                .tabItem{
                    Label("Tab 2", systemImage: "circle")
                }
                .tag(TabType.tab2)
        }
    }
}

@main
struct DisableMenuButtonsMultiWindowApp: App {
    
    @StateObject var viewModel = ViewModel() // CHANGED
    @State var someInformation: String = ""
    
    var body: some Scene {
        WindowGroup {
            ContentView(someInformation: $someInformation)
                .environmentObject(viewModel)
        }
        .commands{MenuCommands(viewModel: viewModel, someInformation: $someInformation)}
    }
}

Slightly less minimal example that doesn't work:

Unfortunately that didn't work in my app, so here is a new minimal example that works closer to the actual app and will observe multiple windows, except it has the same issue as before.

import SwiftUI

enum TabType: String, Codable{
    case tab1 = "first tab"
    case tab2 = "second tab"
}

public class ViewModel: ObservableObject {
    @Published var activeTab: TabType = .tab1
}

struct MenuCommands: Commands {
    @ObservedObject var globalViewModel: GlobalViewModel
    @Binding var someInformation: String
    
    var body: some Commands{
        CommandMenu("My menu"){
            Text(someInformation)
            Button("Go to tab 1"){
                globalViewModel.activeViewModel?.activeTab = .tab1
            }
            .disabled(globalViewModel.activeViewModel?.activeTab == .tab1) // this will not disable when activeTab changes, but it will when someInformation changes
            Button("Go to tab 2"){
                globalViewModel.activeViewModel?.activeTab = .tab2
            }
            .disabled(globalViewModel.activeViewModel?.activeTab == .tab2) // this will not disable when activeTab changes, but it will when someInformation changes
            Button("Print active tab"){
                print(globalViewModel.activeViewModel?.activeTab ?? "") // this always returns correctly
            }
        }
    }
}

struct Tab: View{
    var tabText: String
    @Binding var someInformation: String
    
    var body: some View{
        VStack{
            Text("Inside tab "   tabText)
            TextField("Info", text: $someInformation)
        }
    }
}


struct ContentView: View {
    
    @EnvironmentObject var globalViewModel : GlobalViewModel
    @Binding var someInformation: String
    
    @StateObject var viewModel: ViewModel  = ViewModel()
    
    var body: some View {
        HostingWindowFinder { window in
          if let window = window {
            self.globalViewModel.addWindow(window: window)
            print("New Window", window.windowNumber)
            self.globalViewModel.addViewModel(self.viewModel, forWindowNumber: window.windowNumber)
          }
        }
        
        TabView(selection: $viewModel.activeTab){
            Tab(tabText: "1", someInformation: $someInformation)
                .tabItem{
                    Label("Tab 1", systemImage: "circle")
                }
                .tag(TabType.tab1)
            Tab(tabText: "2", someInformation: $someInformation)
                .tabItem{
                    Label("Tab 2", systemImage: "circle")
                }
                .tag(TabType.tab2)
        }
    }
}

@main
struct DisableMenuButtonsMultiWindowApp: App {
    
    @StateObject var globalViewModel = GlobalViewModel()
    @State var someInformation: String = ""
    
    var body: some Scene {
        WindowGroup {
            ContentView(someInformation: $someInformation)
                .environmentObject(globalViewModel)
        }
        .commands{MenuCommands(globalViewModel: globalViewModel, someInformation: $someInformation)}
    }
}

// everything below is from other solution for observing multiple windows

class GlobalViewModel : NSObject, ObservableObject {
  
  // all currently opened windows
  @Published var windows = Set<NSWindow>()
  
  // all view models that belong to currently opened windows
  @Published var viewModels : [Int:ViewModel] = [:]
  
  // currently active aka selected aka key window
  @Published var activeWindow: NSWindow?
  
  // currently active view model for the active window
    @Published var activeViewModel: ViewModel?
  
  override init() {
    super.init()
    // deallocate a window when it is closed
    // thanks for this Maciej Kupczak            
  • Related