NSPopover not aligned to NSStatusItem menubar icon - swift

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)
}
}

Related

How can I use native macOS windows in ZStack form in SwiftUI?

I want be able to use macOS native windows like we can use views in SwiftUI, with this condition that the first window has isMovableByWindowBackground = false and the second one has isMovableByWindowBackground = true and with moving the movable window the unmovable window following the movable one.
The current code make the all window movable, which is not my goal, I want just the red view be movable, and with movement of this red view the yellow window is following the red window.
struct ContentView: View {
var body: some View {
VStack {
Button("show window") {
ZStack {
myView()
myView2()
}
.openInWindow(sender: nil)
}
}
.frame(width: 200, height: 200)
}
func myView() -> some View {
return Color.yellow.frame(width: 400, height: 400)
}
func myView2() -> some View {
return Color.red.frame(width: 25, height: 25).overlay(Image(systemName: "arrow.up.and.down.and.arrow.left.and.right"))
}
}
extension View {
#discardableResult
func openInWindow(sender: Any?) -> NSWindow {
let controller = NSHostingController(rootView: self)
let window = NSWindow(contentViewController: controller)
window.contentViewController = controller
window.makeKeyAndOrderFront(sender)
window.titleVisibility = .hidden
window.toolbar = nil
window.styleMask = .fullSizeContentView
window.isMovableByWindowBackground = true
return window
}
}

Hiding bottom line on navigation controller in SwiftUI

How do I hide this bottom bar on a UINavigationController with SwiftUI? So far I have found only solutions for UIKit, but nothing for SwiftUI.
Look at the accepted answer: SwiftUI Remove NavigationBar Bottom Border
Before:
After:
import SwiftUI
struct TestView: View {
init(){
let appearance = UINavigationBarAppearance()
appearance.shadowColor = .clear
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
}
var body: some View {
NavigationView{
ScrollView{
ForEach(0 ..< 20){ num in
Text("Num - \(num)")
.padding()
}
}
.navigationTitle("Learn")
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
I had the same issue when using a UIHostingController. So I ended up adding a child UIHostingController to a UIViewController and setting the shadow that way.
#IBOutlet weak var theContainer: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = UIColor(Color("navbackground"))
appearance.shadowColor = .clear
self.navigationController?.navigationBar.scrollEdgeAppearance = appearance
self.navigationController?.navigationBar.standardAppearance = appearance
let childView = UIHostingController(rootView: SettingsView())
addChild(childView)
childView.view.frame = theContainer.bounds
theContainer.addSubview(childView.view)
childView.didMove(toParent: self)
}

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 macOS hover on Button change cursor mouse

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
}
}

Add New Window and make it key window in SWIFTUI

I want to add a new window as I want to create a a full screen loader. I have tried to add a new window in and set that as rootviewcontroller. But it is not adding into the windows hierarchy. Below is my code. I am learning swiftUI. Any help is appreciated.
let window = UIWindow()
window.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
window.backgroundColor = .blue
window.isHidden = false
window.rootViewController = UIHostingController(rootView: Text("Loading...."))
window.makeKeyAndVisible()
You need to wrap UIActivityIndicator and make it UIViewRepresentable.
struct ActivityIndicator: UIViewRepresentable {
#Binding var isAnimating: Bool
style: UIActivityIndicatorView.Style
func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
Then you can use it as follows – here’s an example of a loading overlay.
Note: I prefer using ZStack, rather than overlay(:_), so I know exactly what’s
going on in my implementation
struct LoadingView: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text("Loading...")
ActivityIndicator(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
If you want to show alternate window, you have to connect new UIWindow to existed window scene, so here is a demo of possible approach to do this in SceneDelegate, based on posted notifications.
// notification names declarations
let showFullScreenLoader = NSNotification.Name("showFullScreenLoader")
let hideFullScreenLoader = NSNotification.Name("hideFullScreenLoader")
// demo alternate window
struct FullScreenLoader: View {
var body: some View {
VStack {
Spacer()
Button("Close Loader") {
NotificationCenter.default.post(name: hideFullScreenLoader, object: nil)
}
}
}
}
// demo main window
struct MainView: View {
var body: some View {
VStack {
Button("Show Loader") {
NotificationCenter.default.post(name: showFullScreenLoader, object: nil)
}
Spacer()
}
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow? // << main window
var loaderWindow: UIWindow? // << alternate window
private var subscribers = Set<AnyCancellable>()
func makeAntherWindow() { // << alternate window creation
if let windowScene = window?.windowScene {
let newWindow = UIWindow(windowScene: windowScene)
let contentView = FullScreenLoader()
newWindow.rootViewController = UIHostingController(rootView: contentView)
self.loaderWindow = newWindow
newWindow.makeKeyAndVisible()
}
}
override init() {
super.init()
NotificationCenter.default.publisher(for: hideFullScreenLoader)
.sink(receiveValue: { _ in
self.loaderWindow = nil // remove alternate window
})
.store(in: &self.subscribers)
NotificationCenter.default.publisher(for: showFullScreenLoader)
.sink(receiveValue: { _ in
self.makeAntherWindow() // create alternate window
})
.store(in: &self.subscribers)
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = MainView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
...