简体   繁体   中英

Create a windowless SwiftUI macOS application?

I'm creating a menu bar app for macOS using SwiftUI.I have figured out how to create a menu bar item, and how to hide the icon from the Dock. But there is one thing left I need to figure out to have a proper menu bar app, and that is how to not show a window on screen (see screenshot).

在此处输入图像描述

When using SwiftUI for the @main App class, it looks like I have to return a WindowGroup with some content. I've tried EmptyView() etc. but it has to be "some view that conforms to Scene ".

What I have

Here is the code I have so far.

import SwiftUI
import Combine

class AppViewModel: ObservableObject {
  @Published var showPopover = false
}

@main struct WeeksApp: App {
  @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
  @Environment(\.scenePhase) private var scenePhase
  
  var body: some Scene {
    WindowGroup { Text("I don't want to show this window. 🥲") }
    .onChange(of: scenePhase) { phase in
      switch phase {
      case .background:
        print("Background")
      case .active:
        print("Active")
      case .inactive:
        print("Inactive")
      @unknown default:
        fatalError()
      }
    }
  }
}

class AppDelegate: NSObject, NSApplicationDelegate {
  var popover = NSPopover.init()
  var statusBarItem: NSStatusItem?
  var viewModel = AppViewModel()
  var cancellable: AnyCancellable?
  
  override init() {
    super.init()
    cancellable = viewModel.objectWillChange.sink { [weak self] in
      if (self?.viewModel.showPopover == false) {
        self?.closePopover(self)
      }
    }
  }
  
  func applicationDidFinishLaunching(_ notification: Notification) {
    popover.behavior = .transient
    popover.animates = false
    popover.contentViewController = NSViewController()
    popover.contentViewController?.view = NSHostingView(rootView: ContentView(viewModel: viewModel))
    statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
    statusBarItem?.button?.title = "Week \(getCurrentWeekNumber())"
    statusBarItem?.button?.action = #selector(AppDelegate.togglePopover(_:))
  }
  
  @objc func showPopover(_ sender: AnyObject?) {
    if let button = statusBarItem?.button {
      popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
    }
  }
  @objc func closePopover(_ sender: AnyObject?) {
    popover.performClose(sender)
  }
  
  @objc func togglePopover(_ sender: AnyObject?) {
    if popover.isShown {
      closePopover(sender)
    } else {
      showPopover(sender)
    }
  }
  
  func getCurrentWeekNumber() -> Int {
    let calendar = Calendar.current
    let weekOfYear = calendar.component(.weekOfYear, from: Date())
    return weekOfYear
  }
}

So how can I launch the app without showing a window? I would prefer a SwiftUI solution. I know it can be done the "old" way.

I'm not sure if it's a hack and if there is a better way. I wrote an app as an agent, with the possibility to show a window later in the process. However, at startup, the app should not show a window.

I made an application delegate with the following code:

class Appdelegate: NSObject, NSApplicationDelegate {
    
    @Environment(\.openURL) var openURL
    var statusItem: NSStatusItem!

    func applicationDidFinishLaunching(_ notification: Notification) {
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)

        statusItem.button!.image = NSImage(named: "AppIcon")
        statusItem.isVisible = true
        statusItem.menu = NSMenu()
        addConfigrationMenuItem()
        
        BundleLocator().checkExistingLaunchAgent()
        
        if let window = NSApplication.shared.windows.first {
            window.close()
        }
    }
}

Notice at the end I close the window, which let my app start in as a menubar app only. However, I do have a content view that can be opened later. The app therefore looks like this:

@main
struct RestartAtDateTryoutApp: App {
    
    @NSApplicationDelegateAdaptor(Appdelegate.self) var appDelegate
    var scheduler = Scheduler()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .handlesExternalEvents(preferring: Set(arrayLiteral: "ContentView"), allowing: Set(arrayLiteral: "*"))
                .environmentObject(scheduler)
                .frame(width: 650, height: 450)
        }
        .handlesExternalEvents(matching: Set(arrayLiteral: "ContentView"))
    }
}

This gives me the menubar app with items of which one is a window with a user interface. I hope this also works in your app.

单击菜单栏应用程序图标,显示菜单

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM