Contact us

BOOK A PRESENTATION

Creating a Custom Onboarding Swiper in iOS using MVVM architecture and without UIPageViewController

March 6, 2023
NO NAME
Creating an iOS onboarding swiper can be a challenging task, especially when trying to implement a design that doesn't fit with the standard UIPageViewController. Fortunately, we can use UICollectionView to build our custom swiper.

In this blog post, we will go through the process of building a custom onboarding swiper using the MVVM architecture and UICollectionView. This method will give us greater control over the design and functionality of the swiper, allowing us to make it more engaging and user-friendly. The final result is showcased in the video below.

1. Set up the Project

The first step is to set up the project. We will be using Cocoapods for this project. We will use PureLayout to quickly add constraints and AdvancedPageControl for beautiful page indicators that work great with a scroll view that we will implement.

If you haven't used CocoaPods before, you can install it by following the instructions available on their website. After that, you need to create a Podfile and add the following code:

# Uncomment the next line to define a global platform for your project

platform :ios, '13.0'

def shared_pods

  pod 'PureLayout'

  pod 'AdvancedPageControl'

  pod 'RxSwift'

  pod 'RxCocoa'

end

target 'MVVMOnboarding' do

  # Comment the next line if you don't want to use dynamic frameworks

  use_frameworks!

  shared_pods

end

Note: If you have multiple targets, using shared_pods function you can easily add new pods to all targets.

Once you have added the above code to your Podfile, move to the location of the Podfile in Terminal and run pod install. After that, your project should be ready, and you can now open your .workspace file.

2. Add Resources and Architecture

For this project, we will be using the MVVM architecture. First, we will create a model that holds all the texts for pages and images. Using an enum structure, we can easily add more pages later. Also, Page model conforms to CaseIterable to easily create an array from all cases.

import Foundation

enum Page: CaseIterable {

    case pageZero

    case pageOne

    case pageTwo

    var title: String {

        switch self {

        case .pageZero:

            return "Welcome to MVVM Onboarding App"

        case .pageOne:

            return "Create beautiful onaboarding experience for your user"

        case .pageTwo:

            return "Smooth and interactive"

        }

    }

    var image: String {

        switch self {

        case .pageZero:

            return "page1"

        case .pageOne:

            return "page2"

        case .pageTwo:

            return "page3"

        }

    }

    var index: Int {

        switch self {

        case .pageZero:

            return 0

        case .pageOne:

            return 1

        case .pageTwo:

            return 2

        }

    }

}

Next, we create all the pages in the view model and add helper functions. Finally, all the views are in the view file.

import Foundation

import RxSwift

import RxCocoa

class OnboardingViewModel {

    enum Event {

        case registerClicked

        case loginClicked

    }

    let events = PublishSubject<Event>()

    let pages: BehaviorRelay<[Page]> = BehaviorRelay<[Page]>(value: [])

    var index = 0

    init() {

        getPages()

    }

    func getPages(){

        let allPages: [Page] = Page.allCases

        self.pages.accept(allPages)

    }

    func getNumberOfPages() -> Int {

        return self.pages.value.count

    }

    func getPageForRow(index: Int) -> Page {

        return self.pages.value[index]

    }

    func getTitle(index: Int) -> String {

        return pages.value[index].title

    }

    func getImage(index: Int) -> String {

        return pages.value[index].image

    }

    func getPageIndex() -> Int {

        return index

    }

    func registerClicked() {

        events.onNext(.registerClicked)

    }

    func loginClicked() {

        events.onNext(.loginClicked)

    }

}

For images, we use https://undraw.co to get beautiful, modern images.

3. Build the Custom Swiper

Every view will be made programmatically. We will first create a UICollectionView that will mimic a page swiper by enabling collectionView.isPagingEnabled to true and collectionView.decelerationRate set to UIScrollView.DecelerationRate.fast.

lazy var titleCollectionView: UICollectionView = {

        let collectionViewLayout = UICollectionViewFlowLayout()

        collectionViewLayout.minimumLineSpacing = .zero

        collectionViewLayout.sectionInset = .zero

        collectionViewLayout.minimumInteritemSpacing = .zero

        collectionViewLayout.scrollDirection = .horizontal

        let collectionView = UICollectionView(frame: view.frame, collectionViewLayout: collectionViewLayout)

        collectionView.delegate = self

        collectionView.dataSource = self

        collectionView.isScrollEnabled = true

        collectionView.bounces = true

        collectionView.backgroundColor = .clear

        collectionView.decelerationRate = UIScrollView.DecelerationRate.fast

        collectionView.register(OnboardingCollectionViewCell.self, forCellWithReuseIdentifier: OnboardingCollectionViewCell.reuseIdentifier)

        collectionView.contentInset = .zero

        collectionView.showsHorizontalScrollIndicator = false

        collectionView.showsVerticalScrollIndicator = false

        collectionView.alwaysBounceHorizontal = false

        collectionView.isPagingEnabled = true

        return collectionView

    }()

We will have two image views behind the UICollectionView that will fade depending on the scroll.

lazy var firstImageView: UIImageView = {

        let imageView = UIImageView(image: UIImage(named: viewModel.getImage(index: 0)))

        imageView.backgroundColor = .clear

        imageView.contentMode = .scaleAspectFit

        return imageView

    }()

    lazy var secondImageView: UIImageView = {

        let imageView = UIImageView(image: UIImage(named: viewModel.getImage(index: 1)))

        imageView.backgroundColor = .clear

        imageView.contentMode = .scaleAspectFit

        return imageView

    }()

Next, we will add page indicators from the AdvancedPageControl framework.

lazy var pageControlStackView: AdvancedPageControlView = {

        let pageControl = AdvancedPageControlView()

        pageControl.drawer = ExtendedDotDrawer(height: 8, width: 8, space: 8, raduis: 8, indicatorColor: .purple)

        pageControl.numberOfPages = viewModel.getNumberOfPages()

        return pageControl

    }()

After creating all the views, we add them to the view and set up the constraints.

We set up the UICollectionView constraints from top to the paging indicators to be able to swipe on images, not just on text.

func setupUI() {

        view.backgroundColor = .white

        view.addSubview(firstImageView)

        view.addSubview(secondImageView)

        view.addSubview(titleCollectionView)

        view.addSubview(createAccountButton)

        view.addSubview(loginButton)

        view.addSubview(pageControlStackView)

        firstImageView.autoSetDimensions(to: CGSize(width: view.frame.width, height: UIScreen.main.bounds.height / 2.5 + 40))

        firstImageView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)

        firstImageView.autoPinEdge(.bottom, to: .top, of: pageControlStackView, withOffset: 50)

        firstImageView.autoAlignAxis(toSuperviewAxis: .vertical)

        secondImageView.autoSetDimensions(to: CGSize(width: view.frame.width, height: UIScreen.main.bounds.height / 2.5 + 40))

        secondImageView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)

        secondImageView.autoPinEdge(.bottom, to: .top, of: pageControlStackView, withOffset: 50)

        secondImageView.autoAlignAxis(toSuperviewAxis: .vertical)

        view.sendSubviewToBack(secondImageView)

        titleCollectionView.autoAlignAxis(toSuperviewAxis: .vertical)

        titleCollectionView.autoPinEdge(.bottom, to: .top, of: createAccountButton, withOffset: -100)

        titleCollectionView.autoPinEdge(toSuperviewEdge: .left)

        titleCollectionView.autoPinEdge(toSuperviewEdge: .right)

        titleCollectionView.autoPinEdge(toSuperviewEdge: .top)

        view.bringSubviewToFront(titleCollectionView)

        pageControlStackView.autoPinEdge(.bottom, to: .top, of: createAccountButton, withOffset: -30)

        pageControlStackView.autoAlignAxis(toSuperviewAxis: .vertical)

        pageControlStackView.autoSetDimensions(to: CGSize(width: view.frame.width, height: 30))

        createAccountButton.autoPinEdge(.bottom, to: .top, of: loginButton, withOffset: -10)

        createAccountButton.autoSetDimensions(to: CGSize(width: UIScreen.main.bounds.width - 40, height: 60))

        createAccountButton.autoPinEdge(toSuperviewEdge: .left, withInset: 20)

        createAccountButton.autoPinEdge(toSuperviewEdge: .right, withInset: 20)

        loginButton.autoPinEdge(toSuperviewSafeArea: .bottom, withInset: 20)

        loginButton.autoPinEdge(toSuperviewEdge: .left, withInset: 20)

        loginButton.autoPinEdge(toSuperviewEdge: .right, withInset: 20)

        loginButton.autoSetDimensions(to: CGSize(width: UIScreen.main.bounds.width - 40, height: 60))

        view.bringSubviewToFront(titleCollectionView)

    }

4. Add Animations

UICollectionView conforms to scrollView methods all our logic will be in the scrollViewDidScroll method.

To enable animating page indicators, when scrollViewDidScroll method is called, we calculate the offset that happened with this formula and pass it to the pageControlSTackView:

let x = scrollView.contentOffset.x

let width = scrollView.frame.width

let offset = x / width

pageControlStackView.setPageOffset(offset)

In the same scrollViewDidScroll method, we first want to check in which direction the scrollview scrolled so that we can appropriately animate images fading. We add a helper enum to achieve this:

enum Direction {

        case left

        case right

    }

if scrollView.panGestureRecognizer.translation(in: scrollView.superview).x < 0 {

            fadeImages(direction: .left, x: x, width: width)

        } else {

            fadeImages(direction: .right, x: x, width: width)

        }

To fade images, we will change their alpha. Alpha is changed by calculating the offset and putting it in the space from 0.0 to 1.0. Depending on the image index, images will fade. Here is the formula we will use:

func fadeImages(direction: Direction, x: CGFloat, width: CGFloat) {

        let index: Int = viewModel.getPageIndex()

        let pagesCount = viewModel.getNumberOfPages()

        switch direction {

        case .left:

            let fadeInAlpha = (x - (width * CGFloat(index))) / width

            let fadeOutAlpha = 1 - fadeInAlpha

            let imageToFadeInIndex = index % pagesCount + 1

            if (index % pagesCount) % 2 == 0 && imageToFadeInIndex < pagesCount {

                secondImageView.image = UIImage(named: viewModel.getImage(index: imageToFadeInIndex))

                firstImageView.alpha = fadeOutAlpha

                secondImageView.alpha = fadeInAlpha

            } else if (index % pagesCount) % 2 == 1 && imageToFadeInIndex < pagesCount {

                firstImageView.image = UIImage(named: viewModel.getImage(index: index % pagesCount + 1))

                secondImageView.alpha = fadeOutAlpha

                firstImageView.alpha = fadeInAlpha

            }

        case .right:

            let fadeInAlpha = -1 * ((x - (width * CGFloat(index))) / width)

            let fadeOutAlpha = 1 - fadeInAlpha

            let imageToFadeInIndex = index % pagesCount - 1

            if (index % pagesCount) % 2 == 1 && imageToFadeInIndex >= 0 {

                firstImageView.image = UIImage(named: viewModel.getImage(index: imageToFadeInIndex))

                firstImageView.alpha = fadeInAlpha

                secondImageView.alpha = fadeOutAlpha

            } else if (index % pagesCount) % 2 == 0 && imageToFadeInIndex >= 0 {

                secondImageView.image = UIImage(named: viewModel.getImage(index: imageToFadeInIndex))

                secondImageView.alpha = fadeInAlpha

                firstImageView.alpha = fadeOutAlpha

            }

        }

    }

Depending on the index we are at, we will alternate between the two UIImageViews we have created.

Also, you have to keep in mind to not go out of bound in the array.

Conclusion

By taking this custom approach, we have more control over the design and functionality of our onboarding swiper, allowing us to create a more engaging and effective user experience. Plus, by using MVVM architecture, we can ensure our code is modular, easy to maintain, and testable.

In this post, we've explored an alternative approach to creating an onboarding swiper in iOS using UICollectionView and MVVM architecture. By doing so, we've seen how we can create a smoother, more user-friendly onboarding experience that gives us greater control over the design and functionality of our swiper. With the tips and techniques outlined here, you'll be able to create your own custom onboarding swiper that engages your users and guides them through your app's key features and functionality.

In case you're curious, feel free to contact us. Our ASEE team will be happy to hear you out.

Author: Karolina Škunca

Karolina is a Junior iOS Software Developer. She works on preventing security attacks on iOS phones and frequently tests ASEE’s applications. Her greatest passions are designing and developing new applications.

Want to learn more about cybersecurity trends and industry news?

SUBSCRIBE TO OUR NEWSLETTER

CyberSecurityhub

chevron-down linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram