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
Related
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.
I need to be able to drag a file representation (a pdf in my case) from an NSView contained within my application onto the Desktop or another application that supports opening PDF files.
I spent a few hours trying to get this working in my own app, and I thought I'd add my solution here as there's a lot of half-solutions online, some of which rely on Obj-C extensions and others which are outdated and are no longer supported. I'm hoping this post os the sort of post I'd wished I'd found during my own searches. I'm also aware of all of the minutae of the system (for example, using file coordinators instead of a direct write) but this seems to be the minimum code required to implement.
I've also provided a simple Swift NSView implementation.
The operation occurs in three main stages.
Basic overview
You'll need to make your view (or other control) a 'Data Provider' for the drag by implementing the NSPasteboardItemDataProvider protocol. The majority of the work required (other than starting the drag) occurs in the following protocol function.
func pasteboard(_ pasteboard: NSPasteboard?, item _: NSPasteboardItem, provideDataForType type: NSPasteboard.PasteboardType)
Starting the drag
This section occurs when the drag starts. In my case, I was doing this in mouseDown(), but you could also do this in the mouseDragged for example.
Tell the pasteboard that we will provide the file type UTI for the drop (kPasteboardTypeFilePromiseContent)
Tell the pasteboard that we will provide a file promise (kPasteboardTypeFileURLPromise) for the data type specified in (1)
Responding to the receiver asking for the content that we'll provide
kPasteboardTypeFilePromiseContent
This is the first callback from the receiver of the drop (via pasteboard(pasteboard:item:provideDataForType:))
The receiver is asking us what type (UTI) of file we will provide.
Respond by setting the UTI (using setString("") on the pasteboard object) for the type kPasteboardTypeFilePromiseContent
Responding to the receiver asking for the file
kPasteboardTypeFileURLPromise
This is the second callback from the receiver (via pasteboard(pasteboard:item:provideDataForType:))
The receiver is asking us to write the data to a file on disk.
The receiver tells us the folder to write our content to (com.apple.pastelocation)
Write the data to disk inside the folder that the receiver has told us.
Respond by setting the resulting URL of the written file (using setString() on the pasteboard object) for the type kPasteboardTypeFileURLPromise. Note that the format of this string needs to be file:///... so .absoluteString() needs to be used.
And we're done!
Sample
// Some definitions to help reduce the verbosity of our code
let PasteboardFileURLPromise = NSPasteboard.PasteboardType(rawValue: kPasteboardTypeFileURLPromise)
let PasteboardFilePromiseContent = NSPasteboard.PasteboardType(rawValue: kPasteboardTypeFilePromiseContent)
let PasteboardFilePasteLocation = NSPasteboard.PasteboardType(rawValue: "com.apple.pastelocation")
class MyView: NSView {
override func mouseDown(with event: NSEvent) {
let pasteboardItem = NSPasteboardItem()
// (1, 2) Tell the pasteboard item that we will provide both file and content promises
pasteboardItem.setDataProvider(self, forTypes: [PasteboardFileURLPromise, PasteboardFilePromiseContent])
// Create the dragging item for the drag operation
let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
draggingItem.setDraggingFrame(self.bounds, contents: image())
// Start the dragging session
beginDraggingSession(with: [draggingItem], event: event, source: self)
}
}
Then, in your Pasteboard Item Data provider extension...
extension MyView: NSPasteboardItemDataProvider {
func pasteboard(_ pasteboard: NSPasteboard?, item _: NSPasteboardItem, provideDataForType type: NSPasteboard.PasteboardType) {
if type == PasteboardFilePromiseContent {
// The receiver will send this asking for the content type for the drop, to figure out
// whether it wants to/is able to accept the file type (3).
// In my case, I want to be able to drop a file containing PDF from my app onto
// the desktop or another app, so, add the UTI for the pdf (4).
pasteboard?.setString("com.adobe.pdf", forType: PasteboardFilePromiseContent)
}
else if type == PasteboardFileURLPromise {
// The receiver is interested in our data, and is happy with the format that we told it
// about during the kPasteboardTypeFilePromiseContent request.
// The receiver has passed us a URL where we are to write our data to (5).
// It is now waiting for us to respond with a kPasteboardTypeFileURLPromise
guard let str = pasteboard?.string(forType: PasteboardFilePasteLocation),
let destinationFolderURL = URL(string: str) else {
// ERROR:- Receiver didn't tell us where to put the file?
return
}
// Here, we build the file destination using the receivers destination URL
// NOTE: - you need to manage duplicate filenames yourself!
let destinationFileURL = destinationFolderURL.appendingPathComponent("dropped_file.pdf")
// Write your data to the destination file (6). Do better error handling here!
let pdfData = self.dataWithPDF(inside: self.bounds)
try? pdfData.write(to: destinationFileURL, options: .atomicWrite)
// And finally, tell the receiver where we wrote our file (7)
pasteboard?.setString(destinationFileURL.absoluteString, forType: PasteboardFileURLPromise)
}
}
If anyone finds issues with this or it's completely incorrect please let me know! It seems to work for my app at least.
As Willeke has pointed out, Apple has some sample code for using the (newer) NSFilePromiseProvider mechanism for drag drop.
https://developer.apple.com/documentation/appkit/documents_files_and_icloud/supporting_drag_and_drop_through_file_promises
I wish my search had started at Apple's Developer pages instead of Google 🙃. Oh well! The sample provided is valid and still works, so if this post helps someone locate more cohesive info regarding drag drop then fantastic.
I have an app which is inconsistently returning 'nil' when using FileHandle to open a file for Read. I'm on OSX (10.13.4), XCode 9.4, Swift 4.1
This OSX app uses the NSOpenPanel() to get a list of files selected by the user. My 'model' class code opens these files to build a collection of data structures The code which does this starts out like this and successfully gets a FileHandle EVERY TIME for any file and is able to read data from the file.
private func getFITHeader(filename: String) {
let file: FileHandle? = FileHandle(forReadingAtPath: filename)
if file == nil {
print("FITFile >>> File open failed for file \(filename)")
}
else {
var databuffer: Data
databuffer = (file?.readData(ofLength: 80))!
:
:
}
The files also contain a block of binary data which I process in another part of the app. While I develop the code for this I'm temporarily hard coding one of the same filenames as works above for test purposes. BUT this code (below) ALWAYS throws an exception 'Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value' when it gets to fileHandle?.seek() - for some reason the attempt to create a FileHandle is always returning 'nil' despite the code being functionally identical to tha above.
#IBAction func btnProcFile(_ sender: Any) {
var data: Data
let filename = "/Users/johncneal/Dropbox/JN Astronomy/Astronomy/Spectroscopy/RSpec_Analyses/Gamma_Cas/20161203/Gamma Cas_065.fit"
let fileHandle: FileHandle? = FileHandle(forReadingAtPath: filename)
fileHandle?.seek(toFileOffset: 2880) //skip past the headers
let dataLenToRead = 1391 * 1039 * 2
data = (fileHandle?.readData(ofLength: dataLenToRead))!
:
:
}
The code in the second function works fine in a Playground (not attaching too much meaning to that) and, wierdly, has also worked when temporarily added to a different project. Probably also worth mentioning the length of the file path doesn't seem to matter - it behaves the same on short paths.
So the question is - why is this behaviour of FileHandle reliably inconsistent?
print()'ing the filenames presented to FileHandle() showed they were identical in each case (see below). So I'm stumped and frustrated by this - any perspectives or workarounds would be appreciated.
/Users/johncneal/Dropbox/JN Astronomy/Astronomy/Spectroscopy/RSpec_Analyses/Gamma_Cas/20161203/Gamma Cas_065.fit
/Users/johncneal/Dropbox/JN Astronomy/Astronomy/Spectroscopy/RSpec_Analyses/Gamma_Cas/20161203/Gamma Cas_065.fit
Found the answer - Sandboxing !!
Darren - coincidentally I did look at the URL based route and discovering it 'throws' put some proper error reporting in the catches. Low and behold they reported I didn't have permissions on the file (which initially surprised me since I'm obviously admin on my Mac's and all the files ar local and under my username.
I bit more research turned up. this article - https://forums.developer.apple.com/thread/96062 which quickly revealed its a sandboxing problem :-) Looks like recent versions of XCode have it turned on in 'Entitlements'. The post also points out that the NSOpenPanel FileOpen dialog returns 'Security scoped urls'. At first I thought this explained why the code in the first function worked but I'm not totally convinced because I was only feeding the url.path property to FileHandle.
However, turning off Sandbox in Entitlements makes everything work just fine. Yes, I know thats not the right thing to do longer term (or if I want this to go to the App Store) so I'll be checking out the right way to do this. At least I can get on now - thanks for the input.
The FileHandle initializers are not well named.
You should use FileHandle(forReadingFrom:URL) instead of FileHandle(forReadingAtPath:String). The former is newer API that throws an error instead of returning nil. You can use the thrown error to see why it is failing, and your variables are guaranteed to be non-nil.
For example:
#IBAction func btnProcFile(_ sender: Any) {
do {
let fileUrl = URL(fileURLWithPath:"/Users/johncneal/Dropbox/JN Astronomy/Astronomy/Spectroscopy/RSpec_Analyses/Gamma_Cas/20161203/Gamma Cas_065.fit")
let fileHandle = try FileHandle(forReadingFrom: fileUrl)
fileHandle.seek(toFileOffset: 2880) //skip past the headers
let dataLenToRead = 1391 * 1039 * 2
let data: Data = fileHandle.readData(ofLength: dataLenToRead)
// etc...
} catch let error as NSError {
print("FITFile >>> File open failed: \(error)")
NSApp.presentError(error)
}
}
Here a simple issue I am facing while starting to experiment with WatchKit and complications.
I created a simple app which is showing a complication with a public string “Y” and by clicking it, the Apple Watch app is shown with a simple switch.
I wrote my code in getPlaceholderTemplateForComplication in ComplicationController.swift and added a switch IBAction in InterfaceController.swift.
By changing the value of the switch, the public string cycles between “N” and “Y”. I would like to have it changed in the complication as well. However I am noticing that the complication stays as it is initially at “Y”.
I found a similar question about forcing complication updates, but it was related to a TimeLine complication func and not placeHolder.
func updateComplication() {
let complicationServer = CLKComplicationServer.sharedInstance()
for complication in complicationServer.activeComplications {
complicationServer.reloadTimelineForComplication(complication)
}
}
It is not clear to me on where and how to use this in my case.
As suggested I worked on getCurrentTimelineEntryForComplication .
In order to test a ModularComplication only, I used:
switch complication.family {
case .ModularSmall:
let modularSmallTemplate =
CLKComplicationTemplateModularSmallRingText()
modularSmallTemplate.textProvider =
CLKSimpleTextProvider(text: stringa)
modularSmallTemplate.fillFraction = 0.95
modularSmallTemplate.ringStyle = CLKComplicationRingStyle.Closed
let template = CLKComplicationTimelineEntry(
date: NSDate(), complicationTemplate: modularSmallTemplate)
handler(template)
default:
handler(nil)
}
I have the switch IBAction in InterfaceController.swift.
I am encountering issues in using:
func updateComplication() {
let complicationServer = CLKComplicationServer.sharedInstance()
for complication in complicationServer.activeComplications {
complicationServer.reloadTimelineForComplication(complication)
}
}
Where do I have to write the above mentioned func, in order to be able to call it from inside the IBAction?
If I write it in ComplicationController.swift, by calling it from the IBAction in InterfaceController.swift as:
ComplicationController.updateComplication()
I get the error “Missing argument for parameter #1 in call”,
while if I write it in InterfaceController and call it in the IBAction, although the build is successful, when running the App and changing the value of the switch I get the following error: “fatal error: unexpectedly found nil while unwrapping an Optional value” on the line:
for complication in complicationServer.activeComplications
of func updateComplication.
It's not the placeholder text that you want to update. The placeholder template is a static template that's displayed in the selection screen for your complication while you are customizing your watch face. It's only called once to get the placeholder text, and doesn't get called when a timeline is reloaded.
The complication's current (actual, live) timeline entry is provided by getCurrentTimelineEntryForComplication. This function is where you should use your model's on state of the switch to create a CLKTextProvider containing a "N" or "Y" for the active complication family.
Finally, you should call updateComplication() from your switch IBAction to reload your complication timeline, which would update the current entry to show the new state of the switch on the watch face.
Since your complication is updated manually, you should return a nil updateDate in getNextRequestedUpdateDateWithHandler to avoid scheduling your complication for any regular updates.
Update for your edited question:
You should include the updateComplication function in your interface controller, as your complication controller isn't meant to be instantiated by you.
In regard to the "Unexpectedly found nil while unwrapping an Optional value" error, this is an issue that has been mentioned on the Apple Developer Forums.
If you are already running watchOS 2.1, you could try the latest watchOS beta to see if the issue has been fixed yet for 2.2. You should also file a bug report and dupe radar 22947535.
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)
}