实现 iOS PageMenu 滑动或者点击切换 ViewCon

2017-07-06  本文已影响0人  阿朱先生

本文会实现一个轻量级的 PageMenu, 用户可以点击按钮或者滑动视图切换各个 ViewController,点击的按钮会放在 titleView 内。导入到项目的时候只需要添加一个类就OK了。

ScrollView

我们知道 ScrollView 有一个属性 contentSize,它表示的是 ScrollView 的内容可滚动区域,在实现的时候,如果滚动视图有 n 个, 则可设置:

scrollView.contentSize = CGSize(width: scrollView.bounds.width * n, height: scrollView.bounds.height)

由于切换各个视图的时候需要一个分页的动画效果,所以需要设置 ScrollView

 scrollView.isPagingEnabled = true

此时,假如都设置好了的话,就可以在各个视图来回滑动切换了,就是这么简单!

点击切换按钮

点击切换按钮的时候,ScrollView 需要滚动到相对应的视图,此时需要修改的是 ScrollView 的 contentOffset 属性:

scrollView.setContentOffset(viewControllersFrame[index].origin, animated: true)

提示动画条

当用户滑动到第 n 个界面的时候,此时提示条应该移动到第 n 个界面对应的点击按钮

此时需要在 滑动视图点击按钮 时做相应的处理

在 ScrollView 的代理方法内,根据对应按钮的 frame 比例,执行滚动动画

func scrollViewDidScroll(_ scrollView: UIScrollView) {
   UIView.animate(withDuration: moveDuration, animations: {
       let x = scrollView.contentOffset.x * self.scale + self.itemsOriginX[0]
       self.indicatorView.frame.origin.x = x
     })
}

在点击按钮添加的方法内,添加滚动动画:

UIView.animate(withDuration: moveDuration, animations: {
    self.indicatorView.frame.origin.x = self.itemsOriginX[index]
 })

我的实现类:

最终实现的滚动容器类代码如下:

import UIKit

class MenuContainerViewController: UIViewController {
    
    var menus: [UIButton] = [UIButton]()
    var viewControllers: [UIViewController] = [UIViewController]()

    var itemColor = UIColor.black
    var indicatorColor = UIColor.blue {
        didSet {
            indicatorView?.backgroundColor = indicatorColor
        }
    }
    
    // designated init view controller
    init(menus: [UIButton], viewControllers: [UIViewController]) {
        super.init(nibName: nil, bundle: nil)
        
        if menus.count != viewControllers.count {
            fatalError("menus.count != viewControllers.count")
        } 
        
        self.menus = menus
        self.viewControllers = viewControllers
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Private properties
    private var scrollView: UIScrollView!

    private var itemsTitle: [String] = [String]()
    private var viewControllersFrame: [CGRect] = [CGRect]()
    fileprivate var itemsOriginX: [CGFloat] = [CGFloat]()
    
    fileprivate var indicatorView: UIView!
    
    fileprivate var indicatorViewLastOriginX: CGFloat = 0.0 {
        didSet {
            indicatorCopyView?.frame.origin.x = indicatorViewLastOriginX
        }
    }
    
    fileprivate let indicatorViewWidth: CGFloat = 30
    
    fileprivate var scale: CGFloat!
    
    fileprivate let moveDuration: TimeInterval = 0.2
    
    // Due to 'sectionIndicatorView' will reset frame when viewDidDisappear did called,
    // so, add 'indicatorCopyView' as the copy view
    fileprivate var indicatorCopyView: UIView!
    fileprivate var shouldAdjustCopyIndicatorView = false
    
    // MARK: - View controller lifecycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        automaticallyAdjustsScrollViewInsets = false
        self.navigationController?.navigationBar.isTranslucent = false
        
        scrollView = UIScrollView()
        let customTitleView = UIView()
        let titleStackView = UIStackView()
        indicatorView = UIView()
        indicatorCopyView = UIView()
        
        scrollView.delegate = self
        scrollView.isPagingEnabled = true
        scrollView.showsVerticalScrollIndicator = false
        scrollView.showsHorizontalScrollIndicator = false
        
        for button in menus {
            button.setTitleColor(itemColor, for: .normal)
            itemsTitle.append(button.currentTitle!)
        }
        
        customTitleView.backgroundColor = UIColor.white
        for item in menus {
            titleStackView.addArrangedSubview(item)
        }
        titleStackView.alignment = .center
        titleStackView.axis = .horizontal
        titleStackView.distribution = .fillEqually
        
        for i in 0 ..< viewControllers.count {
            let subvc = viewControllers[i]
            self.addChildViewController(subvc)
            scrollView.addSubview(subvc.view)
            subvc.didMove(toParentViewController: self)
        }
        
        let titleViewWidth: CGFloat = 200
        let titleViewHeight: CGFloat = 44
        let stackViewHeight: CGFloat = 40
        
        let titleViewFrame = CGRect(x: 0, y: 0, width: titleViewWidth, height: titleViewHeight)
        let stackViewFrame = CGRect(x: 0, y: 0, width: titleViewWidth, height: stackViewHeight)
        let indicatorViewFrame = CGRect(x: 0, y: titleViewHeight - 2, width: indicatorViewWidth, height: 1)
        
        customTitleView.frame = titleViewFrame
        customTitleView.frame.origin.x = self.view.frame.midX - titleViewWidth/2
        
        titleStackView.frame = stackViewFrame
        
        indicatorView.frame = indicatorViewFrame
        indicatorView.backgroundColor = indicatorColor
        
        // for menuItems originX
        var itemOriginX: CGFloat = 0
        let itemWidth: CGFloat = titleViewWidth/3
        for item in menus {
            item.addTarget(self, action: #selector(contentOffSetXForButton(sender:)), for: .touchUpInside)
            let itemFrame = CGRect(x: itemOriginX, y: 0, width: itemWidth, height: stackViewHeight)
            item.frame = itemFrame
            let indicatorOriginX = itemFrame.midX - indicatorViewWidth/2
            itemsOriginX.append(indicatorOriginX)
            itemOriginX += itemWidth
        }
        
        // for sectionIndicatorView
        indicatorView.frame.origin.x = itemsOriginX[0]
        indicatorViewLastOriginX = indicatorView.frame.origin.x
        
        // indicator copy view
        indicatorCopyView.frame = indicatorView.frame
        indicatorCopyView.backgroundColor = indicatorView.backgroundColor
        indicatorCopyView.isHidden = true
        
        // indicator scroll scale
        let indicatorScale = itemsOriginX[1] - itemsOriginX[0]
        scale = indicatorScale / UIScreen.main.bounds.size.width
        
        customTitleView.addSubview(titleStackView)
        customTitleView.addSubview(indicatorView)
        customTitleView.addSubview(indicatorCopyView)
        
        self.parent?.navigationItem.titleView = customTitleView
        
        view.addSubview(scrollView)
    }
    
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        var scrollViewFrame = view.frame
        scrollViewFrame.size.height -= 49
        
        scrollView.frame = view.frame
        
        let width = scrollViewFrame.width
        let height = scrollViewFrame.height
        
        scrollView.contentSize = CGSize(width: width * 3, height: height)
        
        // has [viewControllersFrame]
        var vcOriginX: CGFloat = 0
        for _ in 0 ..< viewControllers.count {
            viewControllersFrame.append(CGRect(x: vcOriginX, y: 0, width: width, height: height))
            vcOriginX += width
        }
        
        for i in 0 ..< viewControllers.count {
            viewControllers[i].view.frame = viewControllersFrame[i]
        }
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        if shouldAdjustCopyIndicatorView {
            UIView.animate(withDuration: 0.0, animations: {
                self.indicatorView?.frame.origin.x = self.indicatorViewLastOriginX
            }) { (_) in
                self.indicatorCopyView?.isHidden = true
                self.indicatorView?.isHidden = false
                
                self.shouldAdjustCopyIndicatorView = false
            }
        }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        indicatorCopyView.isHidden = false
        indicatorView.isHidden = true
        shouldAdjustCopyIndicatorView = true        
    }
    
    // MARK: - Helper
    @objc private func contentOffSetXForButton(sender: UIButton){
        let currentTitle = sender.currentTitle!
        let index = itemsTitle.index(of: currentTitle)!
        
        scrollView.setContentOffset(viewControllersFrame[index].origin, animated: true)
        UIView.animate(withDuration: moveDuration, animations: {
            self.indicatorView.frame.origin.x = self.itemsOriginX[index]
            self.indicatorViewLastOriginX = self.indicatorView.frame.origin.x
        })
    }
}

// MRAK: - Scroll view delegate

extension MenuContainerViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        UIView.animate(withDuration: moveDuration, animations: {
            let x = scrollView.contentOffset.x * self.scale + self.itemsOriginX[0]
            self.indicatorView.frame.origin.x = x
            self.indicatorViewLastOriginX = x
        })
    }
}

调用的测试类:

import UIKit

class ViewController: UIViewController {
    
    // *********************************************
    //  Add MenuContainerViewController like this
    private var addedPageViewController = false
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        if !addedPageViewController {
            addedPageViewController = true
            
            let scrollContainerVC = MenuContainerViewController(menus: pageItems(),
                                                                viewControllers: pageViewContorllers())
            self.addChildViewController(scrollContainerVC)
            scrollContainerVC.view.frame = view.bounds
            view.addSubview(scrollContainerVC.view)
            scrollContainerVC.didMove(toParentViewController: self)
        }
    }
    // *********************************************
    
    // MRAK: Test data
    private func pageItems() -> [UIButton] {        
        let red = UIButton()
        let gray = UIButton()
        let purple = UIButton()
        
        red.setTitle("red", for: .normal)
        gray.setTitle("gray", for: .normal)
        purple.setTitle("purple", for: .normal)
        
        var items = [UIButton]()

        items.append(red)
        items.append(gray)
        items.append(purple)

        return items
    }
    
    private func pageViewContorllers() -> [UIViewController] {        
        let storyboard = UIStoryboard(name: "Main", bundle: nil)

        let firstViewController = storyboard.instantiateViewController(withIdentifier: "FirstViewController") as!
        FirstViewController
        let secondViewController = storyboard.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
        let thirdViewController = storyboard.instantiateViewController(withIdentifier: "ThirdViewController") as!
        ThirdViewController
        
        var vcs = [UIViewController]()

        vcs.append(firstViewController)
        vcs.append(secondViewController)
        vcs.append(thirdViewController)

        return vcs
    }

}

Demo地址

上一篇下一篇

猜你喜欢

热点阅读