I am creating all of my display objects programatically and adding them to my storyboard scenes subview.
For example:
let button: UIButton = UIButton( frame: CGRect( x: 0, y: 0, width: 160, height: 50 ) )
self.view.addSubview( button )
When changing storyboard scenes, I am experiencing huge memory leaks (memory usage almost doubling between every scene change), which of course shows that these objects are not being removed automatically when changing scenes. I have read some things about garbage collection in Swift, but didn't find much info on this particular use case.
By changing storyboard scenes, I mean via present view controller, like so:
self.presentViewController( targetController, animated: true, completion: nil )
So with that said, my question is:
1) Removing objects can be done like so:
button.removeFromSubview()
button = nil
correct?
2) Is there a way to remove any object inserted into the view's subview in a loop and also assign it to nil to completely remove any references associated with the object? I can remove them one by one, but it's a pain staking task.
3) Is there anyway to get objects removed automatically from memory after changing scenes? This would be the best solution.
An example of a solution would be most appreciated.
Thanks in advance.
Some things that might be clarifying:
1) Buttons that are added via storyboard (those tagged #IBOutlet) are weak; you do not need to nil them out, because your reference to them doesn't keep them in memory.
2) presentViewController probably doesn't behave in the way you expect. Most importantly, doesn't replace the existing view controller with the new view controller; it "presents" the new view controller from the old view controller. To illustrate this, you can call self.presentingViewController from the new view controller, which will provide you with a reference to the previous view controller; it's still in memory.
However, view controllers aren't super big, and they have their views unloaded when they aren't on screen. If however you are holding on to some large resources in the presenting view controller, these will persist. I would suggest addressing this by loading those assets in viewWillAppear/viewDidAppear (instead of viewDidLoad) and then unloading them in either prepareForSegue: or viewDidDisappear (I think there's some long-standing issue with didDisappear not getting called consistently? I have some sort of mental flag around there but I'm not sure what the source is...)
Alternatively, if what you actually want to do is to fully change to a new root view controller, you can do this through your AppDelegate's .window property, like this:
let storyboard = UIStoryboard(named: "NewStoryboard", bundle: nil)
let newVC = storyboard.instantiateInitialViewController
UIApplication.sharedApplication().delegate.window.rootViewController = newVC
I wouldn't overuse this, though; it's mostly useful in places where you maybe show a tutorial on first launch, and then afterwards want to load the normal view hierarchy.
If by changing scenes you mean that you are adding a new view to the stack, then it is my understanding that the previous view remains there just waiting to be popped to the top of the stack again and therefore your objects should not be removed. Someone will correct me if I have this wrong. Regardless, the code to remove all of the objects from a view is:
for sv in view.subviews {
sv.removeFromSuperview()
}
Related
I am getting a "Presenting view controllers on detached view controllers is discouraged" warning in a somewhat specialized architecture. And - there are some fairly big UI issues resulting from it. I have an architecture with 2 distinct unconnected groups in my storyboard. The first group is the main interface of my app and includes an UIStackView. The second group consists of an UIView plus attached popover segue as shown in the image below.
I dynamically populate the UIStackView of group 1 with up to 8 instances of the UIView of group 2. This is done in a function called loadViews() in the UIStackView subclass which is called as needed. Here is the cleaned up pseudo code for illustration:
for i in 0 ..< green.count {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let greenVC = storyboard.instantiateViewControllerWithIdentifier("greenViewController") as! GreenVC
greenVC.progressionStackView = self
greenVC.index = i;
greenViewControllers.append(greenVC)
if let greenView = greenVC.view as! GreenView! {
greenView.fillColor = UIColor.orangeColor()
greenView.setNeedsDisplay()
self.addArrangedSubview(greenView)
}
}
self.layoutIfNeeded()
Every time I trigger the popover on one of the embedded green views in the stack view I get the warning from above. More importantly, when running on an iPad in split view mode, the stack view loses a green view each time until there are none left. The latter is just a display issue, because on refresh all views are back.
I am completely stumped and am not sure how to fix this or implement things differently. If the issue is that the loaded views are not attached, can they be "re-attached"? Or is there a way to dynamically load a stack view with up to 256 views that are attached to it?
Solved:
Another lesson in taking Xcode warnings to heart - even if the word discouraged is used. As in this case, things tend to break. The solution was obvious in hindsight. The new view controllers that were instantiated as greenVC had to be attached to the containing view controller - i.e. the one several layers up in the view hierarchy that contains this UIStackView:
vcContainingStackView.addChildViewController(greenVC)
The line above is called right before appending greenVC to my array of added view controllers. Of course now removeFromParentViewController() has to be called as well where instances of GreenVC get removed, but otherwise that's it. The warning is gone and so is the issue of vanishing views.
I'm trying to implement the following behavior:
Long press on a collection view brings a full-window view (call it LetterView) to the front
Subsequent gestures/touches are only processed by the LetterView.
(edit: I should mention that I want a transparency effect of seeing the collectionview items beneath LetterView)
I seem to be running into behavior that everyone else is trying to implement, though - my touches get processed by both the LetterView and the collection view. I.e. I can scroll the collection view AND have hits processed by my topmost view. Showing the view hierarchy in XCode clearly shows LetterView at the front, and both the UICollectionView and the LetterView are subviews of UICollectionWrapperView.
LetterView is a UIView subclass with a UIViewController subclass. It's added to the view hierarchy programmatically, inside my UICollectionViewController subclasses's viewDidLoad method, like so:
super.viewDidLoad()
letterDrawingViewController = LetterDrawingViewController()
let viewFrame : CGRect = self.collectionView!.frame
letterDrawingViewController.view = LetterDrawingView.init(frame:viewFrame)
letterDrawingView = letterDrawingViewController.view
self.addChildViewController(letterDrawingViewController)
letterDrawingViewController.didMoveToParentViewController(self)
collectionView?.addSubview(letterDrawingView)
It doesn't appear to be a first responder issue, as I tried overriding canBecomeFirstResponder in LetterView and assigning it first responder status when I move it to the front
I tried setting userInteractionEnabled=FALSE on the CollectionView, but keeping it true on the LetterView after I moved LetterView to the front. This disabled all touch events for both views
I tried setting exclusiveTouch=True for LetterView when I moved it to the front. This didn't appear to do anything.
Aside from any specific tips, are there any general techniques for debugging hit-testing like this? According to the docs on hit-testing in iOS, iOS should prefer the "deepest" subview that returns yes for hitTest:withEvent:, which, since LetterView is a subview of collectionview, and in front of all it's cells, should be the front? Is there any logging I can enable to see a hit test over the view hierarchy in action?
Thank you!
Nate.
If letterView is full screen, you probably don't want to add it as a subview of the collection view like you are. Maybe try adding it to the application's window instead and see how that does. At least in that instance it should intercept all the touch events.
Another method, although admittedly a more fragile feeling one, would be to enable and disable user interaction on the collectionView as you present and dismiss letterView.
So, when letterView is about to be presented, you can call
self.collectionView.userInteractionEnabled = NO;
and if you also know when that view is about to be dismissed you can call
self.collectionView.userInteractionEnabled = YES;
The only thing here to worry about is that you don't get into a bad state where your letterView is not presenting and your collectionView is also ignoring a user's touch. That will feel totally broken.
Whilst I think you can deal with your issue somewhat easily I think you are making a design mistake. It feels like you are trying to code this thinking like a web developper by adding a child view to your view and trying to intercept the touches there like one would do in a modern JavaScript single page app. In iOS I think this is bad design. You should segue or present the new viewController using the methods provided by apple.
So your code should look soothing like:
letterDrawingViewController = LetterDrawingViewController()
self.presentViewController(letterDrawingViewController, animated: true, completion: nil)
iOS8 has the added benefit of allowing you to have awesome custom transitions. Take a look at this : http://www.appcoda.com/custom-segue-animations/
Hi all I am doing a course in Udemy, and the code calls for placing code in the viewDidLoad function as shown below:
override func viewDidLoad() {
super.viewDidLoad()
placesArray.append(["name":"Taj Mahal", "lat":"27.175607", "lon":"78.042112"])
}
The array append should only run once, however, when I segue to another viewController and come back, it runs the code to append again. So I now have an array with 2 rows, both of which are Taj Mahal.
I thought that the viewDidLoad function only runs code once?
Is there a way around this?
Thanks.
Addendum:
I am using Swift, so I don't see any alloc and init while creating and launching the viewController. And weird as it sounds, the video tutorial has it working in the viewDidLoad and the trainer is using the storyboard to segue from the initial table view controller to a map view on a view controller and just has a back button on the map view that segue's back to the table view controller via the storyboard as well. - Could be because I have the latest version of the Swift language and the trainer was using an earlier version, (cause I noticed some slight differences in coding earlier) but you never know. Either way whenever he touches the back button it does not run the append code anymore.
I am trying to get in contact with the trainer as some of the suggestions here, though they are good don't seem to work.
I will put the solution in here once I get in contact with the trainer.
The viewDidLoad method is called when your view controller's view finishes loading. The view will load when a view controller's view property is nil and something attempts to access it.
UIViewController *myVC = [[UIViewController alloc] init];
UIView *aView = myVC.view; // this loads myVC's view; viewDidLoad is called when it completes loading.
If the view has unloaded (usually due to memory limitations), it will be called when the view property is next accessed.
Manipulation of data sets should generally not be done within view methods. Consider moving this to the init of the view controller (or to a different "datasource" class).
I suppose you are trying to do data initialisation in viewDidLoad. If there is no other operation on placesArray before viewDidLoad, then instead of append, what about setting the placesArray directly:
placesArray = ["name":"Taj Mahal", "lat":"27.175607", "lon":"78.042112"]
Then even if your view is unloaded for some reasons. Taj Mahal will still be added once only.
viewDidLoad is called whenever the view controller's view property is set. When does this happen? It depends on how the view controller is contained:
UINavigationController
- View Controller views are loaded as they are added to the navigation stack and "unloaded" (although the viewDidUnload method is deprecated) as they are removed.
UITabBarController
- View Controller views are loaded as they are added to the tab bar regardless of whether they are on screen or not. They stay loaded as you change from tab to tab.
Depending on your needs and use case, you can create your own view controller container that does what you need. Checkout the Apple docs on the proper way to do this:
https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/CreatingCustomContainerViewControllers/CreatingCustomContainerViewControllers.html
I am navigating between screens in my iOS application.
BaseView.NavigationController.ViewControllers
As I switch screens, I keep a reference to the previous screen in a static variable.
At some point, one of my items gets removed from BaseView.NavigationController.ViewControllers even though it's still a valid viewcontroller and IsLoaded is still set to True/YES.
When I use (pardon my C#/MonoTouch)
BaseView.NavigationController.PopToViewController(CurrentViewController,false);
to show it again, I get NSInternalInconsistencyException Reason: Tried to pop to a view controller that doesn't exist. This is understandable because it's no longer in the ViewController collection.
The way I am switching screens is I am keeping a reference to he various screens and calling a common method to show the screen. In that method I use this logic to determine if I should push or pop.
if (CurrentViewController.IsViewLoaded)
{
BaseView.NavigationController.PopToViewController(CurrentViewController,false);
}
else
{
BaseView.NavigationController.PushViewController(CurrentViewController,true);
}
My question is where did it go and why would it have been removed from ViewControllers collection and when it's StillLoaded=true/YES?
If I understand correctly, you're using NavigationController.PopToViewController(controller); to navigate back to a certain view controller but keep a reference of the View Controllers that are popped from the navigation stack.
What I think is happening is because you're keeping a reference to these View Controllers, they're still in memory and thus the IsViewLoaded property is still true despite the View Controller not actually existing on the navigation stack.
Rather than using the IsViewLoaded property, you should check whether the View Controller exists in the NavigationController.ViewControllers array, if it does then Pop to it, if it doesn't then push it.
E.g.
if (BaseView.NavigationController.ViewControllers.Contains(CurrentViewController))
{
BaseView.NavigationController.PopToViewController(CurrentViewController,false);
}
else
{
BaseView.NavigationController.PushViewController(CurrentViewController,true);
}
Edit
So you mention you'd like a view to persist on the navigation stack. Well, using PopToViewController will remove ALL View Controllers between the TopViewController and the specified Controller.
In order to achieve what you're wanting, you could directly manipulate the NavigationControllers.ViewControllers array. Only problem with this is you'll lose the nice animations that the Push/Pop methods provide.
// Changes order of View Controllers currently in the stack. You can also add/remove
// controllers using this method.
NavigationController.ViewControllers = new UIViewController[]{
NavigationController.ViewControllers[1],
NavigationController.ViewControllers[0],
NavigationController.ViewControllers[3],
NavigationController.ViewControllers[2]};
Im transitioning from one view controller to another UINavigationController by using a modal segue. Its important for me that this view controller (and its child view controllers) stay in memory so specific references are kept up. Although obviously exactly this not happening. When debugging the viewWillAppear function the rootViewController (viewControllers[0]) reference points to different memory addresses between calls (and contains nil values, my actual problem).
Now there two possibilities which could cause this issue:
The UiNavigationController became destroyed
The rootViewController became destroyed
But to make it really confusing, none of them did happen; neither the UINavigationController nor the rootViewController became destroyed (viewDidUnload not called!).
Edit: Further investigation discovered that the UINavigationController is really recreated for every modal segue. I hope that by maintaining a property i can solve the problem.
I finally ended up by creating my own IBAction functions wich present the controller manually. This works just fine and is coded in less than 5 minutes. One just need to init the controller one time on ViewDidLoad from the storyboard.
Create a strong reference in the main view controller and point your new view controllers to that property. This will keep the view around as long as you need, although this is not recommended for n number of views because it defeats the purpose of a nav controller handling its own creation and removing of views.