Chapter 20: UIScrollView & Paging

*Many apps feature content that must be scrolled through or paged through, but not many coding teachers will teach you how to use UIScrollView to do this. In this book, you will learn how so you're prepared.*


What you will learn

  • Creating a UIScrollView
  • Allowing UIScrollView paging
  • Adding UIImageView as a Subview
  • Setting UIScrollView's contentSize property

Key Terms

  • UIScrollView

Resources

Download here: https://github.com/devslopes/book-assets/wiki

Apps like the Devslopes app use UIScrollView to allow a user to page between relevant content. In the case of the Devslopes app, you can scroll between pages including the different slopes you can learn from as in 2.6.0.

Figure 2.6.0 scrollview-in-devslopes-app.png

Setting up

Creating a new Xcode project

First, open Xcode if you haven't already and click Create New Project. Click Single View Application. Click Next. Give your project a name like PageTheScroll. Below the name field, there are a few drop down menus but for the sake of this chapter, you won't need to change any of them. Click Next. Choose somewhere to save this project file and click Create to save it. You should see a screen like the one in Figure 2.6.1.

Figure 2.6.1 Screen Shot 2016-10-17 at 5.12.54 AM.png

Building the User Interface

Click on Main.storyboard to open up Interface Builder. Next, download the assets for this project from the link above. Then, click on Assets.xcassets and drag all downloaded assets inside like in Figure 2.6.2:

Figure 2.6.2 Screen Shot 2016-11-03 at 5.24.19 AM (2).png

Adding a UIImageView

Now we need to give our app a background image. In the bottom right of the Xcode window, search for UIImageView and drag one onto our ViewController. Position it so that it extends to the full size of the ViewController (Figure 2.6.3):

Figure 2.6.3

Screen Shot 2016-11-03 at 5.28.53 AM.png

With the UIImageView selected, click the Pin button (pin-button.png) at the bottom of the Xcode window and give it the following constraints pinning it a distance of 0 from all sides of the view (Figure 2.6.4):

Figure 2.6.4

Screen Shot 2016-11-03 at 5.35.34 AM.png

Next, click on the UIImageView and select the Attributes Inspector (attributes-inspector.png) if it isn't selected already. Set the Image property to be "Sky" and the Content Mode to be Apsect Fill (Figure 2.6.5). This is so that our image maintains it's aspect ratio, while filling to the edges of the UIImageView like a picture in a frame (Figure 2.6.6). It will look best on all screen sizes this way.

Figure 2.6.5

Screen Shot 2016-11-03 at 5.37.35 AM.png

Figure 2.6.6

Screen Shot 2016-11-03 at 5.43.20 AM.png

Next, drag on another UIImageView and position it in the top right corner. In the Attributes Inspector for this UIImageView, set the Image property to "Sun" and the Content Mode to be Aspect Fit (Figure 2.6.7):

Figure 2.6.7

Screen Shot 2016-11-03 at 5.45.14 AM.png

Drag the Sun UIImageView to the top right corner of the screen and position it like in Figure 2.6.8. Click the Pin button at the bottom and pin it to the right and top sides. Make sure you tick the Height and Width boxes to give it a fixed width and height so that it remains the size we want (Figure 2.6.9).

Figure 2.6.8

Screen Shot 2016-11-03 at 5.48.05 AM.png

Figure 2.6.9

Screen Shot 2016-11-03 at 5.52.12 AM.png

Drag on one last UIImageView and position it like so (Figure 2.6.10):

Figure 2.6.10

Screen Shot 2016-11-03 at 5.54.54 AM.png

Click the Pin button and give it constraints which will pin it to the bottom, left side, and right side. Also, tick the Height box to give it a fixed height (Figure 2.6.11):

Figure 2.6.11

Screen Shot 2016-11-03 at 5.56.39 AM.png

In the Attributes Inspector, set the Image property to "Mountains" and the Content Mode to Aspect Fill (Figure 2.6.12):

Figure 2.6.12

Screen Shot 2016-11-03 at 5.58.04 AM.png

Your app's background should look like Figure 2.6.13:

Figure 2.6.13

Screen Shot 2016-11-03 at 5.16.21 PM.png

Alright, so now we have a nice pretty background to be in the back of our app. We didn't have to spend time making this, but it's nice to always make things look nice in addition to running well.

Adding UIScrollView

Next, we are going to add in a UIScrollView which will perform all of the scrolling and paging we want for this app. Click on the search bar in the bottom-right corner of the Xcode window, search for "UIScrollView", and drag one into the Document Outline on the left-hand side of Interface Builder like so (Figure 2.6.14):

Figure 2.6.14

Screen Shot 2016-11-03 at 5.23.37 PM.png

The reason we didn't drop the UIScrollView on top of the ViewController is because sometimes there are issues with UIScrollView in Interface Builder, so adding it to the Document Outline ensures that we position it in the right place – as the top-most item in front of everything else.

Next, drag and position the UIScrollView so that it takes up the entire frame of the ViewController – just like we did for the UIImageView with the background image (Figure 2.6.15):

Figure 2.6.15

Screen Shot 2016-11-03 at 5.29.03 PM.png

UIScrollView is created transparent, but you can see the text 'UIScrollView' in the center of Figure 2.6.15 above.

Next, with the UIScrollView selected untick the boxes for: Shows Horizontal Indicator, Shows Vertical Indicator and tick the box Paging Enabled (Figure 2.6.16).

Figure 2.6.16

Screen Shot 2016-11-03 at 5.32.07 PM.png

Adding content to UIScrollView

Great, now that we've added on a UIScrollView we need to give it some content so that it is useful! First we need to add the assets for this. From the source code you downloaded at the beginning of this chapter, drag on the three images called icon0, icon1, and icon2. You should now have an iOS, Android, and Angular icons in your Assets folder.

Click on ViewController.swift in the Navigator on the left-hand side of the Xcode window and delete the boilerplate code leaving viewDidLoad() intact. Like so:

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
}

Next up, let's create an empty array of UIImageViews to contain all of our course icons we just added to our Assets folder. We also will create a for loop to create images to store inside each UIImageView. Add the following code above viewDidLoad():

import UIKit

class ViewController: UIViewController {

    var images = [UIImageView]()

    override func viewDidLoad() {
        super.viewDidLoad()

        for x in 0...2 {
            let image = UIImage(named: "icon\(x).png")
            let imageView = UIImageView(image: image)
            images.append(imageView)
        }
    }
}

So you just wrote the code to create an array of UIImageView. Then you wrote a for-in loop which cycles from 0 up until 2. After, you created a constant called image which creates a UIImage and passes in the value for x into the filename for the picture (icon0.png, icon1.png, icon2.png). Then we create a constant of type UIImageView and pass in the UIImage. At the end of the loop, we append a UIImageView (containing a UIImage) to our array called images.

So after the loop runs, we have the following inside the images Array:

  • UImageView -> UIImage -> icon0.png
  • UIImageView -> UIImage -> icon1.png
  • UIImageView -> UIImage -> icon2.png

If you build and run your app at this point, you will definitely see our nice mountain background image, but the icons are nowhere to be found. That is because we haven't yet placed them anywhere. We only created them.

Now let's get our images actually showing up in the UIScrollView.

Before we move forward though, I want you to think of UIScrollView like it's a magical window looking out into the world. When you drag your finger across the window, the world outside moves with your finger. Instead of you needing to move your head to see what is outside you can just swipe your finger over the glass.

UIScrollView has a property called contentSize which allows you to define how much content can be inside of a UIScrollView. As per the example above, the content size allows you to choose how big the world is through the window. Figure 2.6.17 shows how this will work in our app. We will set the contentSize momentarily.

Figure 2.6.17 scrollView-content-size.png

Connecting UIScrollView to our ViewController.swift file

Let's create an @IBOutlet so that we can interact with our UIScrollView and add in those images.

Click on Main.storyboard and then click the Assistant Editor on the upper right-hand side of Xcode (assistant-editor.png) to bring up the associated ViewController.swift file.

Right-click and hold on "Scroll View" from the Document Outline and drag the cursor over to ViewController.swift. Release the cursor above viewDidLoad() like in Figure 2.6.18:

Figure 2.6.18

@IBAction.png

When you release the cursor give the @IBOutlet a name of scrollView and leave all other properties as they are like in Figure 2.6.19:

Figure 2.6.19

Screen Shot 2016-11-04 at 6.22.30 AM.png

Now we can access the UIScrollView and make it do cool things!

Positioning UIImageView on UIScrollView

Now we need to set up some code which will be responsible for helping us to scroll our content and contain pages of content, too.

Inside the for-in loop you wrote earlier add these following lines of code:

...
for x in 0...2 {
    let image = UIImage(named: "icon\(x).png")
    let imageView = UIImageView(image: image)
    images.append(imageView)

    var newX: CGFloat = 0.0

    newX = view.frame.midX + view.frame.size.width * CGFloat(x)
}

What we have done is create a variable of type CGFloat called newX. We then set it's value, but it may be confusing to a new programmer to understand these values with no explanation.

Let me explain.

1.) view.frame.midX is a value that takes the current view (in this case, our screen) and looks for the size of it's frame (in this case, the same size as our screen). Then it calculates the midpoint on the X axis.

So, if your screen was 320 pixels wide, the midX point would be 160. Figure 2.6.19 shows how the grid actually works in iOS:

Figure 2.6.19

320-example-phone-shadow.png

As you can see, the top left corner is x: 0, y: 0 and as you move to the right or down towards the bottom the values increase.

2.) view.frame.size.width is a value that captures the width of our screen in this example. It looks inside of the view, then the frame, then captures it's size and stores the value for the width.

Since our imaginary screen is 320 pixels wide, it would return a value of 320.

3.) At the end we multiply view.frame.midX + view.frame.size.width by CGFloat(x). Remember that we are in a for-in loop passing in values of 0, 1, and 2.

When the loop runs for the first time, we pass in the value 0 for x. Continuing with our example of a screen width of 320 pixels, newX calculation would go like this:

view.frame.midX = 160 view.frame.size.width = 320 CGFloat(0) = 0.0

So, for the first time through the for loop we are calculating 160 + 320 * 0 which gives us a grand total of 160 if we follow the order of operations.

The loop now will run again and our calculation changes. It now looks like this:

view.frame.midX = 160 view.frame.size.width = 320 CGFloat(1) = 1.0

We've just performed 160 + 320 * 1 which results in 480.

The loop runs one last time before stopping.

view.frame.midX = 160 view.frame.size.width = 320 CGFloat(2) = 2.0

We performed 160 + 320 * 2 resulting in 800.

So by the end we have changed newX to first be equal to 160, then 480, then 800.

Remember Figure 2.6.17 about contentSize from above? We need to offset each icon to have an entire screen width of space so that we can page between them properly. These newX values are what we will use to accomplish this.

We want our icons to show up in the middle of our screen which is why we are using midX as a property.

Adding each UIImageView as a Subview

Now that our loop creates a UIImageView and fills it with one of our icon images, we need to add it as a subview of UIScrollView. Think of this sort of similarly to how you would add a page in a Microsoft Word or Pages document.

At the bottom of the for-in loop, add the following:

...
for x in 0...2 {
    let image = UIImage(named: "icon\(x).png")
    let imageView = UIImageView(image: image)
    images.append(imageView)

    var newX: CGFloat = 0.0

    newX = view.frame.midX + view.frame.size.width * CGFloat(x)

    scrollView.addSubview(imageView)
}

We also need to give our images a frame size so that they are bound to a certain size. Set up the frame property of UIImageView like so:

...
for x in 0...2 {
    let image = UIImage(named: "icon\(x).png")
    let imageView = UIImageView(image: image)
    images.append(imageView)

    var newX: CGFloat = 0.0

    newX = view.frame.midX + view.frame.size.width * CGFloat(x)

    scrollView.addSubview(imageView)

    imageView.frame = CGRect(x: 0, y: view.frame.size.height / 2, width: 150, height: 150)
}

Now every UIImageView we make in our loop will be bound to a frame size of 150 x 150.

We've positioned it at 0 on the x-axis and the middle of the screen (view.frame.size.height / 2) on the y-axis. We want it to be in the middle of the x and y-axis but as of now it's only centered on the y-axis.

We will modify the x-axis value using newX shortly.

Let's build and run our app at this moment to check and see how we did. As you can see in Figure 2.6.20, our image is now showing up, but sadly it is too low. That's because we set it's y-axis value to be view.frame.size.height / 2 meaning that the top of our UIImageView is positioned in the middle of our screen on the y-axis. We want it to be centered, so let's do that now by subtracting half of the height of our Angular image.

Figure 2.6.20

Simulator Screen Shot Nov 4, 2016, 12.53.25 PM.png

We know the height of our UIImageView is 150 because we set it to be so. We just need to subtract half of our UIImageView's height to bring it up to the center. Do that by surrounding view.frame.size.height / 2 with parentheses and subtracting 75 like so:

swift
...
imageView.frame = CGRect(x: 0, y: (view.frame.size.height / 2) - 75, width: 150, height: 150)

Build and run the app again and check to see how the positioning has changed (Figure 2.6.21). Yay! It is nice and centered now!

Figure 2.6.21

Simulator Screen Shot Nov 4, 2016, 12.59.14 PM.png

Let's make the image centered on the x-axis now.

We already have the value in place to move our image to the center of the x-axis so change the x value in imageView.frame from 0 to use newX from earlier.

...
    imageView.frame = CGRect(x: newX, y: (view.frame.size.height / 2) - 75, width: 150, height: 150)

Changing the x-axis value here works identically to how we changed the y-axis above – meaning that the left side of the image (0 on the x-axis) will be positioned in the center. To make the image appear centered on our screen, we need to subtract 75 just like before.

...
    imageView.frame = CGRect(x: newX - 75, y: (view.frame.size.height / 2) - 75, width: 150, height: 150)

Build and run to see if it worked. You should see something like Figure 2.6.22:

Figure 2.6.22

Simulator Screen Shot Nov 4, 2016, 1.09.24 PM.png

Woohoo! It's lookin' so good, but we still can't scroll. With everything centered and looking fancy, we only have a few more steps to go.

Let's make that UIScrollView scroll and page, too!

Making UIScrollView play nicely

Like I said before, UIScrollView has a property called contentSize which operates sort of like a magic window that allows us to move the world behind it as we move our finger around the glass. If we have content that is three times the width of our screen, we can set our UIScrollView to understand that.

Let's create a property to store the width of our content and change it's value based on the loop that we create so it expands as each UIImageView is created.

Add the following to ViewController.swift:

import UIKit

class ViewController: UIViewController {

    var images = [UIImageView]()
    var contentWidth: CGFloat = 0.0

    @IBOutlet weak var scrollView: **UIScrollView**!

    override func viewDidLoad() {
        super.viewDidLoad()

        for x in 0...2 {
            let image = UIImage(named: "icon\(x).png")
            let imageView = UIImageView(image: image)
            images.append(imageView)

            var newX: CGFloat = 0.0

            newX = view.frame.midX + view.frame.size.width * CGFloat(x)

            contentWidth += newX

            scrollView.addSubview(imageView)

            imageView.frame = CGRect(x: newX - 75, y: (view.frame.size.height / 2) - 75, width: 150, height: 150)
        }
    }
}

As you can see, we added a variable of type CGFloat called contentWidth and set it to equal 0.0. Then, inside of the for-in loop we incremented it's value by newX each time the for loop runs. This creates enough space for a page for each image.

All we need to do now is to set the contentSize on our UIScrollView by adding a line of code beneath the for-in loop like so:

...
override func viewDidLoad() {
    super.viewDidLoad()

    for x in 0...2 {
        let image = UIImage(named: "icon\(x).png")
        let imageView = UIImageView(image: image)
        images.append(imageView)

        var newX: CGFloat = 0.0

        newX = view.frame.midX + view.frame.size.width * CGFloat(x)

        contentWidth += newX

        scrollView.addSubview(imageView)

        imageView.frame = CGRect(x: newX - 75, y: (view.frame.size.height / 2) - 75, width: 150, height: 150)
    }

    scrollView.contentSize = CGSize(width: contentWidth, height: view.frame.size.height
}

Build and run the app to see how if our UIScrollView is now scrollable.

You should now be able to scroll from page to page seeing all three of our images snap nicely into place. 🤘

Wrapping up

At this point, you've learned all that you need to know about UIScrollView and I hope you can see how useful it can be to make accessing your content dynamic and fun in iOS apps.

I'd like to challenge you to extend this app by figuring out a way to make the entire screen page left and right instead of just being able to scroll on the UIImageViews.

Hint: Check out Chapter 23 for how to use Gestures in iOS.* 😀

Exercise

Extend this app by figuring out a way to make the entire screen page left and right instead of just being able to scroll on the UIImageView. Hint: Check out Chapter 23 for how to use Gestures in iOS. 😀

Section 2 Project

Sweet! You made it through section 2 and now have a good understanding of the fundamentals of Swift! That wasn't so bad, right? Now let's put your skills to the test.


Requirements:

Task 1

  1. Create a new Xcode project.

  2. In the initial ViewController made in Main.storyboard, create a login screen similar to this: mail-app-tupakulavijay.jpg Hint: You will need to use UIButton, UILabel, UIImageView, and UIStackView.

  3. Rename this initial ViewController to LoginVC.

  4. In Interface Builder, drag on a UITabBarController which will act as our main app UI. Rename "Item 1" to say "Home" and "Item 2" to say "Settings".

  5. Make the UITabBarController the "Initial ViewController" by dragging the arrow from LoginVC to be on top of the Tab Bar Controller until it turns blue like so: tabbar.png 6.) Create a new Cocoa Touch Class file by right-clicking on the project folder on the left-hand side of Xcode. It's class should be called "HomeVC" and it's subclass should be UIViewController. Be sure to delete the extra "ViewController" text it automatically adds in to the Class field. Click 'Next', click 'Create' and return to Main.storyboard. Click on the "Home" ViewController in the Tab Bar Controller and click the Identity Inspector. Set the identity of the "Class" to be "HomeVC" and press Enter. 7.) Drag a UITableView into HomeVC and drag a custom UITableViewCell inside. Create a custom cell like this Instagram-style post like so: example.png

This app will need to contain several photo posts. Our app will not be able to make posts, but will need to read from static data (for the purposes of this challenge). So, you will need to create a data model to store several posts. Create a data model to store:

  • A Dictionary containing usernames (key) and posts (value).
  • An Array of Photos (obtain nice free photos from pexels.com – no attribution required.)
  • Use UIImage here
  • The UIButtons don't need to actually do anything.

Helpful Hint: Be a good programmer. Do your research, read back through the chapters in this section, and don't be afraid to reach out for help in the Devslopes chat or online. Struggling through this is the best way to learn and retain what you've learned. Good programmers are always seeking to learn and improve their practice.

Task 2

  1. Open Terminal and cd into your Xcode project folder.
  2. Create a new repository on Github.
  3. If you haven't already, follow the guide in chapter 16 (Setting Up Github) to get your computer's SSH key on Github.
  4. On your computer, from the Terminal app, initialize your project folder as a local Git repository.
  5. Add all files and then make a commit with a message.
  6. Add your SSH Github repository URL (click the clipboard button on Github) as a remote repository (remember: git remote add)
  7. Pull from Github (which will merge any files you already have in that repo like README.md)
  8. Push to Github

The End Result

If you refresh your repository on github.com you should see your project loaded up on your Github page.