How to Make Document Based Swift App

I'm trying to make a docmunent based Swift macOS app. I've done it before using Objective-C, but can't seem to figure it out for Swift. I've done a bunch of research but can't seem to find any resources on setting up the Document.swift file for saving.

If someone can give the full code for the Document.swift file for just saving the contents of a text field that would be much appreciated.

Accepted Reply

Your old Objective-C code is not a good example of implementing a subclass of `NSDocument`. You should not set `*outError` when you have no errors.


It would be re-written in Swift like this:


import Cocoa
class MyDocument: NSDocument {
    var assignments: [String] = []
  
    override func data(ofType typeName: String) throws -> Data {
        return NSKeyedArchiver.archivedData(withRootObject: assignments)
    }
  
    override func read(from data: Data, ofType typeName: String) throws {
        //### This may cause unrecoverable error...
        assignments = NSKeyedUnarchiver.unarchiveObject(with: data) as! [String]
    }

    //...(irrelevant parts omitted)
}

(Your `assignments` may be an array of some class conforming to `NSCoding`, the code above is simplified in many ways.)


Some points, to make your Document-Based app Loadable and Savable:


- Your subclass of `NSDocument` should have a property representing the Document Model

- You need to implement `data(ofType:)` and `read(from:ofType:) throws` consistently


So, you can declare a property which can contain the value of your `textField`. If it is a container class which conforms to `NSCoding`, you can use most part of my code above as is.

Replies

Xcode can generate app templates for Document-Based Apps both in Swift and Objective-C. If you say you can do it with Objective-C, you should be able to do it in Swift.


Please show your Document.m in Objective-C, and explain what part of it you feel difficult to port to Swift.

I made the new app making sure that document based app was enabled and I got the additional file for a document, so I guess I'm using the template.

For my old app I was saving the contents of a table, not a text field. For this new app I want to be saving the contents of a text field, sort of a notepad app.

This is the code that I used years and years ago for the document.m file:

#import "MyDocument.h"
@implementation MyDocument
@synthesize assignments;
- (id)init {
    self = [super init];
    if (self) {
        assignments = [[NSMutableArray alloc] init];
    }
    return self;
}
- (NSString *)windowNibName {
    /
    /
    return @"MyDocument";
}
- (void)windowControllerDidLoadNib:(NSWindowController *)aController {
    [super windowControllerDidLoadNib:aController];
}
- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError {
    if (outError) {
        *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:NULL];
    }
    return [NSKeyedArchiver archivedDataWithRootObject:assignments];
}
- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError {
    if (outError) {
        *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:NULL];
    }
    [self setAssignments:[NSKeyedUnarchiver unarchiveObjectWithData:data]];
    return YES;
}
+ (BOOL)autosavesInPlace {
    return YES;
}
- (void)dealloc {
    [assignments release];
    [super dealloc];
}
- (NSPrintOperation *)printOperationWithSettings:(NSDictionary<NSString *,id> *)printSettings error:(NSError * _Nullable *)outError {
    return [NSPrintOperation printOperationWithView:self.coolView];
}

Your old Objective-C code is not a good example of implementing a subclass of `NSDocument`. You should not set `*outError` when you have no errors.


It would be re-written in Swift like this:


import Cocoa
class MyDocument: NSDocument {
    var assignments: [String] = []
  
    override func data(ofType typeName: String) throws -> Data {
        return NSKeyedArchiver.archivedData(withRootObject: assignments)
    }
  
    override func read(from data: Data, ofType typeName: String) throws {
        //### This may cause unrecoverable error...
        assignments = NSKeyedUnarchiver.unarchiveObject(with: data) as! [String]
    }

    //...(irrelevant parts omitted)
}

(Your `assignments` may be an array of some class conforming to `NSCoding`, the code above is simplified in many ways.)


Some points, to make your Document-Based app Loadable and Savable:


- Your subclass of `NSDocument` should have a property representing the Document Model

- You need to implement `data(ofType:)` and `read(from:ofType:) throws` consistently


So, you can declare a property which can contain the value of your `textField`. If it is a container class which conforms to `NSCoding`, you can use most part of my code above as is.

>> You should not set `*outError` when you have no errors.


This is a side issue, since this thread is not really about Obj-C, but just to clarify a subtle point:


It is not wrong to set "*outError" when there is no error. The API contract for Obj-C error handling only requires that it be set to a valid error when the method returns "nil" or "NO" (false). If there is no error, the caller cannot assume that its NSError* parameter hasn't been changed.


This trips up incautious programmers up, because they'll do something like this:


NSError* error = nil;
result = [self doSomethingThatCouldFailWithError: &error];
if (error != nil)
     … // handle the error


This pattern is a recipe for disaster, because "error" may be non-nil even on success.


However, you are correct in that the original code is not very elegant. In the first case, the error is too generic and unhelpful, and in the second case the error is unnecessary because the method always returns YES.