Git Product home page Git Product logo

menubarextraaccess's Introduction

MenuBarExtraAccess

Platforms - macOS 13.0 Swift 5.7-5.8 Xcode 14 License: MIT

Gives you Extra access to SwiftUI MenuBarExtra.

  • Programmatically hide, show, or toggle the menu (by way of a Bool binding)
  • Access to the underlying NSStatusItem
  • Access to the underlying NSWindow (when using the .window style)
  • Works with one or multiple MenuBarExtra
  • Works with both menu and window based styles (see Known Issues)

Why?

There is no 1st-party MenuBarExtra API to obtain or set the menu presentation state, access the status item, or access the popup's NSWindow. (As of Xcode 14.3 / SwiftUI 4)

Library Features

  • A new .menuBarExtraAccess(isPresented:) { statusItem in } scene modifier with
    • a binding to hide/show/toggle the menu, and
    • direct access to the NSStatusItem if needed
  • A new .introspectMenuBarExtraWindow { window in } view modifier passing in the NSWindow reference
  • Window-based menu extra status items now remain highlighted while the window is open so it feels more like a native menu
  • No private API used, so it's Mac App Store safe

Getting Started

The library is available as a Swift Package Manager (SPM) package.

Use the URL https://github.com/orchetect/MenuBarExtraAccess when adding the library to a project or Swift package.

Then import the library:

import SwiftUI
import MenuBarExtraAccess

Standard Menu Style

An example of showing the menu extra menu by clicking a button in a window:

@main struct MyApp: App {
    @State var isMenuPresented: Bool = false
    
    var body: some Scene {
        WindowGroup {
            Button("Show Menu") { isMenuPresented = true }
        }
        
        MenuBarExtra("MyApp Menu", systemImage: "folder") {
            Button("Menu Item 1") { print("Menu Item 1") }
            Button("Menu Item 2") { print("Menu Item 2") }
        }
        .menuBarExtraStyle(.menu)
        .menuBarExtraAccess(isPresented: $isMenuPresented) { statusItem in // <-- the magic ✨
             // access status item or store it in a @State var
        }
    }
}

Window Style

An example of a button in the popup window dismissing the popup and performing an action:

@main struct MyApp: App {
    @State var isMenuPresented: Bool = false
    
    var body: some Scene {
        MenuBarExtra("MyApp Menu", systemImage: "folder") {
            MyMenu(isMenuPresented: $isMenuPresented)
            	.introspectMenuBarExtraWindow { window in // <-- the magic ✨
                    window.animationBehavior = .alertPanel
                }
        }
        .menuBarExtraStyle(.window)
        .menuBarExtraAccess(isPresented: $isMenuPresented) { statusItem in // <-- the magic ✨
             // access status item or store it in a @State var
        }
    }
}

struct MyMenu: View {
    @Binding var isMenuPresented: Bool

    var body: some View {
        Button("Perform Action") { 
            isMenuPresented = false 
            performSomeAction()
        }
    }
}

Multiple MenuBarExtra

MenuBarExtraAccess is fully compatible with one or multiple MenuBarExtra in an app.

Just add an index number parameter to .menuBarExtraAccess() and .introspectMenuBarExtraWindow() that reflects the order of MenuBarExtra declarations.

var body: some Scene {
    MenuBarExtra("MyApp Menu A", systemImage: "folder") {
        MyMenu(isMenuPresented: $isMenuPresented)
            .introspectMenuBarExtraWindow(index: 0) { window in // <-- add index 0
                // ...
            }
    }
    .menuBarExtraStyle(.window)
    .menuBarExtraAccess(index: 0, isPresented: $isMenuPresented) // <-- add index 0
    
    MenuBarExtra("MyApp Menu B", systemImage: "folder") {
        MyMenu(isMenuPresented: $isMenuPresented)
            .introspectMenuBarExtraWindow(index: 1) { window in // <-- add index 1
                // ...
            }
    }
    .menuBarExtraStyle(.window)
    .menuBarExtraAccess(index: 1, isPresented: $isMenuPresented) // <-- add index 1
}

Future

The hope is that Apple implements native versions of these features (and more) in future iterations of SwiftUI!

Until then, a radar has been filed as a feature request: FB11984872

Menu Builder

Check out MacControlCenterUI, a SwiftUI package built on MenuBarExtraAccess for easily building Control Center style menus.

Known Issues

  • When using .menuBarExtraStyle(.menu), SwiftUI causes the popup menu to block the runloop while the menu is open, which means:

    • Observing the isPresented binding will not work as expected.
    • Setting the isPresented binding to false while the menu is presented has no effect.
    • The user must dismiss the menu themself to allow event flow to continue. We have no control over this until Apple decides to change the MenuBarExtra behavior.
  • There are edge cases where the status item gets confused and may invert its state. It may be related to how the underlying MenuBarExtra works. This is being investigated and a workaround may be possible for a future release.

Author

Coded by a bunch of 🐹 hamsters in a trenchcoat that calls itself @orchetect.

License

Licensed under the MIT license. See LICENSE for details.

Sponsoring

If you enjoy using MenuBarExtraAccess and want to contribute to open-source financially, GitHub sponsorship is much appreciated. Feedback and code contributions are also welcome.

Contributions

Contributions are welcome. Posting in Discussions first prior to new submitting PRs for features or modifications is encouraged.

menubarextraaccess's People

Contributors

orchetect avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

menubarextraaccess's Issues

isMenuPresented using ObservableObject...

Bug Description, Steps to Reproduce, Crash Logs, Screenshots, etc.

Hello.
First of all, thank you for making a good library.
I found something while using the functions of the library, so I left a post.

I didn't want to keep passing the variable isMenuPresented to the subview with Binding for MenuBarExtraAccess, so I tried to use it wrapped in an ObservableObject.
However, using it in this way caused menubar to close and open abnormally slow (state -> binding is normal)
Below is the code of the way I implemented it.
Is Presented for controlling MenuBarExtraAccess, but is ObservableObject hard to use?

class AppState: ObservableObject {
    @Published var isMenuPresented: Bool = false
}
struct MyApp: App {

@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject var appState = AppState()
        
        MenuBarExtra("ListeningDogApp") {
            MenuBarExtraView()
                .environmentObject(appDelegate)
                .environmentObject(appState)
        }
        .menuBarExtraStyle(.window)
        .menuBarExtraAccess(isPresented: $listengDogAppState.isMenuPresented) { statusItem in // <-- the magic ✨
            // access status item or store it in a @State var
        }
}
    struct PreferencesView: View {
        
        @EnvironmentObject var appDelegate: AppDelegate
        @EnvironmentObject var AppState: AppState
        @State private var isHovered = false
        
        var body: some View {
            
            Button {
                appState.isMenuPresented = false
                appDelegate.showMainWindow()
            } label: {
                HStack {
                    Text("Preferences...")
                    Spacer()
                }
                .frame(height: 30)
            }
            .buttonStyle(.plain)
        }
2023-07-08.10.12.34.mov

App with high CPU usage/mem/hangs possibly after wake

Bug Description, Steps to Reproduce, Crash Logs, Screenshots, etc.

Hi there, first of all thank you for MenuBarExtraAccess!

Problem: my macOS app starts showing high CPU/mem usage possibly after computer is awaken from sleep. Profiling indicates some good amount of time spent on NSStatusItem.

I think it might be the way I'm using MenuBarExtraAccess. I'm reproducing it consistently putting the computer to sleep and then clicking on the app in the menu bar. Edit: now I'm able to consistently trigger it by simply clicking on the app in the menu bar, don't even need to put it to sleep.

Would appreciate any help or pointers you may have. Code and profiling screenshots can be found below.

Screenshot 2024-02-08 at 3 18 09 PM

Environment

Environment: macOS Sonoma 14.2.1, MBP M1 2020, one external monitor
System settings: menu bar auto-hide enabled

Code

This is sample code that very closely resembles how my app uses MenuBarExtraAccess and MacControlCenterUI:

import SwiftUI
import MacControlCenterUI
import SettingsAccess

@main
struct MyAppApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate: AppDelegate
    @State var isMenuPresented: Bool = false

    var body: some Scene {
        Settings {
            SettingsView().environmentObject(appState)
        }

        MenuBarExtra() {
            MenuBarView(isMenuPresented: $isMenuPresented)
                .openSettingsAccess()
                .environmentObject(appState)
        } label: {
            Label(Constants.appName, image: "MenuBarIcon")
        }
        .menuBarExtraStyle(.window)
        .menuBarExtraAccess(isPresented: $isMenuPresented)
    }
}
import SwiftUI
import MacControlCenterUI
import Combine
import OSLog

struct MenuBarView: View {
    @Environment(\.openSettings) private var openSettings
    @Binding var isMenuPresented: Bool

    @State private var counter: Int = 0
    @State private var isCounterEnabled: Bool = false
    @State private var timer: AnyCancellable?

    func update() {
        // Update counter every second only when the UI is being presented
        counter += 1
    }

    func startTimer() {
        timer = Timer.publish(every: 1, on: .main, in: .common)
            .autoconnect()
            .sink { _ in update() }
    }

    func stopTimer() {
        timer?.cancel()
        timer = nil
    }

    var body: some View {
        MacControlCenterMenu(isPresented: $isMenuPresented) {
             VStack {
                 Toggle("Enable", isOn: $isCounterEnabled)
                 Text("\(counter)")
             }
        }
        .onChange(of: isMenuPresented) { _ in
            if isMenuPresented {
                if isCounterEnabled: {
                    startTimer()
                }
            } else {
                stopTimer()
            }
        }
    }
}

Screenshots

52.28 s 36.8% 1.00 ms -[NSStatusItem _updateReplicantsUnlessMenuIsTracking:]

Screenshot 2024-02-08 at 4 09 30 PM

MenuBarExtras.setupEventsMonitor

Screenshot 2024-02-08 at 4 08 18 PM

statusItem.for(:)

Screenshot 2024-02-08 at 4 07 20 PM

Screenshot 2024-02-08 at 3 51 57 PM

doesn't work with `@main final class MyApp: App`

Thank you for creating this library, it is incredibly useful.

I am new to SwiftUI and have been conducting some experiments, I encountered an issue when I changed the default @main struct MyApp: App to @main final class MyApp: App. The MenuBarExtraAccess ceased functioning. When I reverted back to the original code, it started working again.

isPresented doesn't change after the Item becomes visible

Hello,

First of all, thank you for the package! I have an app called Wunderbar that shows words on the Menu Bar to help people to learn new words. I'm using Timer to change the words after certain period of time. Sometimes some words are too long and some users are using MacBook with a notch, therefore the words become invisible on the Menu Bar.

I was looking for a solution for the problem and I've found your package. Unfortunately, when I started to test the package, I realised that it is not working as expected for my use case. I really appreciate if you can somehow fix the issue or let me know if I'm doing something wrong.

Here is a way to reproduce the issue:

@main struct MyApp: App {
    @State var isMenuPresented: Bool = false
    @State var text: String = "very very very long text here so the MenuBarItem is not going to be visible on the Menu Bar"
    
    var body: some Scene {        
        MenuBarExtra() {
            
        } label: {
            Text(text).onAppear {
                Timer.scheduledTimer(withTimeInterval: 3, repeats: true) {_ in 
                    text = "short"
                }
            }
        }.menuBarExtraAccess(isPresented: $isMenuPresented) { statusItem in
            print(isMenuPresented)
        }
    }
}

Output of the print statement:

false
false // after 3 seconds

Somehow we are observing the visibility change of the item but the value is not set to true when the item becomes visible. Also, statusItem.isVisible is always true whether it is visible or not.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.