Reputation: 656
I have a single window utility app for OS X written using storyboards in Swift, Xcode 6. I have some helper windows which are presented modally with segues. These are dismissed using dismissViewController(self). However, the dismissed view controller is never deallocated due to the presentedViewControllers property of my root view controller, which maintains a reference. In addition, each time a helper window is reopened, a new instance of the associated view controller is created rather than the previous instance, resulting in multiple instances and a memory leak.
How can a dismissed view controller be deallocated?
Upvotes: 2
Views: 1307
Reputation: 2624
I also noticed that the presentedViewControllers
property of my root ViewController was retaining multiple copies of its presented VC.
After some digging, I discovered I was calling dismiss(self)
on the presented VC not on the presenting VC.
// From inside the presented VC:
// Does dismiss, but is still retained by the presenting VC:
self.dismiss(self)
// Does dismiss, and also does not retain:
presentingViewController?.dismiss(self)
Weirdly both these do visually dismiss my presented VC, but only the latter actually removes it from the presenting VC's presentedViewControllers
property.
Can anyone tell me why the former actually does visually dismiss the presented VC?
Upvotes: 2
Reputation: 656
This line of code was creating a reference cycle:
center.addObserverForName(Notification.NewImageDraggedToIKImageView, object: nil, queue: queue) { notification in
self.imageDidChange(self.coverIKImageView)
}
However, that is only part of the answer. Even without this line of code, the destination view controller would not deallocate after being dismissed. This line of code is the culprit:
dismissViewController(self)
For reasons unknown to me, and only discovered through trial and error, replacing that line of code with this:
dismissController(self)
results in the view controller being deinitialized when dismissed.
Upvotes: 0
Reputation: 656
This is precisely the problem I see:
Memory Leak with Bare Storyboard Project
I did the same thing, created a barebones project like in this example. The destination view controller is never deinitialized after closing the segued window, and a new destination view controller instance is created for each segue. References for each destination view controller instance are kept in the presentedViewControllers property of the root view controller.
The only workaround I see so far is to abandon using segues, and instead use the presentViewControllerAsSheet method with a single instance of the destination view controller saved as a property in my root view controller. I have not found a way to initiate a segue with a specified destination view controller.
Upvotes: 0
Reputation: 9845
I already asked you about this line:
coverIKImageView.delegate = self
What is with this delegate? How is it declared in CoverIKImageView? "weak"?
Instruments is not going to warn you about a "leak" in this case. You have to do further analysis. Short version: press Cmd+"I" in Xcode to start Instruments -> Choose "Allocations" -> Press the red button to Start Profiling -> Use your App and try to produce the memory leak -> Stop Instruments -> Set "Allocation Lifespan" to "Created & Persistent" -> Set the allocation type to "Allocations List" -> Type in your class name in the SearchBar "Instrument Detail" -> your class should be now in the list -> Press the small arrow button on the right of the memory address of your object ->
You should see now a list of Objects which are retaining and releasing your class instance. This will give you a hint about the leak.
Upvotes: 0
Reputation: 9845
NSViewController property "presentedViewControllers" should not be the reason for the memory-leak, as it is just holding the reference as long as the (presented) view controller is presented.
What about this one?:
coverIKImageView.delegate = self
Is this delegate declared weak?
And how did you detect the memory leak? Please add this to check if the presented view controller is really not deallocated:
deinit {
NSLog("CoverIKImageViewController deinit called")
}
Is deinit really not called?
Upvotes: 0
Reputation: 656
Here's the essentials of my view controller code:
class CoverIKImageViewController: NSViewController {
let zoomInFactor: CGFloat = 1.414214
let zoomOutFactor: CGFloat = 0.7071068
let center = NSNotificationCenter.defaultCenter()
var coverCGImage: CGImage!
weak var coverArtImageView: CoverImageView!
var imageProperties = [NSObject: AnyObject]()
@IBOutlet weak var toolControl: NSSegmentedControl!
@IBOutlet weak var resolution: NSTextField!
@IBOutlet weak var coverIKImageView: CoverIKImageView! {
didSet {
if coverIKImageView == nil { return }
coverCGImage = coverArtImageView.image?.cgImage
setIKImage(coverCGImage, url: coverArtImageView.imageURL)
coverIKImageView.autoresizes = true
coverIKImageView.delegate = self
}
}
func setIKImage(cgImage: CGImage!, url: NSURL!) {
if url == nil {
imageProperties[kCGImagePropertyPixelWidth] = CGImageGetWidth(cgImage)
imageProperties[kCGImagePropertyPixelHeight] = CGImageGetHeight(cgImage)
coverIKImageView.setImage(cgImage, imageProperties: imageProperties)
} else {
coverIKImageView.setImageWithURL(url)
}
imageDidChange(coverIKImageView)
}
/* Additional functions here (not included) */
override func viewWillAppear() {
self.view.window?.minSize = NSSize(width: 480, height: 560)
setResolutionLabel()
super.viewWillAppear()
}
override func viewDidAppear() {
self.view.window?.makeFirstResponder(self.view)
coverIKImageView.undoManager?.levelsOfUndo = 8
let queue = NSOperationQueue.mainQueue()
center.addObserverForName(Notification.NewImageDraggedToIKImageView, object: nil, queue: queue) { notification in
self.imageDidChange(self.coverIKImageView)
}
super.viewDidAppear()
}
override func viewWillDisappear() {
IKImageEditPanel.sharedImageEditPanel().close()
coverIKImageView.undoManager?.removeAllActions()
super.viewWillDisappear()
}
override func viewDidDisappear() {
super.viewDidDisappear()
coverCGImage = nil
coverIKImageView.delegate = nil
center.removeObserver(self)
}
}
And here's how it is invoked in code:
override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject?) {
if let identifier = segue.identifier {
switch identifier {
case "Edit Cover Image":
if let evc = segue.destinationController as? CoverIKImageViewController {
evc.coverArtImageView = coverArtView
}
default: break
}
}
}
Upvotes: 0
Reputation: 9845
A dismissed view controller can be deallocated by not holding any referenced to it anymore on dismissal. But you said that you are holding a reference. So if you are holding a reference in your root view controller then this is not going to happen. The question is: why are you holding a reference to it? Do you need to the model view controller again? If yes then everything is fine - you need it so it will stay.
Another usual reason that a view controller is not de-allocated is a delegate in it. In this case the delegate has to be marked as 'weak'.
Posting your source code would be helpful.
Upvotes: 0