PackagedDocument/MyTextPictDocument.m
/* |
Copyright (C) 2017 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
The NSDocument subclass for reading/writing it data and connecting with iCloud. |
*/ |
#import "MyTextPictDocument.h" |
#import "MyAppDelegate.h" |
#import "WindowController.h" |
#import "ViewController.h" |
// File names for the package content. |
NSString *ImageFileName = @"Image.png"; |
NSString *TextFileName = @"Text.txt"; |
NSString *MetaDataFileName = @"MetaData.plist"; |
NSString *MetaDataDisclosedKey = @"disclosedKey"; // View disclosed state in MetaData.plist. |
NSString *MetaDataValue2Key = @"value2"; // Special string value in MetaData.plist. |
// Encoding used to encode the notes text file. |
NSStringEncoding kTextFileEncoding = NSUTF8StringEncoding; |
@interface MyTextPictDocument () <ViewControllerDelegate> |
// Text content for this document. |
@property (copy) NSString *notes; |
// Meta data for this document (disclosure state + special string value). |
@property (strong) NSMutableDictionary *metaDataDict; |
// Document's main file wrapper (to contain subcontent for image, text and metadata). |
@property (strong) NSFileWrapper *documentFileWrapper; |
@end |
#pragma mark - |
@implementation MyTextPictDocument |
// ------------------------------------------------------------------------------- |
// autosavesInPlace |
// ------------------------------------------------------------------------------- |
+ (BOOL)autosavesInPlace |
{ |
// This gives us autosave and versioning for free in 10.7 and later. |
return YES; |
} |
// ------------------------------------------------------------------------------- |
// canAsynchronouslyWriteToURL:url:typeName:saveOperation |
// |
// Turn this on for async saving allowing saving to be asynchronous, making all our |
// save methods (dataOfType, saveToURL) to be called on a background thread. |
// ------------------------------------------------------------------------------- |
- (BOOL)canAsynchronouslyWriteToURL:(NSURL *)url ofType:(NSString *)typeName forSaveOperation:(NSSaveOperationType)saveOperation |
{ |
return YES; |
} |
// ------------------------------------------------------------------------------- |
// init |
// ------------------------------------------------------------------------------- |
- (instancetype)init |
{ |
self = [super init]; |
if (self != nil) |
{ |
// Setup our internal default metaData dictionary, |
// (used to illustrate reading/writing plist data to our file package). |
// |
// Note: these are the default values. |
// If a document was previously saved to disk "readFromFileWrapper" will load the real metadata. |
// |
self.metaDataDict = [NSMutableDictionary dictionaryWithObjectsAndKeys: |
@YES, MetaDataDisclosedKey, |
@"someText", MetaDataValue2Key, |
nil]; |
} |
return self; |
} |
// ------------------------------------------------------------------------------- |
// makeWindowControllers |
// |
// NSDocumentController invokes this method when creating or opening new documents. |
// We override it to use our own custom subclass of NSWindowController. |
// ------------------------------------------------------------------------------- |
- (void)makeWindowControllers |
{ |
// Load our hosting NSWindowController add attached it to this document. |
WindowController *windowController = |
[[NSStoryboard storyboardWithName:@"Main" bundle:nil] instantiateControllerWithIdentifier:@"WindowController"]; |
[self addWindowController:windowController]; |
// As a delegate we want to be notified when the user discloses the attachment view holding/displaying our image. |
[self ourViewController].delegate = self; |
// Notify our view controller to set its disclosure state for the attachmentView, |
// this will make sure the disclosure state in our window matches what's stored in this document. |
// |
BOOL disclosed = [[self.metaDataDict valueForKey:MetaDataDisclosedKey] boolValue]; |
[self ourViewController].disclosed = disclosed; |
} |
// ------------------------------------------------------------------------------- |
// setDisplayName |
// ------------------------------------------------------------------------------- |
- (void)setDisplayName:(NSString *)displayNameOrNil |
{ |
// Trim off the extension when used for displaying in the window title. |
super.displayName = displayNameOrNil.stringByDeletingPathExtension; |
} |
#pragma mark - Accessors |
// ------------------------------------------------------------------------------- |
// ourWindowController |
// |
// Convenience method, we have only one window controller. |
// ------------------------------------------------------------------------------- |
- (WindowController *)ourWindowController |
{ |
return self.windowControllers[0]; |
} |
// ------------------------------------------------------------------------------- |
// ourViewController |
// |
// Convenience method to access our content view controller. |
// ------------------------------------------------------------------------------- |
- (ViewController *)ourViewController |
{ |
return (ViewController *)[self ourWindowController].contentViewController; |
} |
#pragma mark - Package Support |
// ------------------------------------------------------------------------------- |
// fileWrapperOfType:typeName:error |
// |
// Called when the user saves this document or when autosave is performed. |
// Create and return a file wrapper that contains the contents of this document, |
// formatted for our specified type. |
// ------------------------------------------------------------------------------- |
- (NSFileWrapper *)fileWrapperOfType:(NSString *)typeName error:(NSError **)outError |
{ |
// If the document was not read from file or has not previously been saved, |
// it doesn't have a file wrapper, so create one. |
// |
if (self.documentFileWrapper == nil) |
{ |
NSFileWrapper *docfileWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:@{}]; |
self.documentFileWrapper = docfileWrapper; |
} |
NSDictionary *fileWrappers = self.documentFileWrapper.fileWrappers; |
// If there isn't a wrapper for the text file, create one too. |
if (fileWrappers[TextFileName] != nil) |
{ |
NSFileWrapper *textWrapper = fileWrappers[TextFileName]; |
[self.documentFileWrapper removeFileWrapper:textWrapper]; |
} |
NSData *textData = [[self ourViewController].textView.string dataUsingEncoding:kTextFileEncoding]; |
NSFileWrapper *textFileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:textData]; |
textFileWrapper.preferredFilename = TextFileName; |
[self.documentFileWrapper addFileWrapper:textFileWrapper]; |
// If the document file wrapper doesn't contain a file wrapper for an image and the image is not nil, |
// then create a file wrapper for the image and add it to the document file wrapper. |
// |
if ((self.documentFileWrapper.fileWrappers[ImageFileName] == nil) && (self.image != nil)) |
{ |
NSArray *imageRepresentations = self.image.representations; |
NSData *imageData = [NSBitmapImageRep representationOfImageRepsInArray:imageRepresentations |
usingType:NSPNGFileType |
properties:@{}]; |
if (imageData == nil) |
{ |
NSBitmapImageRep *imageRep = nil; |
@autoreleasepool |
{ |
imageData = self.image.TIFFRepresentation; |
imageRep = [[NSBitmapImageRep alloc] initWithData:imageData]; |
} |
imageData = [imageRep representationUsingType:NSPNGFileType properties:@{}]; |
} |
NSFileWrapper *imageFileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:imageData]; |
imageFileWrapper.preferredFilename = ImageFileName; |
[self.documentFileWrapper addFileWrapper:imageFileWrapper]; |
} |
// Check if we already have a meta data file wrapper, first remove the old one if it exists. |
NSFileWrapper *metaDataFileWrapper = self.documentFileWrapper.fileWrappers[MetaDataFileName]; |
if (metaDataFileWrapper != nil) |
[self.documentFileWrapper removeFileWrapper:metaDataFileWrapper]; |
// Write the new file wrapper for our meta data. |
NSError *plistError = nil; |
NSData *propertyListData = [NSPropertyListSerialization dataWithPropertyList:self.metaDataDict format:NSPropertyListXMLFormat_v1_0 options:0 error:&plistError]; |
if (propertyListData == nil || plistError != nil) |
{ |
NSLog(@"Could not create metadata plist data: %@", plistError.localizedDescription); |
return nil; |
} |
NSFileWrapper *newMetaDataFileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:propertyListData]; |
newMetaDataFileWrapper.preferredFilename = MetaDataFileName; |
[self.documentFileWrapper addFileWrapper:newMetaDataFileWrapper]; |
return self.documentFileWrapper; |
} |
// ------------------------------------------------------------------------------- |
// readFromFileWrapper:fileWrapper:typeName:outError |
// |
// Set the contents of this document by reading from a file wrapper of a specified type, |
// and return YES if successful. |
// ------------------------------------------------------------------------------- |
- (BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper ofType:(NSString *)typeName error:(NSError **)outError |
{ |
/* |
When opening a document, look for the image and text file wrappers. For each wrapper, |
extract the data from it and keep the file wrapper itself. The file wrappers are kept |
so that, if the corresponding data hasn't been changed, they can be resused during a |
save and thus the source file itself can be reused rather than rewritten. This avoids |
the overhead of syncing data unnecessarily. If the data related to a file wrapper changes |
(a new image is added or the text is edited), the corresponding file wrapper object is |
disposed of and a new file wrapper created on save (see fileWrapperOfType:error:). |
*/ |
NSDictionary *fileWrappers = fileWrapper.fileWrappers; |
// Load the text file from it's wrapper. |
NSFileWrapper *imageFileWrapper = fileWrappers[ImageFileName]; |
if (imageFileWrapper != nil) |
{ |
NSData *imageData = imageFileWrapper.regularFileContents; |
NSImage *targetImage = [[NSImage alloc] initWithData:imageData]; |
self.image = targetImage; |
} |
// Load the image file from it's wrapper. |
NSFileWrapper *textFileWrapper = fileWrappers[TextFileName]; |
if (textFileWrapper != nil) |
{ |
NSData *textData = textFileWrapper.regularFileContents; |
NSString *targetNotes = [[NSString alloc] initWithData:textData encoding:kTextFileEncoding]; |
self.notes = targetNotes; |
} |
// Load the metaData file from it's wrapper. |
NSFileWrapper *metaDataFileWrapper = fileWrappers[MetaDataFileName]; |
if (metaDataFileWrapper != nil) |
{ |
// We have meta data in this document. |
NSData *metaData = metaDataFileWrapper.regularFileContents; |
NSMutableDictionary *finalMetadata = [NSPropertyListSerialization propertyListWithData:metaData options:NSPropertyListImmutable format:NULL error:outError]; |
self.metaDataDict = finalMetadata; |
} |
self.documentFileWrapper = fileWrapper; |
return YES; |
} |
#pragma mark - Model Support |
// ------------------------------------------------------------------------------- |
// entireRange |
// ------------------------------------------------------------------------------- |
- (NSRange)entireRange |
{ |
NSTextView *targetTextView = [self ourViewController].textView; |
return NSMakeRange(0, targetTextView.string.length); |
} |
// ------------------------------------------------------------------------------- |
// updateTextView:textView |
// |
// Called by our window controller to update our text view after a read operation. |
// ------------------------------------------------------------------------------- |
- (void)updateTextView:(NSTextView *)inTextView |
{ |
// Take our model data and apply it to the textView. |
if (_notes != nil) |
{ |
[inTextView replaceCharactersInRange:[self entireRange] withString:_notes]; |
} |
} |
// ------------------------------------------------------------------------------- |
// updateImage:image |
// |
// Called by our window controller to update our image view after a read operation. |
// ------------------------------------------------------------------------------- |
- (void)updateImageView:(NSImageView *)inImageView |
{ |
// Take our model data and apply it to the textView. |
inImageView.image = self.image; |
} |
// ------------------------------------------------------------------------------- |
// updateImageModel:image |
// |
// This is called from our NSWindowController when an new image is dragged in. |
// ------------------------------------------------------------------------------- |
- (void)updateImageModel:(NSImage *)inImage |
{ |
self.image = inImage; |
// Remove the image file wrapper, if it exists. |
NSFileWrapper *imageFileWrapper = self.documentFileWrapper.fileWrappers[ImageFileName]; |
if (imageFileWrapper != nil) |
{ |
[self.documentFileWrapper removeFileWrapper:imageFileWrapper]; |
} |
// Auto save this change. |
[self updateChangeCount:NSChangeDone]; |
} |
// ------------------------------------------------------------------------------- |
// updateTextModel:text |
// |
// This is called from our NSWindowController when the text has changed. |
// ------------------------------------------------------------------------------- |
- (void)updateTextModel:(NSString *)text |
{ |
// Remove the text file wrapper, if it exists. |
NSFileWrapper *textFileWrapper = self.documentFileWrapper.fileWrappers[TextFileName]; |
if (textFileWrapper != nil) |
{ |
[self.documentFileWrapper removeFileWrapper:textFileWrapper]; |
} |
// Auto save this change. |
[self updateChangeCount:NSChangeDone]; |
} |
// ------------------------------------------------------------------------------- |
// updateChangeCount:change |
// ------------------------------------------------------------------------------- |
- (void)updateChangeCount:(NSDocumentChangeType)change |
{ |
// Keep track of changes are handled automatically by NSDocument, |
// but in case we want to track these changes independently of the built in NSUndoManager, we do it here. |
// |
[super updateChangeCount:change]; |
if (change == NSChangeDone) |
{ |
// We are up to date. |
} |
} |
#pragma mark - ViewControllerDelegate |
- (void)viewController:(ViewController *)viewController didDiscloseImage:(BOOL)disclosedImage |
{ |
// As a delegate we are notified by our content view controller subclass that the user disclosed |
// the attachmentView (holding/displaying the image). |
// |
[self.metaDataDict setValue:@(disclosedImage) forKey:MetaDataDisclosedKey]; |
// Disclosure state is also saved as part of the docyment, so auto save this change. |
[self updateChangeCount:NSChangeDone]; |
} |
@end |
Copyright © 2017 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2017-03-09