Swift open new window programmatically in macOS - swift

In Swift using macOS:
By removing #NSApplicationMain (and making a subclass of NSWindowController) in AppDelegate I create the main window programmatically, without using storyboards, etc.:
//#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var viewController: NSViewController!
var windowController: NSWindowController!
func configMainWindow(_ viewController: NSViewController) {
window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
styleMask: [NSWindow.StyleMask.closable, NSWindow.StyleMask.titled, NSWindow.StyleMask.resizable, NSWindow.StyleMask.miniaturizable],
backing: NSWindow.BackingStoreType.buffered,
defer: false)
window.title = "My App"
window.setFrameAutosaveName("My App")
window.center()
window.isOpaque = false
window.isMovableByWindowBackground = true
window.backgroundColor = NSColor.white
window.makeKeyAndOrderFront(nil)
window.contentViewController = viewController
windowController = WindowController(window: window)
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
viewController = ViewController()
configMainWindow(viewController)
}
}
The windowController attaches a toolbar, statusBar and menuBar:
(Only the menuBar is loaded from a NIB. A class MainMenuAction handles the menu choices.)
class WindowController: NSWindowController, NSWindowDelegate {
var toolbarController = ToolbarController()
var statusBarController = StatusBarController()
var mainMenuAction: MainMenuAction?
override init(window: NSWindow?) {
super.init(window: window)
window?.toolbar = toolbarController.toolbar
window?.delegate = self
var topLevelObjects: NSArray? = []
Bundle.main.loadNibNamed("MainMenu", owner: self, topLevelObjects: &topLevelObjects)
NSApplication.shared.mainMenu = topLevelObjects?.filter { $0 is NSMenu }.first as? NSMenu
self.mainMenuAction = MainMenuAction.shared
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func windowDidLoad() {
if let window = window {
if let view = window.contentView {
view.wantsLayer = true
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
window.backgroundColor = .white
}
}
}
}
Additionally, I needed to add a main.swift file:
(thanks for reminding me, apodidae)
let delegate = AppDelegate()
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
I tried:
let vc = NSViewController()
let win = configWindow(vc, windowWidth: 420, windowHeight: 673)
let wc = NSWindowController(window: win)
wc.window?.present
vc.view.window?.contentViewController = vc
Where I copied the method configMainWindow from AppDelegate, to create configWindow that allowed me the specify size and the vc.
But how can I open a new window (from some method in a new class - programmatically) with a custom size and style?
Please provide a code example.

The following demo creates a second window called by a method in a custom class. It may be run in Xcode by adding a 'swift.main' file and replacing AppDelegate with the following code:
import Cocoa
class Abc : NSObject {
var panel: NSPanel!
func buildWnd2() {
let _panelW : CGFloat = 200
let _panelH : CGFloat = 200
panel = NSPanel(contentRect:NSMakeRect(9300, 1300, _panelW, _panelH), styleMask:[.titled, .closable, .utilityWindow],
backing:.buffered, defer: false)
panel.isFloatingPanel = true
panel.title = "NSPanel"
panel.orderFront(nil)
}
}
let abc = Abc()
class AppDelegate: NSObject, NSApplicationDelegate {
var window:NSWindow!
#objc func myBtnAction(_ sender:AnyObject ) {
abc.buildWnd2()
}
func buildMenu() {
let mainMenu = NSMenu()
NSApp.mainMenu = mainMenu
// **** App menu **** //
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu()
appMenuItem.submenu = appMenu
appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q")
}
func buildWnd() {
let _wndW : CGFloat = 400
let _wndH : CGFloat = 300
window = NSWindow(contentRect:NSMakeRect(0,0,_wndW,_wndH),styleMask:[.titled, .closable, .miniaturizable, .resizable], backing:.buffered, defer:false)
window.center()
window.title = "Swift Test Window"
window.makeKeyAndOrderFront(window)
// **** Button **** //
let myBtn = NSButton (frame:NSMakeRect( 100, 100, 175, 30 ))
myBtn.bezelStyle = .rounded
myBtn.autoresizingMask = [.maxXMargin,.minYMargin]
myBtn.title = "Build Second Window"
myBtn.action = #selector(self.myBtnAction(_:))
window.contentView!.addSubview (myBtn)
// **** Quit btn **** //
let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
quitBtn.bezelStyle = .circular
quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
quitBtn.title = "Q"
quitBtn.action = #selector(NSApplication.terminate)
window.contentView!.addSubview(quitBtn)
}
func applicationDidFinishLaunching(_ notification: Notification) {
buildMenu()
buildWnd()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
let appDelegate = AppDelegate()
// **** main.swift **** //
let app = NSApplication.shared
app.delegate = appDelegate
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps:true)
app.run()

Related

NSStatusItem menu Action not firing

The NSStatusItem menu shows correctly but when I click on menu the action not fire.
Since the app is compatible with Mac Catalyst, I created framework then passing Framework to main app in order to show the menu of NSStatusItem, it work correctly but I have the issue for action of the menu that doesn't work.
Here is my code:
#objc class AppKitController: NSObject {
var statusBarItem: StatusBarItemControler!
override init() {
super.init()
print("[AppKitController] Loaded successfully")
self.statusBarItem = StatusBarItemControler()
self.statusBarItem.updateTitle()
self.statusBarItem.updateMenu()
}
}
class StatusBarItemControler {
let item: NSStatusItem
init() {
self.item = NSStatusBar.system.statusItem(
withLength: NSStatusItem.variableLength
)
let statusBarMenu = NSMenu(title: "APPMenu")
self.item.menu = statusBarMenu
}
func updateTitle() {
let title = "AppMenu"
print("Update title")
DispatchQueue.main.async {
if let button = self.item.button {
button.title = "\(title)"
button.target = self
}
}
}
func updateMenu() {
if let statusBarMenu = self.item.menu {
statusBarMenu.autoenablesItems = false
statusBarMenu.removeAllItems()
statusBarMenu.addItem(NSMenuItem.separator())
statusBarMenu.addItem(NSMenuItem.separator())
self.createPreferencesSection()
}
}
func createPreferencesSection() {
self.item.menu!.addItem(
withTitle: "Open",
action: #selector(openPrefecencesWindow),
keyEquivalent: ",")
self.item.menu!.addItem(
withTitle: "Quit",
action: #selector(quit),
keyEquivalent: "q")
}
#objc func openPrefecencesWindow(_: NSStatusBarButton?) {
print("Open preferences window")
}
#objc func quit(_: NSStatusBarButton?) {
print("Open preferences window")
}
}
Thank you #Alexander, I have found the solution and it works.
class AppKitController: NSObject,NSApplicationDelegate,NSWindowDelegate {
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
override init() {
super.init()
NSApplication.shared.delegate = self
NSApplication.shared.mainWindow?.delegate = self
statusItem.button?.title = "Your_App_Name"
statusItem.menu = createMenu()
print("[AppKitController] Loaded successfully")
}
func createMenu() -> NSMenu{
let menu = NSMenu()
let openMenuItem = menu.addItem(withTitle: "Open", action: #selector(openMenu), keyEquivalent: "")
openMenuItem.target = self
return menu
}
#objc func openMenu(_ sender:Any?){
print("Open menu called")
}
func windowShouldClose(_ sender: NSWindow) -> Bool {
print("Window should close")
return false
}
}

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

IBAction stops working when I connect IBOutlet

I have an IBAction wired up to a button in my storyboard that plays a sound. This works just fine. However, when I wire up an IBOutlet to the same button, the code for the outlet takes over and the IBAction stops working. In this case, the IBOutlet serves the purpose of making the button pulse with animation. Here is my code:
import UIKit
import Firebase
import AVFoundation
class Page1: UIViewController, AVAudioPlayerDelegate, UIAlertViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
#IBOutlet weak var testpulse: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let url = Bundle.main.url(forResource: "ahh", withExtension: "mp3")
do {
ahh = try AVAudioPlayer(contentsOf: url!)
ahh.prepareToPlay()
}
catch let error as NSError {
print(error.debugDescription)
}
testpulse.isUserInteractionEnabled = true
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(Page1.addPulse))
tapGestureRecognizer.numberOfTapsRequired = 1
testpulse.addGestureRecognizer(tapGestureRecognizer)
}
#objc func addPulse(){
let pulse = Pulsing(numberOfPulses: 1, radius: 110, position: testpulse.center)
pulse.animationDuration = 0.8
pulse.backgroundColor = UIColor.purple.cgColor
self.view.layer.insertSublayer(pulse, below: testpulse.layer)
}
#IBAction func ahh(_ sender: Any) {
ahh.play()
}
}
And for my Swift animation file I have this:
import UIKit
class Pulsing: CALayer {
var animationGroup = CAAnimationGroup()
var initialPulseScale:Float = 0
var nextPulseAfter:TimeInterval = 0
var animationDuration:TimeInterval = 1.5
var radius:CGFloat = 200
var numberOfPulses:Float = Float.infinity
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
init (numberOfPulses:Float = Float.infinity, radius:CGFloat, position:CGPoint) {
super.init()
self.backgroundColor = UIColor.black.cgColor
self.contentsScale = UIScreen.main.scale
self.opacity = 0
self.radius = radius
self.numberOfPulses = numberOfPulses
self.position = position
self.bounds = CGRect(x: 0, y: 0, width: radius * 2, height: radius * 2)
//self.cornerRadius = radius
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
self.setupAnimationGroup()
DispatchQueue.main.async {
self.add(self.animationGroup, forKey: "pulse")
}
}
setupAnimationGroup()
self.add(animationGroup, forKey: "pulse")
}
func createScaleAnimation () -> CABasicAnimation {
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
scaleAnimation.fromValue = NSNumber(value: initialPulseScale)
scaleAnimation.toValue = NSNumber(value: 1)
scaleAnimation.duration = animationDuration
return scaleAnimation
}
func createOpacityAnimation() -> CAKeyframeAnimation? {
let opacityAnimation = CAKeyframeAnimation(keyPath: "opacity")
opacityAnimation.duration = animationDuration
opacityAnimation.values = [0.4, 0.8, 0]
opacityAnimation.keyTimes = [0, 0.2, 1]
return opacityAnimation
}
func setupAnimationGroup() {
self.animationGroup = CAAnimationGroup()
self.animationGroup.duration = animationDuration + nextPulseAfter
self.animationGroup.repeatCount = numberOfPulses
let defaultCurve = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault)
self.animationGroup.timingFunction = defaultCurve
self.animationGroup.animations = [createScaleAnimation(), createOpacityAnimation()!]
}
}
When I connect the IBAction to my button, the sound works. However when I connect the IBOutlet, the animation works but the IBAction doesn't. What could be causing this? It's still connected in the storyboard.
import UIKit
import Firebase
import AVFoundation
class Page1: UIViewController, AVAudioPlayerDelegate, UIAlertViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
#IBOutlet weak var testpulse: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let url = Bundle.main.url(forResource: "ahh", withExtension: "mp3")
do {
ahh = try AVAudioPlayer(contentsOf: url!)
ahh.prepareToPlay()
}
catch let error as NSError {
print(error.debugDescription)
}
testpulse.isUserInteractionEnabled = true
}
#objc func addPulse(){
let pulse = Pulsing(numberOfPulses: 1, radius: 110, position: testpulse.center)
pulse.animationDuration = 0.8
pulse.backgroundColor = UIColor.purple.cgColor
self.view.layer.insertSublayer(pulse, below: testpulse.layer)
}
#IBAction func ahh(_ sender: Any) {
ahh.play()
addPulse()
}
}

Swift 2.2 selector in NSMenuItem

I have a simple one file menu bar app in swift:
import Cocoa
class StatusBarApp : NSObject {
func buildMenu() {
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)
statusItem.title = "StatusBarApp"
let menu = NSMenu()
let aboutMenuItem = NSMenuItem()
aboutMenuItem.title = "About"
aboutMenuItem.target = self
aboutMenuItem.action = #selector(about)
menu.addItem(aboutMenuItem)
statusItem.menu = menu
}
func about() {
print("XXX")
}
}
NSApplication.sharedApplication()
StatusBarApp().buildMenu()
NSApp.run()
I can't make the "About" menu bar item to connected to the about() function. When I run the app, the "About" item is disabled.
How do I pass the selector to menu item action in Swift 2.2? Thanks
The selector is supposed to have a parameter (the NSMenuItem instance)
aboutMenuItem.action = #selector(StatusBarApp.about(_:))
...
func about(sender : NSMenuItem) {
print("XXX")
}
Edit:
The solution is to run the app as full Cocoa app including its delegate.
I added a second menu item to terminate the app.
import Cocoa
class StatusBarApp : NSObject, NSApplicationDelegate {
var statusItem : NSStatusItem!
func applicationDidFinishLaunching(aNotification: NSNotification) {
statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)
statusItem.title = "StatusBarApp"
let menu = NSMenu()
let aboutMenuItem = NSMenuItem(title:"About", action:#selector(StatusBarApp.about(_:)), keyEquivalent:"")
aboutMenuItem.target = self
let quitMenuItem = NSMenuItem(title:"Quit", action:#selector(StatusBarApp.quit(_:)), keyEquivalent:"")
quitMenuItem.target = self
menu.addItem(aboutMenuItem)
menu.addItem(quitMenuItem)
statusItem.menu = menu
}
func about(sender : NSMenuItem) {
print("XXX")
}
func quit(sender : NSMenuItem) {
NSApp.terminate(self)
}
}
NSApplication.sharedApplication()
let statusBarApp = StatusBarApp()
NSApp.delegate = statusBarApp
NSApp.run()
update action
aboutMenuItem.action = Selector("about")
and add
aboutMenuItem.enabled = true
Consider this:
import Cocoa
class StatusBarApp : NSObject {
func buildMenu() {
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)
statusItem.title = "StatusBarApp"
let menu = NSMenu()
let aboutMenuItem = NSMenuItem()
aboutMenuItem.title = "About"
aboutMenuItem.target = self
aboutMenuItem.action = #selector(about)
menu.addItem(aboutMenuItem)
statusItem.menu = menu
}
func about() {
print("XXX")
}
}
let app = StatusBarApp()
NSApplication.sharedApplication()
app.buildMenu()
NSApp.run()

NSSegmentedControl action not firing

I am not using any Storyboards/NIBs, I'm creating all my UI programmatically.
Here's the main window controller:
class MainWindowController: NSWindowController, NSToolbarDelegate {
var toolbar: NSToolbar!
var segmentedControl: NSSegmentedControl!
override func loadWindow() {
self.window = NSWindow(contentRect: .init(origin: .zero, size: .init(width: 640, height: 480)),
styleMask: NSWindow.StyleMask(rawValue: (NSWindow.StyleMask.closable.rawValue | NSWindow.StyleMask.titled.rawValue | NSWindow.StyleMask.miniaturizable.rawValue | NSWindow.StyleMask.resizable.rawValue)),
backing: .buffered, defer: true)
}
override init(window: NSWindow?) {
super.init(window: window)
loadWindow()
self.window?.center()
self.segmentedControl = NSSegmentedControl(labels: ["1", "2", "3"], trackingMode: NSSegmentedControl.SwitchTracking.selectOne, target: self, action: #selector(switchTabs))
self.segmentedControl.setSelected(true, forSegment: 0)
self.toolbar = NSToolbar(identifier: .init("MainToolbar"))
self.toolbar.delegate = self
self.toolbar.displayMode = .iconOnly
self.window?.toolbar = self.toolbar
self.window?.contentViewController = MainSplitViewController()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Selectors
#objc func switchTabs(sender: Any) {
let segmentedControl = sender as! NSSegmentedControl
let tabVC = (self.window!.contentViewController as! MainSplitViewController!).tabViewController
tabVC.tabView.selectTabViewItem(at: segmentedControl.selectedSegment)
}
// MARK: - NSToolbarDelegate
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [
NSToolbarItem.Identifier.init("Add"),
NSToolbarItem.Identifier.flexibleSpace,
NSToolbarItem.Identifier.init("NSSegmentedControl"),
NSToolbarItem.Identifier.flexibleSpace,
NSToolbarItem.Identifier.init("Search")
]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [
NSToolbarItem.Identifier.flexibleSpace,
NSToolbarItem.Identifier.init("NSSegmentedControl"),
NSToolbarItem.Identifier.init("Search"),
NSToolbarItem.Identifier.init("Add")
]
}
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
switch itemIdentifier {
case NSToolbarItem.Identifier.flexibleSpace:
return NSToolbarItem(itemIdentifier: itemIdentifier)
case NSToolbarItem.Identifier.init("NSSegmentedControl"):
let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
toolbarItem.view = self.segmentedControl
return toolbarItem
case NSToolbarItem.Identifier.init("Search"):
let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
let searchField = NSSearchField(frame: NSRect(origin: .zero, size: CGSize(width: 64, height: 64 )))
toolbarItem.view = searchField
return toolbarItem
case NSToolbarItem.Identifier.init("Add"):
let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
let addButton = NSButton(title: "Add", target: self, action: nil)
toolbarItem.view = addButton
return toolbarItem
default:
return nil
}
}
}
I have three NSViewControllers embedded in an NSTabViewController in my window. I want to be able to connect them to the selection of the NSSegmentedControl in my NSToolbar.
However, the action is never being fired. The switchTabs method is never being called. If I call the function directly, then it works! But nothing happens when I select a NSSegmentedCell.
What's going wrong here?
Am I doing the instantiation of the window correctly? Is my usage of loadWindow correct?
I've replaced your MainSplitViewController with a simply NSViewController subclass with its own nib (because there's only so much 'creating the interface in code' I'm willing to do), and it runs just fine - the toolbar gets created, the segments fire their action, selectedSegment reports the correct tag.
This makes your contentViewController and its associated view the most likely culprit: your toolbar code works just fine.
I was not able to get this working in it's current set up, so I set the target and action to my NSTabViewController and a selector in that class.