Implement your own onboarding flow using a UIScrollView

Onboarding

Objective

Implement your own application onboarding flow using a UIScrollView

 

Many popular applications, Google Translate, Pinterest, Fitbit, SwiftKey, to name but a few, have a series of screens to introduce the applications features/concepts to new users. Commonly, these screens consist of an image, small amount of text and a UIPageControl, allowing navigation between pages.

UIPageControl

You can use the UIPageControl class to create and manage page controls. A page control displays a horizontal series of dots, each of which corresponds to a page in the application’s document (or other data-model entity). The currently viewed page is indicated by a white dot.

The UX Archive have documented some great examples of onboarding flows in popular applications. With this in mind, I set about looking at approaches for implementing an onboarding process. As you will have seen from the objective of this blog post, “Implement your own application onboarding flow using a UIScrollView,” I didn’t go for using a UIPageViewController, even though, arguably, a UIPageViewController is a more obvious choice for an onboarding process.

on boarding process

A page view controller lets the user navigate between pages of content, where each page is managed by its own controller object. Navigation can be controlled programatically by your app or directly by the user using gestures. When navigating page to page, the page view controller uses the transition that you specify to animate the change.

In a UIPageViewController, the data source is responsible for providing the content view controllers on demand and must conform to the UIPageViewControllerDataSource protocol. Appearance-related information and gesture support is supported through the UIPageViewControllDelegate. So, why didn’t I use a UIPageViewController? Well, I have some requirements for my onboarding process which made a UIScrollView more appropriate, or easier to implement at least.

Requirements
1. Customise the position of the UIPageControl;
2. Common elements which appear on each onboarding screen.

It is possible to create a Master Page using a UIPageViewController, if you create a view and embed a UIPageViewController within it. Customising the UIPageControl is another matter, as of yet, I haven’t found a decent way of doing it. I have seen approaches which involve hiding the default one, then implementing your own UIPageControl. I found it easier to just implement another way, rather than fighting with the way that UIPageViewController wants to work.

Step 1 Solution Overview

To keep things simple, I have designed a very basic onboarding flow. The onboarding flow, what the user will see, is the three screens on the right. The screen on the left is a master page, representing the elements which remain constant throughout the whole of the onboarding flow.

Onboarding Design

The master page will have its own custom UIViewController subclass, OnboardingMasterViewController. The three content pages will also have their own UIViewController subclass, OnboardingContentPageViewController.

The solution we are going to implement is using the OnboardingMasterViewController to programmatically add a UIScrollView, set the layout constraints and allow a calling class to add UIViewController instances to the UIScrollView, which has paging enabled.

The UIScrollView class provides support for displaying content that is larger than the size of the application’s window. It enables users to scroll within that content by making swiping gestures, and to zoom in and back from portions of the content by making pinching gestures.

When paging is enabled on a UIScrollView, the scroll view stops on multiples of the scroll view’s bounds when the user scrolls. This is going to create the effect of multiple pages, and we will set the current page of the UIPageControl manually.

Step 2 Create Xcode Project

Create a new Xcode project, under iOS Application select Single View Application. Within the options dialog for your new project, fill out as follows:

Product Name: OnboardingFlowTutorial
Organization Name: DeveloperDave
Organization Identifier: co.uk.developerdave
Language: Swift
Devices: Universal
Use Core Data: No
Include Unit Tests: No
Include UI Tests: No

 

Step 3 Create the UI

We are first going to layout all of the screens within a storyboard, open up Main.storyboard and add another four View Controller objects to the storyboard. We are going to use one View Controller as an entry point into the onboarding process, one View Controller as the Onboarding Master Page, three View Controller objects as the Onboarding screens.

Storyboard

 

Let’s start with the entry point into the onboarding process, add a UIButton onto the first View Controller. Double click the button and change the title to `Show Walkthrough`, centre the UIButton vertically and horizontally. You will want to add Layout Constraints to ensure the UIButton  is centred vertically and horizontally on all device sizes. I am not going to go through all of the Layout Constraints, it is a complex subject and with our tests and everything else we are doing this post would turn into a book. Instead, check out Ray Wenderlich’s tutorial on auto layout constraints. Once you are done you should end up with something like this.

Show Walkthrough Screen

 

Let’s tackle the Master Page next, double click the View Controller underneath the View Controller with an arrow pointing to it. double clicking the View Controller ensures the view is selected, within the Attributes Inspector (⌘+⌥+4) and change the Background color to #9B001A. Click the View Controller icon in the top bar of the View Controller (it is the left-most icon), make sure the Attributes Inspector (⌘+⌥+4) is visible and change the Status Bar to Light Content.

View Controller Top Bar

 

Open Assets.xcassets, click the + icon at the bottom of the page and select New Image Set, name the new image set, Logo. You can get the logo from the following zip file, Image Assets. Drag the logo, @2x, and @3x into the relevant placeholders inside the logo image set. Jump back to the Main.storyboard and add an Image View onto the master page. Within the Attributes Inspector (⌘+⌥+4) set the Image to Logo, set the Mode to Center. Now, give the Image View some layout constraints, center horizontally and set Vertical Spacing to Top Layout Guide to 100. Add a Page Control, and again add some layout constraints, center horizontally and set Vertical Spacing to Bottom Layout Guide to 120. We now need to add the two buttons to the bottom of the master page. Drag two Button objects onto the master page, within the Attributes Inspector (⌘+⌥+4) set the Text Color to #ffffff and the opacity to 61% for both buttons. Change the text of the left button to SIGN UP, and the right button to LOGIN. Change the font size of both buttons to 18.0, and resize so you can see the text. Now, drag a View onto the master page and within the Size Inspector (⌘+⌥+5) set the width to 1, the height to 35 and position between the two buttons to make a vertical separator, now add layout constraints to the Page Control, Button objects, and View. Once you are done the master page should look as follows:

Master Page

 

The next series of screens are easier, we just need two pieces of text to each screen. So, on the screen 1 within the Attributes Inspector (⌘+⌥+4) change the Background color to #9B001A. Click the View Controller icon in the top bar of the View Controller (it is the left-most icon), make sure the Attributes Inspector (⌘+⌥+4) is visible and change the Status Bar to Light Content. Whilst you are at it, repeat this for screens 2 and 3. Once you are done, drag two Label objects onto screen 1. Change the text color to white on both Label objects, change font size of the title label (the top label) to 20.0 and the bottom label to 12.0. Change the title text to “Do something incredible”, and the bottom label to “An onboarding flow allows you to introduce your app before requiring personal info” Then add layout constraints so your view looks like the following:

Screen 1

Notice where the text has been laid out, once the master page content is rendered onto screen 1 everything will look good together. Repeat the process for screen 2 but this time set the title text to “Introduce features” and the body text to “Chance to introduce application features”. Now repeat for the third screen, this time with the title text “Sell benefits” and the body text “Sell the benefits of your app”. Once you are done the Main.storyboard should like the following:

Onboarding Flow

Step 4 Create the OnboardingMasterViewController

Right-click on the OnboardingFlowTutorial folder and select New File… under iOS Source and select the Cocoa Touch Class, name the new file OnboardingMasterViewController, make sure it is a subclass of UIViewController and the language is Swift, go ahead and click next and save the file. Delete the contents of the file so you are left with the following:

import UIKit

class OnboardingMasterViewController: UIViewController {


}

Let’s start off by creating an IBOutlet for the PageControl some variables for a UIScrollView, array of UIViewController objects and NSArray of view constraints for the UIScrollView.

import UIKit

class OnboardingMasterViewController: UIViewController {

  @IBOutlet var pageControl: UIPageControl?
  private let scrollView: UIScrollView!
  private var controllers: [UIViewController]!
  private var scrollviewViewConstraint: NSArray?
  
}

Now, go ahead and add some init methods:

required init?(coder aDecoder: NSCoder) {
    scrollView = UIScrollView()
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false
    scrollView.pagingEnabled = true
    
    controllers = Array()
    
    super.init(coder: aDecoder)
  }
  
  override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
    scrollView = UIScrollView()
    controllers = Array()
    
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
  }

We are now going to add some properties, we can tell which page is currently being viewed based on the contentOffset of the UIScrollView and we can tell the number of pages based on the count of the controller array.

var currentPage: Int {
    get {
      let page = Int((scrollView.contentOffset.x / view.bounds.size.width))
      return page
    }
  }
  
  var numberOfPages: Int {
    get {
      return self.controllers.count
    }
  }

We are now going to add the viewDidLoad and viewWillAppear methods.

override func viewDidLoad() {
    
    super.viewDidLoad()
    
    pageControl?.addTarget(self, action: "pageControlDidTouch", forControlEvents: .TouchUpInside)
    
    scrollView.delegate = self
    scrollView.translatesAutoresizingMaskIntoConstraints = false
    view.insertSubview(scrollView, atIndex: 0)
    
    view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[scrollview]-0-|", options:[], metrics: nil, views: ["scrollview":scrollView]))
    view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[scrollview]-0-|", options:[], metrics: nil, views: ["scrollview":scrollView]))
    
  }
  
  override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    
    pageControl?.numberOfPages = self.numberOfPages
    pageControl?.currentPage = 0
  }

We add a target to PageControlpageControlDidTouch so we can go to the next page when the user interacts with the PageControl. We then configure the UIScrollView and add some layout constraints. Within the viewWillAppear method we set-up the PageControl, setting the numberOfPages and currentPage.

Next, add an extension to the OnboardingMasterViewController for the UIScrollViewDelegate.

// MARK: - UIScrollViewDelegate
extension OnboardingMasterViewController : UIScrollViewDelegate {
  
  func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
    pageControl?.currentPage = currentPage
  }
  
  func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
    pageControl?.currentPage = currentPage
  }
  
}

Next we are going to deal with adding pages to the OnboardingMasterViewController, go ahead and add an extension to the OnboardingMasterViewController with the following code.

// MARK: - Helper Methods
extension AppIntroMasterViewController {
  
  @IBAction func close(sender: AnyObject) {
    delegate?.closeButtonPressed?()
  }
  
  @IBAction func nextPage() {
    if currentPage + 1 < self.numberOfPages {
      delegate?.nextButtonPressed?()
      navigateToPage(currentPage + 1)
    }
  }
  
  @IBAction func previousPage() {
    if currentPage > 0 {
      delegate?.previousButtonPressed?()
      navigateToPage(currentPage - 1)
    }
  }
  
  func pageControllDidTouch() {
    if let pageControl = pageControl {
      navigateToPage(pageControl.currentPage)
    }
  }
  
  func addViewController(vc: UIViewController) -> Void {
    controllers.append(vc)
    
    vc.view.translatesAutoresizingMaskIntoConstraints = false
    scrollView.addSubview(vc.view)
    
    let metrics = ["w":vc.view.bounds.size.width,"h":vc.view.bounds.size.height]
    vc.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[view(h)]", options:[], metrics: metrics, views: ["view":vc.view]))
    vc.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[view(w)]", options:[], metrics: metrics, views: ["view":vc.view]))
    scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[view]|", options:[], metrics: nil, views: ["view":vc.view,]))
    
    if self.numberOfPages == 1 {
      scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[view]", options:[], metrics: nil, views: ["view":vc.view,]))
    } else {
      let previousVC = controllers[self.numberOfPages - 2]
      let previousView = previousVC.view;
      
      scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[previousView]-0-[view]", options:[], metrics: nil, views: ["previousView":previousView,"view":vc.view]))
      
      if let cst = lastViewConstraint{
        scrollView.removeConstraints(cst as! [NSLayoutConstraint])
      }
      lastViewConstraint = NSLayoutConstraint.constraintsWithVisualFormat("H:[view]-0-|", options:[], metrics: nil, views: ["view":vc.view])
      scrollView.addConstraints(lastViewConstraint! as! [NSLayoutConstraint])
    }
  }
  
  private func updateUI() {
    pageControl?.currentPage = currentPage
  }
  
  private func navigateToPage(page: Int) {
    if page < self.numberOfPages {
      var frame = scrollView.frame
      frame.origin.x = CGFloat(page) * frame.size.width
      scrollView.scrollRectToVisible(frame, animated: true)
    }
  }
  
}

Step 5Connect the UI

Open up Main.storyboard on your master page click the View Controller icon in the top bar of the View Controller (it is the left-most icon), make sure the Identity Inspector (⌘+⌥+3) is visible, and change the class to OnboardingMasterViewController. Enter onboardingFlow into the Storyboard ID and set the Module to OnboardingFlowTutorial. Control click the View Controller icon and within the Outlets section drag the Page Control to the Page Control on your view.

Connecting the Page Control

 

On your first screen of the onboarding flowclick the View Controller icon in the top bar of the View Controller (it is the left-most icon), make sure the Identity Inspector (⌘+⌥+3) is visible, and change the storyboard id to screen1.

On your second screen of the onboarding flowclick the View Controller icon in the top bar of the View Controller (it is the left-most icon), make sure the Identity Inspector (⌘+⌥+3) is visible, and change the storyboard id to screen2.

On your third screen of the onboarding flowclick the View Controller icon in the top bar of the View Controller (it is the left-most icon), make sure the Identity Inspector (⌘+⌥+3) is visible, and change the storyboard id to screen3.

Step 6Configure the Onboarding Flow

Open the ViewController.swift file and replace with the following code:

import UIKit

class ViewController: UIViewController {
  
  @IBAction func startAppIntro() {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let onboardingFlow = storyboard.instantiateViewControllerWithIdentifier("onboardingFlow") as! OnboardingMasterViewController
    let screen1 = storyboard.instantiateViewControllerWithIdentifier("screen1")
    let screen2 = storyboard.instantiateViewControllerWithIdentifier("screen2")
    let screen3 = storyboard.instantiateViewControllerWithIdentifier("screen3")
    
    onboardingFlow.addViewController(screen1)
    onboardingFlow.addViewController(screen2)
    onboardingFlow.addViewController(screen3)
    
    self.presentViewController(onboardingFlow, animated: true, completion: nil)
  }
  
}

Now go back to Main.storyboard, control click the View Controller icon on the screen which has the button in the middle for starting the walkthrough, and within the Received Actions section drag from startAppIntro to the Show Walkthrough button and select Touch Up Inside.

Step 7Check Out The Results

With all that in place when you build and run the project you should see our first screen displayed, tapping on the show walkthrough button will display our onboarding process. In a future tutorial I will show you how to link up the buttons in the onboarding flow and how to cancel out of the onboarding flow if you need to.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *