Prevent NSDocument's auto-saving while its content is editing - cocoa

I develop a document-based Cocoa application allowed saving documents asynchronous. Namely, my NSDocument subclass returns ture on canAsynchronouslyWrite(to:typeOf:for:).
I want dynamically and silently delay (or cancel) regular auto-saving if the document content is editing. At first, I thought it's enough when I throw an error in checkAutosavingSafety(), but it displays an error message dialog for user.
I believe there is a standard way for such a standard demand. But I'm not sure either where in a NSDocument subclass I should prevent saving and to which method I should say "please wait".
Does someone have any idea for this?
For the reference, the content of document is text which is managed by NSTextView subclass.

I finally found that throwing an .userCalcelled error in a saving process with autosavingIsImplicitlyCancellable can cancel autosaving.
/// make autosaving cancellable
override var autosavingIsImplicitlyCancellable: Bool {
return true
}
/// save or autosave the document contents
override func save(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType, completionHandler: #escaping (Error?) -> Void) {
// cancel if something is working
guard saveOperation != .autosaveInPlaceOperation || !self.isEditing else {
completionHandler(CocoaError(.userCancelled))
return
}
super.save(to: newUrl, ofType: typeName, for: saveOperation, completionHandler: completionHandler)
}
/// whether your document is currently being edited
var isEditing: Bool {
// check your document state
}

Related

How do I implement drag-and-drop from Photos to my Cocoa app with full quality?

In my drag destination view (NSView subclass), I have implemented:
override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool {
// check for Photos promises
var gotPromised = false
draggingInfo.enumerateDraggingItems(options: [], for: self, classes: [NSFilePromiseReceiver.self], searchOptions: [:], using: {(draggingItem, idx, stop) in
let filePromiseReceiver = draggingItem.item
print("got a file promise receiver: \(filePromiseReceiver)")
gotPromised = true
// Use filePromiseReceiver here for your task.
})
return gotPromised
}
When I run this and drag something over from Photos.app, I get this warning:
How do I fix my drag destination to not have this warning? I would like to get full-quality photos into my app.
Well. Figured it out myself after some investigation. You need to do multiple things (register for correct types, enumerate the items correctly etc), and the warning is not there if you just implement the promise receiving completely and correctly. Apple has provided an example project which implements everything correctly.

URL based document saving

I have a prototype project of url based documents, where I cache document info to user defaults. User actions tally the change count which I'd like to autosave - once I figure out how, so an explicit close triggers the standard dialog to save first.
Choosing not to save, the window closes - first calling the window's delegate, method - windowShouldClose, as expected. Choosing save, triggers a call to the document's method
override func save(to url: URL, ofType typeName: String, for saveOperation: NSSaveOperationType, completionHandler: #escaping (Error?) -> Void) {
do {
try self.write(to: url, ofType: typeName)
Swift.print("save(to: \(url.absoluteString) ofType: \(typeName)) ")
} catch let error {
NSApp.presentError(error)
}
}
which runs to completion - I get the debug output, ok but I suspect something's wrong as the window delegate method isn't called; a 2nd try to close the window closes without incident.
I'm confused what state is not right that the change clearing didn't reset, allow the doc to close its window after saving? Btw I do not update change count.

How to associate existing file type for Mac OS application in XCode 7?

I am making a simple viewer for jpeg pictures in Xcode 7, and trying to associate Jpeg file type with my application. This is a Cocoa application for OS X in Swift that uses storyboards, and is not a document-based application.
Tutorials that I found online suggest going through Info tab, adding a new document type there. Now that's where things start to look different from those in tutorials: in them there is only 3 field to fill out (Name, Types, Icon), but I have many more. I tried to experiment around and here is what I put in fields that I got:
Name: JPEG image
Class: left it blank
Extensions: .jpg, .jpeg, .JPEG, .JPG
Icon: left it blank
Identifier: public.jpeg
Role: Viewer
Mime types: image/jpeg
"Document is distributed as a bundle" is partially checked by default; I just left it as is.
Did not touch additional document type properties either.
As a result I got my application showing in a list in "Open with" when I secondary-click a Jpeg file along with other installed applications, but once I try to open it that way, I get a popup saying The document "picture.jpg" could not be opened. MyApp cannot open files in the "JPEG image" format.
What am I doing wrong?
The Class field is compulsory, and you have to have corresponding implementation.
Try:
Class: $(PRODUCT_MODULE_NAME).Document
And then add the Document.swift:
import Cocoa
class Document : NSDocument
{
override init() {
super.init()
// Add your subclass-specific initialization here.
}
override class func autosavesInPlace() -> Bool {
return true
}
override func makeWindowControllers() {
// Returns the Storyboard that contains your Document window.
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: "Document Window Controller") as! NSWindowController
self.addWindowController(windowController)
}
override func data(ofType typeName: String) throws -> Data {
// Insert code here to write your document to data of the specified type. If outError != nil, ensure that you create and set an appropriate error when returning nil.
// You can also choose to override fileWrapperOfType:error:, writeToURL:ofType:error:, or writeToURL:ofType:forSaveOperation:originalContentsURL:error: instead.
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
override func read(from data: Data, ofType typeName: String) throws {
// Insert code here to read your document from the given data of the specified type. If outError != nil, ensure that you create and set an appropriate error when returning false.
// You can also choose to override readFromFileWrapper:ofType:error: or readFromURL:ofType:error: instead.
// If you override either of these, you should also override -isEntireFileLoaded to return false if the contents are lazily loaded.
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}
^ Modify as you need.
Also, you specified the role "Viewer", means you can open it with the space bar, and not the double click (open) - which is the role "Edit" right?

I cannot find the currentUser() in parse

I am making an app so whenever somebody can press the add button and then type in one of their family member's emails and it will add the email to an array in parse. This is what my code looks like:
func Save() {
PFUser.currentUser()!.addObject(memberName.text!, forKey: "Family_Emails")
}
memberName is the TextField's IBOutlet connection name
"Family_Emails" is the parse array object I have set up
Whenever I click the save button it doesn't do anything. What do I do?
You're just not calling save on the user object.
PFUser.currentUser().saveInBackgroundWithBlock {
(success: Bool, error: NSError?) -> Void in
if (success) {
// The object has been saved.
} else {
// There was a problem, check error.description
}
}
https://parse.com/docs/ios/guide#objects-saving-objects

Can I receive a callback whenever an NSPasteboard is written to?

I've read Apple's Pasteboard Programming Guide, but it doesn't answer a particular question I have.
I'm trying to write a Cocoa application (for OS X, not iOS) that will keep track of everything that is written to the general pasteboard (so, whenever any application copies and pastes, but not, say, drags-and-drops, which also makes use of NSPasteboard). I could (almost) accomplish this by basically polling the general pasteboard on a background thread constantly, and checking changeCount. Of course, doing this would make me feel very dirty on the inside.
My question is, is there a way to ask the Pasteboard server to notify me through some sort of callback any time a change is made to the general pasteboard? I couldn't find anything in the NSPasteboard class reference, but I'm hoping it lurks somewhere else.
Another way I could imagine accomplishing this is if there was a way to swap out the general pasteboard implementation with a subclass of NSPasteboard that I could define myself to issue a callback. Maybe something like this is possible?
I would greatly prefer if this were possible with public, App Store-legal APIs, but if using a private API is necessary, I'll take that too.
Thanks!
Unfortunately the only available method is by polling (booo!). There are no notifications and there's nothing to observe for changed pasteboard contents. Check out Apple's ClipboardViewer sample code to see how they deal with inspecting the clipboard. Add a (hopefully not overzealous) timer to keep checking for differences and you've got a basic (if clunky) solution that should be App-Store-Friendly.
File an enhancement request at bugreporter.apple.com to request notifications or some other callback. Unfortunately it wouldn't help you until the next major OS release at the earliest but for now it's polling until we all ask them to give us something better.
There was once a post on a mailing list where the decision against a notification api was described. I can't find it right now though. The bottom line was that probably too many applications would register for that api even though they really wouldn't need to. If you then copy something the whole system goes through the new clipboard content like crazy, creating lots of work for the computer. So i don't think they'll change that behavior anytime soon. The whole NSPasteboard API is internally built around using the changeCount, too. So even your custom subclass of NSPasteboard would still have to keep polling.
If you really want to check if the pasteboard changed, just keep observing the changeCount very half second. Comparing integers is really fast so there's really no performance issue here.
Based on answer provided by Joshua I came up with similar implementation but in swift, here is the link to its gist: PasteboardWatcher.swift
Code snippet from same:
class PasteboardWatcher : NSObject {
// assigning a pasteboard object
private let pasteboard = NSPasteboard.generalPasteboard()
// to keep track of count of objects currently copied
// also helps in determining if a new object is copied
private var changeCount : Int
// used to perform polling to identify if url with desired kind is copied
private var timer: NSTimer?
// the delegate which will be notified when desired link is copied
weak var delegate: PasteboardWatcherDelegate?
// the kinds of files for which if url is copied the delegate is notified
private let fileKinds : [String]
/// initializer which should be used to initialize object of this class
/// - Parameter fileKinds: an array containing the desired file kinds
init(fileKinds: [String]) {
// assigning current pasteboard changeCount so that it can be compared later to identify changes
changeCount = pasteboard.changeCount
// assigning passed desired file kinds to respective instance variable
self.fileKinds = fileKinds
super.init()
}
/// starts polling to identify if url with desired kind is copied
/// - Note: uses an NSTimer for polling
func startPolling () {
// setup and start of timer
timer = NSTimer.scheduledTimerWithTimeInterval(2, target: self, selector: Selector("checkForChangesInPasteboard"), userInfo: nil, repeats: true)
}
/// method invoked continuously by timer
/// - Note: To keep this method as private I referred this answer at stackoverflow - [Swift - NSTimer does not invoke a private func as selector](http://stackoverflow.com/a/30947182/217586)
#objc private func checkForChangesInPasteboard() {
// check if there is any new item copied
// also check if kind of copied item is string
if let copiedString = pasteboard.stringForType(NSPasteboardTypeString) where pasteboard.changeCount != changeCount {
// obtain url from copied link if its path extension is one of the desired extensions
if let fileUrl = NSURL(string: copiedString) where self.fileKinds.contains(fileUrl.pathExtension!){
// invoke appropriate method on delegate
self.delegate?.newlyCopiedUrlObtained(copiedUrl: fileUrl)
}
// assign new change count to instance variable for later comparison
changeCount = pasteboard.changeCount
}
}
}
Note: in the shared code I am trying to identify if user has copied a
file url or not, the provided code can easily be modified for other general
purposes.
For those who need simplified version of code snippet that gets the job done in Swift 5.7,
it just works (base on #Devarshi code):
func watch(using closure: #escaping (_ copiedString: String) -> Void) {
let pasteboard = NSPasteboard.general
var changeCount = NSPasteboard.general.changeCount
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
guard let copiedString = pasteboard.string(forType: .string),
pasteboard.changeCount != changeCount else { return }
defer {
changeCount = pasteboard.changeCount
}
closure(copiedString)
}
}
how to use is as below:
watch {
print("detected : \($0)")
}
then if you attempt copy any text in your pasteboard, it will watch and print out to the console like below..
detected : your copied message in pasteboard
detected : your copied message in pasteboard
in case, full code sample for how to use it for example in SwiftUI:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
watch {
print("detect : \($0)")
}
}
}
}
func watch(using closure: #escaping (_ copiedString: String) -> Void) {
let pasteboard = NSPasteboard.general
var changeCount = NSPasteboard.general.changeCount
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
guard let copiedString = pasteboard.string(forType: .string),
pasteboard.changeCount != changeCount else { return }
defer {
changeCount = pasteboard.changeCount
}
closure(copiedString)
}
}
}
It's not necessary to poll. Pasteboard would generally only be changed by the current view is inactive or does not have focus. Pasteboard has a counter that is incremented when contents change. When window regains focus (windowDidBecomeKey), check if changeCount has changed then process accordingly.
This does not capture every change, but lets your application respond if the Pasteboard is different when it becomes active.
In Swift...
var pasteboardChangeCount = NSPasteboard.general().changeCount
func windowDidBecomeKey(_ notification: Notification)
{ Swift.print("windowDidBecomeKey")
if pasteboardChangeCount != NSPasteboard.general().changeCount
{ viewController.checkPasteboard()
pasteboardChangeCount = NSPasteboard.general().changeCount
}
}
I have a solution for more strict case: detecting when your content in NSPasteboard was replaced by something else.
If you create a class that conforms to NSPasteboardWriting and pass it to -writeObjects: along with the actual content, NSPasteboard will retain this object until its content is replaced. If there are no other strong references to this object, it get deallocated.
Deallocation of this object is the moment when new NSPasteboard got new content.

Resources