NSDocumentController的currentDocument方法返回nil

11
我正在开发我的第一个基于Mac的文档型应用程序。
我已经创建了NSDocument子类,并重新实现了一些方法,例如:
- (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError;
- (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError;
- (void)makeWindowControllers;

主窗口控制器是NSWindowsController的子类,其中包含两个NSViewController的子类。

我面临的问题是需要从这些视图控制器中访问当前文档。我的解决方法是调用:

MyDocument *myDocument = [[NSDocumentController sharedController] currentDocument];

在应用程序启动后的第一时间,会创建一个新的文档。然后,主窗口及其视图控制器被创建,但是上面的方法返回了nil。这是我使用NSLog记录下来的日志:

Just created this new document: <MyDocument: 0x10040ff10>
I'm in a view controller and current document is (null)

接着,创建一个新文档并调用该方法会导致非nil指针,但它并没有指向正确的文档,而是指向第一个文档:

Just created this new document: <MyDocument: 0x100437e10>
I'm in a view controller and current document is <MyDocument: 0x10040ff10>

请注意,在第二个文档创建后,currentDocument 指向的是第一个文档而不是第二个文档。

我这里缺少什么或者做错了什么?NSDocumentController 何时设置 currentDocument


适当的语法: '[NSDocumentController sharedDocumentController]' - Martin-Gilles Lavoie
4个回答

6

我将上一个答案保留,因为它回答了有关NSView子类的问题,但现在原始发布者已经声明他正在使用NSViewController,因此需要考虑不同的情况。

对于由NSViewController控制的视图,NSViewController旨在使用representedObject属性将其与数据连接。这种抽象意味着应该通过NSViewController的包含控制器来管理,这听起来就像你的NSWindowController

根据您要提供/需要提供多少封装,您可以将文档推送到NSViewControllers(如果它们对整个文档进行操作),或仅将与特定NSViewController相关的文档信息推送到其中。

例如,假设一个软件编辑有关火车的设计信息:机车、车厢和尾车。NSDocument子类包含单个引擎对象、单个尾车对象和0个或多个车辆对象。在这种情况下,您可能有3个NSView,每个视图都有自己的NSViewController来处理从其对象中移动数据进出的工作。

NSWindowController处理将每个NSViewControllerrepresentedObject设置为它理解的对象。例如,当视图加载完成时,窗口控制器将:

    [engineViewController setRepresentedObject: engine];
    [cabooseViewController setRepresentedObject: caboose];

然后,您可以使用NSTableView来显示汽车列表,并且(在查看汽车时),窗口控制器可以在选择更改时使用[carViewController setRepresentedObject: car];(或者根据代码结构使用绑定)。这样,您就可以最好地利用MVC范例,因为控制器根据需要将视图链接到模型,但文档结构实际上只被顶层NSWindowController理解。

3
从苹果有关NSDocumentController currentDocument的文档中,它说:
此方法在调用其应用程序不活动时返回nil。例如,在readSelectionFromPasteboard:的实现中处理拖放操作时可能会出现这种情况。在这种情况下,从与文档相关联的NSView子类而不是调用该方法,请发送以下消息: [[[self window] windowController] document]; 这有点模糊,因为它没有真正说明“不活动”意味着什么。可能拖放操作是唯一的触发器,但它并没有说明这是应用程序变得不活动的唯一触发器。
也许苹果建议的替代方案对您有用。

谢谢,但我已经尝试过了,结果仍然是nil。对于苹果的“未激活”是什么意思,我也不确定,但在我的情况下,它发生在启动应用程序后。没有拖放,没有特殊操作。 - msoler

1
这是一个老话题,但仍然会让人头痛。应用程序的每个窗口控制器必须设置其document属性,并且它的canBecomeKey getter必须返回true,这样应用程序就知道它必须根据前置窗口设置当前文档。
这可能很麻烦,特别是在处理只有一种类型文档和一些辅助窗口(检查器、首选项等)的应用程序时。将这些窗口带到前面会使当前文档为空,还有其他我不太理解的原因。文档对此主题的说明并不像它应该的那样有用(这就是在堆栈溢出上提出这个问题的原因)。
个人而言,在大多数情况下,我通过使AppDelegate成为NSWindowDelegate来自己管理这个问题。
然后我创建方便的访问器以获取当前窗口、文档、数据...
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    static var shared: AppDelegate { return NSApplication.shared.delegate as! AppDelegate }
        
    weak var mainWindow: NSWindow?
    
    var activeDocument: Document? {
        mainWindow?.windowController?.document as? Document
    }

    var activeData: MyDocumentData? {
        mainWindow?.representedObject as? MyDocumentData
    }
    
    var mainViewController: DocumentViewController? {
        mainWindow?.contentViewController as? DocumentViewController
    }
}

extension AppDelegate: NSWindowDelegate {

    func windowDidBecomeKey(_ notification: Notification) {
        let window = notification.object as? NSWindow
        if window != mainWindow {
            mainWindow = window
        }
    }
    
    func windowWillClose(_ notification: Notification) {
        let window = notification.object as? NSWindow
        if window == mainWindow {
            mainWindow = nil
        }
    }
}

在Document类中创建窗口控制器时,会设置委托。

    override func makeWindowControllers() {
        let storyboard = NSStoryboard(name: NSStoryboard.Name("Document"), bundle: nil)
        let windowController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as! NSWindowController
        self.addWindowController(windowController)
        windowController.contentViewController?.representedObject = self.myDocumentData
        windowController.window?.delegate = AppDelegate.shared
    }

这是我目前发现的最简单的方法,可以随时拥有一致的当前文档集。


0

你为什么不能从你的NSWindowController子类中调用-[self document](或者在你的NSViewController子类中调用-[[self window] document])?这通常是在Cocoa的基于文档的应用程序中完成的。

基本上,当创建NSDocument(子类)时,它会创建所有的NSWindowControllers并将它们附加到文档上。

更重要的是,如果你有两个文档打开并突然需要绘制两个文档的内容,[[NSDocumentController sharedController] currentDocument]将无法返回正确的信息。相反,NSWindowController应该控制信息流到其窗口中的视图,以便您可以同时管理前景和背景更改(例如,如果所有窗口的后备存储都需要同时刷新)。


从NSViewController子类中,无法通过调用[[self window] document]来访问文档,因为NSViewController具有_view_属性而不是_window_属性。从NSView使用[[[self window] windowController] document]和从NSViewController使用[[[[self view] window] windowController] document](如@roger所指出的)确实可以返回正确的文档。重要的是要提到它们在窗口/视图被初始化后才能正常工作,这对我来说是可以理解的。 - msoler
抱歉,我没有注意到您正在使用NSViewControllers。我已经添加了另一个答案,更加符合在使用NSViewControllers时的MVC范例。希望这个答案更有用。 - gaige

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接