I had a view that contained a list of camps, and touching one takes users to the camp details. I wanted to allow the user to swipe left and right to move through the list of camps displaying the details of each. I wanted a visual animation that showed the swipe, and that the 'Back' button always referred to moving back up the navigation stack, ie, returning to the list.
The first view that has the table with results is the only object that knows what "previous" and "next" mean, so they will be responsible for implementing the swipe action. Because we will be making changes to the "currently displayed" row/section in that view, while the view is not the current view, you will need to add vars/properties to track that.
Because the navigation controller can already animate view changes, I let it do the majority of work. When moving "previous", I create a new view with the previous entry details, insert it into the navigation stack (between the LIST view and the current DETAIL view), and do a popViewControllerAnimated which provides a visual effect and also unloads the details view just animated off.
To move to the "next" camp details, I create a new view with the next entry details, add it onto the end of the navigation stack, it animates in, then I clean up the navigation stack by removing the details view that just animated off.
So, in a nutshell:
The details view will detect the swipe gestures and inform the parent.
The parent determines the next/previous row that should be displayed.
The parent replaces the current view with a new one, animating the replacement left/right to visually indicate the effect. The parent also updates the navigation stack to ensure that the "Back" button always acts as a pop to the LIST view, not to a previously shown DETAIL view.
I can't post all my code here, but below is the majority. Some specific GOTCHAS to watch out for:
Manipulating the navigation stack can cause warning errors at runtime if you try to delete a VC while it is animating out. Therefore we wait until it is completely replaced, then remove it, by registering as the delegate for the navigation controller, and using the "didShowViewController" method to detect when it is safe to make changes. This complication is only needed when moving forward in the list, since in the "back" logic, the navigation controller cleans up itself after the popViewController.
In order to use didShowViewController you must set a delegate. The delegate must not be a VC that may go away, otherwise you will get a crash. I only have the controlling LIST view set itself as a delegate.
As you manipulate which row/section the user is viewing the details of, I also move the highlight (and scroll the table) on the hidden LIST view, so that when they eventually go "BACK" to it, the last viewed item is shown.
When creating a DETAIL view, pass in the parent, define methods to let the parent know a swipe occurred, and register swipe recognizers on the DETAIL view, in the viewDidLoad method,
Code in the LIST view (parent)
-(NSString *) nameOfPreviousCampAndUpdateCurrents;
{
// pseudo code
// targetsection = srcSection
// targetrow = srcRow-1.
// if targetrow < 0
// targetsection = srcSection - 1
// targetrow = last row of targetsection
// if targetSection < 0
// return nil;
//
// return name at targetsection, targetrow
NSInteger targetSection;
NSInteger targetRow;
NSString *results = nil;
targetSection = self.currentDetailViewSection;
targetRow = self.currentDetailViewRow-1;
if (targetRow < 0)
{
targetSection--;
if (targetSection <0)
{
return nil;
}// end if
NSInteger numberOfRowsInSection = [self tableView:self.myTable numberOfRowsInSection:targetSection];
targetRow = numberOfRowsInSection-1;
}// end if
results = [self getCampNameInSection:targetSection atOffset:targetRow];
self.currentDetailViewSection = targetSection;
self.currentDetailViewRow = targetRow;
return results;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Navigation logic may go here. Create and push another view controller.
CampDetails *detailViewController = [[[CampDetails alloc] initWithNibName:nil bundle:nil] autorelease];
detailViewController.campName = [self getCampNameInSection:indexPath.section atOffset:indexPath.row];
detailViewController.campID = [self getCampIDForSection:indexPath.section atOffset:indexPath.row];
detailViewController.parent = self;
self.currentDetailViewSection = indexPath.section;
self.currentDetailViewRow = indexPath.row;
// ...
// Pass the selected object to the new view controller.
[[self navigationController] pushViewController:detailViewController animated:YES];
//[detailViewController release];
}
-(void) viewDidLoad;
{
// The ROOT view controller should do this.
if ([[self.navigationController viewControllers] count] == 1)
{
self.navigationController.delegate = self;
}// end if
}
-(void) moveToNextCamp;
{
NSString *nextCamp = [self nameOfNextCampAndUpdateCurrents];
if (nextCamp == nil)
{
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Warning"
message:@"You are already at the last item in the list of camps."
delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
return;
}// end if
CampDetails *detailViewController = [[[CampDetails alloc] initWithNibName:nil bundle:nil]autorelease];
detailViewController.campName = nextCamp;
detailViewController.campID = [self getCampIDForSection:self.currentDetailViewSection atOffset:self.currentDetailViewRow];
detailViewController.parent = self;
// do the animation to the right
[self.navigationController pushViewController:detailViewController animated:YES];
// remove the previous controller so that popping the current one takes us "up"
// WHILE THE FOLLOWING CODE DOES WORK, it also results in a runtime warning.
// so instead, we tinker with the controller stack only when it's safe (see below)
// NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
// [viewControllers removeObjectAtIndex:1];
// [self.navigationController setViewControllers:viewControllers animated:NO];
//
// clean up the stack AFTER the child is shown.
self.userJustSwiped = YES;
[self updateTableHighlightAndScrollPosition];
}
-(void) moveToPreviousCamp;
{
NSString *previousCamp = [self nameOfPreviousCampAndUpdateCurrents];
if (previousCamp == nil)
{
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Warning"
message:@"You are already at the first item in the list of camps."
delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
return;
}// end if
CampDetails *detailViewController = [[[CampDetails alloc] initWithNibName:nil bundle:nil]autorelease];
detailViewController.campName = previousCamp;
detailViewController.campID = [self getCampIDForSection:self.currentDetailViewSection atOffset:self.currentDetailViewRow];
detailViewController.parent = self;
// add the controller so that popping the current one takes us there
NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
NSInteger lastNavStackEntryIndex = [viewControllers count]-1;
[viewControllers insertObject:detailViewController atIndex:lastNavStackEntryIndex];
[self.navigationController setViewControllers:viewControllers animated:NO];
// do the animation (which also releases the previously current vc)
[self.navigationController popViewControllerAnimated:YES];
[self updateTableHighlightAndScrollPosition];
}
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
// IF we just swiped to a details view, do some clean up
if (self.userJustSwiped)
{
self.userJustSwiped = NO;
// clean up the stack AFTER the child is shown. remove the previous controller so that popping the current one takes us "up"
NSMutableArray *viewControllersArray = [NSMutableArray arrayWithArray:navigationController.viewControllers];
NSInteger lastNavStackEntryIndex = [viewControllersArray count] - 1;
[viewControllersArray removeObjectAtIndex:lastNavStackEntryIndex-1];
[navigationController setViewControllers:viewControllersArray animated:NO];
}// end if
}
-(void) userSwipedLeftOnChild;
{
[self moveToNextCamp];
}
-(void) userSwipedRightOnChild;
{
[self moveToPreviousCamp];
}
Code in the DETAILS view (child):
-(void) leftSwipe:(UIGestureRecognizer*)recognizer;
{
[self.parent userSwipedLeftOnChild];
}
-(void) rightSwipe:(UIGestureRecognizer*)recognizer;
{
[self.parent userSwipedRightOnChild];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// add swipe recognizers
UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(leftSwipe:)];
[leftSwipe setDirection:UISwipeGestureRecognizerDirectionLeft];
[self.view addGestureRecognizer:leftSwipe];
[leftSwipe release];
UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(rightSwipe:) ];
[rightSwipe setDirection:UISwipeGestureRecognizerDirectionRight];
[self.view addGestureRecognizer:rightSwipe];
[rightSwipe release];
}