NSDocument: how do you save & restore a document's window position? - swift

I have a NSDocument-based app, and I'd like my window positions to be saved and restored when re-opening documents. Apple's documentation on this is pretty sparse, but what I've been able to piece together is that at some point, something in the app needs to call NSWindow.setFrameUsingName() and NSWindow.setFrameAutosaveName().
What I haven't quite figured out is what point this needs to happen at, and what things need to do this. For example, this doesn't work at all:
// In my NSDocument class
override func windowControllerDidLoadNib(aController: NSWindowController) {
super.windowControllerDidLoadNib(aController)
// Add any code here that needs to be executed once the windowController has loaded the document's window.
aController.window?.setFrameUsingName("MainWindow")
aController.window?.setFrameAutosaveName("MainWindow")
}
I've read various different pieces of documentation or forum answers that point to awakeFromNib() to be another area to do this, but I can't get that to work either.
I'm also confused / worried that this is somehow being affected by Auto Layout or something I've done wrong in Interface Builder - for example, this is how my window is set up in IB:
I don't particularly want my window centered, but the other options seem to lock it in place in fixed horizontal or fixed vertical positions, which I also don't really want. A side effect of having my window be centered is that my document windows no longer cascade, which I neither want nor can seem to stop from happening (note that windowController.shouldCascadeWindows = true isn't helping either).
So - what's going on here? I'm finding knowledge on this topic to be particularly unclear or misleading, and likely out of date for Cocoa development 2015, so a modern refresher on this would be great.

Step 1: In IB, give the window an autosave name.
Step 2: There is no step 2.

The easiest place to set the name of the autosave name for the window frame is probably in your implementation of NSDocument.
If you override makeWindowControllers() as part of the implementation, you are creating the window controller(s) manually, and thus can set the name there:
override func makeWindowControllers() {
let storyboard = NSStoryboard(name: NSStoryboard.Name("MyDocumentStoryboard"), bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("MyDocument Window Controller")) as! MyDocumentWindowController
// this assumes you have a stored property or a computed property `someUniqueIdentifier` that is as unique as possible for your document
// and that you store on disk together with the rest of the document properties
windowController.windowFrameAutosaveName = NSWindow.FrameAutosaveName(rawValue: someUniqueIdentifier)
self.addWindowController(windowController)
}
If you instantiate your document window by overriding windowNibName, you should instead override the method windowControllerDidLoadNib(_:), to set the autosave name on the window. I have not tested this code, but I assume it will work:
func windowControllerDidLoadNib(_ windowController: NSWindowController) {
// this assumes you have a stored property or a computed property `someUniqueIdentifier` that is as unique as possible for your document
// and that you store on disk together with the rest of the document properties
windowController.windowFrameAutosaveName = NSWindow.FrameAutosaveName(rawValue: someUniqueIdentifier)
}

Related

"target is not running or doesn't have entitlement" message: is this connected to crashes?

Update: this problem is more focused now, and not on quite the same topic. I've asked this question as a follow-on
ORIGINAL QUESTION:
I am getting a crash on a subclassed WKWebView-provisioned app.
ProcessAssertion::acquireSync Failed to acquire RBS assertion 'ConnectionTerminationWatchdog' for process with PID=87121, error: Error Domain=RBSServiceErrorDomain Code=1 "target is not running or doesn't have entitlement com.apple.runningboard.assertions.webkit" UserInfo={NSLocalizedFailureReason=target is not running or doesn't have entitlement com.apple.runningboard.assertions.webkit}
The problem is, that I can't tell if this is related or not. The actual error on crash is
Thread 1: EXC_BAD_ACCESS (code=1, address=0xbdb2dfcf0470)
Which I was assuming was something running off the end of an array. This makes some sense: I'm selecting from a table that filters out some entries from the data source; but I've checked that carefully; there is no point when a row index greater than the actual rows is accessed (and yes, I'm accounting for the difference between count and index).
The main change here is that I previously had a UIView that acted as a container for a number of CAShapeLayers. I also wanted to overlay text view, but with the proviso that this be via a WKWebView. With two separate views, I would have to either have the CAShapeLayer objects in front of, or behind the WebView. I was seeking a fix to that.
What I have done is substitute a WKWebView for the original UIView. I can add the CAShapes to it, so it performs the original function. It also can, presumably, display the html. And the original suggestion in this answer to a question I asked is what I am working towards. The idea being that it would allow the effect sought, with shapes in front of or behind the html elements.
But the error is thrown after the DidSelect call on the table:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
Tracker.track("getting row \(indexPath.row)")
let ptv = tableView as? NovilloTableView
if ptv!.uiType == .textTable {
let gp = Projects.currentProject?.getPaths(type: PaletteView.getCurrentPane())
GitPaths.currentGitPath = gp![indexPath.row]
NotificationCenter.default.post(name: NNames.updateWebText.nn(), object: nil)
return
}
let svgs = Projects.currentProject!.getPaths(type : PaletteView.getCurrentPane())
var gitPath = svgs[indexPath.row]
var gitPaths = GitPaths.getMediaBoundingBoxes(paths: [gitPath])
guard let pathArrays = gitPath.parseForRegBeziers() else { return }
let rslt = pathArrays.0
let regBeziers = pathArrays.1
gitPath.boundingBox = gitPath.getBoundsParamsForPaths(src: regBeziers.isEmpty ? rslt : regBeziers)
GitPaths.currentGitPath = gitPath
// Tracker.track("sending notification")
NotificationCenter.default.post(name: NNames.updateMedia.nn(), object: nil,
userInfo: ["path" : gitPath])
Tracker.track("completed didSelect")
return
}
In other words, the interaction has ended; but I get this crash, even though I can se that the expected result (the rendering of the shapes into the view) has been completed correctly. It seems to be happening right after everything has completed, and no amount of breakpoints has shown anything else to be happening.
This is confusing to me, and I have hit a limit on what I know how to do to dig further into this. Given the nature of web connections, I've wondered if it's some asynchronous issue that I can't debug sequentially; but that's guesswork without any direct evidence.
I suspect that there is a possible configuration problem with the WebView that shows up once I interact with it, by changing its contents. I'm not actually trying to get it to load anything when it crashes, it's only performing its original function as a container for the CAShapeLayers, so I'm confused.
The main view that contains the WKWebView (which is subclassed, to support a function to determine if it should display the web content, and which I've commented out), is set as the delegate for the WKWebView, and that seems to be fine, though there are no actual protocol functions added to that view, not sure if that matters.
The other detail is that the WebView when it does load web content is only loading local text, and not connected to any services. This testing is happening in the Simulator, and I've come across advice elsewhere to allow for background processes that include enabling Background Fetch, etc., but this has done nothing to change the situation...
EDIT: this is the extent of the configuration of the subclassed WKWebView: maybe this is the issue?
mediaDisplay = NovilloWebView()
mediaPane.addSubview(mediaDisplay)
mediaDisplay.navigationDelegate = self
mediaDisplay.uiDelegate = self
mediaDisplay.backgroundColor = .clear

How to drag and drop an external item for Xcode-ui-testing

In my application I allow the user to drag and drop items from Finder (or any other source of a file based URL) into my application. What I want to do is to add a mechanism that will allow me to test this in the Xcode UI testing.
I can see how to use XCUIElement.press(forDuration:thenDragTo:) to test the drag and drop of a source and destination within the application, but I have been unable to find a way to test when the source of the drag is outside of the application.
In a somewhat related test, I test the copy and paste portion of the application by setting the string I want to paste into NSPasteboard.general, then using XCUIElement.typeKey("v", modifierFlags: .command) to paste it into the desired element. That is a little less than ideal as it depends on Command-v actually being implemented as the paste command, but that is unlikely to change so it is acceptable for my needs. (In fact I've written an XCUIElement.paste(_ s: String) extension that makes it easy for me to add this in a test.)
I believe that drag and drop is also using an NSPasteboard for its communications, so with a little investigation into the underlying mechanism, I should be able to set my object into the correct pasteboard just like I do for the cut and paste. I'm reasonably certain I can figure that part out. But I haven't figured out how to perform the actual drop.
My goal would be to create an XCUIElement.drop(_ url) that would setup the proper "public.file-url" object into the correct pasteboard, and then simulate/perform the drop into the element.
Any ideas?
I should note that I have already tried the following two items:
First, I did use the Xcode record feature to attempt to record the drag and drop operation and see what events it would give me. Unfortunately, it records absolutely nothing.
Second, I do have a menu based alternative where the user selects a file via the file selector. So if I could simulate the file selection, that would be a suitable testing alternative for my purposes. Unfortunately, I didn't make any progress along that path either. When I used Xcode to record the events, it recorded the menu selection, nothing that was actually done in the dialog.
Based on your comments I would recommend you to read this article documentation piece
https://developer.apple.com/documentation/xctest/xcuiapplication
Notice the init(bundleIdentifier: String) and init(url: URL) methods. These allow you to interact with apps apart from the target application.
Then you can use XCUIElement.press(forDuration:thenDragTo:)
import XCTest
import XCTApps
import ScreenObject
let notes = XCTApps.notes.app
let photos = XCTApps.photos.app
class Tests: XCTestCase {
func testDragAndDrop() {
photos.launch()
notes.launch()
photos.images.lastMatch.press(forDuration: 1, thenDragTo: notes.textViews["Note Body Text View"])
}
}
P.S. In this example I use XCTApps because I don't want to remember or google bundle identifiers :D
https://github.com/rzakhar/XCTApps
Ok, so I haven't yet figured out the answer to my question (how to test a drag and drop), but I have come up with an acceptable workaround for my test.
Specifically, as I thought more about the pasteboard I realized that if I'm allowing the user to drag and drop a file into my application, then I should also be allowing them to cut and paste a file into the application.
Once I had that realization, then it was a reasonably simple process to test the necessary feature of my application by pasting a URL instead of dragging and dropping the URL. This has the added advantage that I can add the necessary test file to my testing package, keeping everything nicely self contained.
To this end I've added the following function to my XCUIElement extension:
extension XCUIElement {
func paste(url: URL) {
precondition(url.isFileURL, "This must be a file URL to match the pasteboard type.")
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(url.absoluteString, forType: .fileURL)
click()
typeKey("v", modifierFlags: .command)
}
}
Then in my test code I add the following to trigger the event:
let mainWindow = app.windows[/*...my main window name goes here...*/]
let testBundle = Bundle(for: type(of: self))
let fileURL = testBundle.url(forResource: "Resources/simple", withExtension: "json")
mainWindow.paste(url: fileURL!)
Granted, this doesn't actually test the drag and drop, but it does test the same portion of my code, since in my AppDelegate I have my onPaste action method calling the same underlying method as my performDrop method.
I will wait a couple of days to see if anyone comes up with an answer to the actual question (since I would still find that useful), but if no one does, I'll accept my own answer.

Ambiguous reference to member 'close()'

I am trying to create an application that will allow a user to cycle through next/previous text document files in a folder, the way photo-viewing apps will often allow next/previous picture view. It seems to me so far that the most effective way to do this is to replace the document in the currently open window. The edited answer to my previous question on the topic suggests that this is indeed possible. I want to be able to use the document architecture for opening and saving; I don't want to have to generalize the framework, I'm trying to keep it simple. I tried to use the code recommended in the previous question as follows:
let prevDocument = windowController.document
let newDocument = Document(contentsOf: newURL, ofType: myDocumentType) // add do-catch
NSDocumentController.shared.addDocument(newDocument);
newDocument.addWindowController(windowController)
prevDocument.close()
But when I do this, the prevDocument.close() command gives me the odd error "Ambiguous reference to member 'close()'". Another thread tells me that "This kind of error appears when there's more than one variable/method with the same name". OK, but which ones, and how do I fix it? Underneath the "ambiguous reference" error I get two messages: "Found this candidate (Foundation.Stream)" and "Found this candidate (AppKit.NSBezierPath)". A brief look at the docs for Foundation.Stream and NSBezierPath suggests that Foundation.Stream and not NSBezierPath is what I'm trying to work with, but I have no idea how to tell the system that (or why NSBezierPath would be involved in the first place).
The document property of an NSWindowController has type AnyObject?, hence why there's no close method and the compiler struggles to figure out what type of object it could be, such that it could have a close method.
Clicking through to the documentation of the document property in Xcode (ctrl-cmd click) shows a comment that the document property is usually of type NSDocument, which does have a close method. Typecasting is worth a try:
guard let prevDocument = windowController.document as? NSDocument else {
// ...
}
// Rest of your code
Alternatively, if you can guarantee that only one document at a time will be managed by your application:
guard let previousDocument = NSDocumentController.shared.documents.first else {
// ...
return
}
// Rest of your code

Timestamped Event Matching Error: Failed to find matching element

I'm trying to generate a UItest in Xcode. When I try to swipe UIview I get
an error:
Timestamped Event Matching Error: Failed to find matching element
error window
This also happens if I try to tap UIView.
You should verify that the 'Accessibility' option is enabled for the UIView object you are swiping from, for example:
Usually this issue is observed when the parent element of the element yo want to record is set to isAccessibilityElement = true. In general, you have to have the parent element set to false to access the child element.
For example: if you have a UILabel inside a view, the accessibility should be set to false for the view and set to true for the UILabel.
For recording a new test, I don't think there's a solution yet. But, if you use an extension forcing tap with a test that already exists, works.
Example of use:
extension XCUIElement {
func forceTapElement() {
if self.hittable {
self.tap()
}
else {
let coordinate: XCUICoordinate = self.coordinateWithNormalizedOffset(CGVectorMake(0.0, 0.0))
coordinate.tap()
}
}
}
func testSomethingWithCells() {
let app = XCUIApplication()
let cells = app.tables.cells
sleep(1)
cells.elementBoundByIndex(0).forceTapElement()
}
You can check the original post here:
Xcode UI test - UI Testing Failure - Failed to scroll to visible (by AX action) when tap on Search field "Cancel' button
I've been occasionally running into this problem. Delete the app's directory from DerivedData seems to help.
In Xcode 9.3, where this is apparently still a problem, what I did was:
Quit Xcode
Reset the Simulator's settings (Hardware -> Erase all
contents and settings)
Quit the Simulator
Delete the derived data for the current app
Restart Xcode
Try recording again - it worked this time for me.
A solution that worked for myself was to identify the object differently.
In Xcode 8 I was able to use the following:
XCUIApplication().tables.cells["Camera Roll"].buttons["Camera Roll"].tap()
With Xcode 9 I got the error mentioned in this question. Ended up using the following, which worked (al beit more flakey than the original option)
XCUIApplication().cells.element(boundBy: 1).tap()
Even if you have Accessibility enabled for the element, you have to make sure that it has a unique accessibility identifier. In my case, I had copied & pasted a UISwitch and assigned a different outlet to it, but it kept the same accessibility ID as the original one.

How would I go about limiting a users action utilizing Parse?

This is all purely for educational purposes, to help me get a better understanding of how Parse as well as Swift operates.
I would like to make it so a user is only able to like an item once (not being able to hit a button multiple times), as currently, I'm utilizing an anonymous system with Parse.
Would I essentially use an if method with PFUser.CurrentUser() in the likeButton method to halt a user from hitting like again or would I use NSUserDefaults?
I'm not able to post code currently as I'm not near my laptop, however I could later if it helps. Still curious if I could get some info before that however.
Sample code I found on here from a previous question, which essentially implements the same idea.
#IBAction func likeButton(sender: AnyObject) {
let hitPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
let hitIndex = self.tableView.indexPathForRowAtPoint(hitPoint)
let object = objectAtIndexPath(hitIndex)
object.incrementKey("count")
object.saveInBackground()
self.tableView.reloadData()
}
Would I call NSUsersDefaults to stunt the user from hitting it more than once?
Instead of calling saveInBackground(), you'd better call the method saveInBackgroundWithBlock: instead. So the strategy is very simple:
First of all, define a 'busy' flag for the object (For example: savingInBackground) and store it wherever you like ( If you are showing 1 item then simply declare a Bool property / If you are showing a list of Item then declare a Dictionary with format ["objectID/Index": Bool]). This flag should be set to true for the item being saved in the background.
Whenever use taps on a Like button
If current item's savingInBackground flag is true, then do nothing
Else:
Set item's savingInBackground to true
Increase Like count and Call saveInBackgroundWithBlock:
In the completion block of saveInBackgroundWithBlock:, set savingInBackground back to false.
I am on train now so I can't write example code, but I hope that it's clear enough to help you to achieve what you want.