Xamarin iOS swipable multiple step process used for onboarding
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.
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.
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);
}
- Previous post: Xamarin Summary