SwiftUI macOS hover on Button change cursor mouse - swift

I have a Button as seen in the code below, I would like when the mouse passed over the Button the cursor would change as if to mean that it is clickable.
Is there anything like this?
Version:
macOs: 10.15.4
Xcode: 11.7
Code:
Button(action: { ... }) {
ImageMac(systemName: "heart")
}.buttonStyle(PlainButtonStyle())
AppDelegate:
import Cocoa
import SwiftUI
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var popover = NSPopover.init()
var statusBar: StatusBarController?
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
popover.contentViewController = MainViewController()
popover.contentSize = NSSize(width: 360, height: 360)
popover.contentViewController?.view = NSHostingView(rootView: contentView)
// Create the Status Bar Item with the Popover
statusBar = StatusBarController.init(popover)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}

Related

How could I pass StateObject to both main struct and AppDelegate(or just AppDelegate)?

I began to learn Swift four days ago and I met a problem.
#main
struct GmailNotificationApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
#StateObject var authViewModel = AuthenticationViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(authViewModel) // <-----first place
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
private var statusItem: NSStatusItem!
private var popover: NSPopover!
func applicationDidFinishLaunching(_ notification: Notification) {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let statusButton = statusItem.button {
statusButton.image = NSImage(systemSymbolName: "g.circle.fill", accessibilityDescription: "Gmail Notification")
statusButton.action = #selector(togglePopover)
}
self.popover = NSPopover()
self.popover.contentSize = NSSize(width: 210, height: 300)
self.popover.behavior = .transient
self.popover.contentViewController = NSHostingController(rootView: ContentView().environmentObject(AuthenticationViewModel())) // <-----second place
}
#objc func togglePopover(){
if let button = statusItem.button {
if popover.isShown {
self.popover.performClose(nil)
} else {
self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
}
}
}
My question is: How could I pass StateObject inside AppDelegate?
Also:
I knew little about AppKit, it seems I have two "window", one is displayed just below menu bar and another shows when Google Auth calls back.
I wish the windows in the center of my screen never shows.
Now I use
NSApplication.shared.windows.first?.close()
var window: NSWindow!
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 0, height: 0),
styleMask: [.borderless],
backing: .buffered, defer: false)
window.center()
window.makeKeyAndOrderFront(nil)
to work around.
But I think it's very stupid.
So is there a way to never show the window in the screen center and just show the windows below menu bar?
I'm quite new to Swift/SwiftUI/AppKit and sorry for these dumb questions.
Notice that you don't give SwiftUI an instance of your AppDelegate. You only give it the class object, AppDelegate.self, and SwiftUI creates the instance once and keeps it around forever.
I think a better pattern is to create the AuthenticationViewModel in your AppDelegate. Then you don't need to use #StateObject to keep the model alive, because the app delegate will do that. So you can just use #ObservedObject:
class AppDelegate: NSObject, NSApplicationDelegate {
let authViewModel = AuthenticationViewModel()
// blah blah blah
}
#main
struct MacStudyApp: App {
#NSApplicationDelegateAdaptor private var appDelegate: AppDelegate
#ObservedObject private var authViewModel: AuthenticationViewModel
init() {
authViewModel = _appDelegate.wrappedValue.authViewModel
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(authViewModel)
}
}
}
But in your case, you don't even need to use #ObservedObject because MacStudyApp doesn't use any properties of authViewModel in its body. So you could in fact just do this:
class AppDelegate: NSObject, NSApplicationDelegate {
let authViewModel = AuthenticationViewModel()
// blah blah blah
}
#main
struct MacStudyApp: App {
#NSApplicationDelegateAdaptor private var appDelegate: AppDelegate
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appDelegate.authViewModel)
}
}
}

NSPopover not aligned to NSStatusItem menubar icon

I'm trying to make a menubar app for macOS 12 that shows a popover when the menubar icon is clicked. As you can see from the attached screenshot, I have the popover showing but it's not actually aligned to the menubar icon. Here's the relevant code:
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 status item
statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
// Create the popover
let popover = NSPopover()
popover.behavior = .transient
popover.contentViewController = NSHostingController(rootView: contentView)
self.popover = popover
if let button = statusBarItem.button {
button.image = NSImage(systemSymbolName: "eyes", accessibilityDescription: "Eyes")
button.action = #selector(togglePopover(_:))
}
}
#objc func togglePopover(_ sender: AnyObject?) {
guard let button = statusBarItem.button else { return }
if popover.isShown {
popover.performClose(sender)
} else {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY)
}
}
}
You can assign contentSize to the popover which is the same size with contentView:
popover.contentViewController = NSHostingController(rootView: contentView)
popover.contentSize = NSSize(width: popoverWidth, height: popoverHeight)
ContentView:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello World")
}
.frame(width: viewWidth, height: viewHeight)
}
}

SwiftUI: Changing values in a Class

This is a menubar macOS app. I'm trying to change the dropdown window size with a button from the content view. This is my attempt so far.
struct ContentView: View {
#ObservedObject var app = AppDelegate()
var body: some View {
VStack{
Text("Hello World")
.padding(.top, 10)
Button {
biggerwindow()
} label: {
Image("tv")
}
}
}
func biggerwindow(){
app.popover.contentSize = NSSize(width: 800, height: 800 )
}
}
Nothing happens when clicking on the button.
This is the AppDelegate class that contains the default values
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
var popover = NSPopover.init()
var statusBar: StatusBarController?
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the contents
let contentView = ContentView()
// Set the SwiftUI's ContentView to the Popover's ContentViewController
popover.contentViewController = MainViewController()
//default size of the menu bar window
popover.contentSize = NSSize(width: 360, height: 360 )
popover.contentViewController?.view = NSHostingView(rootView: contentView)
// Create the Status Bar Item with the Popover
statusBar = StatusBarController.init(popover)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
The problem is here:
#ObservedObject var app = AppDelegate()
You're creating a new instance of AppDelegate here, with its own instance of popover. You need to use the existing instance of AppDelegate that was created at app launch. You can access the existing instance through the global NSApplication.shared object.
struct ContentView: View {
var app: AppDelegate { NSApplication.shared.delegate as! AppDelegate }

SwiftUI: Passing a variable from Content View to a Class

In my menu bar macOS app, i'm trying to access a toggle that's in my Content View from my StatusBarController class. This switch is meant to keep the popover shown on screen even when a user clicks outside the app.
Content View
struct ContentView: View {
#State private var lock = false
var body: some View {
VStack{
Text("Hello World")
Toggle( isOn: $lock, label: {
})
.toggleStyle(SwitchToggleStyle())
}
.frame(width: 360.0, height: 160.0, alignment: .top)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
StatusBarController
class StatusBarController {
private var eventMonitor: EventMonitor?
private var content: ContentView?
func unlockpopover() {
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: mouseEventHandler)
}
if lock {
// Popover window is locked if user clicks outside the app
}else {
unlockpopover() // Popover window is unlocked if user clicks outside the app
}
}
}
}
How do i access the 'lock' variable from Content View?
'Cannot find 'lock' in scope'
is the error that's currently showing.
MenuBar project template i'm using.
EDIT
#lorem ipsum's suggestion of using EnvironmentObject and observable object.
StatusBarController
class StatusBarController:ObservableObject {
private var eventMonitor: EventMonitor?
private var content: ContentView?
#Published var lock: Bool = false
func unlockpopover() {
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: mouseEventHandler)
}
if lock {
// Popover window is locked if user clicks outside the app
}else {
unlockpopover() // Popover window is unlocked if user clicks outside the app
}
}
}
}
Content View
struct ContentView: View {
#EnvironmentObject var test: StatusBarController
var body: some View {
VStack{
Text("Hello World")
Toggle( isOn: $test.lock, label: {
})
.toggleStyle(SwitchToggleStyle())
}
.environmentObject(self.test)
.frame(width: 360.0, height: 160.0, alignment: .top)
}
}
App crashes with this error highlighted on $test.lock
Thread 1: Fatal error: No ObservableObject of type StatusBarController
found. A View.environmentObject(_:) for StatusBarController may be
missing as an ancestor of this view.
AppDelegate
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var popover = NSPopover.init()
var statusBar: StatusBarController?
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the contents
let contentView = ContentView()
// Set the SwiftUI's ContentView to the Popover's ContentViewController
popover.contentViewController = MainViewController()
popover.contentSize = NSSize(width: 360, height: 360)
popover.contentViewController?.view = NSHostingView(rootView: contentView)
// Create the Status Bar Item with the Popover
statusBar = StatusBarController.init(popover)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}

Embed a SwiftUI view in an AppKit view

I have a SwiftUI view MySwiftUIView:
import SwiftUI
struct MySwiftUIView: View {
var body: some View {
Text("Hello, World!")
}
}
I want to use it as part of an AppKit view. I tried the following code:
import Cocoa
import SwiftUI
class MyViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview( NSHostingView(rootView: MySwiftUIView()) )
}
}
with the corresponding storyboard:
After the code is built, the result is an empty window:
What I want is this:
How should I make this happen?
You setup subview programmatically, so constraints are on your responsibility, no exception for SwiftUI.
Here is correct variant (tested with Xcode 11.4):
override func viewDidLoad() {
super.viewDidLoad()
let myView = NSHostingView(rootView: MySwiftUIView())
myView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(myView)
myView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
myView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
}