Capture Function keys on Mac OS X - swift

I'm a long-time Objective-C user and slowly migrating towards Swift with new projects. I'm using CocoaPods for the bigger things and can't find a good library to cover this.
So I have this code inside my NSViewController viewDidLoad to start with:
_ = NSEvent.addGlobalMonitorForEventsMatchingMask(NSEventMask.KeyDownMask) {
(event) -> Void in
print("Event is \(event)")
}
let event = NSEvent.keyEventWithType(NSEventType.KeyDown,
location: CGPoint(x:0,y:0),
modifierFlags: NSEventModifierFlags(rawValue: 0),
timestamp: 0.0,
windowNumber: 0,
context: nil,
characters: "\n",
charactersIgnoringModifiers: "",
isARepeat: false,
keyCode: 0) //NSF7FunctionKey
NSApplication.sharedApplication().sendEvent(event!)
So the first event capturing works perfect after having my app checked in the System Preferences' Accessibility list. Anywhere in OS X it will capture key-presses. Now in the docs it says for Function-keys I should use keyEventWithType.
I found this gist and noticed that it addresses the same sharedApplication instance, yet I don't know how to catch the event. Do I delegate in a certain way? Also the F-key constant is int and the method says it only wants to receive Uint16. I can typecast it, but I guess I'm using it wrong.

Fixed it by using a CocoaPods pod that I found later. Works perfectly.

Related

Async dispatch inside a function - compiler getting too aggressive?

Ran across an interesting one today. Loading up a window controller that I pop up for some task-specific UI stuff. I was just working on the actual window layout dynamics, and wanted to suppress the loading of a configured view controller into one of the views - Just slapped a "return" in the load-up function, thinking that it would exit the function before the async call to load the VC.
func setContainer( container:ListContainer ){
self.container = container
return
DispatchQueue.main.async {
// return
let vs = ListsViewController()
vs.setLists(lists: self.container.itemLists)
vs.view.frame = CGRect(x: 0, y: 0, width: self.listsBox.frame.width, height: self.listsBox.frame.height)
self.listsBox.subviews.removeAll()
self.listsBox.addSubview(vs.view)
}
}
Interestingly, with that first return in there, the stuff inside the async call still executed, like the compiler is saying "that return doesn't apply to the async call because I said so."
The (currently commented out) return inside the async block does exactly the right thing, but I thought it odd that the async block would still run, with appearing after the return. Am I nuts?
macOS Catalina,
macOS application,
Xcode 12.0.1,
Swift 5 (presumably)
This is expected. Your function returns Void, a type that has a single value (). DispatchQueue.main.async also returns Void. With your return you are returning the result of DispatchQueue.main.async (which is again ()). As matt suggested in the comments add a ; after return.

Changing Badge Notification count with Pushy in Capacitor/Cordova on iOS

I have a Capacitor/Cordova app using Pushy (https://pushy.me) to handle push notifications. Sending push notifications to devices seems to work fine, and as part of that transaction I can set the app's notification count on the icon when the app is closed.
However, Pushy only seems to have an JS option to clear the counter Pushy.clearBadge(); rather than change it to a specific number from within the app when it's running. The scenario being if a user has read one message, but leaves two unread and then closes the app, I want the counter to be correct.
Digging through the PushyPlugin.swift code, the function for Pushy.clearBadge(); looks like this:
#objc(clearBadge:)
func clearBadge(command: CDVInvokedUrlCommand) {
// Clear app badge
UIApplication.shared.applicationIconBadgeNumber = 0;
// Always success
self.commandDelegate!.send(
CDVPluginResult(
status: CDVCommandStatus_OK
),
callbackId: command.callbackId
)
}
Which is tantalisingly close to giving me what I need, if only I could pass an integer other than zero in that line UIApplication.shared.applicationIconBadgeNumber = 0;
My knowledge of Swift is zilch, other than recognising familiar programming syntax. I thought I'd have a hack at it and tried adding this to the PushyPlugin.swift file:
#objc(setBadge:)
func setBadge(command: CDVInvokedUrlCommand) {
// Clear app badge
UIApplication.shared.applicationIconBadgeNumber = 100;
// Always success
self.commandDelegate!.send(
CDVPluginResult(
status: CDVCommandStatus_OK
),
callbackId: command.callbackId
)
}
But the app coughed up Pushy.setBadge is not a function when I tried to give that a spin (note, I was testing with 100 just to see what would happen, ideally I'd want to pass an integer to that newly hacked in function).
So what I've learnt so far is I know nothing about Swift.
Am I sort of on the right lines with this, or is there a better way to set the badge counter?
Ok, so I spotted in the pushy-cordova/www folder a Pushy.js file which I needed to add my new function to
{
name: 'setBadge',
noError: true,
noCallback: true,
platforms: ['ios']
},
Then, a bit of trial and error with the PushyPlugin.swift function I added to this:
#objc(setBadge:)
func setBadge(command: CDVInvokedUrlCommand) {
UIApplication.shared.applicationIconBadgeNumber = command.arguments[0] as! Int;
// Always success
self.commandDelegate!.send(
CDVPluginResult(
status: CDVCommandStatus_OK
),
callbackId: command.callbackId
)
}
Then this can be called in my Capacitor project with Pushy.setBadge(X); (X being the int value you want to display on your icon).

MacOS Quartz Event Tap listening to wrong events

I am trying to intercept mouse move events using the CGEvent.tapCreate(tap:place:options:eventsOfInterest:callback:userInfo:) method as shown below:
let cfMachPort = CGEvent.tapCreate(tap: CGEventTapLocation.cghidEventTap,
place: CGEventTapPlacement.headInsertEventTap,
options: CGEventTapOptions.defaultTap,
eventsOfInterest:CGEventMask(CGEventType.mouseMoved.rawValue),
callback: {(eventTapProxy, eventType, event, mutablePointer) -> Unmanaged<CGEvent>? in event
print(event.type.rawValue) //Breakpoint
return nil
}, userInfo: nil)
let runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, cfMachPort!, 0)
let runLoop = RunLoop.current
let cfRunLoop = runLoop.getCFRunLoop()
CFRunLoopAddSource(cfRunLoop, runloopSource, CFRunLoopMode.defaultMode)
I pass as event type eventsOfInterest mouseMoved events with a raw value of 5 as seen in the documentation. But for some reason my print() is not executed unless I click with the mouse. Inspecting the send mouse event in the debugger gives me a raw value of 2, which according to the documentation is a leftMouseUp event.
In the documentation for CGEvent.tapCreate(tap:place:options:eventsOfInterest:callback:userInfo:) it says:
Event taps receive key up and key down events [...]
So it seems like the method ignores mouseMoved events in general?! But how am I supposed to listen to mouseMoved events? I am trying to prevent my cursor (custom cursor) from being replaced (for example when I hover over the application dock at the bottom of the screen).
You need to bitshift the CGEventType value used to create the CGEventMask parameter. In Objective-C, there is a macro to do this: CGEventMaskBit.
From the CGEventMask documentation:
to form the bit mask, use the CGEventMaskBit macro to convert each constant into an event mask and then OR the individual masks together
I don't know the equivalent mechanism in swift; but the macro itself looks like this:
*/ #define CGEventMaskBit(eventType) ((CGEventMask)1 << (eventType))
In your example, it's sufficient to just manually shift the argument; e.g.
eventsOfInterest:CGEventMask(1 << CGEventType.mouseMoved.rawValue),
I would point out that the code example given in the question is a little dangerous; as it creates a default event tap and then drops the events rather than allowing them to be processed. This messes up mouse click handling and it was tricky to actually terminate the application using the mouse. Anyone running the example could set the event tap type to CGEventTapOptions.listenOnly to prevent that.
Here is a way to listen for mouseMove global events (tested with Xcode 11.2+, macOS 10.15)
// ... say in AppDelegate
var globalObserver: Any!
var localObserver: Any!
func applicationDidFinishLaunching(_ aNotification: Notification) {
globalObserver = NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) { event in
let location = event.locationInWindow
print("in background: {\(location.x), \(location.y)}")
}
localObserver = NSEvent.addLocalMonitorForEvents(matching: .mouseMoved) { event in
let location = event.locationInWindow
print("active: {\(location.x), \(location.y)}")
return event
}
...
There's another thing incorrect in your code, although you might be lucky and it isn't normally causing a problem.
As documented for the mode parameter to CFRunLoopAddSource: "Use the constant kCFRunLoopCommonModes to add source to the set of objects monitored by all the common modes."
That third parameter should instead be CFRunLoopMode.commonModes.
What you have, CFRunLoopMode.defaultMode aka kCFRunLoopDefaultMode, is instead for use when calling CFRunLoopRun.

safari app extensions: broadcast a message to all tabs from swift background process

In a legacy extension it was possible to iterate over safari.application.activeBrowserWindow.tabs to send a message to all tabs registered with the extension.
Is there any equivalent available with the new safari app extensions?
I've been trough the docs but did not find any hints on how to achieve this very basic thing.
A horrible workaround would be to have all tabs ping the Swift background, but really this is such a basic thing it seems absurd that it is not available or covered by the docs, am I missing something?
I also tried keeping a weak map of all "page" instances as seen by "messageReceived" handler in the hope the SFSafariPage reference would be kept until a tab is closed but they are instead lost almost immediately, suggesting they are more message channels than actual Safari pages.
The way should be next:
in injected.js you send the message to your app-ext, e.g.
document.addEventListener("DOMContentLoaded", function (event) {
safari.extension.dispatchMessage('REGISTER_PAGE')
})
And in app-ext handle it with smth like this:
var pages: [SFSafariPage] = []
class SafariExtensionHandler: SFSafariExtensionHandler {
override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String : Any]?) {
switch messageName {
case "REGISTER_PAGE":
if !pages.contains(page) {
pages.append(page)
}
default:
return
}
}
}
Well, then you can send the message to all opened pages during runtime by smth like this:
for p in pages {
p.dispatchMessageToScript(withName: "message name", userInfo: userInfo)
}
It looks hacky but yet workable. Enjoy :)

AppleWatch Speech-to-Text functionality not working

I am trying to implement Speech-to-text feature for watchkit app.
I referred this question which has sample code.
Following is the code I tried:
self.presentTextInputControllerWithSuggestions(["Start it", "Stop it"], allowedInputMode: .Plain, completion: { (selectedAnswers) -> Void in
if selectedAnswers.count > 0 {
if let spokenReply = selectedAnswers[0] as? String {
self.label.setText("\(spokenReply)")
}
}
})
label is a label to display text I speak.
When I run it, it shows the screen where you are supposed to speak (Siri kind of screen) and you have two options on top: ‘Cancel', and ‘Done'. Once I am done speaking, I tap on ‘Done’ but screen doesn’t go away or shows me initial screen, I always have to tap on ‘Cancel’ to go back, and I don’t get any speech data in form of text. I checked it and seems like selectedAnswers is always an empty array, unless I tap on the "Start it"/"Stop it" options.
Can anyone help me with this? I want to show the spoken message on label. I have code inside awakeWithContext method in InterfaceController.swift file, am I supposed to put it somewhere else?
I am using iPhone with iOS 9 beta 2 and watchOS 2 beta on AppleWatch.
Thanks!
You can ask for user input and give him suggestion (see Swift example bellow).
self.presentTextInputControllerWithSuggestions(["suggestion 1", "suggestion 2"] allowedInputMode: .Plain, completion: { (answers) -> Void in
if answers && answers.count > 0 {
if let answer = answers[0] as? String {
println("\answer")
}
}
})
If suggestion is nil it goes directly to dictation. It is not working on the simulator but it is on real watch.
Your approach is correct but something is wrong with your SIRI , try changing the language.
It should work like these.