How to bind selector to method inside NSObject [duplicate] - swift

This question already has an answer here:
NSStatusItem in NSStatusBar, action selector method not responding
(1 answer)
Closed 2 years ago.
In the macOS swiftui project I have the following code
import Cocoa
import SwiftUI
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var statusItem: StatusItem = StatusItem()
func applicationDidFinishLaunching(_ aNotification: Notification) {
}
#objc public func statusBarButtonClicked(sender: NSStatusBarButton) {
let event = NSApp.currentEvent!
if event.type == NSEvent.EventType.rightMouseUp {
print("Right click! (AppDelegate)")
} else {
print("Left click! (AppDelegate)")
}
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
import Cocoa
class StatusItem : NSObject {
private let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
override init() {
super.init()
self.item.button?.title = "title"
self.item.button?.action = #selector(self.statusBarButtonClicked(sender:))
self.item.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])
}
#objc public func statusBarButtonClicked(sender: NSStatusBarButton) {
let event = NSApp.currentEvent!
if event.type == NSEvent.EventType.rightMouseUp {
print("Right click! (NSObject)")
} else {
print("Left click! (NSObject)")
}
}
}
But when I click NSStatusBarButton it prints "Left click! (AppDelegate)" and "Right click! (AppDelegate)" to console.
Why does it happen? And how to make it call statusBarButtonClicked method defined in StatusItem class?

Setting the button's action is only one half of what you need to do. You also need to specify a target. Add
self.item.button?.target = self
and I believe you will get the result you are looking for.
What's happening is action specifies the selector to invoke and target specifies the object on which to invoke it.

Related

How to programmatically save the content written in TextView to a text file when terminating the app?

I want to save the content of the text view when the user closes the app.
I used the following codes to do so, but I cannot get the up-to-date string of the textview when closing the app. So, the produced text file is blank.
How should I access to the NSTextView from AppDelegate to save its content?
ViewController.swift
import Cocoa
class ViewController: NSViewController {
static var textViewString: String = ""
#IBOutlet var textView: NSTextView!{
didSet{
ViewController.textViewString = textView.string
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// start with hidden and show after moving to the main screen
DispatchQueue.main.async {
//keep the window top
self.view.window?.level = .floating
//set up the main display as the display where window shows up
let screens = NSScreen.screens
var pos = NSPoint()
pos.x = screens[0].visibleFrame.midX
pos.y = screens[0].visibleFrame.midY
self.view.window?.setFrameOrigin(pos)
self.view.window?.zoom(self)
self.view.window?.level = .floating
//self.view.window?.backgroundColor = NSColor.white
//stop the user from moving window
self.view.window?.isMovable = false
//disable resizable mode
self.view.window?.styleMask.remove(.resizable)
self.view.window?.setIsVisible(true)
}
//set up font for the reflectionForm
textView.font = NSFont.systemFont(ofSize: 30)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func saveTextViewString(){
if let documentDirectoryFileURL = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last {
let fileName = "savedText.txt"
let targetTextFilePath = documentDirectoryFileURL + "/" + fileName
do {
try ViewController.textViewString.write(toFile: targetTextFilePath, atomically: true, encoding: String.Encoding.utf8)
print("successfully recorded: \(ViewController.textViewString.description) at \(fileName.utf8CString)")
} catch let error as NSError {
print("failed to write: \(error)")
}
}
}
}
AppDelegate.swift
import Cocoa
#main
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
//save the string in the textview into a text file
ViewController().saveTextViewString()
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
Thank you for the #jnpdx's comments, I was able to solve this by just declaring ViewController in the AppDelegate by stating var viewController: ViewController!
ViewController.swift
import Cocoa
class ViewController: NSViewController {
#IBOutlet var textView: NSTextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// start with hidden and show after moving to the main screen
DispatchQueue.main.async {
//keep the window top
self.view.window?.level = .floating
//set up the main display as the display where window shows up
let screens = NSScreen.screens
var pos = NSPoint()
pos.x = screens[0].visibleFrame.midX
pos.y = screens[0].visibleFrame.midY
self.view.window?.setFrameOrigin(pos)
self.view.window?.zoom(self)
self.view.window?.level = .floating
//self.view.window?.backgroundColor = NSColor.white
//stop the user from moving window
self.view.window?.isMovable = false
//disable resizable mode
self.view.window?.styleMask.remove(.resizable)
self.view.window?.setIsVisible(true)
}
//set up font for the reflectionForm
textView.font = NSFont.systemFont(ofSize: 30)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func saveTextViewString(){
if let documentDirectoryFileURL = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last {
let fileName = "savedText.txt"
let targetTextFilePath = documentDirectoryFileURL + "/" + fileName
do {
try textView.string.write(toFile: targetTextFilePath, atomically: true, encoding: String.Encoding.utf8)
print("successfully recorded: \(textView.string.description) at \(fileName.utf8CString)")
} catch let error as NSError {
print("failed to write: \(error)")
}
}
}
}
AppDelegate.swift
import Cocoa
#main
class AppDelegate: NSObject, NSApplicationDelegate {
//connect viewController with ViewController
var viewController: ViewController!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
//save the string in the textview into a text file
viewController.saveTextViewString()
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}

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

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.

Xcode Project Setting for CGEventTap?

A while ago, I created a extremely simple Xcode project to test CGEventTap, and it works perfectly fine when I run from Xcode. The code is at the bottom.
However, if I create a new project on Xcode, paste the exactly the same code below, and run from Xcode, I get "Failed to create event tap".
Is there a project setting that I need to change in order to get CGEventTap working? I even tried to copy and paste info.plist from the old testing project to the new one.
I'm very puzzled. Thank you for your help!
// ViewController.swift
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
func myCGEventCallback(proxy : CGEventTapProxy, type : CGEventType, event : CGEvent, refcon : UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
if type == .keyDown || type == .keyUp || type == .flagsChanged {
let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
print(keyCode)
}
return Unmanaged.passRetained(event)
}
let eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue) | (1 << CGEventType.flagsChanged.rawValue)
guard let eventTap = CGEvent.tapCreate(tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: CGEventMask(eventMask), callback: myCGEventCallback, userInfo: nil) else {
debugPrint("Failed to create event tap")
exit(1)
}
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, CFRunLoopMode.commonModes)
CGEvent.tapEnable(tap: eventTap, enable: true)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
// AppDelegate.swift
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
The answer was to uncheck Sandbox from capabilities.

Swift function textfield got focus OSX

Currently I am having multiple textfields in a view. If the user taps at one of them there should be a function responding to the event. Is there a way on how to do react (if a textfield got the focus)? I tried it with the NSTextFieldDelegate method but there is no appropriate function for this event.
This is how my code looks at the moment:
class ViewController: NSViewController, NSTextFieldDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let textField = NSTextField(frame: CGRectMake(10, 10, 37, 17))
textField.stringValue = "Label"
textField.bordered = false
textField.backgroundColor = NSColor.controlColor()
view.addSubview(textField)
textField.delegate = self
let textField2 = NSTextField(frame: CGRectMake(30, 30, 37, 17))
textField2.stringValue = "Label"
textField2.bordered = false
textField2.backgroundColor = NSColor.controlColor()
view.addSubview(textField2)
textField2.delegate = self
}
func control(control: NSControl, textShouldBeginEditing fieldEditor: NSText) -> Bool {
print("working") // this only works if the user enters a charakter
return true
}
}
The textShouldBeginEditing function only handles the event if the user tries to enter a character but this isn't what I want. It has to handle the event if he clicks on the textfield.
Any ideas, thanks a lot?
Edit
func myAction(sender: NSView)
{
print("aktuell: \(sender)")
currentObject = sender
}
This is the function I want to call.
1) Create a subclass of NSTextField.
import Cocoa
class MyTextField: NSTextField {
override func mouseDown(theEvent:NSEvent) {
let viewController:ViewController = ViewController()
viewController.textFieldClicked()
}
}
2) With Interface building, select the text field you want to have a focus on. Navigate to Custom Class on the right pane. Then set the class of the text field to the one you have just created.
3) The following is an example for ViewController.
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
func textFieldClicked() -> Void {
print("You've clicked on me!")
}
}
4) Adding text fields programmatically...
import Cocoa
class ViewController: NSViewController {
let myField:MyTextField = MyTextField()
override func viewDidLoad() {
super.viewDidLoad()
//let myField:MyTextField = MyTextField()
myField.setFrameOrigin(NSMakePoint(20,70))
myField.setFrameSize(NSMakeSize(120,22))
let textField:NSTextField = NSTextField()
textField.setFrameOrigin(NSMakePoint(20,40))
textField.setFrameSize(NSMakeSize(120,22))
self.view.addSubview(myField)
self.view.addSubview(textField)
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
func textFieldClicked() -> Void {
print("You've clicked on me!")
}
}
I know it’s been answered some while ago but I did eventually find this solution for macOS in Swift 3 (it doesn’t work for Swift 4 unfortunately) which notifies when a textfield is clicked inside (and for each key stroke).
Add this delegate to your class:-
NSTextFieldDelegate
In viewDidLoad() add these:-
imputTextField.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: Notification.Name.NSTextViewDidChangeSelection, object: nil)
Then add this function:-
func textDidChange(_ notification: Notification) {
print("Its come here textDidChange")
guard (notification.object as? NSTextView) != nil else { return }
let numberOfCharatersInTextfield: Int = textFieldCell.accessibilityNumberOfCharacters()
print("numberOfCharatersInTextfield = \(numberOfCharatersInTextfield)")
}
Hope this helps others.