【问题标题】:How do I use a subclass of NSDocumentController in XCode 4?如何在 XCode 4 中使用 NSDocumentController 的子类?
【发布时间】:2011-11-14 11:09:42
【问题描述】:

我目前正在尝试自学 Cocoa 开发。为此,我购买了一本非常出色的书,Cocoa Recipes for Mac OS X: Vermont Recipes,其中介绍了如何创建示例应用程序。它非常好,但它是针对 XCode 3.2 而不是 XCode 4 编写的。到目前为止,我已经能够自己解决这个问题,但是我遇到了一个我无法弄清楚如何遵循指令的问题。

本质上,本书通过了一个子类化NSDocumentController 的示例案例,以便应用程序可以处理两种(最终可能是任意数量)不同类型的文档,并为每种类型打开相应的窗口。因此,我创建了NSDocumentController 的自定义子类(本书称之为VRDocumentController),现在我需要使该控制器的实例在应用程序启动过程中相对较早地加载。基本上,这个类是一个单例类,所以我必须在应用程序实例化标准类之前实例化我的类,这必须在过程的早期完成。很公平。

这本书引用了Apple documentation for subclassing NSDocumentController,它指出有两种方法可以解决这个问题:在MainMenu.xib 文件中实例化类或在-applicationWillFinishLaunching: 委托方法中实例化一个。 Apple 文档没有明确说明如何执行这些操作(稍后会详细介绍),并且本书仅涵盖第一个版本,我认为这可能是我的首选方法。

我的问题:我终生无法在 XCode 4 中实现这一点。本书为 XCode 3.2 提供的说明不再准确,因为 Interface Builder 现在已被改组为 XCode 本身,并且新版本的“类选项卡”不再显示我的项目的类。我发现this question on Stack Overflow 问了一个类似的问题,所以我尝试按照那里接受的答案。但是,当我打开 Identity Inspector 并尝试输入 VRDocumentController 时,它只会对我发出哔哔声并且不接受它。我编写的其他控制器类似乎都不是可接受的输入。

我也很乐意走另一条路;在-applicationWillFinishLaunching 方法中实例化一个副本。但是,我不知道该方法实际上属于哪个类,或者它的返回类型是什么。我也进行了大量的搜索,但没有运气。

【问题讨论】:

  • 至于为什么不能在 nib 文件中实例化自定义类的对象,我不确定。我会检查 VRDocumentController.{h,m} 是否已有效添加到项目中。如果您在网上发布您的项目,我很乐意看一看。
  • 我很乐意将其发布到网上。 (私人的,我可以让你访问)github repo 会起作用吗,还是有更好的方法?
  • 我通过在 Project Navigator 中单击鼠标右键、单击 Add File... 并浏览这些屏幕来添加类。那应该自动“将其添加到项目中”,是吗?如果没有,我需要做什么?
  • 项目位于 github.com/lukesneeringer/Vermont-Recipes(如果有其他人想帮助我查看它,请发表评论;我会将其公开,但因为它是来自书,我不确定这是不是犹太教)

标签: objective-c cocoa xcode4


【解决方案1】:

只需在主故事板中创建一个对象,将其类设置为您的 NSDocumentController 子类,它将用作应用程序共享文档控制器。

【讨论】:

    【解决方案2】:

    您可以在 Swift 3 中轻松地进行子类化:

    class AppDelegate: NSObject {
        let docController = DocController()
    }
    
    class DocController: NSDocumentController {
    }
    

    【讨论】:

      【解决方案3】:

      获取及时创建的自定义文档控制器更简单的方法是实现+ (void) load方法:

      这就是您实现自定义文档控制器所需的全部内容。

      @implementation AppDocumentController
      
      + (void) load
      {
          [AppDocumentController new];
      }
      
      // ... your overriding goes here
      
      @end
      

      这很酷,因为代码没有分离。 不能 100% 确定控制器是否创建得太早,但对我来说效果很好。

      【讨论】:

      • 这是最好的答案!一个应用程序中只有一个 NSDocumentController 实例。与初始化新实例的大多数其他类初始化方法不同,多次向 NSDocumentController 发送 init 只会返回相同的对象实例。因此,当 NSDocumentController 的子类通过 NSDocumentController 的 init 设置自己时,NSDocumentController 使该子类的实例成为应用程序中唯一可能的 NSDocumentController。再次调用 init 将始终返回与调用 sharedDocumentController 相同的结果。所以越早越好!
      • 优秀的答案。 Also shown 在 Galaxy Verge 上有一个完整的例子。
      【解决方案4】:

      这里有一个解决方案:

      // In MyDocumentController.h
      @interface MyDocumentController : NSDocumentController
      @end
      
      // In MyDocumentController.m
      @implementation MyDocumentController
        // ... your custom code here
      @end
      
      // In MyAppDelegate.h
      @interface AppDelegate : NSObject <NSApplicationDelegate>
      @property (nonatomic, strong) IBOutlet MyDocumentController *myController;
      @end
      

      现在,进入 MainMenu.xib 并将自定义对象添加到您的 nib。请务必在此对象上使用检查器,并在检查器的第三个窗格中,将自定义类设置为 MyDocumentController。

      现在通过在 nib 左侧的事物列表中按住 ctrl 单击新对象并拖动(同时按住 ctrl 单击)到 App Delegate,将此对象连接到您的插座。释放,它应该会闪烁并显示 myController。单击它,一切就绪。

      现在您可以使用以下代码测试您是否获得了自定义控制器:

      // In MyAppDelegate.m
      - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
          NSLog(@"sharedController is %@", [NSDocumentController sharedController]);
      }
      

      这应该打印类似的东西

      <MyDocumentController: 0x600000044710>
      

      你已经完成了!

      关键是在 MainMenu.xib 中连接您的自定义 MyDocumentController。如果您尝试仅在 applicationDidFinishLaunching 中对其进行初始化,则通常为时已晚,并且 AppKit 已经创建并设置了 sharedDocumentController(其中只能有一个)。

      此外,插座需要强而不是弱,因为您的自定义控制器是 nib 中的顶级对象,不被 nib 中的任何其他对象引用。

      这适用于我在多个版本的 OS X 和 Xcode 上。

      如果对您有用,请将其标记为正确答案! :-)

      【讨论】:

        【解决方案5】:

        在您的应用程序委托中:

        // LukeAppDelegate.h
        #import "LukeAppDelegate.h"
        #import "VRDocumentController"
        
        - (void)applicationWillFinishLaunching:(NSNotification *)notification {
            VRDocumentController *dc = [[VRDocumentController alloc] init];
        }
        

        这将确保 VRDocumentController 的实例被创建并注册为共享文档控制器,防止 Cocoa 使用默认的 NSDocumentController

        至于为什么您无法在 nib 文件中使用自定义对象,请确保在拖动新对象时选择对象(蓝色立方体)而不是对象控制器(绿色球体内的蓝色立方体)到 nib 文件中。


        编辑:如果您的目标是支持恢复的 OS X 版本,-applicationWillFinishLaunching: 注册自定义文档控制器可能为时已晚。如果应用程序委托放置在 MainMenu.xib 中,它应该在任何文档恢复之前由 nib 加载过程实例化,因此您可以将 NSDocumentController 子类初始化移动到应用程序委托的 init 方法:

        // LukeAppDelegate.h
        #import "LukeAppDelegate.h"
        #import "VRDocumentController"
        
        - (id)init {
            self = [super init];
            VRDocumentController *dc = [[VRDocumentController alloc] init];
            return self;
        }
        

        【讨论】:

        • 我看不到任何看起来像 AppDelegate.hAppDelegate.m 的东西,不管有没有前缀。我可以在项目开始后制作一个吗?
        • 你可以,但标准的 Cocoa 应用程序模板应该已经为你创建了一个。此外,应用程序委托通常是 MainMenu.nib 中自定义类的对象,就像您想要的 VRDocumentController 一样。
        • 嗯。我使用了基于文档的应用程序模板,它创建了一个 MyDocument 类。这可能是作为代表吗?
        • 好的,Bavarious 找到了问题的过去部分。答案是转到“对象库”,向下滚动到“对象”(它有一个蓝色立方体的图像),将其拖到 XIB 中的对象列表中,然后编辑类名。
        • 恕我直言,如果想处理 NSWindowRestoration 序列,这种方法将不起作用,这在调用 applicationWillFinishLaunching 时已经完成。
        【解决方案6】:

        标记为正确的答案 (@Bavarious'answer) 无法在基于文档的应用程序上运行,因为文档通常在调用任何 applicationWillFinishLaunching: 之前加载。在阅读了@Graham Perks 答案中的有用线索后,我尝试了几种不同的方法,这似乎可以可靠地工作:

        @implementation AppDelegate
        
        - (id)init
        {
            self = [super init];
            if (self) {
                MyDocumentController *dc = [[MyDocumentController alloc] init];
                if (dc) {};
            }
            return self;
        }
        

        注意显然(如果尚未创建)您需要先自己创建一个 AppDelegate 并将其链接到 MainMenu.XIB。

        【讨论】:

        • 在您的测试中,如果您在 MainMenu.xib 中实例化应用程序委托,它会失败吗?即,是否在加载 MainMenu.xib 之前进行恢复?
        • 在调用 - (id)init 之前不会发生恢复,无论它在 IB 中链接到什么。
        • 是否有关于主线程和文档控制器方法调用的断言?我问是因为我想将文档控制器放入服务实例(也应该共享)以防止巨大的 appDelegate 方法大小。
        【解决方案7】:

        如果 - 有时会发生 - 你的 NSDocumentController 子类似乎随机地没有作为 sharedDocumentController 返回,请在 -[NSDocumentController init] 上添加一个断点,并在堆栈上查看你的代码是什么(可能是意外)创建常规NSDocumentController。创建的第一个控制器总是从 sharedDocumentController 返回。

        当您的 XIB 文件中的某些内容被初始化并且(甚至间接地)调用 sharedDocumentController 时,可能会发生这种情况。在 XIB 文件加载期间创建对象的顺序不是确定性的,因此有时会首先创建您的 NSDocumentController 子类,有时会先初始化另一个有问题的对象,然后您的子类会被忽略。

        您必须将另一个对象的创建移到稍后的时间点(例如 applicationDidFinishLaunching:)。或者同时创建对象,但延迟它所做的任何初始化。

        (刚刚遇到了一个检查员小组搞砸的事情,认为这里值得注意!)

        【讨论】:

        • 打开基于文档的应用程序时,从上次会话中打开的文档会在 AppDelegate 之前立即加载。如何阻止这种情况?
        【解决方案8】:
        // This looked like the best thread to post this.
        // This is incomplete skeleton example of NSDocument/NSUndoManager usage.
        // Use at your own risk
        // Ultimately, would love if Apple reviewed/corrected this and added correct sample code to their docs.
        // I put pseudo code in implementation specific spots:
        //    "... implementation specific ..."
        //    "... implement something inherently dangerous... loading your data from a file"
        //    "... implement something inherently dangerous... commit changes to a file ..."
        //    "... in my implementation, I prompt user with save/quit options when edit window is closed ..."
        //
        
        // Apple's documentation states that NSDocumentController should *rarely* be subclassed, 
        // but Apple fails to provide sample code to accomplish what you need.
        // After trying to subclass NSDocumentController, I decided to rip all that code out.
        // What I really needed, in my case, was this:
        //    @interface NSObject(NSApplicationDelegate)
        //    - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename
        //
        // My requirements in a nutshell:
        // NSUndoManager, only allow 1 document open at a time (for now), multiple editing windows for 1 document, 
        // automatically save to temporary file, ability to quit and relaunch without saving,
        // Custom "open", "save", "save as", "revert to saved", etc... MyDocumentController
        // subclass below provides a simple way to bypass much of the NSDocumentController
        // file dialogs, but still use other features of NSDocument (like track unsaved changes).
        // Override only "NSDocumentController documentForURL".
        
        /* your NSDocument subclass MUST be defined in your Info.plist.
            If you don't, Lion will complain with a NSLog message that looks like this:
          -[NSDocumentController openDocumentWithContentsOfURL:display:completionHandler:] failed during state restoration. Here's the error:
           Error Domain=NSCocoaErrorDomain Code=256 "The document “blah.myfiletype” could not be opened.  cannot open files in the “blah Document” format." 
        In this example, files of type .myfiletype are associated with my NSDocument subclass
        Personally, I hate having to put a class name in my Info.plist, because its not maintainable!
        Note to Apple developers: if you are going to force us to do this, then pleasssse make
        sure that an XCode search "In Project" for "MyDocument" finds the entry in the plist file!
        Oh wow, I just answered my own question: Apple! please make "All candidate files" the
        default option for search!
        
        MyDocument:
        <key>CFBundleDocumentTypes</key>
        <array>
            <dict>
                <key>CFBundleTypeName</key>
                <string>My Funky App File</string>
                <key>NSDocumentClass</key>
                <string>MyDocument</string>
                <key>CFBundleTypeExtensions</key>
                <array>
                    <string>myfiletype</string>
                </array>
                <key>CFBundleTypeIconFile</key>
                <string>My_File_Icon.icns</string>
                <key>CFBundleTypeRole</key>
                <string>Editor</string>
            </dict>
        </array>
        */
        #define kMyFileTypeExtension @"myfiletype"
        
        @interface MyDocument : NSDocument
        {
        }
        - (void) registerForUndoGrouping;
        - (NSString *)filePath; // convenience function
        - (void) setFilePath: (NSString *)filePath; // convenience function
        
        + (BOOL) openMyDocument: (NSString *)filename; // class method
        @end
        
        extern int gWantsToQuit; // global indicator that it's time to stop drawing/updating
        
        // track my startup state so I can control order of initialization, and so I don't
        // waste time drawing/updating before data is available.
        enum // MyLaunchStatus
        {
           kFinishedPreWaking       = 0x01, // step 1: applicationWillFinishLaunching called
           kFinishedOpenFile        = 0x02, // step 2: (optionally) application:openFile: called (important for detecting double-click file to launch app)
           kFinishedWaking          = 0x04, // step 3: NSApp run loop ready to run. applicationDidFinishLaunching
           kFinishedPreLaunchCheck  = 0x08, // step 4: error recovery check passed
           kFinishedLoadingData     = 0x10, // step 5: data loaded
           kFinishedAndReadyToDraw  = 0x20, // step 6: run loop ready for drawing
        
        };
        
        typedef NSUInteger MyLaunchStatus;
        
        #pragma mark -
        @interface MyAppController : NSResponder <MidiProtocol, NSOpenSavePanelDelegate, NSTextFieldDelegate, NSWindowDelegate>
        MyDocument *toDocument;
        @end
        
        
        
        #pragma mark -
        @implementation MyDocument
        
        - (id)init
        {
           if ( !(self = [super init]) ) return self;
           return self;
        }
        
        - (void) registerForUndoGrouping
        {
        
        
           [[NSNotificationCenter defaultCenter] addObserver:self
                                                    selector:@selector(beginUndoGroup:) 
                                                        name:NSUndoManagerDidOpenUndoGroupNotification 
                                                      object:nil]; 
        }
        
        - (void)canCloseDocumentWithDelegate:(id)delegate shouldCloseSelector:(SEL)shouldCloseSelector contextInfo:(void *)contextInfo
        {
           if ( [[MyAppController instance] windowShouldClose: delegate] )
              if ( [delegate respondsToSelector:@selector(close)] )
                 [delegate close];
           //[delegate performSelector:shouldCloseSelector];
           //if ( [delegate isKindOfClass:[NSWindow class]] )
           //   [delegate performClose:self]; // :self];
           return; // handled by [[MyAppController instance] windowShouldClose:(id)sender
        }
        
        - (void)shouldCloseWindowController:(NSWindowController *)windowController delegate:(id)delegate shouldCloseSelector:(SEL)shouldCloseSelector contextInfo:(void *)contextInfo
        {
           if ( [[MyAppController instance] windowShouldClose: [windowController window]] )
              if ( [[windowController window] respondsToSelector:@selector(close)] )
                 [[windowController window] close];
        }
        
        - (void) beginUndoGroup: (NSNotification *)iNotification
        {
           NSUndoManager *undoMgr = [self undoManager];
           if ( [undoMgr groupingLevel] == 1 )
           {
              // do your custom stuff here
           }
        }
        
        // convenience functions:
        - (NSString *)filePath { return [[self fileURL] path]; }
        - (void) setFilePath: (NSString *)filePath 
        { 
           if ( [filePath length] )
              [self setFileURL:[NSURL fileURLWithPath: filePath]];
           else
              [self setFileURL:nil];
        }
        
        - (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem
        {
           if ( [self isDocumentEdited] && [anItem action] == @selector(revertDocumentToSaved:) )
              return YES;
           BOOL retVal = [super validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem];
           return retVal;
        }
        
        - (IBAction)revertDocumentToSaved:(id)sender
        {
           NSInteger retVal = NSRunAlertPanel(@"Revert To Saved?", [NSString stringWithFormat: @"Revert to Saved File %@?", [self filePath]], @"Revert to Saved", @"Cancel", NULL);
           if ( retVal == NSAlertDefaultReturn )
              [[MyAppController instance] myOpenFile:[self filePath]];
        }
        
        + (BOOL) openMyDocument: (NSString *)filename
        {
           if ( ![[filename pathExtension] isEqualToString: kMyAppConsoleFileExtension] )
              return NO;
        
           // If the user started up the application by double-clicking a file, the delegate receives the application:openFile: message FIRST
           MyLaunchStatus launchStatus = [[MyAppController instance] isFinishedLaunching];
           BOOL userDoubleClickedToLaunchApp = !( launchStatus & kFinishedPreLaunchCheck );
        
           MyDocument *currDoc = [[MyAppController instance] document];
           NSString *currPath = [currDoc filePath];
           NSInteger retVal;
           NSLog( @"open file %@ currPath %@ launchStatus %d", filename, currPath, launchStatus );
           if ( userDoubleClickedFileToLaunchApp )
           {
              // user double-clicked a file to start MyApp
              currPath = [[NSUserDefaults standardUserDefaults] objectForKey:@"LastSaveFile"];
              if ( [currPath isEqualToString: filename] )
              {
                 sWasAlreadyOpen = YES;
                 if ( [[[NSUserDefaults standardUserDefaults] objectForKey:@"isDocumentEdited"] boolValue] == YES )
                 {
                    retVal = NSRunAlertPanel(@"Open File", @"Revert to Saved?", @"Revert to Saved", @"Keep Changes", @"Quit", NULL);
                    if ( retVal == NSAlertDefaultReturn )
                    {
                       [[MyAppController instance] myOpenFile:filename];
                    }
                    else if ( retVal == NSAlertOtherReturn )
                       exit(0);
                 }
        
                 // proceed with normal startup
                 if ( currDoc )
                    return YES;
                 else
                    return NO;
              }
           }
        
           if ( !(launchStatus & kFinishedPreLaunchCheck ) ) // not done launching
              return YES; // startup in whatever state we were before
        
           if ( [currPath isEqualToString: filename] )
           {
              sWasAlreadyOpen = YES;
              NSLog( @"is edited %d currDoc %@", [currDoc isDocumentEdited], currDoc );
              if ( [currDoc isDocumentEdited] )
                 [currDoc revertDocumentToSaved:self]; // will prompt
              else // document is already open, so do what Apple's standard action is... 
                 [currDoc showWindows];
           }
           else 
           {
              if ( [currDoc isDocumentEdited] )
                 retVal = NSRunAlertPanel(@"Open File", [NSString stringWithFormat: @"The current file has unsaved changes.  Discard unsaved changes and switch to file '%@'?", filename], @"Discard unsaved changes and switch to file", @"Keep Current", NULL);
              else
                 retVal = NSRunAlertPanel(@"Switch to File", [NSString stringWithFormat: @"Switch to File '%@'?\n\nCurrent file '%@'", filename, currfilePath ? currfilePath : @"Untitled"], @"Switch", @"Keep Current", NULL);
              if ( retVal == NSAlertDefaultReturn )
                 [[MyAppController instance] myOpenFile:filename];
           }
        
           // user cancelled
           if ( currDoc )
              return YES;
           else
              return NO;
        }
        
        
        // Note: readFromURL is here for completeness, but it should never be called,
        // because we override NSDocumentController documentForURL below.
        - (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
        {
           if ( outError )
              *outError = nil;
           if ( ![typeName isEqualToString: kMyFileTypeExtension ] ) // 
              return NO;
           return YES;
        }
        
        // Note: writeToURL is here for completeness, but it should never be called,
        // because we override NSDocumentController documentForURL below.
        - (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
        {
           if ( outError )
              *outError = nil;
           return YES;
        }
        @end
        
        
        // kpk migrating slowly toward NSDocument framework 
        // (currently most functionality is in MyAppController)
        // Must bypass default NSDocumentController behavior to allow only 1 document
        // and keep MyAppController responsible for read, write, dialogs, etc.
        @implementation MyDocumentController
        
        // this should be the only override needed to bypass NSDocument dialogs, readFromURL,
        // and writeToURL calls.
        // Note: To keep Lion happy, MainInfo.plist and Info.plist must define "MyDocument" for key "NSDocumentClass"
        - (id)documentForURL:(NSURL *)absoluteURL
        {
           MyDocument *currDoc = [[MyAppController instance] document];
           if ( [[currDoc filePath] isEqualToString: [absoluteURL path]] )
              return currDoc;
           else
              return nil;
        }
        
        @end
        
        #pragma mark -
        @implementation MyAppController
        static MyAppController *sInstance;
        
        + (MyAppController *)instance
        {
           return sInstance; // singleton... why is this not in all Apple's sample code?
        }
        
        
        // called by main.mm before MyAppController (or NSApp for that matter) is created.
        // need to init some global variables here.
        + (void) beforeAwakeFromNib
        {
           NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        
           ... implementation specific ...
        
           // disable fancy stuff that slows launch down
           [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey: @"NSAutomaticWindowAnimationsEnabled"];
           [pool release];
        }
        
        
        - (void) awakeFromNib
        {
           NSLog(@"MyAppController awake\n");
           sInstance = self;
        
           [toWindow setNextResponder:self];
           [NSApp setDelegate:self];
        
        
           [self addWindowToDocument:toWindow];
        }
        
        - (MyDocument *)document 
        { 
           if ( !toDocument )
           {
              toDocument = [[MyDocument alloc] init];
           }
           return toDocument; 
        }
        
        - (NSUndoManager *)undoManager
        {
           // !!! WARNING: there are multiple NSUndoManager's in this App
        
           // Note: when an editable text field is in focus, 
           // NSTextField will create 
           // a separate undo manager for editing text while that field is in focus.
           // This means that hitting undo/redo while editing a text field will not go beyond the scope of that field.
        
           // This will return the global undo manager if the keyWindow was registered
           // via [self addWindowToDocument:];
           // Windows which are NOT part of the document (such as preferences, popups, etc.), will
           // return their own undoManager, and undo will do nothing while those windows are in front.
           // You can't undo preferences window changes, so we don't want to surprise the user.
           NSUndoManager *undomgr =  [[NSApp keyWindow] undoManager];
           if ( undomgr )
           {
              static bool sFirstTime = true;
              if ( sFirstTime )
              {
                 sFirstTime = false;
                 [undomgr setLevelsOfUndo:1000]; // set some sane limit
                 [[NSNotificationCenter defaultCenter] addObserver:self
                                                          selector:@selector(beginUndo:) 
                                                              name:NSUndoManagerWillUndoChangeNotification 
                                                            object:nil]; 
                 [[NSNotificationCenter defaultCenter] addObserver:self
                                                          selector:@selector(beginUndo:) 
                                                              name:NSUndoManagerWillRedoChangeNotification 
                                                            object:nil];  
        
                 [toDocument registerForUndoGrouping];
        //         [[NSNotificationCenter defaultCenter] addObserver:self
        //                                                  selector:@selector(endUndo:) 
        //                                                      name:NSUndoManagerDidUndoChangeNotification 
        //                                                    object:nil];       
        //         [[NSNotificationCenter defaultCenter] addObserver:self
        //                                                  selector:@selector(endUndo:) 
        //                                                      name:NSUndoManagerDidRedoChangeNotification 
        //                                                    object:nil];  
              }
        
           }
           return undomgr;
        }
        
        - (void) showStatusText: (id)iStatusText
        {
          ... implementation specific ...
        }
        
        - (void) beginUndo:(id)sender
        {
          // implementation specific stuff here
             NSUndoManager *undomgr =  [[NSApp keyWindow] undoManager];
        
           if ( [sender object] == undomgr )
           {
              if ( [undomgr isUndoing] )
                 [self showStatusText: [NSString stringWithFormat:@"Undo %@", [undomgr undoActionName]]];
              else if ( [undomgr isRedoing] )
                 [self showStatusText: [NSString stringWithFormat:@"Redo %@", [undomgr redoActionName]]];
           }
        }
        
        // Add a window (with a window controller) to our document, so that the window
        // uses the document's NSUndoManager.  In the future, we may want to use other features of NSDocument.
        - (void)addWindowToDocument:(NSWindow *)iWindow
        {
           NSString *autosaveName = [iWindow frameAutosaveName]; // preserve for "mainWindow", others.
           NSWindowController *winController = [iWindow windowController];
           if ( !winController )
              winController = [[NSWindowController alloc] initWithWindow:iWindow];
        
           // create document if needed, and add window to document.
           [[self document] addWindowController: winController];
           if ( autosaveName )
              [iWindow setFrameAutosaveName:autosaveName]; // restore original for "mainWindow", others.
           [winController setNextResponder:self]; // keep last hotkey destination... see keyDown:
        
        }
        
        - (void) myOpenFile:(NSString*)path
        {
          // this is just a skeleton of what I do to track unsaved changes between relaunches
        
           [toDocument setFilePath:path];
        
          ... implementation specific ...
              [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:@"isDocumentEdited"];
              [toDocument updateChangeCount:NSChangeCleared];
              [[NSUserDefaults standardUserDefaults] setObject:[toDocument filePath] forKey:@"LastSaveFile"];
        
              BOOL success = [[NSUserDefaults standardUserDefaults] synchronize]; // bootstrap
              // kpk very important... resetStandardUserDefaults forces the immutable
              // tree returned by dictionaryWithContentsOfFile to be mutable once re-read.
              // Apple: "Synchronizes any changes made to the shared user defaults object and releases it from memory.
              //         A subsequent invocation of standardUserDefaults creates a new shared user defaults object with the standard search list."
              [NSUserDefaults resetStandardUserDefaults];
        
              NSString *name = [[NSUserDefaults standardUserDefaults] objectForKey:@"LastSaveFile"]; 
        }
        
        - (void) mySaveData:(NSString*)path
        {
          // this is just a skeleton of what I do to track unsaved changes between relaunches
             @try 
           {
          ... implement something inherently dangerous... commit changes to a file ...
                 if ( !errorStr )
                 {
                    if ( [toDocument isDocumentEdited] )
                    {
                       // UInt64 theTimeNow = VMPGlue::GetMilliS();
                       [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:@"isDocumentEdited"];
                       [[NSUserDefaults standardUserDefaults] synchronize]; // bootstrap
                       // DLog( @"synchronize success %d ms", (int)(VMPGlue::GetMilliS() - theTimeNow) );
                    }
                    // tbd consider MyDocument saveToURL:ofType:forSaveOperation:error:
                    [toDocument updateChangeCount:NSChangeCleared];
                 }
           @catch (...)
           {
              ... run critical alert ...
           }
        }
        
        - (void) finishLoadingData
        {
           @try 
           {
        
        
             if ( dataexists )
             {
                  ... implement something inherently dangerous... loading your data from a file
        
        
              [toDocument setFilePath: [[NSUserDefaults standardUserDefaults] objectForKey:@"LastSaveFile"]];
              NSNumber *num = [[NSUserDefaults standardUserDefaults] objectForKey:@"isDocumentEdited"];
              if ( [num boolValue] == YES )
                 [toDocument updateChangeCount:NSChangeDone];
             }
             else
             {
                [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:@"isDocumentEdited"];
                [toDocument updateChangeCount:NSChangeCleared];
             }
        
             sFinishedLaunching |= kFinishedLoadingData;
           }
           @catch (...)
           {
              // !!! will not return !!!
              ... run critical alert ...
              // !!! will not return !!!
           }
        }
        #pragma mark NSApplication delegate
        
        // Apple: Sent directly by theApplication to the delegate. The method should open the file filename, 
        //returning YES if the file is successfully opened, and NO otherwise. 
        //If the user started up the application by double-clicking a file, the delegate receives the application:openFile: message before receiving applicationDidFinishLaunching:. 
        //(applicationWillFinishLaunching: is sent before application:openFile:.)
        - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename
        {
           BOOL didOpen = [MyDocument openMyDocument: filename];
           sFinishedLaunching |= kFinishedOpenFile;
           return didOpen;
        }
        
        // NSApplication notification
        - (void) applicationDidFinishLaunching:(NSNotification*)note
        {
           // kpk note: currentEvent is often nil at this point! [[NSApp currentEvent] modifierFlags]
           CGEventFlags modifierFlags = CGEventSourceFlagsState(kCGEventSourceStateHIDSystemState);
        
           sFinishedLaunching |= kFinishedWaking;
           if ( modifierFlags & (kCGEventFlagMaskShift | kCGEventFlagMaskCommand) )
           {
              ... implementation specific ... alert: @"Shift or Command key held down at startup.\nWhat would you like to do?" 
                                   title: @"Startup Options"
                             canContinue: @"Continue" ];
           }
           sFinishedLaunching |= kFinishedPreLaunchCheck;
           [self finishLoadingData];
           sFinishedLaunching |= kFinishedAndReadyToDraw;   
        }
        
        - (BOOL)windowShouldClose:(id)sender
        {
           if ( [sender isKindOfClass: [NSWindow class]] && sender != toWindow )
              return YES; // allow non-document-edit windows to close normally
        
           ... in my implementation, I prompt user with save/quit options when edit window is closed ...
           return NO;
        }
        
        - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication*)sender
        {
           if ( !gWantsToQuit && [toDocument isDocumentEdited] )
           {
              if ( ![self windowShouldClose:self] )
                 return NSTerminateCancel;
        
           }
           return NSTerminateNow;
        }
        
        - (void) applicationWillTerminate:(NSNotification *)notification
        {
           if ( gWantsToQuit )
           {
              ... implementation specific ... dont save potentially wonky data if relaunch is required
           }
           else
           {  
              [self saveData: [toDocument filePath]];
           }
        }
        @end
        

        【讨论】:

        • 想解释一下这里发生了什么?
        猜你喜欢
        • 2020-05-19
        • 1970-01-01
        • 2011-11-24
        • 1970-01-01
        • 1970-01-01
        • 2011-09-06
        • 2023-03-18
        • 2011-12-25
        • 1970-01-01
        相关资源
        最近更新 更多