Search code examples
macosswiftosx-yosemiteappkit

NSViewController reference cycle with storyboards


NSViewController, when instantiated from storyboards under Swift, seems to have a reference cycle somewhere.

Calling the following code multiple times will instantiate and set a new view controller, but the old view controller is never dealloced. In the code, containerViewController is an NSViewController which should contain a single NSViewController, containerView is a subview within containerViewController, and identifier is the storyboard identifier to instantiate.

// Remove any sub viewcontrollers and their views
for viewController in containerViewController.childViewControllers as [NSViewController] {
    viewController.view.removeFromSuperview()
    viewController.removeFromParentViewController()
}
// Create and set up the new view controller and view.
let viewController = storyboard!.instantiateControllerWithIdentifier(identifier) as NSViewController
let view = viewController.view
view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(viewController.view)
containerViewController.addChildViewController(viewController)
containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[view]|", options: nil, metrics: nil, views: ["view": view]))
containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: nil, metrics: nil, views: ["view": view]))

(Sample project no longer available)

I used an Apple TSI and they agree it's a bug, which I have filed, but I expected someone else to have come up against this by now seeing as NSViewControllers and storyboards are now the de facto on OSX. How have you worked around this problem? Or does it not affect anyone else and I am doing something wrong?

Pre-bounty edit: Each view controller must be able to link to any other view controller from code as the destination is determined on the fly. This seems to remove segues as an option.

Bug fixed

As of Xcode 6.3 this is no longer a bug.


Solution

  • Another answer.

    It seems, only the segues defined on Storyboard can perform view controller deallocation. So, Here is a very ugly but working workaround.

    screenshot

    class DismissSegue: NSStoryboardSegue {
    
        var nextViewControllerIdentifier:String?
    
        override func perform() {
            let src = self.sourceController as NSViewController
            let windowController = src.view.window!.windowController() as TopLevelWindowController
    
            src.view.removeFromSuperview()
            src.removeFromParentViewController()
    
            if let identifier = nextViewControllerIdentifier {
                windowController.setNewViewController(identifier)
            }
        }
    }
    
    class TopLevelWindowController: NSWindowController {
    
        var containerView: NSView!
        var containerViewController: ContainerViewController! {
            didSet {
                setNewViewController("FirstView")
            }
        }
    
        func setNewViewController(identifier: String) {
            // Create and set up the new view controller and view.
            let viewController = storyboard!.instantiateControllerWithIdentifier(identifier) as NSViewController
            let view = viewController.view
            view.translatesAutoresizingMaskIntoConstraints = false
            containerView.addSubview(viewController.view)
            containerViewController.addChildViewController(viewController)
            containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[view]|", options: nil, metrics: nil, views: ["view": view]))
            containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: nil, metrics: nil, views: ["view": view]))
        }
    }
    
    class ContainerViewController: NSViewController {
    
        @IBOutlet var containerView: NSView!
    
        override func viewDidAppear() {
            super.viewDidAppear()
            if let window = view.window {
                if let topLevelWindowController = window.windowController() as? TopLevelWindowController {
                    topLevelWindowController.containerView = containerView
                    topLevelWindowController.containerViewController = self
                }
            }
        }
    
    }
    
    class FirstViewController: NSViewController {
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            let pointerAddress = NSString(format: "%p", unsafeBitCast(self, Int.self))
            NSLog("First VC init at \(pointerAddress)")
        }
    
        deinit {
            let pointerAddress = NSString(format: "%p", unsafeBitCast(self, Int.self))
            NSLog("First VC de-init at \(pointerAddress)")
        }
    
        override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject?) {
            if let segue = segue as? DismissSegue {
                segue.nextViewControllerIdentifier = "SecondView"
            }
        }
    }
    
    class SecondViewController: NSViewController {
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            let pointerAddress = NSString(format: "%p", unsafeBitCast(self, Int.self))
            NSLog("Second VC init at \(pointerAddress)")
        }
    
        deinit {
            let pointerAddress = NSString(format: "%p", unsafeBitCast(self, Int.self))
            NSLog("Second VC de-init at \(pointerAddress)")
        }
    
        override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject?) {
            if let segue = segue as? DismissSegue {
                segue.nextViewControllerIdentifier = "FirstView"
            }
        }
    }
    

    Procedure for modifying your Storyboard:

    1. Disconnect @IBAction from buttons.
    2. Create "DUMMY" view controller scene with normal NSViewController.
    3. Connect custom segues from buttons to "DUMMY".
    4. Configure these segue as shown.

    Please let me know if this solution doesn't meet your demand.