How to avoid retain cycle when assigning NSDocument as NSViewController's representedObject - macos

I am building a macOS Document based application. I am following Apple's example code, which can be found here.
In Apple's example, the NSDocument assigns itself as the NSViewController's representedObject. The NSDocument also retains a strong reference to the NSViewController. As far as I can see, this should cause a retain cycle, which indeed happens in my implementation of the code. But in Apple's example, it does not cause a retain cycle, and I can't work out why. Here is Apple's example code:
class Document: NSDocument {
var contentViewController: ViewController!
override func makeWindowControllers() {
// Returns the storyboard that contains your document window.
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
if let windowController =
storyboard.instantiateController(
withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as? NSWindowController {
addWindowController(windowController)
// Set the view controller's represented object as your document.
if let contentVC = windowController.contentViewController as? ViewController {
contentVC.representedObject = content
contentViewController = contentVC
}
}
}
}
The only difference in my implementation is that my ViewController is further down the hierarchy, as it is contained within an NSSplitViewController, so my NSDocument subclass finds it like so:
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! MainWindowWC
addWindowController(windowController)
if let splitVC = windowController.contentViewController as? NSSplitViewController {
for item in splitVC.splitViewItems {
if let vc = item.viewController as? ViewController {
vc.representedObject = self
contentViewController = vc
}
}
}
}
I can't see that this is causing the problem, and indeed when I close the window, the NSWindowController and NSSplitViewController both successfully deinitialise, but the NSViewController and NSDocument are retained.
I can solve the retain cycle by creating a weak reference to the NSDocument in the NSViewController, instead of using the .representedObject property. but it would be great to know why Apple's code is not causing the retain cycle I think it should.
Thanks,
Dan

Related

Cocoa - Present NSViewController programmatically

Generally, We can able to display next view controller from first view controller by having different kind of NSStoryboardSeque like Present, Show, Sheet etc., But, How we can achieve the same programmatically?.
Comparing with UIViewController, presenting a view controller modally by presentViewController:animated:. Is there any same kind of approach for NSViewController?
Thanks in advance.
The two different presentation types I use are:
func presentViewControllerAsModalWindow(_ viewController: NSViewController)
func presentViewControllerAsSheet(_ viewController: NSViewController)
After doing some more research another way to do using:
func presentViewController(_ viewController: NSViewController, animator: NSViewControllerPresentationAnimator)
And eating a custom presentation animator. Here you have the freedom to do what you like :)
In case someone is looking for the solution in 2022,
extension NSViewController {
func presentInNewWindow(viewController: NSViewController) {
let window = NSWindow(contentViewController: viewController)
var rect = window.contentRect(forFrameRect: window.frame)
// Set your frame width here
rect.size = .init(width: 1000, height: 600)
let frame = window.frameRect(forContentRect: rect)
window.setFrame(frame, display: true, animate: true)
window.makeKeyAndOrderFront(self)
let windowVC = NSWindowController(window: window)
windowVC.showWindow(self)
}
}
1.Create a NSViewController instance with StoryBoard Identifier
let theTESTVCor = self.storyboard?.instantiateController(withIdentifier: "TESTVCor") as! NSViewController
2.Present In Via the current NSViewController
theNSViewController.presentViewControllerAsModalWindow(theTESTVCor)
⚠️ DO NOT FORGET to set the Identifier of the NSViewController in Storyboard
If you have a view controller (presenting) than it's as simple as following function are provided:
open func presentAsSheet(_ viewController: NSViewController)
open func presentAsSheet(_ viewController: NSViewController)
open func present(_ viewController: NSViewController, asPopoverRelativeTo positioningRect: NSRect, of positioningView: NSView, preferredEdge: NSRectEdge, behavior: NSPopover.Behavior)
If you need to present a view controller in a new window (NOT MODAL) you need to create own NSWindow, NSWindowController
let gridView = NSGridView(views: [
[NSTextField(labelWithString: "label1"),NSTextField(labelWithString: "label2")],
[NSTextField(labelWithString: "label3"),NSTextField(labelWithString: "label4")]
])
let viewController = NSViewController()
viewController.view = gridView
let window = NSWindow(contentViewController: viewController)
window.center()
let windowController = NSWindowController(window: window)
windowController.showWindow(nil)
EXPLANATION:
Storyboards are using seques to perform some magic. The show seque is simply calling action "perform:" on object NSStoryboardShowSegueTemplate ([NSApp sendAction:to:from]). This seque will create NSWindowController and NSWindow (private method windowWithContentViewController:) for you and on top it will layoutSubviews/resize and center the window. Magic bonus is self retaining the window so you don't care about memory management.
Example of programatic calling (using Storyboards to instantiate windowController with viewController)
import Cocoa
import Contacts
class ShorteningHistoryWindowController : NSWindowController, Storyboarded {
static var defaultStoryboardName = "ShorteningHistory"
}
struct ShorteningHistory {
static let shared = ShorteningHistory()
private var windowController : NSWindowController
private init() {
windowController = ShorteningHistoryWindowController.instantiate()
}
public func showHistory() {
windowController.showWindow(self)
}
}
extension Storyboarded where Self: NSWindowController {
static var defaultStoryboardName: NSStoryboard.Name { return String(describing: self) }
static var defaultIdentifer: NSStoryboard.SceneIdentifier {
let fullName = NSStringFromClass(self)
let className = fullName.components(separatedBy: ".")[1]
return className
}
static func instantiate() -> Self {
let storyboard = NSStoryboard(name: defaultStoryboardName, bundle: Bundle.main)
guard let vc = storyboard.instantiateController(withIdentifier: defaultIdentifer) as? Self else {
fatalError("Could not instantiate initial storyboard with name: \(defaultIdentifer)")
}
return vc
}
}
PS: Don't forget to set Storyboard Identifiers in Storyboard

how to presentViewControllerAsSheet on OSX Mavericks?

It's a long story, but to cut it short; my first OSX app was written (on Yosemite) in Swift using a storyboard until I found out my (finished) app will not run on Mavericks. I need to run on Mavericks, so I have replaced the storyboard with NIBs.
My problem is with the segues; I was using 'sheet type' segues to show other view controllers in a sheet over the main view controller. A call to the presentViewControllerAsSheet method of NSViewController is a good replacement as it looks the same, but this API was introduced in Yosemite - so I need to work out how to do this for Mavericks.
In the action for a button on the main view, I've tried using beginSheet like this:
secondViewController = SecondViewController(nibName: "SecondViewController", bundle: nil)
self.view.window?.beginSheet(secondViewController!view.window!, completionHandler: nil)
But the second view controller's window is null at runtime. I've tried adding the new view controller as a subview to the application window but this is an unrecognised selector:
NSApplication.sharedApplication().windows[0].addSubView(secondViewController!.view)
I've search high and low for a description of how to show a sheet and all I can find is: Can a view controller own a sheet? but I'm sorry to admit I don't understand the answer. Can anybody help me with some working code? I'm beginning to worry that I'm trying to do something unusual but it looks OK on Yosemite, so how did people do this before Yosemite was released?
EDIT
I still haven't got to the solution, so I have put together a small app which shows the problems I'm having.
In AppDelegate.swift:
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
var mainViewController: FirstView!
func applicationDidFinishLaunching(aNotification: NSNotification) {
mainViewController = FirstView(nibName:"FirstView", bundle: nil)
window.contentView = mainViewController.view
mainViewController.view.frame = (window.contentView as! NSView).bounds
}
}
In FirstView.swift (associated NIB has a 'open sheet' button)
class FirstView: NSViewController {
var secondView: SecondView?
var secondWindow: SecondWinCon?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func pressButton(sender: AnyObject) {
secondView = SecondView(nibName: "SecondView", bundle: nil)!
// method 1 - this is the behaviour I want (but it only works on OSX 10.10)
// presentViewControllerAsSheet(secondView!)
// method 2 - this just creates a floating window
// self.view.addSubview(secondView!.view)
// self.view.window?.beginSheet(secondView!.view.window!, completionHandler: nil)
// method 3 - this also creates a floating window
secondWindow = SecondWinCon(windowNibName: "SecondWinCon")
self.view.window?.beginSheet(secondWindow!.window!, completionHandler: nil)
}
}
In SecondView.swift (associated NIB has a 'close' button)
class SecondView: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func dismissPressed(sender: AnyObject) {
if (presentingViewController != nil) {
presentingViewController?.dismissViewController(self)
} else {
self.view.window?.sheetParent?.endSheet(self.view.window!)
}
}
}
In SecondWinCon.swift (Associated NIB is empty)
class SecondWinCon: NSWindowController {
var secondView: SecondView?
override func windowDidLoad() {
super.windowDidLoad()
secondView = SecondView(nibName: "SecondView", bundle: nil)!
self.window?.contentView.addSubview(secondView!.view)
}
}
If method 1 is uncommented, you will see the behaviour I'm trying to emulate (remember it only works on OS X 10.10). Method 2 or 3 displays the second view, but not as a sheet.
I have the same problem, and found maybe is't an issue related to view life cycle.
When I call presentViewControllerAsSheet in viewDidLoad, sheet will not shown, and you will get this in console:
Failed to set (contentViewController) user defined inspected property on (NSWindow): presentViewController:animator:: View '''s view is not in a window/view hierarchy.
If you trigger this in viewWillAppear or viewDidAppear, it's totally no problem.
UPDATE
Okay, let's make it clear.
For this initial storyboard, NSWindowController is connected with a view controller, think this as a root view controller (RootVC).
Create another view controller desired as a sheet in storyboard (SheetVC).
in viewWillAppear or viewDidAppear of RootVC, [self presentViewControllerAsSheet: SheetVC]
The sheet will show, no additional code required.
If you get here looking for a solution, I was nearly there with method 3. The important step I had missed was to turn off "Visible At Launch" in the NSWindowController's NIB (it's an attribute of the NSWindow). In my sample code, this was in SecondWinCon.nib.

Connect to ViewController from AppDelegate (Swift)

I have created a new OS X Cocoa Application using the standard Xcode Swift template (using StoryBoards).
I have implemented an IBAction in AppDelegate.swift to handle when the users selects "Open..." from the "File" menu. If the chosen file is a valid image file, I create an NSImage which I then want to display in the view of ViewController.
#IBAction func openFile(sender: NSMenuItem) {
var openPanel = NSOpenPanel()
openPanel.beginWithCompletionHandler { (result :Int) -> Void in
if result == NSFileHandlingPanelOKButton {
if let imageURL = openPanel.URL {
let image = NSImage(contentsOfURL: imageURL)
// PRESENT image IN THE VIEW CONTROLLER
}
}
}
However, I don't see any way to connect to ViewController from AppDelegate. I have only managed to find suggestions that I should look at self.window! in AppDelegate, but there is no such thing as a window in AppDelegate.
Thanks,
Michael Knudsen
It seems that AppDelegate can connect to objects only within Application Scene in a storyboard. If you want to get a ViewController, instantiate it from a storyboard.
sample:
#IBAction func menuAction(sender: AnyObject) {
if let storyboard = NSStoryboard(name: "Main", bundle: nil) {
let controller = storyboard.instantiateControllerWithIdentifier("VC1") as NSViewController
if let window = NSApplication.sharedApplication().mainWindow {
window.contentViewController = controller // just swap
}
}
}
You can access the mainWinow property and the contentViewController property to create a reference to your custom ViewController class. This is similar to the iOS rootViewController property.
let rootViewController = NSApplication.shared().mainWindow?.windowController?.contentViewController as! ViewController
Now you can use this reference to access IBOutlets on your main storyboard from your AppDelegate.
rootViewController.myTextView.textStorage?.mutableString.setString("Cats and dogs.")
This is good for a simple app with one Window with one ViewController.
I was stuck trying to do this same thing recently and managed to get the event I needed to update my view by creating the #IBAction in my ViewController and control dragging to my Application's First Responder (above the menu in my storyboard view).
Here's the question that got me out of the woods:
Application Menu Items Xcode
And thanks to Bluedome for the suggestion to connect it to First Responder's action.
If you control-drag from the menu to the first responder (red cube above menu) and picked an existing action, then you can "responder chain" to your view controller. In my case I attached Open to openFile and then in my view controller I added the following
override var acceptsFirstResponder: Bool {
return true
}
func openFile(sender: NSMenuItem) {
print("In view controller")
}
and it worked without any changes in AppDelegate. Most of the menus are already hooked up to first responder so just add the matching function name in your view controller.
See this comment and this document on Event Handling Basics for more info.
In Swift 5 and accessing new windows array:
#IBAction func menuAction(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateInitialViewController()
// The windows in the array are ordered from back to front by window level;
// thus, the last window in the array is on top of all other app windows.
// On app launch, UIApplication.shared.windows.count == 1 anyway.
if let window = UIApplication.shared.windows.last {
window.rootViewController = controller
}
}

Present a view modally in NSDocument's window

I have an NSDocument subclass that has its own xib file. Also I have an NSViewController subclass with its own xib file too, and I want to present its view modally, like this one.
Problem is, it always shows it as a separate floating window without title bar.
The view I'm trying to present is contained in a window in that xib file. And yes, it's Mac OS X 10.10. Here's the code.
#IBAction func didPressEditF(sender: AnyObject) {
let controller = ViewController(nibName: "ViewController", bundle: nil)
let window = self.windowControllers[0].window as NSWindow
window.beginSheet(controller.view.window, completionHandler: didEndPresentingF)
}
It's OK if you help me using Objective-C.
Alright. I figured it out.
At first. We need a property of our ViewController class so it won't get released after showing.
var controller: ViewController?
Then we need a method that will return a window of the current document. Somehow self.windowControllers[0].window as NSWindow doesn't work.
func window() -> NSWindow {
let windowControllers = self.windowControllers
let controller = windowControllers[0] as NSWindowController
let window = controller.window
return window
}
And finally, the code that opens up the sheet window will look like this:
#IBAction func didPressEditF(sender: AnyObject) {
controller = ViewController(nibName: "ViewController", bundle: nil)
self.window().beginSheet(controller!.view.window, completionHandler: didEndPresentingF)
}
Apple HAS to do something with their outdated documentation.
Instead of digging through the document's window controllers, you could call windowForSheet, a method on NSDocument. E.g. self.windowForSheet.

AppDelegate for Cocoa app using Storyboards in Xcode 6

I have an existing OS X app, and after converting to Storyboards as the main interface, my app delegate is no longer being used. Before, the MainMenu.xib had an "App Delegate" object, and I could set its class to my app delegate. However, the Storyboard contains no such object.
How do I get my AppDelegate back and keep storyboards? I feel like I'm missing something obvious.
If you don't specify it to be a Document-Based Application, Xcode will create an AppDelegate.swift class and connect it up in the Application Scene for you.
As of right now (Xcode Beta-2), new Document-Based apps don't come with a stub AppDelegate.swift file. Instead, there's ViewController.swift and Document.swift. Worse, the Document.swift file incorrectly instantiates the same Main.storyboard for documents.
Here's one way I got it to work:
Create an AppDelegate class (e.g.: an NSObject that adopts the NSApplicationDelegate protocol)
Drag an Object object from the Object library, into the Application Scene of Main.storyboard and set it to the AppDelegate class.
Control-drag from the Application object in the Application Scene to the AppDelegate object, and connect up its delegate.
Remove everything else from the Main.storyboard and create a new Document.storyboard for the Document window. Change the Document.swift file to instantiate that Storyboard instead of Main.
If you want to have a main application window and/or a preferences window in addition to your document windows, create an Application.storyboard and/or Preferences.storyboard for those windows, and use the AppDelegate class to instantiate them. This way, the AppDelegate can customize the main window appearance and do other handy things, including receiving IBActions sent from any window in the app.
Here's a working example of an AppDelegate.swift file for a Document-Based app that also has a separate, single main Application window, and a non-modal Preference window:
// AppDelegate.swift
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
//init() {
// super.init()
// remove this if you don't use it
//}
var application: NSApplication? = nil
func applicationDidFinishLaunching(notification: NSNotification) {
application = notification.object as? NSApplication
let path = NSBundle.mainBundle().pathForResource("Defaults", ofType: "plist")
let defaults = NSDictionary(contentsOfFile:path)
NSUserDefaults.standardUserDefaults().registerDefaults(defaults)
NSUserDefaultsController.sharedUserDefaultsController().initialValues = defaults
NSUserDefaultsController.sharedUserDefaultsController().appliesImmediately = true
}
func applicationDidBecomeActive(notification: NSNotification) {
if application?.orderedDocuments?.count < 1 { showApplication(self) }
}
//func applicationWillFinishLaunching(notification: NSNotification) {
// remove this if you don't use it
//}
func applicationWillTerminate(notification: NSNotification) {
NSUserDefaults.standardUserDefaults().synchronize()
}
func applicationShouldOpenUntitledFile(app: NSApplication) -> Bool { return false }
func applicationShouldTerminateAfterLastWindowClosed(app: NSApplication) -> Bool { return false }
var applicationController: NSWindowController?
#IBAction func showApplication(sender : AnyObject) {
if !applicationController {
let storyboard = NSStoryboard(name: "Application", bundle: nil)
applicationController = storyboard.instantiateInitialController() as? NSWindowController
if let window = applicationController?.window {
window.titlebarAppearsTransparent = true
window.titleVisibility = NSWindowTitleVisibility.Hidden
window.styleMask |= NSFullSizeContentViewWindowMask
}
}
if applicationController { applicationController!.showWindow(sender) }
}
var preferencesController: NSWindowController?
#IBAction func showPreferences(sender : AnyObject) {
if !preferencesController {
let storyboard = NSStoryboard(name: "Preferences", bundle: nil)
preferencesController = storyboard.instantiateInitialController() as? NSWindowController
}
if preferencesController { preferencesController!.showWindow(sender) }
}
}
Here's another cheap and easy way to do it, if all you want to do is customize the appearance of the main window before it appears:
Make your own subclass of NSWindowController, and connect it up as the delegate of the main window.
Implement windowDidUpdate as a hook to the window so you can set up the desired options, but also remove the window delegate so the function only gets called once. This is all the code you need to make that work:
// WindowController.swift
import Cocoa
class WindowController: NSWindowController, NSWindowDelegate {
func windowDidUpdate(notification: NSNotification!) {
if let window = notification.object as? NSWindow! {
window.titlebarAppearsTransparent = true
window.titleVisibility = NSWindowTitleVisibility.Hidden
window.styleMask |= NSFullSizeContentViewWindowMask
window.delegate = nil }
}
}
Actually, an even easier way to apply those appearance options to the window, is by using Interface Builder to add them as User Defined Runtime Attributes to the NSWindow object. You don't need to subclass NSWindowController or write any code at all. Just plug in these values to the window object via the Identity Inspector pane:
Keypath: titlebarAppearsTransparent, Type: Boolean, Value: Checked
Keypath: titleVisibility, Type: Number, Value: 1
Keypath: styleMask, Type: Number, Value: 32783
Of course, you can't specify individual bits of the styleMask, but it's easy enough to add them all together and get a single number to specify the style.
With Storyboard architecture, and the new powers given to NSViewController, there's not as much need to subclass NSWindowController anymore.

Resources