NSOpenPanel won't close properly during Swift function - swift2

I'm a newcomer to Swift (2.2) and am having a problem with a simple app using Xcode 7.3 and OS X 10.11. In this app, the user clicks a button and selects a file through NSOpenPanel. The code uses the URL selected to get the file's data and name, then processes the data and saves the result somewhere else. With large files, the processing can take several seconds. When processing large files, once the file is selected, the space where the Open File window had been remains blank, covering the app view and everything else, and stays there until the operation is complete. Also, the app's outlets are frozen until the operation finishes. It appears NSOpenPanel isn't handing window control back to the app and the system.
The code goes like this:
#IBAction func processFile(sender: AnyObject) {
var chosenURL: NSURL?
let openPanel = NSOpenPanel()
openPanel.title = "Choose a file"
openPanel.canChooseDirectories = false
openPanel.allowsMultipleSelection = false
if openPanel.runModal() == NSFileHandlingPanelOKButton {
chosenURL = openPanel.URL
}
let dataBytes = NSData(contentsOfURL: chosenURL!)
let fileName = chosenURL!.lastPathCompnent!
// Remaining code processes dataBytes and fileName
I've tried a few variations but get the same result. Searching for "NSOpenPanel won't close" on the 'net usually just brings up examples in Objective-C, which I know nothing of. Any suggestions of how to make NSOpenPanel shut off and have view and control return to the app window?

Following on from Eric D's suggestion, I looked into Grand Central Dispatch and background processes. My first approach was:
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
dataBytes = NSData(contentsOfURL: chosenURL!)
}
That didn't change anything. I found I had to put the whole remaining process (everything from 'let dataBytes…' onwards) within the dispatch closure, with 'dispatch_async(dispatch_get_main_queue())' statements around UI updates. This stopped the window from freezing and blanking, and returned control to the app. Thanks again, Eric.

Related

Register for global file drag events in Cocoa

I'm trying to be notified when a OS X user is dragging any file in OS X, not only in my app.
My current approach was using addGlobalMonitorForEventsMatchingMask:handler: on NSEvent, as follows:
[NSEvent addGlobalMonitorForEventsMatchingMask:NSLeftMouseDraggedMask handler:^(NSEvent* event) {
NSPasteboard* pb = [NSPasteboard pasteboardWithName:NSDragPboard];
NSLog(#"%#", [pb propertyListForType:NSFilenamesPboardType]);
}];
This works partially - the handler is being called when I start dragging a file from my desktop or Finder, however it also is being called when I perform every other operation that contains a left-mouse-drag, e.g. moving a window. The issue is that the NSDragPboard still seems to contain the latest dragged file URL e.g. when I let off the file and start moving a window, which makes it hard to distinguish between these operations.
TL;DR - I am interested in file drag operations system-wide. I do not need any information about the dragged file itself, just the information that a file drag operation has been started or stopped. I would appreciate any hint to a possible solution for this question.
After having talked to Apple DTS, this is most likely a bug. I have filed rdar://25892115 for this issue. There currently seems to be no way to solve my original question with the given API.
To solve my problem, I am now using the Accessibility API to figure out if the item below the cursor is a file (kAXFilenameAttribute is not NULL).
NSPasteboard* pb = [NSPasteboard pasteboardWithName:NSDragPboard];
NSArray* filenames = [pb propertyListForType:NSFilenamesPboardType];
NSInteger changeCount = pb.changeCount;
//when moving a window. the changeCount is not changed, use it to distinguish
if (filenames.count > 0 && self.lastChangeCount != changeCount){
self.lastChangeCount = changeCount;
//your code here
}

Realtime status bar view

I have a status bar application and when I'm opening it, it shows me informations but if these informations are changing (percentage here), I don't see it directly. I must reopen it to make it shows new informations.
Here I open the app one time it's 90% :
Then I wait some time and reopen it, it's already 100% :
Is there a way to show "in real time" labels and stuff in a status bar application ?
Having come across it before:
https://github.com/adamhartford/PopStatusItem
Does quite well, appears to be thread friendly (having tested with async threads) on the popup item..
Edit
addition as the user needs the actual status bar Image/text updated as opposed to the popup...
let sysStatusBar = NSStatusBar.systemStatusBar()
dispatch_async(dispatch_main(){
let dele = NSApp.delegate as? AppDelegate
dele.statusBarValue = 100.0
// from the delegate or a singleton method, have statusbarValue observed
// and update the App.delegate.statusBarItem.image accordingly.
// make sure it happens on the main thread... then the image / text
// will update...
}
What you're doing is updating the delegate where you've added the NSStatusBarItem and updating the image there. IN my example, I update the "statusBarValue" but if you just add a binding or an observer to the value, you can just as easily update the image.
ONCE AGAIN Make sure this happens on the main thread, or the UI is just going to ignore your updates. So updates from background threads etc... need to call out on the main thread.

Xcode7 | Xcode UI Tests | How to handle location service alert?

I am writing UI Test Cases for one one of my app using the XCUIApplication, XCUIElement and XCUIElementQuery introduced in Xcode7/iOS 9.
I have hit a road block. One of the screens in test case requires iOS's Location Services. As expected the user is prompted about allowing use of location service with alert titled: Allow “App name” to access your location while you use the app? with Allow & Don't Allow buttons.
Problem is or so it seems that since the alert is presented by OS itself it is not present in Application's element sub-tree.
I have logged following:
print("XYZ:\(app.alerts.count)")//0
var existence = app.staticTexts["Allow “App Name” to access your location while you use the app?"].exists
print("XYZ:\(existence)")//false
existence = app.buttons["Allow"].exists
print("XYZ:\(existence)") //false
Even UI recording generated similar code:
XCUIApplication().alerts["Allow “App Name” to access your location while you use the app?"].collectionViews.buttons["Allow"].tap()
I have not found any API that can get me past this problem. For example:
Tap at a position on the screen
Get alerts outside the app
So how can I get past this? Is there a way to configure Test Targets so that Location Service Authorization is not required.
Xcode 9
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let allowBtn = springboard.buttons["Allow"]
if allowBtn.exists {
allowBtn.tap()
}
Xcode 8.3.3
_ = addUIInterruptionMonitor(withDescription: "Location Dialog") { (alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
app.buttons["Request Location"].tap()
app.tap() // need to interact with the app for the handler to fire
Note that it is a bit different as the method name now is addUIInterruptionMonitor and takes withDescription as an argument
Xcode 7.1
Xcode 7.1 has finally fixed a issue with system alerts. There are, however, two small gotchas.
First, you need to set up a "UI Interuption Handler" before presenting the alert. This is our way of telling the framework how to handle an alert when it appears.
Second, after presenting the alert you must interact with the interface. Simply tapping the app works just fine, but is required.
addUIInterruptionMonitorWithDescription("Location Dialog") { (alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
app.buttons["Request Location"].tap()
app.tap() // need to interact with the app for the handler to fire
The "Location Dialog" is just a string to help the developer identify which handler was accessed, it is not specific to the type of alert.
Xcode 7.0
The following will dismiss a single "system alert" in Xcode 7 Beta 6:
let app = XCUIApplication()
app.launch()
// trigger location permission dialog
app.alerts.element.collectionViews.buttons["Allow"].tap()
Beta 6 introduced a slew of fixes for UI Testing and I believe this was one of them.
Also note that I am calling -element directly on -alerts. Calling -element on an XCUIElementQuery forces the framework to choose the "one and only" matching element on the screen. This works great for alerts where you can only have one visible at a time. However, if you try this for a label and have two labels the framework will raise an exception.
This was the only thing that worked for me. Using Xcode 9 fwiw.
Also probably relevant that I was already using addUIInterruptionMonitor for a different alert. I tried reordering them and it didn't make a difference. Could be that it's a problem in 9 when you have two, or could be I was using them wrong. In any event the code below worked. :)
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let allowBtn = springboard.buttons["Allow"]
if allowBtn.exists {
allowBtn.tap()
}
If you want to check if the alert is showing, just check for the existence of the button:
if (app.alerts.element.collectionViews.buttons["Dismiss"].exists)
{
app.alerts.element.collectionViews.buttons["Dismiss"].tap()
}
it checks if the alert is showing, and if it's showing it will tap it
I got it to work with this on Xcode 9.4.1, the trick was to wait for the popup to appear.
// wait for location service popup to appear
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let allowBtn = springboard.buttons["Allow"]
expectation(for: NSPredicate(format: "exists == true"), evaluatedWith: allowBtn, handler: nil)
waitForExpectations(timeout: 10, handler: nil)
//allow location service
if allowBtn.exists {
allowBtn.tap()
}
On xcode 9.1, alerts are only being handled if the test device has iOS 11. Doesn't work on older iOS versions e.g 10.3 etc. Reference: https://forums.developer.apple.com/thread/86989
To handle alerts use this:
//Use this before the alerts appear. I am doing it before app.launch()
let allowButtonPredicate = NSPredicate(format: "label == 'Always Allow' || label == 'Allow'")
//1st alert
_ = addUIInterruptionMonitor(withDescription: "Allow to access your location?") { (alert) -> Bool in
let alwaysAllowButton = alert.buttons.matching(allowButtonPredicate).element.firstMatch
if alwaysAllowButton.exists {
alwaysAllowButton.tap()
return true
}
return false
}
// One interruption monitor is sufficient for multiple alerts
This works for all languages:
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let allowBtn = springboard.buttons.element(boundBy: 1)
if allowBtn.exists {
allowBtn.tap()
}
To tap allow on location alert you can call
element.tap() where element is any element on your screen.
So after calling tap, accessibility will tap Allow on alert and than tap on your element
Here's what I did to accept a notifications permission alert in any language by tapping the second button found in the (two button) dialog. The Allow button is on the right, therefore index 1.
let handler = addUIInterruptionMonitor(withDescription: "System Dialog") { (alert) -> Bool in
alert.buttons.element(boundBy: 1).tap()
return true
}
app.tap()

How to deselect the contents of a TextField in swift

I have a simple desktop app where a TextField should be focused when the window loads. I have this working, but it's a little annoying that, having loaded the users content into the TextField, the entire contents of the field become selected automatically. The user may want to start editing the content, but they will rarely/never want to replace it all at once (imagine a text editor doing this, to see what I mean).
I see there is an Action for selectAll: but what I want is the opposite Action of selectNone:
I tried passing nil to the selectText method, but that doesn't work:
textField.selectText(nil)
I found a number of answers on StackOverflow that mention a selectedTextRange, but this appears to be outdated, because Xcode 6.3 doesn't recognize this as a valid property on TextField.
Can anyone explain how I do this?
It's been a while since I've dealt with NSTextFields to this level (I work mostly in iOS these days).
After doing a little digging I found this on the net:
NSText* textEditor = [window fieldEditor:YES forObject:textField];
NSRange range = {start, length};
[textEditor setSelectedRange:range];
window is the window containing your field, textField.
This requires the field editor to be managing your field, what can be done simply by previously selecting the whole text of the field using the selectText:sender method.
Here is the final swift code that I got working based on what Duncan C posted:
if let window = NSApplication.sharedApplication().mainWindow {
let textEditor = window.fieldEditor(true, forObject: textField)!
let range = NSRange(0..<0)
textEditor.selectedRange = range
}

MacRuby: EXC_BAD_ACCESS on file dialog

I have been using MacRuby and running through the book MacRuby:The Definitive Guide by Matt Aimonetti.
On the Movies CoreData app example, I've got this code:
def add_image(sender)
movie = movies.selectedObjects.lastObject
return unless movie
image_panel = NSOpenPanel.openPanel
image_panel.canChooseDirectories = false
image_panel.canCreateDirectories = false
image_panel.allowsMultipleSelection = false
image_panel.beginSheetModalForWindow(sender.window, completionHandler: Proc.new{|result|
return if (result == NSCancelButton)
path = image_panel.filename
# use a GUID to avoid conflicts
guid = NSProcessInfo.processInfo.globallyUniqueString
# set the destination path in the support folder
dest_path = applicationFilesDirectory.URLByAppendingPathComponent(guid)
dest_path = dest_path.relativePath
error = Pointer.new(:id)
NSFileManager.defaultManager.copyItemAtPath(path, toPath:dest_path, error:error)
NSApplication.sharedApplication.presentError(error[0]) if error[0]
movie.setValue(dest_path, forKey:"imagePath")
})
end
The app loads fine and runs without issue - I can create new movies in CoreData and delete them, etc. However, when I click the button which calls this function it opens the dialog window fine, but either the "cancel" or "open file" buttons cause a crash here:
#import <Cocoa/Cocoa.h>
#import <MacRuby/MacRuby.h>
int main(int argc, char *argv[])
{
return macruby_main("rb_main.rb", argc, argv); << Thread 1: Program received signal EXC_BAD_ACCESS
}
Any help is appreciated. I thought it had something to do with BridgeSupport but either embedding isn't working or my attempts to do so aren't working. Either way, something else seems borked as the example provided with the book also crashes.
Thanks!
ADDED NOTE:
I went and tested out this code from macruby.org and it worked fine:
def browse(sender)
# Create the File Open Dialog class.
dialog = NSOpenPanel.openPanel
# Disable the selection of files in the dialog.
dialog.canChooseFiles = false
# Enable the selection of directories in the dialog.
dialog.canChooseDirectories = true
# Disable the selection of multiple items in the dialog.
dialog.allowsMultipleSelection = false
# Display the dialog and process the selected folder
if dialog.runModalForDirectory(nil, file:nil) == NSOKButton
# if we had a allowed for the selection of multiple items
# we would have want to loop through the selection
destination_path.stringValue = dialog.filenames.first
end
end
Seems either something is borked in the beginSheetModalForWindow call or I'm missing something, trying to track down what. I can make my modal dialog for file selection work with the above code, but it isn't a sheet attached to the window.
NSOpenPanel is a subclass of NSSavePanel so you can use NSSavePanel's beginSheetModalForWindow:completionHandler:. It is runModalForDirectory that is depreciated. However, "depreciated" doesn't mean, "doesn't work" it means "will stop working in the future."
The error you are getting is pointing to the C code that loads MacRuby itself. This indicates a severe crash that MacRuby could not trap. It just shows in Main.m because that is the only place the debugger manages to trap the stack in which the error occurred. Unfortunately, locating the error at the very top/bottom of the stack like this makes it useless for debugging.
I don't see any obvious problem with Matt Aimonetti's code so I'm going to guess it's a problem with MacRuby handling the block that is passed for the completion handler. That would also explain why the error is not trapped because the block will be in a different address space than the object that defines it.
I would suggest contacting Matt Aimonetti directly via either the book site or the MacRuby mailing list (he's very active there.)

Resources