Better way to get the frontmost window of an Application - Swift - swift

its not hard to get a specified application by name
NSWorkspace.shared.runningApplications.filter{$0.localizedName == "Safari"}.first
but how to get the first window of this application, and perform miniaturize with this window?
something similar with this
let app = NSWorkspace.shared.runningApplications.filter{$0.localizedName == "Safari"}.first
app.frontmostWindow.miniaturize()

You can do this using the ScriptingBridge
import ScriptingBridge
let safari = SBApplication(bundleIdentifier: "com.apple.Safari")
let windowClass = appleEvent(keyword: "cwin")
let miniaturized = appleEvent(keyword: "pmnd")
let windows = safari?.elementArray(withCode: windowClass)
let frontMostWindow = (windows?.firstObject as? SBObject)?.get() as? SBObject
frontMostWindow?.property(withCode: miniaturized).setTo(true)
func appleEvent(keyword: StaticString) -> AEKeyword {
keyword
.utf8Start
.withMemoryRebound(to: DescType.self, capacity: 1, \.pointee)
.bigEndian
}
To be able to run this code you will need a code signed app with the com.apple.security.automation.apple-events entitlement set to true (which allows posting of AppleEvents to other applications)

Related

Swift: press button in external app via accessibility

In my function, I run the following code, when a specific event shows up and Safari is in foreground:
if win.safariIsForeground() {
let el = AXUIElementCreateApplication(win.getSafariPid())
var ptr: CFArray?
_ = AXUIElementCopyAttributeNames(el, &ptr)
}
The pointer returns an array that looks like this:
["AXFunctionRowTopLevelElements", "AXFrame", "AXChildren",
"AXFocusedUIElement", "AXFrontmost", "AXRole", "AXExtrasMenuBar",
"AXMainWindow", "AXFocusedWindow", "AXTitle",
"AXChildrenInNavigationOrder", "AXEnhancedUserInterface",
"AXRoleDescription", "AXHidden", "AXMenuBar", "AXWindows", "AXSize",
"AXPosition"]
I'd like to make Safari go one site back in the history. I think I will need AXUIElementCopyAttributeValue and AXUIElementPerformAction to do that but how do I find out the button's attribute and how do I call check AXUIElementCopyAttributeValue for that?
The easiest way to do that is by accessing the menu item. Using AXUIElementCopyAttributeValue works best with the provided constants:
// get menu bar
var menuBarPtr: CFTypeRef?
_ = AXUIElementCopyAttributeValue(safariElement, kAXMenuBarRole as CFString, &menuBarPtr)
guard let menuBarElement = menuBarPtr as! AXUIElement? else {
fatalError()
}
Accessibility Inspector shows me, what items are child of the menu bar:
so lets get the children using kAXChildrenAttribute:
// get menu bar items
var menuBarItemsPtr: CFTypeRef?
_ = AXUIElementCopyAttributeValue(menuBarElement, kAXChildrenAttribute as CFString, &menuBarItemsPtr)
guard let menuBarItemsElement = menuBarItemsPtr as AnyObject as! [AXUIElement]? else {
fatalError()
}
And so on all the way down to the menu item. Items also have an unique identifier that can look like _NS:1008. I'm not sure how to access them directly but by using AXUIElementPerformAction I can simply simulate pressing the menu item (action will be kAXPressAction).
I can use kAXRoleAttribute to identify the type of the item and where it occurs in the Accessibility hierarchy (see "Role:")
As I'm still a beginner at Swift, that was a quite challenging task as this is also not documented very well. Thanks a lot to Dexter who also helped me to understand this topic: kAXErrorAttributeUnsupported when querying Books.app
In your case you don't need necessarily need to use AXUI accessibility API. It's arguably simpler to send key strokes. You can go back in Safari history to previous page with CMD[. As an added bonus Safari does not need to be in foreground anymore.
let pid: pid_t = win.getSafariPid();
let leftBracket: UInt16 = 0x21
let src = CGEventSource(stateID: CGEventSourceStateID.hidSystemState)
let keyDownEvent = CGEvent(keyboardEventSource: src, virtualKey: leftBracket, keyDown: true)
keyDownEvent?.flags = CGEventFlags.maskCommand
let keyUpEvent = CGEvent(keyboardEventSource: src, virtualKey: leftBracket, keyDown: false)
keyDownEvent?.postToPid(pid)
keyUpEvent?.postToPid(pid)
Your app running on Mojave and newer MacOS versions will need the System Preferences -> Security & Privacy -> Accessibility permisions granted to be eligible for sending keystrokes to other apps.
The keyboard codes can be looked up:
here & here visually

How to remove sandbox without xcode

Earlier I asked a question regarding generateCGImagesAsynchronously. Thankfully it got answered and works great.
The issue is that it only works as a Cocoa app on xcode. I am now trying to move this logic to an executable Swift package but AVFoundation code, such as generateCGImagesAsynchronously, won't work. That is, no error is raised but those functions seem to be mocked. I presume this might have to do with my package being sandboxed? I was able to remove the sandbox from the Cocoa app I previously wrote the code for, but I can't figure out how to do that for this executable.
I am new to Swift and trying to understand it and it's kind of frustrating to think that what I want my code to do is dependent on the IDE I am using.
If anyone can point me in the direction of where to read in the docs, or some other sources, on how to make programs without using xcode, that would be great. Thanks!
Here is my code:
import Darwin
import Foundation
import AppKit
import AVFoundation
import Cocoa
#discardableResult func writeCGImage(
_ image: CGImage,
to destinationURL: URL
) -> Bool {
guard let destination = CGImageDestinationCreateWithURL(
destinationURL as CFURL,
kUTTypePNG,
1,
nil
) else { return false }
CGImageDestinationAddImage(destination, image, nil)
return CGImageDestinationFinalize(destination)
}
func imageGenCompletionHandler(
requestedTime: CMTime,
image: CGImage?,
actualTime: CMTime,
result: AVAssetImageGenerator.Result,
error: Error?
) {
guard let image = image else { return }
let path = saveToPath.appendingPathComponent(
"img\(actualTime).png"
)
writeCGImage(image, to: path)
}
let arguments: [String] = Array(CommandLine.arguments.dropFirst())
// For now, we assume the second arg, which is the
// path that the user wants us to save to, always exists.
let saveToPath = URL(fileURLWithPath: arguments[1], isDirectory: true)
let vidURL = URL(fileURLWithPath: arguments[0])
let vidAsset = AVAsset(url: vidURL)
let vidDuration = vidAsset.duration
let imageGen = AVAssetImageGenerator(asset: vidAsset)
var frameForTimes = [NSValue]()
let sampleCounts = 20
let totalTimeLength = Int(truncatingIfNeeded: vidDuration.value as Int64)
let steps = totalTimeLength / sampleCounts
for sampleCount in 0 ..< sampleCounts {
let cmTime = CMTimeMake(
value: Int64(sampleCount * steps),
timescale: Int32(vidDuration.timescale)
)
frameForTimes.append(NSValue(time: cmTime))
}
imageGen.generateCGImagesAsynchronously(
forTimes: frameForTimes,
completionHandler: imageGenCompletionHandler
)
As I said in a comment on your previous question, this has nothing to do with Xcode per se. Xcode just generates a lot of code and build commands for you.
macOS is a complex operating system and programs that want to use its more advanced features must follow certain patterns. One of this patterns is called the run loop. If you create a Cocoa app, you get most of these things for free.
Since you are trying to perform some asynchronous actions, you need a run loop. Appending this should work:
RunLoop.current.run()
Otherwise, your program will simply terminate when the main thread (your code) finishes. The run loop, however, causes the program to run a loop and wait for asynchronous events (this also includes UI interactions, for example) to occur.
Note that inserting this same line also fixes your issues from the other question.

How to print the current local network name in swift

I am creating an iOS app which displays the current local network name at the top of the screen, and so forth. I am trouble-shooting different ways to display this but I can't manage the current program. Can someone help me out?
I've looked at several GitHub, stack overflow, and youtube comments about this, but nome of them worked.
In the current Xcode I'm using which is Xcode(10.4.2) I'm using a label(correct me if I should use something else) to display the current Wifi named --> (WiFi: ......)
Please don't test on the simulator, use the iphone for testing.
Import SystemConfiguration :
import SystemConfiguration.CaptiveNetwork
In ViewDidLoad :
let wifiName = getWiFiName()
print("Wifi: \(String(describing: wifiName))")
Function :
func getWiFiName() -> String? {
var serviceSetIdentifier:String?
if let interfaces = CNCopySupportedInterfaces() as Array? {
interfaces.forEach { interface in
guard let interfaceInfo = CNCopyCurrentNetworkInfo(interface as! CFString) as NSDictionary? else { return }
serviceSetIdentifier = interfaceInfo[kCNNetworkInfoKeySSID as String] as? String
}
}
return serviceSetIdentifier
}

close window based on kCGWindowName value

but I'm not new to programming. I'm trying to have an always running menubar app listen for a specific window in an application to be opened and then close that window, but not the application. Here is my current approach, it is mostly a mix of other SO answers I found.
func applicationDidFinishLaunching(aNotification: NSNotification) {
// listener for when user switches applications
NSWorkspace.sharedWorkspace().notificationCenter.addObserver(self,
selector: #selector(activated),
name: NSWorkspaceDidActivateApplicationNotification,
object: nil)
}
func activated(notification: NSNotification) {
// i feel like i should be using that NSNotification but I'm not
let options = CGWindowListOption(arrayLiteral: CGWindowListOption.ExcludeDesktopElements, CGWindowListOption.OptionOnScreenOnly)
let windowListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0))
let infoList = windowListInfo as NSArray? as? [[String: AnyObject]]
for item in infoList! {
// if i find the window i want to close
if item[kCGWindowName as String] == "Warnings" {
// close window item[kCGWindowName]
}
}
My question is, if I am even doing any of this right, how do I close the window based off of having its windowListInfo?
Also, the item[kCGWindowName as String] is saying Cannot convert value of type 'AnyObject?' to expected argument type String. How would I go about checking windows for the name I want without casting to a String? I have called print() on all objects in infoList and have seen a kCGWindowName value with the name of the window I am looking for, so I know its there.
Thanks in advance.
You can use the accessibility API:
You need to enable accessibility for your application or Xcode (if
you test your application from Xcode).
Open the "Security & Privacy" pane on the "System Preferences"
window. Click the "Privacy" tab.
Select "Accessibility" from the list in the left pane.
Click the lock icon to make changes.
Drag/drop your application or Xcode in the right pane.
Here's an example of the swift code (Swift version 2.2) to simulate a click on the red button of the window whose the title start with "Warnings":
func activated(notification: NSNotification) {
if (notification.userInfo![NSWorkspaceApplicationKey]!.localizedName) == "Pages" { // when "Pages" application is active only
let myApp = AXUIElementCreateApplication(notification.userInfo![NSWorkspaceApplicationKey]!.processIdentifier).takeUnretainedValue()
let val1 = UnsafeMutablePointer<AnyObject?>.alloc(1)
if AXUIElementCopyAttributeValue(myApp, kAXWindowsAttribute, val1).rawValue == 0 { // get windows of the "Pages" application
let windowList = val1.memory as? [AXUIElement] ?? []
for w in windowList { // loop each window, get the title of the window, and check if the title starts with "Warning"
if AXUIElementCopyAttributeValue(w, kAXTitleAttribute, val1).rawValue == 0 && (val1.memory as! String).hasPrefix("Warnings") {
if AXUIElementCopyAttributeValue(w, kAXCloseButtonAttribute, val1).rawValue == 0 {// get the red button of the window
AXUIElementPerformAction(val1.memory as! AXUIElement, kAXPressAction); // close the window
}
}
}
}
val1.dealloc(1)
}
}
To check the title equal "Warnings": use val1.memory as! String == "Warnings"
Informations on the infoList from your code:
How would I go about checking windows for the name I want without
casting to a String?
Use a string instead of the constant, like this:
if item["kCGWindowName"]! as! String == "Warnings" {

NSUserDefaults to AppleWatch

I currently driving crazy cause i can't find my mistake.
I try to pass some information over to my WatchKit App, but this does not work for me.
Both are in the same group
Then i start with the Watch with
NSUserDefaults(suiteName: "group.WatchDate")
NSUserDefaults.standardUserDefaults().setObject("1", forKey: "TEST")
NSUserDefaults.standardUserDefaults().synchronize()
after launch the app once on the watch i switch over to the "phone app" and let it println() the stored value
NSUserDefaults(suiteName: "group.WatchDate")
println("Saved Informations:")
println(NSUserDefaults.standardUserDefaults().objectForKey("TEST"))
The output always is "nil"
I tried the same passing information the other way, also with no success.
But if i simply println() the stored value on the same "device" it works.
Any idea what i m doing wrong?
zisoft has the right point, but to my belief, my code is the safer and cleaner (more readable) way to do it. I´m also taking into consideration, that naming your App Group without using reverse-DNS-style is not considered best practice.
Set data:
import Foundation
if let sharedDefaults = NSUserDefaults(suiteName: "group.com.mycompany.WatchDate") {
sharedDefaults.setObject("1", forKey: "TEST")
sharedDefaults.synchronize()
}
Read data:
import Foundation
if let sharedDefaults = NSUserDefaults(suiteName: "group.com.mycompany.WatchDate") {
if let sharedString = sharedDefaults.objectForKey("TEST") as? String {
println(sharedString)
}
}
NSUserDefaults(suiteName) already returns a NSUserDefaults object, so you must not call standardUserDefaults() again on it. Try it this way:
let sharedDefaults = NSUserDefaults(suiteName: "group.WatchDate")
sharedDefaults?.setObject("1", forKey: "TEST")
sharedDefaults?.synchronize()
on the other side:
let sharedDefaults = NSUserDefaults(suiteName: "group.WatchDate")
let s = sharedDefaults?.objectForKey("TEST") as! String
println(s)