iOS

Better Code: Create a simple paging app

Learn how to create a simple app using the UIPageControl, which can be used for side-swiping through pictures in a collection.

For this tutorial, we will create a simple app demonstrating the use of the UIPageControl, which is very useful for side-swiping through pictures in a collection.

Building the app

Create a Single View Application with Storyboards and ARC for iPhone (& iPad).

Import five pictures or your choice at 250x250 pixels.

Get in the habit of organizing your files because as your projects get bigger you will import more and more different kinds of files, which makes it confusing if they are not properly organized.

Add UIPageControl

Okay let's leave the images for now and focus on the Page control. Find the MainStoryboard_iPhone.storyboard file and drag a UIPageControl from the Object Library onto the scene:

Notice the three dots of the page control are white so they are difficult to visualize. In the Attributes Inspector change the Tint Color to Blue and the Current Page to Orange. Cool! Now let's move on to coding.

Code pageChanged method & connect it. In your ViewController.m implement the following method, which we will connect to the page control later.

- (IBAction) changePage:(id)sender {
NSLog(@"pageControl position %d", [self.pageControl currentPage]);
}

This method will be called whenever the page control changes "page". So we need to do two more things.

Initiate the variables used in that method in our viewDidLoad. We do so by implementing this code. Notice we have an NSArray called views you will need to create an ivar for as well as five UIImageViews. You can do so in the private implementation of the interface inside the .m file up top.

- (void)viewDidLoad{
[super viewDidLoad];
// Set up the views array with 5 UIImageViews
views = [[NSArray alloc] initWithObjects:@"1", @"2", @"3", @"4",@"5", nil];
// Set pageControl's numberOfPages to 5
self.pageControl.numberOfPages = [views count];
// Set pageControl's currentPage to 0
self.pageControl.currentPage = 0;
currentViewIndex = 0;
// Call changePage each time value of pageControl changes
[self.pageControl addTarget:self action:@selector(changePage:) forControlEvents:UIControlEventValueChanged];
}

The private implementation of the interface in the .m file would be:

@interface SecondViewController () {
UIImageView *firstView;
UIImageView *secondView;
UIImageView *thirdView;
UIImageView *fourthView;
UIImageView *fifthView;
NSArray *views;
}
@property (nonatomic, strong) IBOutlet UIPageControl *pageControl;
- (IBAction)changePage:(id)sender;

Notice we also implemented a property for the UIPageControl and the changePage method declaration.

Connect the page control to the IBAction

Now let's hook up the IBAction and outlet to the page control. Go to the storyboard file and select the scene so that it's highlighted in blue. Open the Connections Inspector and connect the pageControl outlet to the page control in the scene as well as the changePage method by dragging from the Connections Inspector to the scene. In both cases you will drag TO the page control inside the scene. In the case of the changePage method, when you drag to the page control, a popup appears from which you will select TouchUpInside. Now let's check everything is properly connected.

Use NSLogs at first. NSLog is your friend. Use it to find out what methods you are in, what methods have run and when a milestone has been reached. Make sure you are running the iPhone Simulator and not the iPad one because so far we have been working on the iPhone storyboard only. You will then have to duplicate the storyboard work for the iPad.

Every time you call the changePage method, we will be logging the currentPage property of the pageControl, which tells us where we are located. Notice the first location is actually 0, as in any array.

Add images to UIImageView offscreen. Now that we have seen how a UIPageControl works, let's make it do something interesting. We want to create an image view on screen as the app launches. So let's create it by adding a new method.

-(void)createUIViews{
UIImage *image1 = [UIImage imageNamed:@"beach.png"];
firstView = [[UIImageView alloc] initWithImage:image1];
firstView.backgroundColor = [UIColor grayColor];
firstView.tag = 1;
CGRect newFrame1 = firstView.frame;
newFrame1.origin.x = (CGRectGetWidth(self.view.frame)-CGRectGetWidth(firstView.frame))/2;
firstView.frame = newFrame1;
[self.view addSubview:firstView]; //added ONSCREEN
}

We assign the beach image to firstView and tag it as 1. Then we create a variable called newFrame1 and set it to the frame of that firstView. We then set the x (horizontal) origin of that newFrame1 by following this procedure:

We take the difference between the screen and the image (which is to say whatever blank space is left onscreen after placing the image) and divide it by half. We divide it by half because that way we distribute the blank space between the left and right side of the image.

Finally we assign that newFrame1 to the firstView's frame and add it to the screen. If you run the app now you will see the beach image appear in the middle of the screen. To make it look a bit better we could add some 10 pixels of white space from the top by setting newFrame1.origin.y to 10.

Now let's add the other imageviews off screen to the right. Add these lines to the createUIViews method.

UIImage *image2 = [UIImage imageNamed:@"beach2.jpeg"];
secondView = [[UIImageView alloc] initWithImage:image2];
secondView.backgroundColor = [UIColor grayColor];
secondView.tag = 2;
CGRect newFrame2 = secondView.frame;
newFrame2.origin.x = CGRectGetWidth(self.view.frame);
newFrame2.origin.y = 10;
secondView.frame = newFrame2;
[self.view addSubview:secondView];
UIImage *image3 = [UIImage imageNamed:@"trees.jpg"];
thirdView = [[UIImageView alloc] initWithImage:image3];
thirdView.backgroundColor = [UIColor grayColor];
thirdView.tag = 3;
CGRect newFrame3 = thirdView.frame;
newFrame3.origin.x = CGRectGetWidth(self.view.frame);
newFrame3.origin.y = 10;
thirdView.frame = newFrame3;
[self.view addSubview:thirdView];
UIImage *image4 = [UIImage imageNamed:@"flowers.jpg"];
fourthView = [[UIImageView alloc] initWithImage:image4];
fourthView.backgroundColor = [UIColor grayColor];
fourthView.tag = 4;
CGRect newFrame4 = fourthView.frame;
newFrame4.origin.x = CGRectGetWidth(self.view.frame);
newFrame4.origin.y = 10;
fourthView.frame = newFrame4;
[self.view addSubview:fourthView];
UIImage *image5 = [UIImage imageNamed:@"mountain.jpg"];
fifthView = [[UIImageView alloc] initWithImage:image5];
fifthView.backgroundColor = [UIColor grayColor];
fifthView.tag = 5;
CGRect newFrame5 = fifthView.frame;
newFrame5.origin.x = CGRectGetWidth(self.view.frame);
newFrame5.origin.y = 10;
fifthView.frame = newFrame5;
[self.view addSubview:fifthView];

If you run the app again, you won't notice a difference. This is because the new four image views are off screen to the right.

The final touch is to make the changePage method move the current image off to the left and bring in the new view from the right. This is a personal preference as you may have seen in previous posts, but I take baby steps towards a final goal.

First let's replace our NSArray init line to this:

views = [[NSArray alloc] initWithObjects:firstView, secondView, thirdView, fourthView, fifthView, nil];

I modified my changePage method like so:

- (IBAction)changePage:(id)sender {
NSLog(@"pageControl position %d", [self.pageControl currentPage]);
//Call to remove the current view off to the left
//Call to add the new view in from the right
}

This gives me a chance to think about the workflow involved. I have imageviews tagged as 1-5 and I have pageControl pages numbered from 0-4. The initial page is always 0. As the user swipes, he will move the page from 0 to 1. We will have to remove view = page + 1 and replace it with view page + 2. Let's test this.

- (IBAction)changePage:(id)sender {
NSLog(@"pageControl position %d", [self.pageControl currentPage]);
//Call to remove the current view off to the left
//when page = 0
//view = 1
//Call to add the new view in from the right
//when page = 1
//add view = 2
}

So now let's make the first thing happen.

- (IBAction)changePage:(id)sender {
//Call to remove the current view off to the left
UIImageView *imageViewToRemove = (UIImageView*)[self.view viewWithTag:[self.pageControl currentPage]];
NSLog(@"imageRemoved %d", [self.pageControl currentPage]);
CGRect oldFrame = imageViewToRemove.frame;
oldFrame.origin.x = -CGRectGetWidth(self.view.frame);
imageViewToRemove.frame = oldFrame;
}

First we get the view we want. Then we create a reference to its frame. Later we set that reference frame's origin to the negative value of the screen's view (self.view). This is a bit tough to understand but we are basically making that reference frame's origin.x start at a whole screen width off to the left. This means it is off screen to the left by the screen's size in pixels. Finally we make our imageViewToRemove's frame equal to that value, effectively moving it off to the left.

Try to understand this before moving on. The screen is created at its origin at the top left corner, (0,0). It moves downwards towards (0,480) at its bottom left corner. It finally grows wider from left to right towards (320,480).

Build and Run the app and watch the original image disappear. While this is effective, it is far from cool. This is something an Android app would look like. But we are iOS developers. Let's make it cool! So replace the last line of the above code with this one:

[UIView animateWithDuration:1.0
animations:^{
imageViewToRemove.frame = oldFrame;
}];

Now Build and Run your app and swipe! Cool huh! We are actually making an animation out of that same process and making it last one second. Now let's do the other half. The other half calls to add in the new imageView from the right. So simply reset its frame to the center of the screen's self.view by appending this code to the above:

//Call to add the new view in from the right
UIImageView *imageViewToAdd = (UIImageView*)[self.view viewWithTag:[self.pageControl currentPage]+1];
NSLog(@"imageAdded %d", [self.pageControl currentPage]+1);
CGRect newFrame = imageViewToRemove.frame;
newFrame.origin.x = CGRectGetMidX(self.view.frame)/4;
[UIView animateWithDuration:1.0
animations:^{
imageViewToAdd.frame = newFrame;
}];

Ok now we actually have an image view moved off to the left and a new one sliding in from the right. Notice that because the pageControl.currentPage starts at 0, we have to modify it to add the currentPage + 1.

The other direction

So it works great from left to right. The problem is in the other direction, when current page needs to be reduced by 1 instead of incremented by 1. We need a way to tell if the currentPage was swiped left or right. There is a cool way to do this with UIGestures but we will cover that in its respective tutorial. We are going to use some good old fashion math to solve this one. We are basically going to assume to do a test such that if the new currentPage is greater, it was swiped left and vice versa.

So modify your changePage method to look like this:

- (IBAction)changePage:(id)sender {
if ([self.pageControl currentPage] > currentViewIndex) {
NSLog(@"pageControl = %d & currentViewIndex = %d", [self.pageControl currentPage], currentViewIndex);
currentViewIndex ++;
//Call to remove the current view off to the left
UIImageView *imageViewToRemove = (UIImageView*)[self.view viewWithTag:[self.pageControl currentPage]];
NSLog(@"imageRemoved %d", [self.pageControl currentPage]);
CGRect oldFrame = imageViewToRemove.frame;
oldFrame.origin.x = -CGRectGetWidth(self.view.frame);
[UIView animateWithDuration:1.0
animations:^{
imageViewToRemove.frame = oldFrame;
}];
//Call to add the new view in from the right
UIImageView *imageViewToAdd = (UIImageView*)[self.view viewWithTag:[self.pageControl currentPage]+1];
NSLog(@"imageAdded %d", [self.pageControl currentPage]+1);
CGRect newFrame = imageViewToRemove.frame;
newFrame.origin.x = CGRectGetMidX(self.view.frame)/4;
[UIView animateWithDuration:1.0
animations:^{
imageViewToAdd.frame = newFrame;
}];
}}

The only thing that's really new here is the "if" enclosing the original code above. Oh well, yeah, and the currentViewIndex. This is nothing more than a variable we added to keep track of the old page. The variable currentViewIndex simply starts at 0 (as set in viewDidLoad) and is incremented by one each time the pageControl's currentPage property is incremented. Now you will see why I added all those NSLogs. Those NSLogs give you an idea of what is going on. Both currentViewIndex and pageControl.currentPage start at 0. However, currentViewIndex is only incremented by one if currentPage > currentViewIndex which it always is so long as the user swipes to the left.

Now we need to tell the app to do the opposite if the user swipes right. So basically, at some point, the pageControl.currentPage will decrement. At this point, currentViewIndex which has remained the same will be greater. We can take advantage and use an ELSE test to reverse the process. We end up with something like this for the else block in changePage:

else if ([self.pageControl currentPage] < currentViewIndex) {
NSLog(@"pageControl = %d & currentViewIndex = %d", [self.pageControl currentPage], currentViewIndex);
currentViewIndex --;
//Call to remove the current view off to the right
UIImageView *imageViewToRemove = (UIImageView*)[self.view viewWithTag:[self.pageControl currentPage]+2];
NSLog(@"imageRemoved %d", [self.pageControl currentPage]+2);
CGRect oldFrame = imageViewToRemove.frame;
oldFrame.origin.x = CGRectGetWidth(self.view.frame);
[UIView animateWithDuration:1.0
animations:^{
imageViewToRemove.frame = oldFrame;
}];
//Call to add the new view in from the left
UIImageView *imageViewToAdd = (UIImageView*)[self.view viewWithTag:[self.pageControl currentPage]+1];
NSLog(@"imageAdded %d", [self.pageControl currentPage]+1);
CGRect newFrame = imageViewToAdd.frame;
newFrame.origin.x = CGRectGetMidX(self.view.frame)/4;
[UIView animateWithDuration:1.0
animations:^{
imageViewToAdd.frame = newFrame;
}];
}

As you can see, two things changed; (a) now that the situation is reversed because currentPage went down instead of going up, the currentViewIndex is reduced by one only after the if/else test and (b) this time the old imageView is moved off in the opposite direction.

Don't be shy to draw it out on paper to see how it works. I had to do it in order to code it. UIGestures is much easier as we will see later on.

I hope you enjoyed it and I'll see you soon!

Also read:

About

My name is Marcio Valenzuela and I am from Honduras. I have been coding in iOS for 5 years and I previously worked on web applications in ASP and PHP/mySQL. I have a few apps up in the app store and am currently working on a couple of Cocos2d games...

1 comments
pocjoc
pocjoc

Hi, Good article and thank you to share it with us. I found a bad practice in the code you expose that I would like to discuss, I call the copy&paste style: For instance, In the SecondViewController interface, you define the views with different names (firstView, secondView...), and the initialization method with a lot of copy&paste code. This could be done with a loop if you use an array of views or if you want to maintain the names, is better to use an init method. Let’s assume you call it initFrame, then the calls will be: firstView = [self initView:@"beach.png" theTag:1]; // We change the frame of the firstView CGRect newFrame1 = firstView.frame; newFrame1.origin.x = (CGRectGetWidth(self.view.frame)-CGRectGetWidth(firstView.frame))/2; firstView.frame = newFrame1; secondView = [self initView:@"beach2.jpeg" theTag:2]; thirdView = [self initView:@"trees.jpg" theTag:3]; fourthView = [self initView:@"flowers.jpg" theTag:4]; fifthView = [self initView:@"mountain.jpg" theTag:5]; Another example is inside the changePage method, the different branches of the if are practically the same and it is better not to use copy&paste. At the end, this coding style could produce errors difficult to found. You can see in your code that inside this method, when you add the new view in from the right you use the imageViewToRemove frame to set the imageViewToAdd frame. Because the only thing it changes is the ‘x’ value, it doesn’t matter, but if the images were of different size, and the animation will scale too, this function will fail. Regards.

Editor's Picks