I am have an issue with my scrollview which holds multiple views. I think the problem is that subviews are being released. I have buttons in the subviews and when I click the buttons I get this error, [GraphDisplayViewController performSelector:withObject:withObject:]:
message sent to deallocated instance
. If there is only one subview then I can just use a property and this works, but since the number of subviews varies(one or more), it does not work and I don't know how to solve this.
I currently load all the views at once in the beginning. I'm working on only loading one subview at a time and assigning the property to that view, but I'm not sure if that will work.
My layout is as follows, a parent view(DetailViewController) contains a scrollview, I add views(GraphDisplayViewController) to the scrollview, the subviews each load a view(GraphView).
Any help or advice would be greatly appreciated. If you need any more details please let me know. Thank you for your time.
Code sample of how I add the subviews,
DetailViewController
- (void)loadScrollViewWithPage:(int)page
{
if (page < 0) return;
if (page >= pageControl.numberOfPages) return;
subView = [viewControllers objectAtIndex:page];
NSString *description;
NSString *packsize;
if ((NSNull *)subView == [NSNull null])
{
subView = [[GraphDisplayViewController alloc] initWithNibName:#"GraphDisplayViewController" bundle:nil];
[viewControllers replaceObjectAtIndex:page withObject:subView];
subView = [[GraphDisplayViewController alloc] init];
subView.molecule = moleculeName;
subView.description = description;
subView.dataArray = moleculePrices;
}
else
{
return;
}
// add the controller's view to the scroll view
if (nil == subView.view.superview)
{
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * page;
frame.origin.y = 0;
subView.view.frame = frame;
[scrollView addSubview:subView.view];
}
}
UPDATE
There was a mistake in the code, testing to see if that solves anything
subView = [[GraphDisplayViewController alloc] initWithNibName:#"GraphDisplayViewController" bundle:nil];
[viewControllers replaceObjectAtIndex:page withObject:subView];
subView = [[GraphDisplayViewController alloc] init]; <- Mistake
you are not retaining the GraphDisplayViewController assigned to subView variable. and hence you are loosing it at some point of time.
as we can see you are fetching the subView object like subView = [viewControllers objectAtIndex:page];
then you should also store it in viewControllers (though i am not sure what logic you have implemented but in ur code for each allocated subView this must be executed : [viewControllers replaceObjectAtIndex:page withObject:subView]; and you need to be sure that viewControllers variable is also retained for everything to work smoothly.) array so that it can be retained to avoid the crash you are facing.
I hope this will work for you..best of luck
There was a simple error in my code, I init the view twice.
subView = [[GraphDisplayViewController alloc] initWithNibName:#"GraphDisplayViewController" bundle:nil];
subView = [[GraphDisplayViewController alloc] init]; <- Mistake
Related
I have a view controller, named AllThingsViewController that dynamically creates other view controllers, named ThingViewController, and adds their top level view to a UIScrollView. (I'm writing proprietary code so I've changed the names of my classes, but the structure of my code is exactly the same.)
Here's what its loadView method contains:
NSArray *things = [[ThingDataController shared] getThings];
if ([things count] == 0) {
// code in this block is not relevant as it's not being executed...
} else {
for(unsigned int i = 0; i < [things count]; ++i) {
ThingViewController *thingViewController = [[ThingViewController alloc] init];
[thingViewController loadView];
[scrollView addSubview:thingViewController.topView];
thingViewController.topView.frame = CGRectNewOrigin(thingViewController.topView.frame,
0, thingViewController.topView.frame.size.height*i);
[thingViewController displayThing:thing[i]];
}
}
ThingViewController's loadView method looks like this:
- (void)loadView
{
NSArray *topLevelObjs = nil;
topLevelObjs = [[NSBundle mainBundle] loadNibNamed:#"ThingView" owner:self options:nil];
if (topLevelObjs == nil)
{
NSLog(#"Error: Could not load ThingView xib\n");
return;
}
}
When my app starts up everything displays correctly, until I try to tap one of the buttons that exists in the xib being loaded by ThingViewController, at which point it crashes due to an exception: "unrecognized selector sent to instance". It seems that ARC is releasing my ThingViewController instances too early.
Looking at my code, I figured it was because they weren't being held on to anything, so I created an NSMutableArray as an instance variable in my AllThingsViewController class, and started adding the ThingViewControllers to it thusly:
NSArray *things = [[ThingDataController shared] getThings];
if ([things count] == 0) {
// not being executed...
} else {
for(unsigned int i = 0; i < [things count]; ++i) {
ThingViewController *thingViewController = [[ThingViewController alloc] init];
[thingViewController loadView];
[scrollView addSubview:thingViewController.topView];
thingViewController.topView.frame = CGRectNewOrigin(thingViewController.topView.frame,
0, thingViewController.topView.frame.size.height*i);
[thingViewController displayThing:thing[i]];
[allThingsViewControllers addObject:thingViewController];
}
}
However, it didn't change anything, even though those objects are being added to the array. Finally, just to confirm that this is ARC releasing it early, I changed "thingViewController" to be an instance variable in AllThingsViewController and changed:
ThingViewController *thingViewController = [[ThingViewController alloc] init];
to be:
thingViewController = [[ThingViewController alloc] init];
Sure enough, the last item in the scrollable list doesn't crash when I tap its buttons, but the other ones do, because its ThingViewController isn't being deallocated.
I'm still relatively new to ARC, but after a bunch of Googling I have no idea how to fix this. What do I do?
Couple of things.
Problem 1:
This looks like the cause of your bug:
[allBillViewControllers addObject:billViewController];
It should be:
[allBillViewControllers addObject:thingViewController];
Right?
Problem 2
You are not properly adding the view controller to your view hierarchy. It should be this:
[self addChildViewController:childViewController];
[childViewController.view setFrame:targetFrame];
[scrollView addSubview:childViewController.view];
[childViewController didMoveToParentViewController:self];
And similarly when removing a child view controller:
[childViewController willMoveToParentViewController:nil];
[childViewController.view removeFromSuperview];
[childViewController removeFromParentViewController];
Problem 3
Never call loadView explicitly on a view controller. It gets called by UIKit whenever you access the view property of a view controller.
Problem 4
You must add the view of the child view controller to your scroll view, not an arbitrary subview topView in its view hierarchy. Refactor your ThingViewController class to make this simpler for yourself. :-)
Let's look at your code:
for(unsigned int i = 0; i < [things count]; ++i) {
ThingViewController *thingViewController = [[ThingViewController alloc] init];
[thingViewController loadView];
[scrollView addSubview:thingViewController.topView];
thingViewController.topView.frame = CGRectNewOrigin(thingViewController.topView.frame,
0, thingViewController.topView.frame.size.height*i);
[billViewController displayThing:thing[i]];
[allBillViewControllers addObject:billViewController];
}
After each loop of the for loop executes, nothing will have a strong reference to the ThingViewController. Thus, it gets released and destroyed.
If ThingViewController is a subclass of UIViewController, then it should be made a "child view controller" of the scrollview's view controller. I recommend reading the section on from the View Controller Programming Guide on creating custom container view controllers (i.e., a view controller that encapsulates and displays other view controllers).
i've got a question regarding a UIScrollView with paging enabled that contains many UIView, each managed by an own UIViewController.
Right now there are about 20 to 30 UIViewControllers that COULD be contained in the UIScrollView. It's a catalog app on the iPad, and I started preloading all the views at the beginning, but with the amount of UIViewControllers getting bigger and bigger, that is not an option any more.
I'm looking for the perfect solution in terms of memory usage. It's no problem to reload the UIViewControllers when the ContentOffset of the ScrollView reaches a specific controller. And I think to nil the UIViewControllers when the ContentOffset tells me that the UIViewControllers is not needed any more isn't that hard as well.
What is the correct way to handle this? Is it enough to alloc the UIViewControllers when needed, putting them into a NSMutableDictionary or NSMutableArray and nil them when they are not needed any more? A little bit of help from someone already having done something similar would be great!
Thanks for your help!
I'm sure there are some good infinite scrolling classes out there, but if you were going to "roll your own", here is a minimalist bit of code that demonstrates the process of infinite scrolling, keeping the current, previous, and next pages in memory, but letting go of anything else. This assumes that:
you're doing horizontal scrolling and have turned on paging;
that you're using view controllers for the child views;
your child view controller class has a page property to keep track of what page it's for; and
you've made your view controller the delegate for your scroll view
Thus, it might look like:
- (void)viewDidLoad
{
[super viewDidLoad];
// my underlying model is just an array of strings, which I'll show on my child
// view; your model will be more elaborate, but I just want to illustrate the concept
self.objects = #[#"1", #"2", #"3", #"4", #"5", #"6", #"7", #"8", #"9"];
// set the `contentSize` for the scrollview
CGRect content = self.view.bounds;
content.size.width *= [self.objects count]; // make it wide enough to hold everything
self.scrollView.contentSize = content.size;
// set our current page and load the first pages (the first and the next pages)
self.currentPage = 0;
[self addChildPage:0 toScrollView:self.scrollView];
[self addChildPage:1 toScrollView:self.scrollView];
}
- (void)addChildPage:(NSInteger)page toScrollView:(UIScrollView *)scrollView
{
// create the child controller
ChildViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"child"];
// set whatever properties you need to in order for it to present its information correctly
controller.text = self.objects[page];
controller.page = page;
// now do the stuff to add it to the right place in the scrollview
CGRect frame = self.view.bounds;
frame.origin.x = frame.size.width * page;
controller.view.frame = frame;
[self addChildViewController:controller]; // containment call for adding child view controller
[scrollView addSubview:controller.view];
[controller didMoveToParentViewController:self]; // containment call when done adding child
}
- (ChildViewController *)childControllerForPage:(NSInteger)page
{
for (ChildViewController *controller in self.childViewControllers)
{
if (controller.page == page)
return controller;
}
return nil;
}
- (void)addChildIfNecessary:(NSInteger)page toScrollView:(UIScrollView *)scrollView
{
if (page < 0 || page >= [self.objects count])
return;
ChildViewController *controller = [self childControllerForPage:page];
if (controller == nil)
[self addChildPage:page toScrollView:scrollView];
}
- (void)removeChildController:(UIViewController *)controller
{
[controller willMoveToParentViewController:nil]; // containment call before removing child
[controller.view removeFromSuperview];
[controller removeFromParentViewController]; // containment call to remove child
}
- (void)updateChildrenViewsForPage:(NSInteger)page forScrollView:(UIScrollView *)scrollView
{
if (page == self.currentPage)
return;
// add child pages as necessary
[self addChildIfNecessary:page toScrollView:scrollView];
[self addChildIfNecessary:(page-1) toScrollView:scrollView];
[self addChildIfNecessary:(page+1) toScrollView:scrollView];
// find any pages that need removing
NSMutableArray *pagesToRemove = [NSMutableArray array];
for (ChildViewController *controller in self.childViewControllers)
{
if (controller.page < (page - 1) ||
controller.page > (page + 1))
{
[pagesToRemove addObject:controller];
}
}
// remove them if they need removing
for (UIViewController *controller in pagesToRemove)
{
[self removeChildController:controller];
}
// update our "current page" index
self.currentPage = page;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSInteger page = scrollView.contentOffset.x / scrollView.frame.size.width + 0.5;
[self updateChildrenViewsForPage:page forScrollView:scrollView];
}
This demonstrates the appropriate custom container calls and the handling of the scroll events. I hope this helps.
I want to check if my view has already been added to the view, so that the user cannot repeatedly add the new UIViewController onto the view.
if (!self.fmovc)
{
self.fmovc = [[FMOVC alloc] initWithNibName: #"FMOVC" bundle:nil];
}
BOOL viewAlreadyDisplayed = NO;
for (UIView * b in self.view.subviews)
{
if ([b isKindOfClass:[FMOVC class]])
{
viewAlreadyDisplayed = YES;
}
}
if (!viewAlreadyDisplayed)
{
[self.view addSubview:self.fmovc.view];
}
This is not working because it never triggers
viewAlreadyDisplayed = YES;
What is the correct way to go about finding if your view is already being displayed?
Thanks,
-Code
Before adding just removeItFromSuperView...It may solve your prob
[self.fmovc.view removeFromSuperview];
[self.view addSubview:self.fmovc.view];
Make the UIView member variable say thatView;
if(!thatView) {
thatView = [[UIView alloc] init];
[self.view addSubView:thatView];
}
Once you release the superview, release thatView & make thatView to nil.
OR
always check & remove thatView if present from superview before allocating thatView.
Hope this Helps !!!
I have an app the loaded many view controllers in a scroll view depending on the number of objects the user has in a tableview. So when I flip between the tableview and the scroll view, the number of view controllers in the scroll view changes according to how many objects the user has in the tableview.
I use the code in Apple's PageControl sample code to build the scroll view with many view controllers inside it, after some modification of course.
- (void)loadScrollViewWithPage:(int)page
{
if (page < 0) return;
if (page >= kNumberOfPages) return;
// replace the placeholder if necessary
MainViewController *countdownController = [viewControllers objectAtIndex:page];
if ((NSNull *)countdownController == [NSNull null])
{
id occasion = [eventsArray objectAtIndex:page];
countdownController = [[MainViewController alloc] initWithPageNumber:page];
[countdownController setOccasion:occasion];
[viewControllers replaceObjectAtIndex:page withObject:countdownController];
[countdownController release];
}
// add the controller's view to the scroll view
if (nil == countdownController.view.superview)
{
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * page;
frame.origin.y = 0;
countdownController.view.frame = frame;
[scrollView addSubview:countdownController.view];
}
}
The problem is the number of living view controllers (MainViewController here) keeps increasing when I flip between the table view and the scroll view (according to Instruments) even though I didn't add any new objects which causes memory problems of course.
I tried so many things in viewWillDisappear of the scroll view like:
- (void) viewWillDisappear:(BOOL)animated
{
//test unloading all views
//Remove all subviews
[[scrollView subviews] makeObjectsPerformSelector:#selector(removeFromSuperview)];
//[[scrollView subviews] makeObjectsPerformSelector:#selector(release)];
//[viewControllers removeAllObjects];
for (unsigned m = 0; m < [viewControllers count]; m++)
{
//[[viewControllers objectAtIndex:m] makeObjectsPerformSelector:#selector(release)];
[viewControllers removeObjectAtIndex:m];
}
}
But it didn't work.
Here is a recording of how the app works youtube.com/watch?v=5W8v_smZSog
And this is the viewWillAppear method of the scroll view:
- (void)viewWillAppear:(BOOL)animated
{
eventsArray = [[NSMutableArray alloc] init];
kNumberOfPages = [self.dataModel occasionCount];
//update the eventsArray from the dataModel
//Fill in the events Array with occasions form the data model
for (unsigned r = 0; r < kNumberOfPages; r++)
{
Occasion* occasion = [self.dataModel occasionAtIndex:r];
[eventsArray insertObject:occasion atIndex:r];
}
// view controllers are created lazily
// in the meantime, load the array with placeholders which will be replaced on demand
NSMutableArray *controllers = [[NSMutableArray alloc] init];
for (unsigned i = 0; i < kNumberOfPages; i++)
{
[controllers addObject:[NSNull null]];
}
self.viewControllers = controllers;
[controllers release];
// a page is the width of the scroll view
scrollView.pagingEnabled = YES;
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * kNumberOfPages, scrollView.frame.size.height);
scrollView.showsHorizontalScrollIndicator = NO;
scrollView.showsVerticalScrollIndicator = NO;
scrollView.scrollsToTop = NO;
scrollView.delegate = self;
pageControl.numberOfPages = kNumberOfPages;
pageControl.currentPage = currentPage;
[self loadScrollViewWithPage:0];
[self loadScrollViewWithPage:1];
}
UPDATE: Video recording of Instruments http://www.youtube.com/watch?v=u1Rd2clvMQE&feature=youtube_gdata_player
And a screen shot showing the responsible caller:
Thank you.
This is for you if you don't want to use UIPageViewController (read my other answer).
The sample project is designed for a constant number of pages (kNumberOfPages). The scrollview content size and the size of the view controller array depends on the number of pages. The sample code set this up in awakeFromNib, which is called only once.
So in order to make this dynamic you could recreate the whole ContentController when the number of pages changes. You just need to add a property for the number of pages.
The other option would be to reset the scrollview and view controller array when the number of pages changes.
I'm assuming you have defined a property for the events:
#property(nonatomic,retain) NSArray* eventsArray;
You could then add a setter method like this:
-(void)setEventsArray:(NSArray *)eventsArray
{
if (eventsArray != _eventsArray) {
[_eventsArray release];
_eventsArray = [eventsArray retain];
NSUInteger eventCount = [eventsArray count];
//reset scrollview contentSize
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * eventCount, scrollView.frame.size.height);
// reset content offset to zero
scrollView.contentOffset = CGPointZero;
//remove all subviews
[[scrollView subviews] makeObjectsPerformSelector:#selector(removeFromSuperview)];
pageControl.numberOfPages = eventCount;
// reset viewcontroller array
NSMutableArray *controllers = [[NSMutableArray alloc] init];
for (unsigned i = 0; i < eventCount; i++)
{
[controllers addObject:[NSNull null]];
}
self.viewControllers = controllers;
[controllers release];
[self loadScrollViewWithPage:0];
[self loadScrollViewWithPage:1];
}
}
You call this method from the table view controller at the time when the user switches to the scroll view.
Apple's PageControl sample code is 2 years old and you can consider it as deprecated because there is a new container view controller in iOS 5 that does all this: UIPageViewController.
You should really start using UIPageViewController, then you don't need that loadScrollViewWithPage method at all. It would be less code and more easy.
Take a look at the PhotoScroller sample code. It has been updated to take full advantage of UIPageViewController.
It doesn't look like you are implementing Apple's View Controller Containment pratices. It would make memory management that much easier and safer.
Plus, hoping that it might save you a lot of future headaches, there is already an open source project that does what you are describing (implementing a self-managing scrollview of an arbritary number of view controllers).
You might want to take a look at it: RHHorizontalSwipe.
The concept of a UIScrollView containing multiple UIViewController views sounds sketchy at best, that design does not sound good at all.
That being said, one potential issue could be this line:
if ((NSNull *)countdownController == [NSNull null])
You would be better off with something like this:
if (!countdownController || [countdownController isKindOfClass:[NSNull class]])
Also, you should call [super viewWillDisappear:animated] in your viewWillDisappear method.
I am presenting a modal view controller which contains a UIScrollView with a bunch of images. When I dismiss my modal view controller I can see in my Allocations (instruments) that memory assigned to the images is hanging around but not causing leaks.
Causing me some confusion and any help would be great. Here is what I have...
UIViewController - DollViewController: This is my main view controller that I am presenting my modal over. Here is the code -
-(IBAction)openDollCloset {
NSLog(#"Open Closet");
[self playSound:#"soundMenuClick"];
ClosetViewController *closet = [[ClosetViewController alloc] initWithNibName:#"ClosetViewController" bundle:nil];
closet.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self setModalPresentationStyle:UIModalPresentationFullScreen];
[self presentModalViewController: closet animated: YES];
closet.delegate = self;
closet.addToCloset = NO;
[closet setCurrentDoll:myDoll];
[closet openCloset];
[closet release];
[self closeSubmenu];
}
I create my scrollview manually to hold the outfit images.
The object closetScroller is defined with #property (nonatomic, retain) UIScrollView *closetScroller; with a release in dealloc.
- (void)setScrollerValues {
if (closetScroller == nil)
{
self.closetScroller = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 320, 400)];
[self.closetScroller setDecelerationRate:UIScrollViewDecelerationRateFast];
[self.closetScroller setDelegate:self];
[self.closetScroller setPagingEnabled:YES];
[self.closetScroller setShowsHorizontalScrollIndicator:NO];
[self.view addSubview:closetScroller];
}
[self.closetScroller setContentSize:CGSizeMake(outfitCount * 320, 400)];
}
After creating the scrollView I add the images to the scroller. I believe I am releasing everything right. I am creating the images with NSData objects and releasing them.
- (void)addOutfitsToScroller {
// Clear out any old images in case we just deleted an outfit
if ([[closetScroller subviews] count] > 0)
{
CATransition *fade = [CATransition animation];
[fade setDelegate:self];
[fade setType:kCATransitionReveal];
[fade setSubtype:kCATransitionFromRight];
[fade setDuration:0.40f];
[[self.closetScroller subviews] makeObjectsPerformSelector: #selector(removeFromSuperview)];
[[closetScroller layer] addAnimation:fade forKey:#"FadeButtons"];
}
// Set the pageControl to same number of buttons
[pageControl setNumberOfPages:outfitCount];
// Load the outfit image and add it to the scroller
for (int i = 0; i < outfitCount; i++)
{
NSArray *savedDataArray = [[NSArray alloc] initWithArray:[closetItemsArray objectAtIndex:i]];
UIImageView *v = [[UIImageView alloc] initWithFrame:CGRectMake(320*i, 0, 320, 400)];
[v setUserInteractionEnabled:NO];
[v setAlpha:0.40f];
NSData *imageData = [[NSData alloc] initWithContentsOfFile:[savedDataArray objectAtIndex:1]];
UIImage *outfitImage = [[UIImage alloc] initWithData:imageData];
UIImageView *closetImage = [[UIImageView alloc] initWithImage:outfitImage];
[imageData release];
[outfitImage release];
[savedDataArray release];
CGSize frameOffset;
#ifdef LITE_VERSION
frameOffset = CGSizeMake(40, 60);
#else
frameOffset = CGSizeMake(20, 10);
#endif
closetImage.frame = CGRectOffset(closetImage.frame, frameOffset.width , frameOffset.height);
[v addSubview:closetImage];
[closetImage release];
[self.closetScroller addSubview:v];
[v release];
}
}
This all works great. Then when the user selects the "Close" button or selects an outfit I call:
[self.delegate closeClosetWindow];
Which calls the delegate method in the parentViewController which simply makes this call:
[self dismissModalViewControllerAnimated:YES];
I have also tried calling dismissModalViewControllerAnimated:YES from the modal view controller itself. It simply forwards the message to it's parentViewController and closes the modal just the same as the first way. Neither makes a difference with regards to the memory.
So what I do instead is use this method to close the view:
[[self.closetScroller subviews] makeObjectsPerformSelector: #selector(removeFromSuperview)];
[self dismissModalViewControllerAnimated:YES];
When I do that, I can see my Object Allocations decrease in Instruments. Prior to opening the closet view I have object alloc at about 1.85mb. When I open the closet it increases to 2.5 or so depending on how many outfit images are loaded into the UIScrollView. Then when I close the view without the above method it stays at 2.5 and increases the next time I open it. Although when using the above method that removes the outfit images from the scroller, my object alloc drops back down close the the 1.85mb. Maybe a little more which tells me it's hanging on to some other stuff also.
Also note that when looking at the object allocations, I do see the ClosetViewController object created and when I close the modal it's refrence is released. I can see it malloc and free the view as I open and close it. It's not retaining the view object itself. i'm so confused.
Not sure what to do here. I am not getting any leaks and I am releasing everything just fine. Any ideas would be great.
mark
I Noticed object alloc in instruments keeping a live reference to UIScrollView every time I opened modal window. So to fix, when allocating for my scrollview, I instead allocated a new UIScrollView object and assigned it to closetScroller and then released. This took care of the problem
I think you were simply not releasing closetScroller in the dealloc of your view controller.