Надежно отслеживать индекс страницы в UIPageViewController (Swift) - PullRequest
0 голосов
/ 23 ноября 2018

Проблема:

У меня есть мастер UIPageViewController (MainPageVC) с тремя встроенными просмотрами страниц (A, B, & C), которые доступны как жестами смахиванияи нажатием соответствующих мест в пользовательском индикаторе страницы * в MainPageVC (* не является истинным UIPageControl, но состоит из трех ToggleButton с - простое повторное воплощение UIButton, чтобы стать кнопкой переключения).Моя установка выглядит следующим образом:

Schematic of my view hierarchy

Предыдущее чтение: Надежный способ отслеживания индекса страницы в UIPageViewController - Swift , Надежный способ получить текущий индекс UIPageViewController и UIPageViewController: вернуть текущее видимое представление , указывающее, что лучший способ сделать это, был вызов didFinishAnimating и вручную отслеживать текущую страницуиндекс, но я обнаружил, что это не относится к определенным крайним случаям.

Я пытался создать безопасный способ отслеживания текущего индекса страницы (с помощью методов didFinishAnimating и willTransitionTo) но у меня возникли проблемы с крайним случаем, когда пользователь видит A, а затем проводит пальцем до C (без поднятия пальца), затем за C, а затем освобождает палец... в этом случае didFinishAnimating не вызывается, и приложение все еще считает, что оно находится в A (т. е. кнопка переключения A все еще нажата, а pageIndex не обновляется корректно viewControllerBefore and viewControllerAfter методов).

Мой код:

@IBOutlet weak var pagerView: UIView!
@IBOutlet weak var aButton: ToggleButton!
@IBOutlet weak var bButton: ToggleButton!
@IBOutlet weak var cButton: ToggleButton!

let viewControllerNames = ["aVC", "bVC", "cVC"]
lazy var buttonsArray = {
    [aButton, bButton, cButton]
}()
var previousPage = "aVC"

var pageVC: UIPageViewController?

func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
    print("TESTING - will transition to")

    let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder);
    let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass);

    if currentViewControllerClass == previousPage {
        return
    }

    let pastIndex = viewControllerNames.index(of: previousPage)
    if buttonsArray[pastIndex!]?.isOn == true {
        buttonsArray[pastIndex!]?.buttonPressed()
    }

    if let newPageButton = buttonsArray[viewControllerIndex!] {
        newPageButton.buttonPressed()
    }

    self.previousPage = currentViewControllerClass
}

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    print("TESTING - did finish animating")

    let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder)
    let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass)

    if currentViewControllerClass == previousPage {
        return
    }

    let pastIndex = viewControllerNames.index(of: previousPage)
    if buttonsArray[pastIndex!]?.isOn == true {
        buttonsArray[pastIndex!]?.buttonPressed()
    }

    if let newPageButton = buttonsArray[viewControllerIndex!] {
        newPageButton.buttonPressed()
    }

    self.previousPage = currentViewControllerClass
}

func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
    let onboardingViewControllerClass = String(describing: viewController.classForCoder)
    let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
    let newViewControllerIndex = viewControllerIndex! - 1
    if(newViewControllerIndex < 0) {
        return nil
    } else {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
        if let vc = vc as? BaseTabVC {
            vc.mainPageVC = self
            vc.intendedCollectionViewHeight = pagerViewHeight
        }
        return vc
    }
}

func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
    let onboardingViewControllerClass = String(describing: viewController.classForCoder)
    let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
    let newViewControllerIndex = viewControllerIndex! + 1
    if(newViewControllerIndex > viewControllerNames.count - 1) {
        return nil
    } else {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
        if let vc = vc as? BaseTabVC {
            vc.mainPageVC = self
            vc.intendedCollectionViewHeight = pagerViewHeight
        }
        return vc
    }
}

Я не знаю, как справиться с этим крайним случаем, проблема в том, что он может привести к летальному исходусбой приложения, если пользователь затем пытается нажать что-то в C, что в противном случае должно быть гарантировано, и возникает неожиданная ошибка nil или indexOutOfBounds.

Ответы [ 2 ]

0 голосов
/ 21 февраля 2019

Собственное решение

Я нашел решение для этого: не используйте UIPageView(Controller), вместо этого используйте CollectionView(Controller). НАМНОГО легче отслеживать положение представления коллекции, чем пытаться вручную отслеживать текущую страницу в UIPageViewController.

.Решение заключается в следующем:

Метод

  • Refactor MainPagerVC в качестве CollectionView (Controller) (или в качестве обычного VC, который соответствует протоколам UICollectionViewDelegate UICollectionViewDataSource).
  • Установите каждую страницу (aVC, bVC и cVC) как UICollectionViewCell подкласс (MainCell).
  • Установите каждую из этих страниц, чтобы заполнить MainPagerVC.collectionView в пределах экрана - CGSize(width: view.frame.width, height: collectionView.bounds.height).
  • Преобразуйте переключатели сверху (A, B и C) в три UICollectionViewCell подкласса (MenuCellMenuController (сам по себе UICollectionViewController.
  • Поскольку представления коллекции наследуются от UIScrollView, вы можете реализовать методы scrollViewDidScroll, scrollViewDidEndScrollingAnimation и scrollViewWillEndDragging вместе с делегированием (с didSelectItemAt indexPath) для объединения MainPagerVC и MenuController представлений коллекции.

Код

class MainPagerVC: UIViewController, UICollectionViewDelegateFlowLayout {

    fileprivate let menuController = MenuVC(collectionViewLayout: UICollectionViewFlowLayout())
    fileprivate let cellId = "cellId"

    fileprivate let pages = ["aVC", "bVC", "cVC"]

    let collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.minimumLineSpacing = 0
        layout.scrollDirection = .horizontal
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.backgroundColor = .white
        cv.showsVerticalScrollIndicator = false
        cv.showsHorizontalScrollIndicator = false
        return cv
    }()


    override func viewDidLoad() {
        super.viewDidLoad()

        menuController.delegate = self

        setupLayout()
    }

    fileprivate func setupLayout() {
        guard let menuView = menuController.view else { return }

        view.addSubview(menuView)
        view.addSubview(collectionView)

        collectionView.dataSource = self
        collectionView.delegate = self


        //Setup constraints (placing the menuView above the collectionView

        collectionView.register(MainCell.self, forCellWithReuseIdentifier: cellId)

        //Make the collection view behave like a pager view (no overscroll, paging enabled)
        collectionView.isPagingEnabled = true
        collectionView.bounces = false
        collectionView.allowsSelection = true

        menuController.collectionView.selectItem(at: [0, 0], animated: true, scrollPosition: .centeredHorizontally)

    }

}

extension MainPagerVC: MenuVCDelegate {

    // Delegate method implementation (scroll to the right page when the corresponding Menu "Button"(Item) is pressed
    func didTapMenuItem(indexPath: IndexPath) {
        collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
    }

}

extension MainPagerVC: UICollectionViewDelegate, UICollectionViewDataSource {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let x = scrollView.contentOffset.x
        let offset = x / pages.count
        menuController.menuBar.transform = CGAffineTransform(translationX: offset, y: 0)
    }

    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        let item = Int(scrollView.contentOffset.x / view.frame.width)
        let indexPath = IndexPath(item: item, section: 0)
        collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .bottom)
    }

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        let x = targetContentOffset.pointee.x
        let item = Int(x / view.frame.width)
        let indexPath = IndexPath(item: item, section: 0)
        menuController.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
    }


    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return pages.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MainCell

        return cell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return .init(width: view.frame.width, height: collectionView.bounds.height)
    }

}

class MainCell: UICollectionViewCell {

    override init(frame: CGRect) {
        super.init(frame: frame)

        // Custom UIColor extension to return a random colour (to check that everything is working)
        backgroundColor = UIColor().random()

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }
}

protocol MenuVCDelegate {
    func didTapMenuItem(indexPath: IndexPath)
}

class MenuVC: UICollectionViewController, UICollectionViewDelegateFlowLayout {

    fileprivate let cellId = "cellId"
    fileprivate let menuItems = ["A", "B", "C"]

    var delegate: MenuVCDelegate?

    //Sliding bar indicator (slightly different from original question - like Reddit)
    let menuBar: UIView = {
        let v = UIView()
        v.backgroundColor = .red
        return v
    }()

    //1px view to visually separate MenuBar region from "pager"-views
    let menuSeparator: UIView = {
        let v = UIView()
        v.backgroundColor = .gray
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.backgroundColor = .white
        collectionView.allowsSelection = true
        collectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellId)

        if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
            layout.scrollDirection = .horizontal
            layout.minimumLineSpacing = 0
            layout.minimumInteritemSpacing = 0
        }

        //Add views and setup constraints for collection view, separator view and "selection indicator" view - the menuBar
    }

    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        delegate?.didTapMenuItem(indexPath: indexPath)
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return menuItems.count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MenuCell
        cell.label.text = menuItems[indexPath.item]

        return cell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = view.frame.width
        return .init(width: width/CGFloat(menuItems.count), height: view.frame.height)
    }

}

class MenuCell: UICollectionViewCell {

    let label: UILabel = {
        let l = UILabel()
        l.text = "Menu Item"
        l.textAlignment = .center
        l.textColor = .gray
        return l
    }()

    override var isSelected: Bool {
        didSet {
            label.textColor = isSelected ? .black : .gray
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        //Add label to view and setup constraints to fill Cell
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }
}

Ссылки

  1. A «Давайте создадим это приложение» YouTube Video: "Мы сделали это на / r / iosprogramming!Функция просмотра страниц в режиме реального кодирования "
0 голосов
/ 23 ноября 2018

Очень хорошо написанный вопрос.Особенно для новичка.(Проголосовано.) Вы четко заявляете о проблеме, с которой вы столкнулись, включая иллюстрации и ваш текущий код.

Решение, которое я предложил в другом потоке, состояло в том, чтобы создать подкласс UIPageControl и заставить его реализовать didSet в currentPage имущество.Затем вы можете сделать так, чтобы элемент управления страницы уведомил контроллер представления о текущем индексе страницы.(Предоставив вашему пользовательскому подклассу свойство делегата, отправив сообщение в центр уведомлений или любой другой метод, который лучше всего соответствует вашим потребностям.)

(Я провел простую проверку этого подхода, и он работал. Я не проверялоднако, исчерпывающе.)

Тот факт, что UIPageViewController надежно обновляет элемент управления страницы, но нет надежного и очевидного способа выяснить индекс текущей страницы, кажется упущением в дизайне этого класса.

...