Create a windowless SwiftUI macOS application? - swift

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.

Related

Can I use .playAudio() to resume playback after stopping?

From the question in this If I assign a sound in Reality Composer, can I stop it programmatically in RealityKit?, I would like to use method to resume playback after Play Music.
Can I do that?
Now, I use this command in stopAudio function to stop the music.
func stopAudio() {
if arView.scene.anchors.count > 0 {
if arView.scene.anchors[0].isAnchored {
arView.scene.anchors[0].children[0].stopAllAudio()
}
}
}
If I want arView.scene.anchors[0] to replay the music again, which command should I use?
Audio Playback Controller
Since RealityKit 2.0 isn't able to control parameters of Reality Composer's behaviors, the best strategy for controlling audio is to create a programmatic AudioPlaybackController. To feed your audio file to the controller, export .rcproject scene to .usdz format and use unzipping trick to extract the .aiff, .caf or .mp3 sound file. When loading audio for playback, you can choose between spatial and non-spatial audio experience.
UIKit version
import UIKit
import RealityKit
extension ViewController {
private func loadAudio() {
do {
let resource = try AudioFileResource.load(
named: "planetarium07.caf",
in: nil,
inputMode: .spatial,
loadingStrategy: .preload,
shouldLoop: true)
self.controller = entity.prepareAudio(resource)
self.controller?.speed = 0.9
self.controller?.fade(to: .infinity, duration: 2)
} catch {
print(error.localizedDescription)
}
}
}
ViewController.
class ViewController : UIViewController {
#IBOutlet var uiView: UIView! // when using #IBAction buttons
#IBOutlet var arView: ARView!
private var entity = Entity()
private var controller: AudioPlaybackController? = nil
override func viewDidLoad() {
super.viewDidLoad()
uiView.backgroundColor = .systemCyan
let boxScene = try! Experience.loadBox()
arView.scene.anchors.append(boxScene)
let anchor = boxScene.anchor
anchor?.addChild(entity)
self.loadAudio()
}
#IBAction func playMusic(_ sender: UIButton) {
self.controller?.play()
}
#IBAction func stopMusic(_ sender: UIButton) {
self.controller?.pause()
// self.controller?.stop()
}
}
SwiftUI version
import SwiftUI
import RealityKit
struct ContentView : View {
#State var arView = ARView(frame: .zero)
#State var controller: AudioPlaybackController? = nil
#State var entity = Entity()
var body: some View {
ZStack {
ARViewContainer(arView: $arView,
entity: $entity).ignoresSafeArea()
VStack {
Spacer()
Button("Play") { loadSound(); controller?.play() }
Button("Stop") { controller?.stop() }
}
}
}
func loadSound() {
do {
let resource = try AudioFileResource.load(
named: "planetarium07.caf",
in: nil,
inputMode: .spatial,
loadingStrategy: .preload,
shouldLoop: true)
self.controller = entity.prepareAudio(resource)
} catch {
print(error.localizedDescription)
}
}
}
ARViewContainer.
struct ARViewContainer: UIViewRepresentable {
#Binding var arView: ARView
#Binding var entity: Entity
func makeUIView(context: Context) -> ARView {
let boxScene = try! Experience.loadBox()
arView.scene.anchors.append(boxScene)
let anchor = boxScene.anchor
anchor?.addChild(entity)
return arView
}
func updateUIView(_ view: ARView, context: Context) { }
}

Displaying NSMenuItem in SwiftUI on it's own

I have been trying to display a custom NSMenuItem (for a preview page of a menu manager) inside a SwiftUI view. But I can't achieve it. I have figured it needs to wrapped inside a menu first, and thought that there might be a way to pop the menu pragmatically but sadly, those efforts have failed and the app crashes.
So far, my code looks like this:
import Foundation
import SwiftUI
struct NSMenuItemView: NSViewRepresentable {
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeNSView(context: Context) -> NSView {
let view = NSView()
let menu = NSMenu()
let item = menu.addItem(withTitle: "Do Action", action: #selector(Coordinator.valueChanged(_:)), keyEquivalent: "")
item.target = context.coordinator
view.menu = menu
return view
}
func updateNSView(_ view: NSView, context: Context) {
// App crashes here :/
view.menu?.popUpMenuPositioningItem(
positioning: view.menu?.item(at: 0),
at: NSPoint(x: 0, y: 0),
in: view
)
}
}
extension NSMenuItemView {
final class Coordinator: NSObject {
var parent: NSMenuItemView
init(_ parent: NSMenuItemView) {
self.parent = parent
}
#objc
func valueChanged(_ sender: NSMenuItem) {
}
}
}
Am I missing something here? Is it even possible to just pragmatically display NSMenuItem?
The NSMenu comfors to NSViewRepresentable so I figured it might just workout, and have seen answers on StackOverflow (granted date a while back) showing similar code that should work.
Without the popUpMenuPositioningItem it works - in a way I guess - when I right click in the View, the MenuItem Appears. But I would like to be able to display the menu without the right click, just like that.
The problem is that the menu is shown while the view are still rendering so that the crash happens. To avoid this you should call popUp(positioning:at:in) after the your view appears on the screen. The way to achieve it, we have to use publisher to trigger an event to show menu inside onAppear modifier and listen it inside Coordinator. Here is the sample for that solution.
struct ContentView: View {
let menuPopUpTrigger = PassthroughSubject<Void, Never>()
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
NSMenuItemView(menuPopUpTrigger)
Text("Hello, world!")
}
.padding()
.onAppear {
/// trigger an event when `onAppear` is executed
menuPopUpTrigger.send()
}
}
}
struct NSMenuItemView: NSViewRepresentable {
let base = NSView()
let menu = NSMenu()
var menuPopUpTrigger: PassthroughSubject<Void, Never>
init(_ menuPopUpTrigger: PassthroughSubject<Void, Never>) {
self.menuPopUpTrigger = menuPopUpTrigger
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeNSView(context: Context) -> NSView {
let item = menu.addItem(withTitle: "Do Action", action: #selector(Coordinator.valueChanged(_:)), keyEquivalent: "")
item.target = context.coordinator
base.menu = menu
context.coordinator.bindTrigger(menuPopUpTrigger)
return base
}
func updateNSView(_ view: NSView, context: Context) { }
}
extension NSMenuItemView {
final class Coordinator: NSObject {
var parent: NSMenuItemView
var cancellable: AnyCancellable?
init(_ parent: NSMenuItemView) {
self.parent = parent
}
#objc func valueChanged(_ sender: NSMenuItem) { }
/// bind trigger to listen an event
func bindTrigger(_ trigger: PassthroughSubject<Void, Never>) {
cancellable = trigger
.delay(for: .seconds(0.1), scheduler: RunLoop.main)
.sink { [weak self] in
self?.parent.menu.popUp(
positioning: self?.parent.menu.item(at: 0),
at: NSPoint(x: 0, y: 0),
in: self?.parent.base
)
}
}
}
}
I hope it will help you to get what you want.

SwiftUI macOS Menu Bar app copy and paste into text field

*EDIT: So I figured it out. It might not be the swiftUI way about doing it but it works for my app. I found this post that answered most my questions. I created a new file used the mainMenu view template and that was it. *
I'm creating a menu bar app with a text field that I want users to be able to paste their clipboards into. I think the issue is because it doesn't have a menu since copy and paste works on a black project in a text field in a normal window. I have read a few other post about using a swift package called hotkey but I don't think that will work since I want it to use the systems clipboards not a custom one to the app.
Can this be done in a menu bar only app?
Thank you for the help as I learn.
Here is the test code I'm using.
ContentView
import SwiftUI
struct ContentView: View {
#State var jobName = String()
var body: some View {
Text("Testing copy and paste")
.padding()
TextField(/*#START_MENU_TOKEN#*/"Placeholder"/*#END_MENU_TOKEN#*/, text: $jobName)
Button("print text") {
print(jobName)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
AppDelegate
import Cocoa
import SwiftUI
class AppDelegate: NSObject, NSApplicationDelegate {
var popover: NSPopover!
var statusBarItem: NSStatusItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Create the popover
let popover = NSPopover()
popover.contentSize = NSSize(width: 400, height: 500)
popover.behavior = .transient
popover.contentViewController = NSHostingController(rootView: contentView)
self.popover = popover
// Create the status item
self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
if let button = self.statusBarItem.button {
button.image = NSImage(named: "Icon")
button.action = #selector(togglePopover(_:))
}
}
#objc func togglePopover(_ sender: AnyObject?) {
if let button = self.statusBarItem.button {
if self.popover.isShown {
self.popover.performClose(sender)
} else {
self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
}
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
Main
import Foundation
import Cocoa
// 1
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
// 2
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
So I figured it out. It might not be the swiftUI way about doing it but it works for my app. I found this post that answered most my questions. I created a new file used the mainMenu view template and that was it.

Conditionally show either a Window or the Menu bar view SwiftUI macOS

I'm creating an app where it simply lives in the menu bar, however I'd like a full-sized normal window to pop up if the user is not logged in, I have made a little pop over window which is sufficient for my main app to go into:
The code I have used to achieve this:
class AppDelegate: NSObject, NSApplicationDelegate{
var statusItem: NSStatusItem?
var popOver = NSPopover()
func applicationDidFinishLaunching(_ notification: Notification) {
let menuView = MenuView().environmentObject(Authentication())
popOver.behavior = .transient
popOver.animates = true
popOver.contentViewController = NSViewController()
popOver.contentViewController?.view = NSHostingView(rootView: menuView)
popOver.contentViewController?.view.window?.makeKey()
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let MenuButton = statusItem?.button{
MenuButton.image = NSImage(systemSymbolName: "gearshape.fill", accessibilityDescription: nil)
MenuButton.action = #selector(MenuButtonToggle)
}
if let window = NSApplication.shared.windows.first {
window.close()
}
}
#objc func MenuButtonToggle(sender: AnyObject? = nil){
if popOver.isShown{
popOver.performClose(sender)
}
else{
if let menuButton = statusItem?.button{
NSApplication.shared.activate(ignoringOtherApps: true)
self.popOver.show(relativeTo: menuButton.bounds, of: menuButton, preferredEdge: NSRectEdge.minY)
}
}
}
#objc func closePopover(_ sender: AnyObject? = nil) {
popOver.performClose(sender)
}
#objc func togglePopover(_ sender: AnyObject? = nil) {
if popOver.isShown {
closePopover(sender)
} else {
MenuButtonToggle(sender: sender)
}
}
}
I make the popover view inside the AppDelegate, I'd like to either render this (with the icon in the menu bar) or just a normal macOS window (without the icon in the menu bar). Then have the ability to switch between the two easily via something like this:
if session != nil{
// show menu bar style
else{
// show window view to log in
}
I think you can reference the demo
Create a reference to an instance of NSWindowController in your AppDelegate class.
private var mainVC: MainViewController?
func showMainWindow() {
if mainVC == nil {
mainVC = MainViewController.create()
mainVC?.onWindowClose = { [weak self] in
self?.mainVC = nil
}
}
mainVC?.showWindow(self)
}
The MainviewController is like following:
class MainViewController: NSWindowController {
var onWindowClose: (() -> Void)?
static func create() -> MainViewController {
let window = NSWindow()
window.center()
window.styleMask = [.titled, .closable, .miniaturizable, .resizable]
window.title = "This is a test main title"
let vc = MainViewController(window: window)
// Use your SwiftUI here as the Main Content
vc.contentViewController = NSHostingController(rootView: ContentView())
return vc
}
override func showWindow(_ sender: Any?) {
super.showWindow(sender)
NSApp.activate(ignoringOtherApps: true)
window?.makeKeyAndOrderFront(self)
window?.delegate = self
}
}
extension MainViewController: NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
onWindowClose?()
}
}

SwiftUI: Reinitialize after popover.show

I am writing a MacOS (10.15 Catalina) application using a popover. The main ContentView includes a custom view with a simple toggle:
class AppDelegate: NSObject, NSApplicationDelegate {
var popover=NSPopover()
func applicationDidFinishLaunching(_ aNotification: Notification) {
self.popover.contentViewController = NSHostingController(rootView: contentView)
self.statusBarItem = NSStatusBar.system.statusItem(withLength: 18)
if let statusBarButton = self.statusBarItem.button {
statusBarButton.title = "☰"
statusBarButton.action = #selector(togglePopover(_:))
}
func show() {
let statusBarButton=self.statusBarItem.button!
self.popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
}
func hide() {
popover.performClose(nil)
}
#objc func togglePopover(_ sender: AnyObject?) {
self.popover.isShown ? hide() : show()
}
}
struct ContentView: View {
var body: some View {
Test("Hello")
// more stuff
}
}
struct Test: View {
var message: String
#State private var clicked: Bool = false
init(message: String) {
self.message = message
_clicked = State(initialValue: false)
print("init")
}
var body: some View {
return HStack {
Text(message)
Button("Click") {
self.clicked = true
}
if !self.clicked {
Text("Before")
}
else {
Text("After")
}
}
}
}
I would like to reinitialize some data in custom view whenever the popup reappears. So, in this example, clicked should reset to false. I have tried every combination of #Binding and #State variables I could find in my many searches, but nothing appears to work. It appears that .onAppear() only fires the first time.
The init() function is there because in my application I also need to include additional content and code. In this example, I have tried to use it to initialize the clicked state variable, but, though the print() function does print, the variable doesn’t seem to get reset.
How can I reinitialize the #State variable?
To initialize state in init you should not initialize it as property (because properties are initialised before init and state is initialised only once), so
struct Test: View {
var message: String
#State private var clicked: Bool // << here, only declare
init(message: String) {
self.message = message
_clicked = State(initialValue: false) // << then this works
print("init")
}
// ... other code
}