How to show progress for `NSExtensionRequestHandling` on macOS for Finder? - macos

Writing a Finder extension / Quick Action (see https://developer.apple.com/documentation/appkit/app_extensions/add_functionality_to_finder_with_action_extensions) I happen to have a long-running operation showing "Estimate time remaining...".
I am able to provide an NSProgress object as result of registerFileRepresentation, but no progress is shown and cancelling is also not working:
itemProvider.registerFileRepresentation(
forTypeIdentifier: kUTTypePDF as String,
fileOptions: [.openInPlace],
visibility: .all,
loadHandler: {
completionHandler -> Progress? in
let op = self.newOperation(url: sourceUrl, completionHandler: completionHandler)
self.queue.addOperation(op)
return op.progress
}
)
What may I need to do, to get progress being calculated for my NSExtension?

Related

Is there a way to clear/refresh the accessibility hierarchy cache

I have a UI test that checks the value of static text element, waits a few seconds and checks again to confirm a change. At first it wasn't working because the hierarchy was not updating. I noticed this in the log;
Use cached accessibility hierarchy for
I've put in a workaround for this by simply adding a tap to a menu and opening/closing it so that an event is synthesized and the hierarchy is updated.
It would be better, however, if there was a way to clear the cache directly or force and update. I haven't found one in the API. Am I missing something?
Any ideas?
this is what I am doing;
XCTAssertEqual(app.staticTexts["myText"].label, "Expected 1")
sleep(20)
menu.tap()
sleep(1)
menu.tap()
XCTAssertEqual(app.staticTexts["myText"].label, "Expected 2")
What I'd like to be able to do it
XCTAssertEqual(app.staticTexts["myText"].label, "Expected 1")
sleep(20)
app.elements.refresh()
XCTAssertEqual(app.staticTexts["myText"].label, "Expected 2")
In order to force an update of the accessibility hierarchy, request the count property for any XCUIElementQuery:
// refresh
_ = XCUIApplication().navigationBars.count
// examine
print(XCUIApplication().debugDescription)
The above results in: "Get number of matches for: Descendants matching type NavigationBar" and "Snapshot accessibility hierarchy for com.myapp".
The following works for me in Xcode 10.2 (10E125):
import XCTest
extension XCUIApplication {
// WORKAROUND:
// Force XCTest to update its accessibility cache. When accessibility data
// like NSObject.accessibility{Label|Identifier} changes, it takes a while
// for XCTest to catch up. Calling this method causes XCTest to update its
// accessibility cache immediately.
func updateAccessibilityCache() {
_ = try? snapshot()
}
}
You should use expectationForPredicate, along the lines of...
let myText = app.staticTexts["myText"]
let waitFor = NSPredicate(format: "label = 'Expected 2'")
label.tap()
self.expectationForPredicate(waitFor, evaluatedWithObject: myText, handler: nil)
self.waitForExpectationsWithTimeout(2.0, handler: nil)
This will wait until either myText's label is 'Expected 2', or the timeout of 2 seconds is reached.
In my case, it is a problem because I'm trying to test for Facebook login, which uses Safari controller. It looks like Facebook has updated the UI after cache.
So you need to wait a bit, use the wait function here https://stackoverflow.com/a/42222302/1418457
wait(for: 2)
let _ = app.staticTexts.count
But the above is just workaround and very flaky. A more correct approach would be to wait for a certain element to appear, see https://stackoverflow.com/a/44279203/1418457

Is there are non-deprecated way to access font collections for OS X 10.11?

I was looking at adding an option for sorting available fonts by user-defined font collections (I wish Pages and Keynote did this!), but it looks like the old ways of accessing these collections are being deprecated in 10.11:
https://developer.apple.com/library/prerelease/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSFontManager_Class/index.html#//apple_ref/occ/clm/NSFontManager/
Is there a new way of accessing and using those font collections?
I've recently been working with font collections, so I may have some info for you.
The NSFontCollection API is kind of strange. It offers access to "Named Font Collections," but the names associated with said collections aren't attached to them. If that makes no sense, it's because it doesn't. However, let me try to break it down:
To add a system-wide font collection:
// Create a font descriptor (or descriptors) with whatever attributes you want
let descriptor = NSFontDescriptor(fontAttributes: nil)
// Create a font collection with the above descriptor(s)
let collection = NSFontCollection(descriptors: [descriptor])
// In Objective-C, the `+ showFontCollection:withName:visibility:error:`
// method returns `YES` if the collection was shown or `NO` if an error occurred.
// The error is passed via pointer supplied to the `error:` parameter.
//
// In Swift, the method returns `()` (aka nil literal), and instead of passing
// the error in a pointer, it `throws`, so you have to wrap the call in a `do`
// statement and catch the error (or suppress error propagation via `try!`):
do {
try NSFontCollection.showFontCollection(collection: NSFontCollection,
withName: "Visible to All Users", visibility: .Computer)
}
catch {
print("There was an error showing font collection. Info: \(error)")
}
To add a user-visible font collection:
Repeat above steps, substituting .Computer with .User:
do {
try NSFontCollection.showFontCollection(collection: NSFontCollection,
withName: "Visible to the Current User", visibility: .User)
}
catch {
print("There was an error showing font collection. Info: \(error)")
}
To add a non-persistent font collection:
Repeat above steps, substituting .Computer with .Process:
do {
try NSFontCollection.showFontCollection(collection: NSFontCollection,
withName: "Visible for Current Process", visibility: .Process)
}
catch {
print("There was an error showing font collection. Info: \(error)")
}
Next steps…
Once you have a collection, you can change it all you want using the NSMutableFontCollection class. Continuing with the example above, you would do something like this:
let mutableCollection = collection.mutableCopy() as! NSMutableFontCollection
let boldTrait = [NSFontWeightTrait: NSFontWeightBold]
let boldAttributes = [NSFontTraitsAttribute: boldTrait]
let boldDescriptor = NSFontDescriptor(fontAttributes: newAttributes)
mutableCollection.addQueryForDescriptors([boldDescriptor])
At this point, the API gets weird again. We've added descriptors to our "named collection," but nothing in the UI is going to show up until you "show" the font collection again. In other words, after making any changes, you have to call showFontCollection(_:withName:visibility:) again.
Likewise, if you want to remove/delete a collection, you have to call hideFontCollectionWithName(_:visibility:). Despite its innocuous name, this method completely removes a persistent collection from disk, so be careful.
Next next steps…
In subsequent launches of your app, you can retrieve any persistent collection using the NSFontCollection(name:visibility:) method, like so:
// Retrieve collection created at an earlier time
let collectionOnDisk = NSFontCollection(name: "Visible to All Users", visibility: .Computer)
I think I've covered most of it, but if I've missed something, or if you have questions, just let me know. Good luck!
There are classes NSFontCollection and NSFontDescriptor.
Look into the NSFontManager header file of Xcode 7 (via ⇧⌘O) to get more information about the deprecated methods and their replacement.

Swift 2.0 and Xcode-beta 7.0 – Using UITextField for numeric input

Completely frustrated noob here. Surely this isn't as hard as it looks?
I want to use a number entered by the user, perform a calculation, and send the result back to the screen.
I have code working that can use a string forced in by code, convert it to double, do the math and send the result to the screen. For example:
#IBAction func buttonPressed() {
NSLog("Button Pressed")
let decimalAsString = "123.45"
let decimalAsDouble = NSNumberFormatter().numberFromString(decimalAsString as String)!.doubleValue
TempLabel.text = "\(decimalAsDouble+2.45)"
}
This simply adds 2.45 to the string "123.45" and sends the result 125.9 back as a string to my label for display, all when the button is pressed. Great. This simpler form also works:
let decimalAsDouble = Double(decimalAsString)
What I have been struggling with is using a number entered into the UITextField.
My UITextField uses a decimal pad for entry, and I've always had a number entered there when the errors were thrown. (Or did I? The numbers show on screen but are they really "entered"? Hmmm...)
No matter what I try, I cannot find code that will both compile and then not blow up at execution, when the button is pressed. The error I get generally complains about unwrapping an optional nil.
I can detail some of the things that DON'T work, if that helps.
OK, well I've finally solved my own problem. My problem was with closing the entry field and dismissing the numeric entry pad. Once I did that, the entered value became available.
The critical code, in the view controller, was:
func textFieldShouldReturn(textField: UITextField!) -> Bool { //delegate method
textField.resignFirstResponder()
return true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?){
view.endEditing(true)
super.touchesBegan(touches, withEvent: event)
}
This dismisses the entry pad when the user clicks anywhere in the view except for the field itself or a button. Programming that looks for the entered value doesn't blow up.

Swift/Xcode6: "found nil unwrapping optional" for my user's location

So I'm experiencing an odd error - "fatal error: unexpectedly found nil while unwrapping an Optional value"...
It's strange because it only ever fails the FIRST time I run the simulator. As long as I don't quit the simulator and just re-run the app, it will not fail, and the user location will be found successfully. If I do quit the simulator and then try to re-run the app, it will fail with the same error.
Why is this happening? Is swift somehow checking for a location before it's actually found?
Thanks in advance.
override func queryForTable() -> PFQuery! {
let query = PFQuery(className: "Yak")
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self // ? delegate
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
var userLocation:CLLocation = manager.location
currLocation = userLocation.coordinate // error occurs here
println(" my location is \([manager.location])")
if let queryLoc = currLocation {
println("ahahahaha")
query.whereKey("location", nearGeoPoint:PFGeoPoint(latitude: queryLoc.latitude as CLLocationDegrees!, longitude: queryLoc.longitude as CLLocationDegrees!), withinMiles: 10)
query.limit = 200
query.orderByDescending("createdAt")
}
My guess is that you are trying to retrieve data coming from an asynchronous service using a linear procedure.
As this line implies:
manager.startUpdatingLocation()
the manager is starting to update the location, but it doesn't mean when it returns a location has already been obtained. It can take some time time, and it can also fail.
The right place to read the location is in the didUpdateLocations method of theCLLocationManagerDelegate, which I presume you have set in the view controller.
The reason why it works the 2nd time you use it is because in the previous run the manager did have the time to detect the location, so what you're getting is the old one, and not an up to date location.
Update
One possible way of solving the problem is:
in viewDidLoad check if userLocation.coordinate is not nil (it's an implicitly unwrapped optional, so you can do that check)
if it's nil, start updating the location, add a UIActivityIndicator, maybe disable user input, and set a flag (instance property) to true, to keep track you're waiting for a location
wait for a location to be obtained in locationManager(_:didUpdateLocations:)
in that method, check if the flag is true, if it is, unset it, remove the progress view and re-enable user input, then call queryForTable

NSTextField controlTextDidEndEditing: called while being edited (inside an NSOutlineView)

In my NSOutlineView, I have a NSTextField inside a NSTableCellView. I am listening for the controlTextDidEndEditing: notification to happen when the user finishes the editing. However, in my case, this notification is being fired even while the user is in the middle of typing, or takes even a second-long pause in typing. This seems bizarre. I tested a NSTextField in the same view, but outside of the NSOutlineView, and it doesn't behave this way; it only calls controlTextDidEndEditing: if the user pressed the Tab or Enter keys (as expected).
Is there something I can do to prevent the NSTextField from sending controlTextDidEndEditing: unless a Enter or Tab key is pressed?
Found a solution for this:
- (void)controlTextDidEndEditing:(NSNotification *) notification {
// to prevent NSOutlineView from calling controlTextDidEndEditing by itself
if ([notification.userInfo[#"NSTextMovement"] unsignedIntegerValue]) {
....
It's an old question, but for reference, I ran into a similar problem where controlTextDidEndEditing: was called at the beginning of the editing session.
My workaround is to check if the text field still has the focus (i.e. cursor):
func controlTextDidEndEditing(_ obj: Notification) {
guard
let textField = obj.object as? NSTextField,
!textField.isFocused
else {
return
}
...
}
public extension NSTextField
{
public var isFocused:Bool {
if
window?.firstResponder is NSTextView,
let fieldEditor = window?.fieldEditor(false, for: nil),
let delegate = fieldEditor.delegate as? NSTextField,
self == delegate
{
return true
}
return false
}
}
Note to self:
I ran into this problem when adding a new item to NSOutlineView and making it editable with NSOutlineView.editColumn(row:,with:,select).
controlTextDidEndEditing() would be called right away at the start of the editing session.
It turns out it was a first responder/animation race condition. I used a NSTableView.AnimationOptions.slideDown animation when inserting the row and made the row editable afterwards.
The problem here is that the row is made editable while it is still animating. When the animation finishes, the first responder changes to the window and back to the text field, which causes controlTextDidEndEditing() to be called.
outlineView.beginUpdates()
outlineView.insertItems(at: IndexSet(integer:atIndex),
inParent: intoParent == rootItem ? nil : intoParent,
withAnimation: .slideDown) // Animating!
outlineView.endUpdates()
// Problem: the animation above won't have finished leading to first responder issues.
self.outlineView.editColumn(0, row: insertedRowIndex, with: nil, select: true)
Solution 1:
Don't use an animation when inserting the row.
Solution 2:
Wrap beginUpdates/endUpdates into an NSAnimationContext group, add a completion handler to only start editing once the animation finished.
Debugging tips:
Observe changes to firstResponder in your window controller
Put a breakpoint in controlTextDidEndEditing() and take a very close look at the stack trace to see what is causing it to be called. What gave it away in my case were references to animation calls.
To reproduce, wrap beginUpdates/endUpdates in an NSAnimationContext and increase the animation duration to a few seconds.

Resources