How to get data from NSDocument's read method - swift

I'm having difficulty trying to put 'data' onto NSPasteboard. By 'data', I mean a type other than the specific PasteboardType formats: text, HTML, image, etc. (It's actually MIDI data.)
override func copy() -> Any {
let pboard = NSPasteboard.general
pboard.clearContents()
pboard.setData(data, forType: .typeMidi)
return true
}
When I try to put my data in, I get:
Cannot convert value of type '(String) throws -> Data' to expected element type 'NSPasteboardWriting'.
That's probably because I've been trying to use NSDocument's data method, but it turns out that's only for writing out to disk.
The data needs to come from the read function:
override func read(from data: Data, ofType typeName: String) throws {
self.theMIDIPlayer = try AVMIDIPlayer.init(data: data, soundBankURL: nil)
if self.theMIDIPlayer == nil {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}
But it seems that the read function's arguments are not NSDocument's data function (which only relates to writing). I've no idea where the arguments come from.
Adding something like self.myData = data to the read function (in an attempt to get the data in a useful property) produce "Expected pattern" errors.

SOLVED: The problem was a schoolboy error of using copy() instead of copy(_:).
Vadian's now deleted answer was helpful in creating a custom Pasteboard type.
The complete (relevant) code is as follows:
extension NSPasteboard.PasteboardType {
static let typeMidi = NSPasteboard.PasteboardType(rawValue: "public.midi-audio")
}
class Document: NSDocument {
var theMIDIPlayer: AVMIDIPlayer?
var myData: Data?
#IBAction func copy(_: Any) {
let pboard = NSPasteboard.general
pboard.clearContents()
pboard.setData(myData, forType: .typeMidi)
}
override func read(from data: Data, ofType typeName: String) throws {
self.theMIDIPlayer = try AVMIDIPlayer.init(data: data, soundBankURL: nil)
self.myData = data
if self.theMIDIPlayer == nil {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}

Related

Cocoa: How to re-read file content after file has been changed from another app using NSDocument?

I have a document based cocoa app that opens an .md file to display the markdown content in a nice format. If I change the .md file in another app like textedit, I want to reload the views in my app.
Here's what I have working so far:
import Cocoa
class Document: NSDocument {
var fileContent = "Nothing yet :("
override init() {
// Add your subclass-specific initialization here.
super.init()
}
override class var autosavesInPlace: Bool {
return false
}
override func makeWindowControllers() {
// Returns the Storyboard that contains your Document window.
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as! NSWindowController
self.addWindowController(windowController)
}
override func data(ofType typeName: String) throws -> Data {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
override func read(from data: Data, ofType typeName: String) throws {
fileContent = (try String(data: data, encoding: .utf8))!
}
// this fn is called every time textEdit changes the file content.
override func presentedItemDidChange() {
// Here is the PROBLEM:
// HOW do I access the new file content?
}
}
Here is the problem
presentedItemDidChange() is called every time textEdit makes a change. That works great. But I can't for the life of me figure out how then to access the new file content, so I can reassign fileContent = newContent. Any thoughts?
I would call for the document readFromURL:ofType:error: as described here.

How can a closure be used to only complete when data is loaded? (swift)

I'm slowly coming to terms with closures.
This is taken from the following post:
https://medium.com/#stasost/ios-three-ways-to-pass-data-from-model-to-controller-b47cc72a4336
I understand the function definition requestData is taking a closure which is called with completion(data):
class DataModel {
func requestData(completion: ((data: String) -> Void)) {
// the data was received and parsed to String
let data = "Data from wherever"
completion(data)
}
}
class ViewController: UIViewController {
private let dataModel = DataModel()
override func viewDidLoad() {
super.viewDidLoad()
dataModel.requestData { [weak self] (data: String) in
self?.useData(data: data)
}
}
private func useData(data: String) {
print(data)
}
}
While I understand requestData is being called in viewDidLoad below and that (data:String) is being passed into requestData I don't quite get what is being done when completion(data) is being called.
Is completion(data) executing the code that is happening after the keyword "in"?
{ [weak self] (data: String) in
self?.useData(data: data)
}
And I had a question about the order in which things are happening. Is it:
a) let data = "Data from wherever"
b) completion(data)
c) self?.useData(data: data)
In the current app I'm working on when the user starts the app I make an api call to load data. But i'm still a tiny bit unsure about how to tell the ViewController that the data has finished loading.
Thanks.
You understand that a function can take, say, an Int as parameter?
func f(_ i:Int) {
// do something with i
}
Well, a function can also take a function as parameter:
func requestData(completion: ((data: String) -> Void)) {
// do something with completion
}
In that declaration, completion is a function — a function that takes one parameter, called data.
What can you do when you receive a function as a parameter? One obvious thing to do with it would be to call it:
func requestData(completion: ((data: String) -> Void)) {
completion(data: "well howdy there")
}
It remains only to talk about the syntax of passing the desired function to requestData as its completion parameter. Here is one way:
func output(data: String) {
print(data)
}
requestData(completion:output)
Here's a nicer way, one that avoids giving the passed function a name:
requestData(completion:{
data in
print(data)
})
Finally, since completion is the only parameter, we are allowed to use "trailing closure" syntax — we delete the completion: label and the parentheses of the call:
requestData {
data in
print(data)
}

How to update data on cloudKit without creating a new record?

Here is what I am trying to do. I have a simple journaling app with two views: a tableView that lists the titles of the entries and a viewController that has a text field for a title, and a textView for the text body (and a save button to save to cloudKit). On the viewController, I hit save and the record is saved to cloudKit and also added to the tableView successfully. This is all good.
I want to be able to edit/update the journal entry. But when I go back into the journal entry, change it in any way, then hit save again, the app returns to the tableView controller with an updated entry, but cloudKit creates a NEW entry separate from the one I wanted to edit. Then when I reload the app, my fetchRecords function fetches any extra records cloudKit has created.
Question: How do I edit/update an existing journal entry without creating a new entry in cloudKit?
Let me know if you need something else to further clarify my question.
Thanks!
Here are my cloudKit functions:
import Foundation
import CloudKit
class CloudKitManager {
let privateDB = CKContainer.default().publicCloudDatabase //Since this is a journaling app, we'll make it private.
func fetchRecordsWith(type: String, completion: #escaping ((_ records: [CKRecord]?, _ error: Error?) -> Void)) {
let predicate = NSPredicate(value: true) // Like saying I want everything returned to me with the recordType: type. This isn't a good idea if you have a massive app like instagram because you don't want all posts ever made to be loaded, just some from that day and from your friends or something.
let query = CKQuery(recordType: type, predicate: predicate)
privateDB.perform(query, inZoneWith: nil, completionHandler: completion) //Allows us to handle the completion in the EntryController to maintain proper MVC.
}
func save(records: [CKRecord], perRecordCompletion: ((_ record: CKRecord?, _ error: Error?) -> Void)?, completion: ((_ records: [CKRecord]?, _ error: Error?) -> Void)?) {
modify(records: records, perRecordCompletion: perRecordCompletion, completion: completion )
}
func modify(records: [CKRecord], perRecordCompletion: ((_ record: CKRecord?, _ error: Error?) -> Void)?, completion: ((_ records: [CKRecord]?, _ error: Error?) -> Void)?) {
let operation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
operation.savePolicy = .ifServerRecordUnchanged //This is what updates certain changes within a record.
operation.queuePriority = .high
operation.qualityOfService = .userInteractive
operation.perRecordCompletionBlock = perRecordCompletion
operation.modifyRecordsCompletionBlock = { (records, _, error) in
completion?(records, error)
}
privateDB.add(operation) //This is what actually saves your data to the database on cloudkit. When there is an operation, you need to add it.
}
}
This is my model controller where my cloudKit functions are being used:
import Foundation
import CloudKit
let entriesWereSetNotification = Notification.Name("entriesWereSet")
class EntryController {
private static let EntriesKey = "entries"
static let shared = EntryController()
let cloudKitManager = CloudKitManager()
init() {
loadFromPersistentStorage()
}
func addEntryWith(title: String, text: String) {
let entry = Entry(title: title, text: text)
entries.append(entry)
saveToPersistentStorage()
}
func remove(entry: Entry) {
if let entryIndex = entries.index(of: entry) {
entries.remove(at: entryIndex)
}
saveToPersistentStorage()
}
func update(entry: Entry, with title: String, text: String) {
entry.title = title
entry.text = text
saveToPersistentStorage()
}
// MARK: Private
private func loadFromPersistentStorage() {
cloudKitManager.fetchRecordsWith(type: Entry.TypeKey) { (records, error) in
if let error = error {
print(error.localizedDescription)
}
guard let records = records else { return } //Make sure there are records.
let entries = records.flatMap({Entry(cloudKitRecord: $0)})
self.entries = entries //This is connected to the private(set) property "entries"
}
}
private func saveToPersistentStorage() {
let entryRecords = self.entries.map({$0.cloudKitRecord})
cloudKitManager.save(records: entryRecords, perRecordCompletion: nil) { (records, error) in
if error != nil {
print(error?.localizedDescription as Any)
return
} else {
print("Successfully saved records to cloudKit")
}
}
}
// MARK: Properties
private(set) var entries = [Entry]() {
didSet {
DispatchQueue.main.async {
NotificationCenter.default.post(name: entriesWereSetNotification, object: nil)
}
}
}
}
Here's a couple threads that might be helpful.
If you were caching the data locally you would use the encodesystemfields method to create a new CKRecord that will update an existing one on the server.
How (and when) do I use iCloud's encodeSystemFields method on CKRecord?
It doesn't appear you are caching locally. I don't have experience doing it without using encodesystemfields, but it looks like you have to pull the record down and save it back in the completion handler of the convenience method:
Trying to modify ckrecord in swift

How to add simple save-load functions to document-based Swift 3 app?

I'm studying Swift (for just a hobby) at the moment, and trying to figure out how I'm supposed to and save and load functions to document.swift file in document-based swift app? I'd like to know how to save and load simple txt-files. I'm using NSTextView, so I guess I have to change that to NSString?
Here are those functions at the moment:
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)
}
In you document.swift file these functions below will load and set your text views string value.
Hope this helps!
var content = ""
override func makeWindowControllers() {
//Make the window controller
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: "Document Window Controller") as! NSWindowController
self.addWindowController(windowController)
//Access the view controller
let vc = windowController.contentViewController as! ViewController
//Set the text view string to the variable that was loaded
vc.textView.string = content
}
override func data(ofType typeName: String) throws -> Data {
//Access the current view controller
if let vc = self.windowControllers[0].contentViewController as? ViewController {
//Get the textView's String Value; or call a function that will create a string of what needs to be saved
return vc.textView.string?.data(using: String.Encoding.utf8) ?? Data()
}else {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}
override func read(from url: URL, ofType typeName: String) throws {
do {
//Set a string variable to the loaded string
//We can't directly set the text views string value because this function is called before the makeWindowControllers() so now text view is created yet.
content = try String(contentsOf: url, encoding: String.Encoding.utf8)
}catch {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}

Subclass AFNetworking JSON Response Serialization to insert JSON response body in to error data in Swift 2

AFNetworking does not return the JSON response body when it fails (e.g. 400 status code), therefore you must subclass AFJSONResponseSerializer and fill in the error object with such. This is the recommended here and an example is here
Up until Swift 2, I used the following code to achieve such:
import Foundation
let JSONResponseSerializerWithDataKey : String = "JSONResponseSerializerWithDataKey"
let JSONResponseUndefinedAPIFailureReason : String = "UNKNOWN_ERROR"
class JSONResponseSerializerWithData: AFJSONResponseSerializer
{
override func responseObjectForResponse (response: NSURLResponse, data: NSData, error: AutoreleasingUnsafeMutablePointer
<NSError?>) -> AnyObject
{
var json : AnyObject = super.responseObjectForResponse(response, data: data, error: error) as AnyObject
if (error.memory? != nil)
{
var errorValue = error.memory!
var userInfo = errorValue.userInfo
if let errorDetail = json["detail"] as? String
{
userInfo![JSONResponseSerializerWithDataKey] = errorDetail
}
else
{
userInfo![JSONResponseSerializerWithDataKey] = JSONResponseUndefinedAPIFailureReason
}
error.memory = NSError(domain: errorValue.domain, code: errorValue.code, userInfo: userInfo)
}
return json
}
}
Start with Swift 2, a new type of Error handling was introduced.
The signature of the above function is now:
override func responseObjectForResponse(response: NSURLResponse!, data: NSData!) throws -> AnyObject
I am having trouble achieving the same as above inside a do-catch statement as it seems the failure does not invoke the catch statement, and thus there is no access to the error object. Further, new ErrorTypes are essentially empty and don't contain
This is what I've tried, but the catch statement is never called:
class JSONResponseSerializerWithData: AFJSONResponseSerializer
{
override func responseObjectForResponse(response: NSURLResponse!, data: NSData!) throws -> AnyObject
{
do
{
return try super.responseObjectForResponse(response, data: data)
}
catch
{
let nsError = (error as NSError)
var userInfo = nsError.userInfo
if let errorDetail = userInfo["detail"] as? String
{
userInfo[JSONResponseSerializerWithDataKey] = errorDetail
}
else
{
userInfo[JSONResponseSerializerWithDataKey] = JSONResponseUndefinedAPIFailureReason
}
throw NSError(domain: nsError.domain, code: nsError.code, userInfo: userInfo)
}
}
}
I've tried stepping through the AFNetworking2 Library an the body of the response is there, so I could sub-class it in Objective-C rather than Swift, but would prefer doing such if possible.
Am I handling this incorrectly with a do-catch statement? Any pointers in the right direction would be greatly appreciated.
After more digging, I have found that the issue is as described here.
The error is not thrown, only filled, therefore catch is never called. To induce this old behaviour you can add "NS_SWIFT_NOTHROW" to the end of the Objective-C signature in the header file as described here
This will change the signature to:
override func responseObjectForResponse(response: NSURLResponse?, data: NSData?, error: NSErrorPointer) -> AnyObject?
Which can then be used like before.