How to Use AXObserverAddNotification in Swift? - swift

How I can use AXObserverAddNotification in Swift to detect an UI change?
There's a great answer here in Obj C.
How can my app detect a change to another app's window?
How to translate the answer to Swift?
The testing code below compiles and runs without an error, but callBack doesn't seem to get executed when I change main window.
func observe(app:pid_t, element:AXUIElement) {
var observer: AXObserver? = nil
func callBack(observer: AXObserver?, element: AXUIElement?, notification: CFString, refcon: UnsafeMutableRawPointer?) {
print("Fired! \(notification)")
}
if AXObserverCreate(app, callBack, &observer) == .success {
print("AXObserverCreate success!")
if AXObserverAddNotification(observer!, element, kAXMainWindowChangedNotification as CFString, UnsafeMutableRawPointer(Unmanaged.passRetained(self).toOpaque())) == .success {
print("AXObserverAddNotification success!")
CFRunLoopAddSource(CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(observer!), .defaultMode)
print("Watching \(element.value(of:"AXTitle"))")
}
}
}

#kyo-ago did a great job on encapsulating those C-like swift APIs in his project.

Related

How use AXObserverAddNotification?

I want to trigger a function when a Window focus is changed
How can I add an observer based on kAXFocusedWindowChangedNotification
I am trying to use like this:
override func viewDidLoad() {
super.viewDidLoad()
var observer: AXObserver? = nil
let pid_app: pid_t = NSWorkspace.shared.runningApplications.first(where: {$0.localizedName == "Safari"})!.processIdentifier as pid_t
let app_ref = AXUIElementCreateApplication(pid_app)
func callBack(observer: AXObserver?, element: AXUIElement?, notification: CFString, refcon: UnsafeMutableRawPointer?) {print("Fired!")}
if AXObserverCreate(pid_app, callBack, &observer) == .success {
print("AXObserverCreate success!")
if AXObserverAddNotification(observer!, app_ref, kAXFocusedWindowChangedNotification as CFString, UnsafeMutableRawPointer(Unmanaged.passRetained(self).toOpaque())) == .success {
print("AXObserverAddNotification success!")
CFRunLoopAddSource(CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(observer!), .defaultMode)
print("Watching")
}
}
}
so, why the func callback is not has being triggered when I change window focus of safari?
As #Willeke has already pointed out, you need to keep a reference to observer (probably you want to make it a property).
For others who are interested, also take a look at this answer here https://stackoverflow.com/a/33262376/1531256 explaining how to pass self to the callback C-function.

observe when AVplayer is ready to play - swift - programmatically

trying to play a video with AVPlayer like this:
if let video = card.pageImageVideoController.controllers[0] as? VideoController{
video.player.play()
}
I noticed that the video doesn't play. So I inspected deeper and found out that when I call the function .play() the AVPlayer current Item is nil.
I thought that the solution for this should be to add KVO observer for the player to see when the item is ready to play. I used this stack overflow question.
And I modified the previous code like this:
var playbackLikelyToKeepUpContext = 0
if let video = card.pageImageVideoController.controllers[0] as? VideoController{
video.player.addObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp",
options: .new, context: &playbackLikelyToKeepUpContext)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let videoController = topCard!.pageImageVideoController.controllers[0] as? VideoController else { return }
if context == &playbackLikelyToKeepUpContext {
if videoController.player.currentItem!.isPlaybackLikelyToKeepUp {
// loadingIndicatorView.stopAnimating() or something else
print("ready")
} else {
// loadingIndicatorView.startAnimating() or something else
print("not ready")
}
}
}
But the function observeValue is never called. I don't know why.
If your idea is to check if the item is ready to play or not. Then better you put observer for status. And check error in the observer function. As mentioned in the following document:
https://developer.apple.com/documentation/avfoundation/avplayeritem

executeJavascript does not call completionHandler when inside a DispatchQueue

I've written a function that's supposed to return the HTML string that makes up a WKWebview. However, the completion handler is never called, and the project freezes indefinitely. I've also already adopted the WKScriptMessageHandler protocol so that's not the problem.
public func getHTML() -> String {
var result = ""
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.webView.evaluateJavaScript("document.documentElement.outerHTML.toString()", completionHandler: {(html: Any?, error: Error?) in
if (error != nil) {
print(error!)
}
result = html as! String
group.leave()
})
}
group.wait()
print("done waiting")
return result
}
I've found several examples on how to get the html, like here, but I don't want to merely print, I want to be able to return its value. I'm not experienced with DispatchQueues, but I do know for that WKWebView's evaluateJavaScript completion handler always runs on the main thread

How to keep AVMIDIPlayer playing?

I'm trying to use Apple's AVMIDIPlayer object for playing a MIDI file. It seems easy enough in Swift, using the following code:
let midiFile:NSURL = NSURL(fileURLWithPath:"/path/to/midifile.mid")
var midiPlayer: AVMIDIPlayer?
do {
try midiPlayer = AVMIDIPlayer(contentsOf: midiFile as URL, soundBankURL: nil)
midiPlayer?.prepareToPlay()
} catch {
print("could not create MIDI player")
}
midiPlayer?.play {
print("finished playing")
}
And it plays for about 0.05 seconds. I presume I need to frame it in some kind of loop. I've tried a simple solution:
while stillGoing {
midiPlayer?.play {
let stillGoing = false
}
}
which works, but ramps up the CPU massively. Is there a better way?
Further to the first comment, I've tried making a class, and while it doesn't flag any errors, it doesn't work either.
class midiPlayer {
var player: AVMIDIPlayer?
func play(file: String) {
let myURL = URL(string: file)
do {
try self.player = AVMIDIPlayer.init(contentsOf: myURL!, soundBankURL: nil)
self.player?.prepareToPlay()
} catch {
print("could not create MIDI player")
}
self.player?.play()
}
func stop() {
self.player?.stop()
}
}
// main
let myPlayer = midiPlayer()
let midiFile = "/path/to/midifile.mid"
myPlayer.play(file: midiFile)
You were close with your loop. You just need to give the CPU time to go off and do other things instead of constantly checking to see if midiPlayer is finished yet. Add a call to usleep() in your loop. This one checks every tenth of a second:
let midiFile:NSURL = NSURL(fileURLWithPath:"/Users/steve/Desktop/Untitled.mid")
var midiPlayer: AVMIDIPlayer?
do {
try midiPlayer = AVMIDIPlayer(contentsOfURL: midiFile, soundBankURL: nil)
midiPlayer?.prepareToPlay()
} catch {
print("could not create MIDI player")
}
var stillGoing = true
while stillGoing {
midiPlayer?.play {
print("finished playing")
stillGoing = false
}
usleep(100000)
}
You need to ensure that the midiPlayer object exists until it's done playing. If the above code is just in a single function, midiPlayer will be destroyed when the function returns because there are no remaining references to it. Typically you would declare midiPlayer as a property of an object, like a subclassed controller.
Combining Brendan and Steve's answers, the key is sleep or usleep and sticking the play method outside the loop to avoid revving the CPU.
player?.play({return})
while player!.isPlaying {
sleep(1) // or usleep(10000)
}
The original stillGoing value works, but there is also an isPlaying method.
.play needs something between its brackets to avoid hanging forever after completion.
Many thanks.

Create a CFRunLoopSourceRef using IOPSNotificationCreateRunLoopSource in Swift

I am trying to subscribe to changes in power state on macOS. I discovered there is a way using IOKit, though it is a bit convoluted. I need to import it using #import <IOKit/ps/IOPowerSources.h> in an ObjC Bridging header. Then I get access to the function IOPSNotificationCreateRunLoopSource, which has the signature:
IOPSNotificationCreateRunLoopSource(_ callback: IOPowerSourceCallbackType!, _ context: UnsafeMutablePointer<Void>!) -> Unmanaged<CFRunLoopSource>!
I got some help from the answer in Callback method to Apple run loop, but still doesn't manage to create a function of type IOPowerSourceCallbackType in Swift. What is the missing piece to have this compile?
The issue is that IOPowerSourceCallbackType is a C function.
According to Apple's documentation these functions are available as closures:
C function pointers are imported into Swift as closures with C function pointer calling convention
https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithCAPIs.html#//apple_ref/doc/uid/TP40014216-CH8-ID148
So the easiest way is to use a closure:
IOPSNotificationCreateRunLoopSource({ (context: UnsafeMutableRawPointer?) in
debugPrint("Power source changed")
}, &context)
A second option is to use a top-level function:
func powerSourceChanged(arg: UnsafeMutableRawPointer?) {
debugPrint("Power source changed")
}
IOPSNotificationCreateRunLoopSource(powerSourceChanged, &context)
For reference the complete implementation of how I'm using this:
class WindowController: NSWindowController {
static var context = 0
override func windowDidLoad() {
super.windowDidLoad()
let loop: CFRunLoopSource = IOPSNotificationCreateRunLoopSource({ (context: UnsafeMutableRawPointer?) in
debugPrint("Power source changed")
}, &WindowController.context).takeRetainedValue() as CFRunLoopSource
CFRunLoopAddSource(CFRunLoopGetCurrent(), loop, CFRunLoopMode.defaultMode)
}
}
UPDATE
To let it interact with the instance the loop was setup from, you have to pass self as context, however self isn't a pointer.
When you try to pass self as pointer by prepending it with & (&self), you'll get an error that self is immutable.
To convert it a to an opaque pointer you can use the Unmanaged class:
let opaque = Unmanaged.passRetained(self).toOpaque()
Which then can be used as an UnsafeMutableRawPointer:
let context = UnsafeMutableRawPointer(opaque)
What we can use as the context for IOPSNotificationCreateRunLoopSource.
And then in the callback, by using the Unmanaged class again, we can resolve this pointer back to its initiating instance:
let opaque = Unmanaged<WindowController>.fromOpaque(context!)
let _self = opaque.takeRetainedValue()
Full example:
func PowerSourceChanged(context: UnsafeMutableRawPointer?) {
let opaque = Unmanaged<WindowController>.fromOpaque(context!)
let _self = opaque.takeRetainedValue()
_self.powerSourceChanged()
}
class WindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
let opaque = Unmanaged.passRetained(self).toOpaque()
let context = UnsafeMutableRawPointer(opaque)
let loop: CFRunLoopSource = IOPSNotificationCreateRunLoopSource(
PowerSourceChanged,
context
).takeRetainedValue() as CFRunLoopSource
CFRunLoopAddSource(CFRunLoopGetCurrent(), loop, CFRunLoopMode.defaultMode)
}
func powerSourceChanged() {
debugLog("Power source changed")
}
}
Bonus
A related article about CFunction pointers