The requirement

I recently had to build an onboarding process in the JustGiving iOS app that consists of a series of screens that can be navigated by swiping forward and backward.

Step 1 Step 2 Step 3 Step 4 Step 5

The solution

I used a UIPageViewController with a UIPageViewControllerDataSource for navigating through steps and a UIPageControl for the progress indicator.

The working sample solution can be found on GitHub

The end result

Steps

Each step is a UIViewController that implements the following interface.

public interface IMultiStepProcessStep : IDisposable
{
    int StepIndex { get; set; }
    event EventHandler<MultiStepProcessStepEventArgs> StepActivated;
    event EventHandler<MultiStepProcessStepEventArgs> StepDeactivated;
}


public class MultiStepProcessStepEventArgs
{
    public int Index { get; set; }
}

UIViewController step

A step publishes its index when it is activated or de-activated as the active step. This is done in ViewDidAppear and ViewWillDisappear.

public class MakeGoodthingsHappenStep : UIViewController, IMultiStepProcessStep
{
    public override void ViewDidAppear(bool animated)
    {
        base.ViewDidAppear(animated);
        StepActivated?.Invoke(this, new MultiStepProcessStepEventArgs { Index = StepIndex });
    }

    public override void ViewWillDisappear(bool animated)
    {
        base.ViewWillDisappear(animated);
        StepDeactivated?.Invoke(this, new MultiStepProcessStepEventArgs { Index = StepIndex });
    }

    public int StepIndex { get; set; }
    public event EventHandler<MultiStepProcessStepEventArgs> StepActivated;
    public event EventHandler<MultiStepProcessStepEventArgs> StepDeactivated;
}

UIPageViewControllerDataSource

The data source is a UIPageViewControllerDataSource that is constructed with a list of IMultiStepProcessStep steps.

public class MultiStepProcessDataSource : UIPageViewControllerDataSource
{
    private readonly List<IMultiStepProcessStep> _steps;
   
    public MultiStepProcessDataSource(List<IMultiStepProcessStep> steps)
    {
        if (steps == null)
        {
          throw new ArgumentNullException(nameof(steps));
        }
        if (!steps.Any())
        {
            throw new ArgumentException("steps cannot be empty.", nameof(steps));
        }
        if (steps.Any(s => !(s is UIViewController)))
        {
            throw new ArgumentException("all steps must be a UIViewController", nameof(steps));
        }

        _steps = steps;
        
        for (int i = 0; i < _steps.Count; i++)
        {
            var step = _steps[i];
            step.StepIndex = i;
        }
    }

    public List<IMultiStepProcessStep> Steps => _steps;

    public override UIViewController GetPreviousViewController(UIPageViewController pageViewController,
        UIViewController referenceViewController)
    {
        var step = referenceViewController as IMultiStepProcessStep;
        if (step == null)
        {
            return null;
        }

        var index = _steps.IndexOf(step);
        if (index <= 0)
        {
            return null;
        }

        return   _steps[index - 1] as UIViewController;
    }

    public override UIViewController GetNextViewController(UIPageViewController pageViewController, 
														   UIViewController referenceViewController)
    {
        var step = referenceViewController as IMultiStepProcessStep;
        if (step == null)
        {
            return null;
        }
        var index = _steps.IndexOf(step);
        if (index + 1 == _steps.Count)
        {
            return null;
        }

        return _steps[(step.StepIndex + 1)] as UIViewController;
    }
}	

UIPageViewController

The UIPageViewController is constructed from the data source.

public sealed class MultiStepProcessHorizontal : UIPageViewController
{
    public MultiStepProcessHorizontal(MultiStepProcessDataSource dataSource) 
		:base(UIPageViewControllerTransitionStyle.Scroll, 
			  UIPageViewControllerNavigationOrientation.Horizontal)
    {
        DataSource = dataSource;
        SetViewControllers(new[] {dataSource.Steps.FirstOrDefault() as UIViewController}, 
						   UIPageViewControllerNavigationDirection.Forward, 
						   false, 
						   null);
    }
} 

UIPageControl

A UIPageControl is used to indicate which step is active.

UIPageControl

Putting it all together in the OnBoardingViewController

The event handlers for when a step is activated and de-activated are used to set the current page index and to update any other parts of the UI as needed.

private void HandleStepActivated(object sender, MultiStepProcessStepEventArgs args)
{
	_pageControl.CurrentPage =  args.Index;
}

private void HandleStepDeactivated(object sender, MultiStepProcessStepEventArgs args)
{
	//update the UI as required while transitioning between steps
}

Get the steps that form part of the process and wire them up to StepActivated and StepDeactivated events.

private List<IMultiStepProcessStep> GetSteps()
{
	var steps = new List<IMultiStepProcessStep>()
		{
			new MakeGoodthingsHappenStep(),
			new FundraiseStep(),
			new ConnectStep(),
			new DiscoverStep(),
			new GetStartedStep()
		};

	steps.ForEach(s => 
	{
		s.StepActivated += HandleStepActivated;
		s.StepDeactivated += HandleStepDeactivated;
	});

	return steps;
}

Setup and add the UIPageViewController and UIPageControl controls to the view.

private MultiStepProcessHorizontal _pageViewController;
private UIPageControl _pageControl;

private List<IMultiStepProcessStep> _steps;
public List<IMultiStepProcessStep> Steps => _steps ?? (_steps = GetSteps());

public override void LoadView()
    {
        View = new UIView();

        _pageViewController = new MultiStepProcessHorizontal(new MultiStepProcessDataSource(Steps));

        _pageControl = new UIPageControl
            {
                CurrentPage = 0,
                Pages = Steps.Count
            };
			
		View.Add(_pageViewController.View);
        View.Add(_pageControl);
}