I am working on an app which uses a UIScrollView to scroll through a bunch of slides. When the app opens, it creates the slides and passes them to the scroll view. My app also has a timer which causes the UIScrollView to scroll through the slides.
I also have a settings button. When I press on that button, it opens the settings panel. The timer is invalidated and set to nil
. When the settings panel opens, the user can change things like the application theme, the slides and the animation direction.
When the settings panel is closed, the settings are applied: The slides are removed from the UIScrollView and the UIScrollView is given a new NSArray of slides to work with. The UIScrollView then lays out the new slides in the proper order and direction. The timer is reset.
Once, approximately every 14-16 times, the app crashes while running dismissAndRestart
(the "close" code). I'm not sure why. I thought I was leaking memory, and apparently I am, but I don't see where.
Here's my code:
This runs on startup:
- (void)viewDidLoad {
[scrollView setPagingEnabled:YES];
}
- (void) viewWillAppear:(BOOL)animated{
[self prepareViews];
[self applyTheme];
}
- (void) viewDidAppear:(BOOL)animated{
[self createTimerWithInterval:kScrollInterval];
}
Workhorse method that loads the views into the scroller. (It also removes the older ones):
- (void)loadViews:(NSArray *)views IntoScroller:(UIScrollView *)scroller withDirection:(NSString *)direction{
[scrollView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[scrollView setShowsHorizontalScrollIndicator: NO];
[scrollView setShowsVerticalScrollIndicator:NO];
scrollView.scrollsToTop = NO;
if([direction isEqualToString:@"horizontal"]){
scrollView.frame = CGRectMake(0, 0, 1024, 768);
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * [[NSNumber numberWithUnsignedInt:[views count]] floatValue], scrollView.frame.size.height);
}else if([direction isEqualToString:@"vertical"]){
scrollView.frame = CGRectMake(0, 0, 1024, 768);
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width, scrollView.frame.size.height * [[NSNumber numberWithUnsignedInt:[views count]] floatValue]);
}
for (int i=0; i<[[NSNumber numberWithUnsignedInt:[views count]] intValue]; i++) {
[[[views objectAtIndex:[[NSNumber numberWithInt:i] unsignedIntValue]] view] setFrame:scrollView.frame];
if([direction isEqualToString:@"horizontal"]){
[[[views objectAtIndex:[[NSNumber numberWithInt:i] unsignedIntValue]] view] setFrame:CGRectMake(i * [[views objectAtIndex:[[NSNumber numberWithInt:i] unsignedIntValue]] view].frame.size.width, 0, scrollView.frame.size.width, scrollView.frame.size.height)];//CGRectMake(i * announcementView.view.frame.size.width, -scrollView.frame.origin.x, scrollView.frame.size.width, scrollView.frame.size.height)];
}else if([direction isEqualToString:@"vertical"]){
[[[views objectAtIndex:[[NSNumber numberWithInt:i] unsignedIntValue]] view] setFrame:CGRectMake(0, i * [[views objectAtIndex:[[NSNumber numberWithInt:i] unsignedIntValue]] view].frame.size.height, scrollView.frame.size.width, scrollView.frame.size.height)];
}
[scrollView addSubview:[[views objectAtIndex:[[NSNumber numberWithInt:i] unsignedIntValue]] view]];
}
}
Create the timer for automated scrolling:
#pragma mark -
#pragma mark Create the Timer to automate scrolling
- (void) createTimerWithInterval:(float)interval{
self.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(scrollWrapperForTimer) userInfo:nil repeats:YES];
}
Wrapper function for the timer function. (This was necessary before the direction was an NSUserDefault, now I shouldn't need this.)
#pragma mark -
#pragma mark Scroll Automatically Every N seconds
- (void) scrollWrapperForTimer{
[self scrollToNewViewInDirection:kDirection];
}
- (void)scrollToNewViewInDirection:(NSString *)direction{
if([[NSUserDefaults standardUserDefaults] boolForKey:@"animated_scrolling"] == YES){
if([direction isEqualToString:@"horizontal"]){
if([[NSNumber numberWithFloat:scrollView.contentOffset.x] compare:[NSNumber numberWithFloat:scrollView.contentSize.width - scrollView.frame.size.width]] == (NSOrderedAscending)){
[scrollView scrollRectToVisible:CGRectMake(scrollView.contentOffset.x + scrollView.frame.size.width, 0, scrollView.frame.size.width, scrollView.frame.size.height) animated:YES];
}else if([[NSNumber numberWithFloat:scrollView.contentOffset.x] compare:[NSNumber numberWithFloat:scrollView.contentSize.width - scrollView.frame.size.width]] == (NSOrderedSame)){
[scrollView scrollRectToVisible:CGRectMake(0, 0, scrollView.frame.size.width, scrollView.frame.size.height) animated:YES];
}
}else if([direction isEqualToString:@"vertical"]){
if([[NSNumber numberWithFloat:scrollView.contentOffset.y] compare:[NSNumber numberWithFloat:scrollView.contentSize.height - scrollView.frame.size.height]] == (NSOrderedAscending)){
[scrollView scrollRectToVisible:CGRectMake(0, scrollView.contentOffset.y + scrollView.frame.size.height, scrollView.frame.size.width, scrollView.frame.size.height) animated:YES];
}else if([[NSNumber numberWithFloat:scrollView.contentOffset.y] compare:[NSNumber numberWithFloat:scrollView.contentSize.height - scrollView.frame.size.height]] == (NSOrderedSame)){
[scrollView scrollRectToVisible:CGRectMake(0, 0, scrollView.frame.size.width, scrollView.frame.size.height) animated:YES];
}
}
}else {
if([direction isEqualToString:@"horizontal"]){
if([[NSNumber numberWithFloat:scrollView.contentOffset.x] compare:[NSNumber numberWithFloat:scrollView.contentSize.width - scrollView.frame.size.width]] == (NSOrderedAscending)){
[scrollView scrollRectToVisible:CGRectMake(scrollView.contentOffset.x + scrollView.frame.size.width, 0, scrollView.frame.size.width, scrollView.frame.size.height) animated:NO];
}else if([[NSNumber numberWithFloat:scrollView.contentOffset.x] compare:[NSNumber numberWithFloat:scrollView.contentSize.width - scrollView.frame.size.width]] == (NSOrderedSame)){
[scrollView scrollRectToVisible:CGRectMake(0, 0, scrollView.frame.size.width, scrollView.frame.size.height) animated:NO];
}
}else if([direction isEqualToString:@"vertical"]){
if([[NSNumber numberWithFloat:scrollView.contentOffset.y] compare:[NSNumber numberWithFloat:scrollView.contentSize.height - scrollView.frame.size.height]] == (NSOrderedAscending)){
[scrollView scrollRectToVisible:CGRectMake(0, scrollView.contentOffset.y + scrollView.frame.size.height, scrollView.frame.size.width, scrollView.frame.size.height) animated:NO];
}else if([[NSNumber numberWithFloat:scrollView.contentOffset.y] compare:[NSNumber numberWithFloat:scrollView.contentSize.height - scrollView.frame.size.height]] == (NSOrderedSame)){
[scrollView scrollRectToVisible:CGRectMake(0, 0, scrollView.frame.size.width, scrollView.frame.size.height) animated:NO];
}
}
}
}
This method gets called when the done button is pressed in the setting panel.
#pragma mark -
#pragma mark Restart the program
- (void) dismissAndRestart{
[self prepareViews];
[self applyTheme];
[self dismissModalViewControllerAnimated:YES];
[self createTimerWithInterval:kScrollInterval];
}
This creates the proper image files and loads them into place:
#pragma mark -
#pragma mark Apply the theme to the main view
- (void) applyTheme{
//Front panel
UIImage *frontImage = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[[NSString stringWithFormat:@"%@_front", kTheme]description] ofType:@"png"]];
[self.overlayImage setImage:frontImage];
[frontImage release];
//Back Panel
if([kTheme isEqualToString:@"walnut"]){
[self.backgroundImage setHidden:NO];
UIImage *backImage = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[[NSString stringWithFormat:@"%@_back", kTheme]description] ofType:@"png"]];
[self.backgroundImage setImage:backImage];
[backImage release];
}else if([kTheme isEqualToString:@"metal"]){
[self.backgroundImage setHidden:YES];
[self.view setBackgroundColor: [UIColor scrollViewTexturedBackgroundColor]];
}
//Gabbai Button
UIImage *gabbaiImage = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[[NSString stringWithFormat:@"%@_settings_button", kTheme]description] ofType:@"png"]];
[self.gabbaiButton setImage:gabbaiImage forState:UIControlStateNormal];
[gabbaiImage release];
}
Another wrapper function that needs to be refactored:
#pragma mark -
#pragma mark Slide View related
- (void) prepareViews{
[self loadViews:[self createandReturnViews] IntoScroller:scrollView withDirection:kDirection];
}
Creates the Array of slides to pass to the scroller:
//recreation of the views causes a delay each time the admin panel is closed.
- (NSArray*) createandReturnViews{
NSMutableArray *announcements = [NSMutableArray array];
NSArray *announcementsArray = [NSArray arrayWithObjects: @"Welcome to\nGabbai HD!",
@"First slide",
@"Second Slide",
@"Third Slide",
@"etc.",
nil];
for (int i = 0; i<[[NSNumber numberWithUnsignedInteger:[announcementsArray count]] intValue]; i++) {
MBAnnouncementViewController *announcement = [[MBAnnouncementViewController alloc] initWithNibName:@"MBAnnouncementViewController" bundle:nil];
[announcement setAnnouncementText:[announcementsArray objectAtIndex:[[NSNumber numberWithInt:i] unsignedIntegerValue]]];
[announcements addObject:announcement];
[announcement release];
}
return announcements;
}
Why would my settings panel be crashing the app on close?
I haven't read your code, but it could be that you are trying to release an object that has already been released. I would recommend that you turn on zombies when developing, but rembember to turn this off when compiling the release version, because when zombies is enabled, memory is not released. Really helpful while debugging, if you are code is trying to access an object that does not exist anymore. Google "enable zombies xcode" or something like that
The code you show has a few alloc calls. After each one, you set a property, and release. I assume that the property is declared as
retain
.If so, then eventually, you need to release these properties. Usually in the dealloc message implementation.
Your classes with retained properties need a dealloc like this
If your properties are declared like this
Then when you reassign the property with either the
set
message orself.prop
syntax, the previous value has release called on it. If you just useprop = newVal;
, then you are accessing the field directly and not calling theset
message, so release will not be called.Generally, always use the
set
message or dot syntax, so that you don't need to worry about it -- it's the main reason you declare the property as retain, so take advantage of it.You can also use xcode analyser using window+shift+a to find out memory leakage.