I am trying to listen for global keyboard events and make my app do specific things when certain key presses are detected. to do this, I would like to use:
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
//Add keyboard event listener.
NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler:
{
print("key down!");
print($0.keyCode);
});
}
}
and I am checking for accessibility permissions in my appDelegate.swift this way:
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
//check if the process is trusted for accessibility. This is important for us to listen for keyboard events system wide.
let checkOptPrompt = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString
let options = [checkOptPrompt: true]
let isAppTrusted = AXIsProcessTrustedWithOptions(options as CFDictionary?);
if(isAppTrusted != true)
{
print( "please allow accessibility API access to this app.");
}
}
I went ahead and granted Xcode accessibility permissions in system preferences. I however see the log statement asking to grant accessibility permissions to this app. simply put, here is my big question, divided into two smaller questions:
How do I give this app full privileges (give accessibility permissions)?
How do I show a pop-up asking the user to grant accessibility permissions if they are not enabled?
Related
I have a Mac app that has multiple windows with different views.
After calling view.window?.close() (which closes one of the open windows as my mac app has multiple open windows) how can I stop all of my other windows from coming to the front?
I want my other windows to stay right where they are and not move to the front.
For example, I have a layering of windows like the following (from back to front): 3 of my application windows, then 2 Finder windows overtop of those, then my front app window. When I close the front window, I want to prevent the back 3 windows from coming forward to cover the 2 Finder windows.
Please note that I am using Appkit/Cocoa.
You can do this by subclassing NSWindow and overriding canBecomeMain and canBecomeKey in the windows that you don't want to come forward.
A simple example is to create a document-based app which allows easy creation of multiple windows, one for each document. In the Document nib file, select the document window, open the Identity inspector and set the custom class to your custom NSWindow subclass (e.g. MDWindow).
My Swift is a bit rusty but the following is one naive implementation:
class MDWindow: NSWindow {
var mdShouldBecomeMain : Bool
override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask,
backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) {
mdShouldBecomeMain = true
super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
}
override func awakeFromNib() {
NSLog("\(type(of: self)).\(#function)() \(self.title)")
NotificationCenter.default.addObserver(self, selector: #selector(mdWindowWillClose),
name: NSWindow.willCloseNotification, object: nil)
}
#objc func mdWindowWillClose(_ notification: Notification) {
NSLog("\(type(of: self)).\(#function)() \(self.title)")
let window = notification.object as! NSWindow
if (window != self) {
mdShouldBecomeMain = false
}
}
override var canBecomeMain : Bool {
NSLog("\(type(of: self)).\(#function)() \(self.title)")
return mdShouldBecomeMain
}
override var canBecomeKey : Bool {
NSLog("\(type(of: self)).\(#function)() \(self.title)")
return mdShouldBecomeMain
}
}
In the NSWindow subclass in awakeFromNib, set up to observe the closing of all windows. That way, all windows will be notified whenever one of them is closing. When the windows receive that notification, they check to see if they are the one that's closing. If they're not the one that's closing, temporarily set the mdShouldBecomeMain flag to false for use in the canBecomeMain and canBecomeKey overridden properties. When you close a window, the app automatically checks the remaining windows to see if they can become key or main. By overriding these values you can prevent the window from coming forward.
Note, however, that you will need to eventually flip that flag back to true (perhaps on a timer?), otherwise even direct clicks on the window won't bring them forward at all.
I have a Swift Cocoa app that runs as an NSStatusItem, i.e., it presents a dropdown menu in Mac's top-right status bar. I would like to display extra "advanced" menu items when the user holds down the Option key while opening my status item menu. I can detect presses of the Option key, but my problem is that I cannot determine whether the key is down or up.
Here is the code I have so far in my AppDelegate.swift:
func applicationDidFinishLaunching(_ aNotification: Notification) {
NSEvent.addGlobalMonitorForEvents(matching: NSEvent.EventTypeMask.flagsChanged, handler: keyEvent);
}
func keyEvent(event: NSEvent) {
if let event = NSApp.currentEvent {
if event.modifierFlags.contains(.option) {
print("option key up/down event")
}
}
}
This works insofar as it prints the message when the Option key is either pressed down or released back up. The problem is that I don't know how to determine which state the Option key is in at any given time.
How can I adapt this code to know if the Option key is down or up?
EDIT: SO suggested that I edit my question to explain why it is not answered by Hide/Show menu item in application's main menu by pressing Option key. Briefly, the answers on that page, while probably workable, seemed either overly complex (adding timers, adding run loop observers, adding zero height custom views) or they were creating menu item alternates whereas I was trying to add additional menu items. Nevertheless, it is a good page and I recommend studying it if you also have this question.
Just ask for the current event and return a Bool, it's true when the key is pressed at the moment
func isOptionKeyPressed() -> Bool {
return NSEvent.modifierFlags.contains(.option)
}
addGlobalMonitorForEvents is not needed
I was able to find a working solution, inspired by (but not the same as) another answer here posted by vadian. This is my solution.
Eliminate the addGlobalMonitorForEvents and keyEvent code in my original question.
Create an an NSMenuDelegate like so:
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var extrasSeparator: NSMenuItem?
#IBOutlet weak var extrasSubmenu: NSMenuItem?
[...rest of AppDelegate...]
}
extension AppDelegate: NSMenuDelegate {
func menuWillOpen(_ menu: NSMenu) {
if NSEvent.modifierFlags.contains(.option) {
extrasSeparator?.isHidden = false
extrasSubmenu?.isHidden = false
} else {
extrasSeparator?.isHidden = true
extrasSubmenu?.isHidden = true
}
}
}
Caveat: this solution only works if the user presses and holds the Option key before clicking the NSStatusItem's icon. That's what was asked in the original question, but it may not be desirable in all cases.
I'm working on implementing ATTrackingManager to display Google AdMob banner ads in my project.
Here's my setup thus far:
func requestAds() {
appDelegate.adBannerView.adUnitID = GlobalVariables.bannerAdUnitID
appDelegate.adBannerView.rootViewController = self
appDelegate.adBannerView.adSize = GADPortraitAnchoredAdaptiveBannerAdSizeWithWidth(view.frame.size.width)
if #available(iOS 14, *) {
ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in
self.appDelegate.adBannerView.load(GADRequest())
// Add to view
self.view.addSubview(self.appDelegate.adBannerView)
})
} else {
self.appDelegate.adBannerView.load(GADRequest())
// Add to view
self.view.addSubview(self.appDelegate.adBannerView)
}
}
I'm calling this within viewDidLoad()
The permission alert displays properly. However, before I select an option for either Ask App Not to Track or Allow, I can see the banner is already displayed on the main View Controller.
This doesn't seem like the intended behavior.. Shouldn't the banner load after the user submits a response for Ask App Not to Track or Allow?
How can I prevent ads from loading & displaying until the user submits a response for the Privacy - Tracking Usage Description alert?
I have tried KeyDown and NSEvent, but they require a NSWindow object to be active. My hope is that I can put an app on the status bar and alert the user when it has pressed CapsLock, even if the user is in any other app. My app idea doesn't have a window for settings or anything else, is just an indicator. Can it even be done? I am guessing the AppDelegate but can't figure out how to make it receive modifier events. Any help really appreciated!
I have looked on stack overflow for a "duplicate" but no one has ever asked this question as far as I searched.
You just need to monitor the NSEvent modifier flags changed for capslock. The easiest way is to create a method that takes a NSEvent and check if the modifierFlags property intersection with .deviceIndependentFlagsMask contains .capsLock:
func isCapslockEnabled(with event: NSEvent) -> Bool {
event.modifierFlags.intersection(.deviceIndependentFlagsMask).contains(.capsLock)
}
Now you just need to add global monitoring event to your applicationDidFinishLaunching method:
func applicationDidFinishLaunching(_ aNotification: Notification) {
NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged, handler: flagsChanged)
}
And create a method to deal with the event of flagsChanged:
func flagsChanged(with event: NSEvent) {
print("Capslock is Enabled:", isCapslockEnabled(with: event))
}
I am attempting to open an app in one of two ways:
If the user has no UserDefaults saved, then open up a WelcomeViewController
If the user has UserDefaults saved, then open up a MenuContainerViewController as a home page
In step 2, if there are UserDefaults saved, then I need to log a user in using Firebase which I have through a function with a completion handler. If step 2 is the case, I want to open MenuContainerViewController within the completion block without any UI hiccups.
Here is the code I have currently:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
FirebaseApp.configure()
guard
let email = UserDefaults.standard.string(forKey: "email"),
let password = UserDefaults.standard.string(forKey: "password")
else {
// User has no defaults, open welcome screen
let welcomeViewController = WelcomeViewController()
self.window?.rootViewController = welcomeViewController
self.window?.makeKeyAndVisible()
return true
}
// User has defaults saved locally, open home screen of app
let authentificator = Authentificator()
authentificator.login(with: email, password) { result, _ in
if result {
let menuContainerViewController = MenuContainerViewController()
self.window?.rootViewController = menuContainerViewController
self.window?.makeKeyAndVisible()
}
}
return true
}
Here is a video of the current UI, when I need to run the completion handler, the transition is not smooth into the app (there is a brief second with a black screen).
Please help me figure out how to make a smooth app launch.
I've had to handle situations similarly in my Firebase applications. What I typically do is make an InitialViewController. This is the view controller that is always loaded, no matter what. This view controller is initially set up to seamlessly look exactly like the launch screen.
This is what the InitialViewController looks like in the interface builder:
And this is what my launch screen looks like:
So when I say they look exactly the same, I mean they look exactly the same. The sole purpose of this InitialViewController is to handle this asynchronous check and decide what to do next, all while looking like the launch screen. You may even copy/paste interface builder elements between the two view controllers.
So, within this InitialViewController, you make the authentication check in viewDidAppear(). If the user is logged in, we perform a segue to the home view controller. If not, we animate the user onboarding elements into place. The gifs demonstrating what I mean are pretty large (dimension-wise and data-wise), so they may take some time to load. You can find each one below:
User previously logged in.
User not previously logged in.
This is how I perform the check within InitialViewController:
#IBOutlet var loginButton: UIButton!
#IBOutlet var signupButton: UIButton!
#IBOutlet var stackView: UIStackView!
#IBOutlet var stackViewVerticalCenterConstraint: NSLayoutConstraint!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
//When the view appears, we want to check to see if the user is logged in.
//Remember, the interface builder is set up so that this view controller **initially** looks identical to the launch screen
//This gives the effect that the authentication check is occurring before the app actually finishes launching
checkLoginStatus()
}
func checkLoginStatus() {
//If the user was previously logged in, go ahead and segue to the main app without making them login again
guard
let email = UserDefaults.standard.string(forKey: "email"),
let password = UserDefaults.standard.string(forKey: "password")
else {
// User has no defaults, animate onboarding elements into place
presentElements()
return
}
let authentificator = Authentificator()
authentificator.login(with: email, password) { result, _ in
if result {
//User is authenticated, perform the segue to the first view controller you want the user to see when they are logged in
self.performSegue(withIdentifier: "SkipLogin", sender: self)
}
}
}
func presentElements() {
//This is the part where the illusion comes into play
//The storyboard elements, like the login and signup buttons were always here, they were just hidden
//Now, we are going to animate the onboarding UI elements into place
//If this function is never called, then the user will be unaware that the launchscreen was even replaced with this view controller that handles the authentication check for us
//Make buttons visible, but...
loginButton.isHidden = false
signupButton.isHidden = false
//...set their alpha to 0
loginButton.alpha = 0
signupButton.alpha = 0
//Calculate distance to slide up
//(stackView is the stack view that holds our elements like loginButton and signupButton. It is invisible, but it contains these buttons.)
//(stackViewVerticalCenterConstraint is the NSLayoutConstraint that determines our stackView's vertical position)
self.stackViewVerticalCenterConstraint.constant = (view.frame.height / 2) + (stackView.frame.height / 2)
//After half a second, we are going to animate the UI elements into place
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
UIView.animate(withDuration: 0.75) {
self.loginButton.alpha = 1
self.signupButton.alpha = 1
//Create correct vertical position for stackView
self.stackViewVerticalCenterConstraint.constant = (self.view.frame.height - self.navigationController!.navigationBar.frame.size.height - self.signupButton.frame.maxY - (self.stackView.frame.size.height / 2)) / 3
self.view.layoutIfNeeded()
}
}
}