Search code examples
swiftviewcontrolleruipageviewcontrollerui-design

How to show same top design in all viewcontroller with UIPageViewController in swift


I have created top design in MainSegPageViewController in storyboard like this and i have 2 other viewcontrollers as well. now i need same upper design in my all three viewcontrollers. so how to show that 2 viewcontrollers in this wight area only? is that possible? if yes please explain me

enter image description here

code: with this code initially i am getting ViewController1 why? how to show MainSegPageViewController initially? and how to show ViewController1 in white area when i click STEP 2. please guide me.

import UIKit

class MainSegPageViewController: UIPageViewController {

private(set) lazy var subViewcontrollers: [UIViewController] = {
    return [
        UIStoryboard(name: "Main", bundle: nil) .
        instantiateViewController(withIdentifier: "ViewController1"),
        UIStoryboard(name: "Main", bundle: nil) .
        instantiateViewController(withIdentifier: "ViewController2")
    ]
}()

override func viewDidLoad() {
    super.viewDidLoad()
    dataSource = self
    modalPresentationStyle = .formSheet

   if let firstViewController = subViewcontrollers.first {
        setViewControllers([firstViewController],
                           direction: .forward,
                           animated: true,
                           completion: nil)
    }

    setUpNavigationBar(with: "Pagination vc", isBackNeed: true)
    self.navigationController?.navigationBar.isHidden = false
    
}
}

// MARK: UIPageViewControllerDataSource
extension MainSegPageViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
    guard let viewControllerIndex = subViewcontrollers.firstIndex(of:viewController) 
 else {
        return nil
    }
    let previousIndex = viewControllerIndex - 1
    guard previousIndex >= 0 else {
        return nil
    }
    guard subViewcontrollers.count > previousIndex else {
        return nil
    }
    if previousIndex == 2 {
        return nil
    }
    return subViewcontrollers[previousIndex]
}

func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
    guard let viewControllerIndex = subViewcontrollers.firstIndex(of:viewController) 
 else {
        return nil
    }
    let nextIndex = viewControllerIndex + 1
    let orderedViewControllersCount = subViewcontrollers.count
    
    guard orderedViewControllersCount != nextIndex else {
        return nil
    }
    guard orderedViewControllersCount > nextIndex else {
        return nil
    }
    if nextIndex == 3 {
        return nil
    }
    return subViewcontrollers[nextIndex]
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
    return subViewcontrollers.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
    guard let firstViewController = viewControllers?.first, let firstViewControllerIndex = subViewcontrollers.firstIndex(of: firstViewController) else {
        return 0
    }
    return firstViewControllerIndex
}
}

And i am reaching to MainSegPageViewController through side menu like this

let vc = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "MainSegPageViewController") as? MainSegPageViewController
        self.navigationController?.pushViewController(vc!, animated: true)

o/p: with the code initially i am getting ViewController1 screen but i need MainSegPageViewController to show first.

enter image description here


Solution

  • To achieve your layout, one approach is to create a "Profile" view controller containing your "Welcome to Profile" label and UISegmentedControl, and then embed the UIPageViewController in a Container View.

    So, since your post says you are pushing to this layout in a navigation controller, we start with this in Storyboard:

    enter image description here

    The button will be connected to this code:

    @IBAction func profileButtonTapped(_ sender: Any) {
        if let vc = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "ProfileViewController") as? ProfileViewController {
            navigationController?.pushViewController(vc, animated: true)
        }
    }
    

    Then, layout ProfileViewController in Storyboard like this:

    enter image description here

    The large view is a UIContainerView:

    enter image description here

    and we've embedded a typical UIPageViewController in it.

    Assuming you have working code for your page view controller, this can be run and you'll be able to swipe back and forth through the pages.

    Next, we need to add some code so tapping on a Segment will change the page.

    We can setup the ProfileViewController like this:

    class ProfileViewController: UIViewController {
        
        @IBOutlet var stepsSegCtrl: UISegmentedControl!
        
        // we will want a reference to the embedded MainSegPageViewController
        //  so we can tell it to change page when the segmented control is tapped
        var mainSegPageVC: MainSegPageViewController?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            self.title = "Pagination VC"
        }
    
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            // this is called because we've embedded the controller in a container view
            if let pgVC = segue.destination as? MainSegPageViewController {
                // save reference so we can access it when the segmented control is tapped
                mainSegPageVC = pgVC
            }
        }
        
        @IBAction func segCtrlChanged(_ sender: UISegmentedControl) {
            // safely unwrap
            if let pgVC = mainSegPageVC {
                pgVC.gotoPage(pg: sender.selectedSegmentIndex)
            }
        }
        
    }
    

    When embedding a controller in a Container View, we automatically get a segue where we can grab a reference to the embedded page view controller.

    If we add this func to our MainSegPageViewController class:

    public func gotoPage(pg: Int) {
        let curPg = presentationIndex(for: self)
        if pg > curPg {
            setViewControllers([subViewcontrollers[pg]],
                               direction: .forward,
                               animated: true,
                               completion: nil)
        } else {
            setViewControllers([subViewcontrollers[pg]],
                               direction: .reverse,
                               animated: true,
                               completion: nil)
        }
    }
    

    we can now change the page from the segmented control.

    Finally, we need to add a closure so the page view controller can inform the "parent" controller when the user drags to a new page.

    So, in MainSegPageViewController class, we add this property:

    var pageChanged: ((Int) -> ())?
    

    and implement:

    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        // user dragged to a new page, so inform the parent controller
        pageChanged?(presentationIndex(for: self))
    }
    

    We set that closure back in prepare(for segue:...) back in ProfileViewController:

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // this is called because we've embedded the controller in a container view
        if let pgVC = segue.destination as? MainSegPageViewController {
            // set the closure
            pgVC.pageChanged = { [weak self] pg in
                guard let self = self else { return }
                // user dragged to a new page, so update the segmented control
                self.stepsSegCtrl.selectedSegmentIndex = pg
            }
            // save reference so we can access it when the segmented control is tapped
            mainSegPageVC = pgVC
        }
    }
    

    I've put up a complete project here - https://github.com/DonMag/ContainerPageViewController so you can inspect the code and Storyboard -- and see it in action -- while building your own.