问题:
我有一个大师UIPageViewController
(MainPageVC
)有三个嵌入页面视图(A
,B
,和C
),可以通过滑动手势和按下MainPageVC
中的自定义页面指示器*中的适当位置(*不是真正的UIPageControl
,但是由三个ToggleButton
s组成 - 一个简单的UIButton
重新实现,成为一个切换按钮)。我的设置如下:
上一篇文章:Reliable way to track Page Index in a UIPageViewController - Swift,A reliable way to get UIPageViewController current index和UIPageViewController: return the current visible view表示,最好的方法是使用didFinishAnimating
调用,并手动跟踪当前页面索引,但我发现这并不涉及某些边缘情况。
我一直在尝试生成一种安全的方式来跟踪当前页面索引(使用didFinishAnimating
和willTransitionTo
方法),但是遇到用户在视图A
中的边缘情况有问题,然后一直滑到C
(没有提起他们的手指),然后超越C
,然后释放他们的手指...在这种情况下didFinishAnimating
没有被调用,应用程序仍然认为它在A
(即A
切换按钮仍然按下并且pageIndex未更新正确的viewControllerBefore
和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
错误被抛出。
写得很好的问题。特别是对于新手。 (投票。)您清楚地说明了您遇到的问题,包括插图和您当前的代码。
我在另一个线程中提出的解决方案是继承UIPageControl
并让它在其currentPage
属性上实现didSet。然后,您可以让页面控件通知视图控制器当前页面索引。 (通过发送通知中心消息或任何最适合您需求的方法,为您的自定义子类提供委托属性。)
(我对这种方法做了一个简单的测试,但它确实有效。但我并没有详尽地测试过。)
事实上,UIPageViewController可靠地更新页面控件但是没有可靠,明显的方法来计算当前页面索引,这似乎是对这个类的设计的疏忽。
我找到了解决方案:不要使用UIPageView(Controller)
,而是使用CollectionView(Controller)
。跟踪集合视图的位置比尝试手动跟踪UIPageViewController
中的当前页面要容易得多。
解决方案如下:
MainPagerVC
重构为CollectionView(控制器)(或作为符合UICollectionViewDelegate
UICollectionViewDataSource
协议的常规VC)。aVC
,bVC
和cVC
)设置为UICollectionViewCell
子类(MainCell
)。MainPagerVC.collectionView
- CGSize(width: view.frame.width, height: collectionView.bounds.height)
。A
,B
和C
)重构为UICollectionViewCell
(本身为MenuCell
)中的三个MenuController
子类(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()
}
}