干貨:蘋果UIPageViewController偏移量contentOffset的bug解決方案

為了博眼球,所以給了個有問題的標題。但如果該文是你通過度娘或是古哥檢索出來的那我很明確的告訴你這就是你想要的。想直接看效果而不想聽我念經的,可直接拉至篇尾那里有全部代碼。

之前見騰訊視頻、優酷視頻的多欄頁面做的挺好看的,尤其是頂部那性感的小黃條,于是乎自己寫了個(細節處理很完美的設計,可直接拿去用的哦)。源碼

IMG_1390.PNG

為了充分利用UIPageViewController的一些特性,底部并沒有用UIScrollView,但又要監聽pageViewControlle的偏移量。由于pageviewcontroller的實現和我們常做的循環輪播原理是一樣一樣的。所以滑動時監聽到的偏移量是極不靠譜的,最后配合UIPageviewcontrollerDelegate花了很大的時間成本才算作出一個一模一樣的效果來。(這個就不細說了,感興趣的話可以下載上面的源碼

  for subView: UIView in view.subviews {
            if subView.isKind(of: UIScrollView.classForCoder()) {
                let tempScrollView = subView as? UIScrollView
                tempScrollView?.delegate = self
            }
        }

這里有個很不好處理的是偏移量是“0->375->750->375->0”這種姿態的, 由于不連續,處理起來就諸多麻煩。
既然如此,我們有沒有辦法改變這個呢?在pageviewcontroller中控制器的邏輯切換是連續的。但視圖切換在本質上卻是不連續的。我們可不可以通過UIPageViewController的邏輯做一個<b>虛擬偏移量</b>呢?(就像web開發中React.js寫出來的不是真實DOM,而是虛擬DOM)。對,就是生成虛擬偏移量。這個能實現的話我想在很多場景我們都是用的上的。下面來說下我的實現思路:

  • 1、我們要知道UIPageViewController有個內嵌的UIScrollView,獲取到他,監聽到scrollview發生了滾動。具體看上面那段代碼。

  • 2、在UIScrollView發生了滾動時,計算出虛擬偏移量。注意我們要的不是contentoffset,此場景下,那個太操蛋了,不可信(陪合UIPageViewControllerDelegate還是能用的,邏輯上極不好操作)。那我們要什么呢?我們通過當前可視的那個viewController的view(取左邊距)映射到UIPageViewCOntroller的view上,就能獲取偏移量了。而這才是我們真實看到的,想要的偏移量。(<b>核心思想就是利用UIView的convert函數計算真實偏移量</b>)

func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let pageWidth = view.frame.width
        for vc in readyViewControllers!{
            let p = vc.view.convert(CGPoint(), to: view)
//找出屏幕可視范圍的控制器視圖
            if (p.x) > CGFloat(0.0) && (p.x) < pageWidth{
                let estimatePage = (readyViewControllers?.index(of: vc))!
                estimateOffSetX = CGFloat(estimatePage) * pageWidth - (p.x)
            }
        }
        //若不是循環,最后一個找不到左邊距
        if estimateOffSetX >= CGFloat((readyViewControllers?.count)!-1)*pageWidth{
            let p = readyViewControllers?[(readyViewControllers?.count)!-1].view.convert(CGPoint(), to: view)
            estimateOffSetX = CGFloat((readyViewControllers?.count)!-1) * pageWidth - (p?.x)!
        }
//        print("矯正前:\(estimateOffSetX)")
        scrollDidScroll!(estimateOffSetX)
    }

當然我們還有兩個問題需要注意:

  • 最后一個控制器的再往左滑的時候,左邊距是不在可視范圍內的。需要去掉如下判斷條件,單獨處理
if (p.x) > CGFloat(0.0) && (p.x) < pageWidth{
  • 細心的你一定發現了上面這句判斷條件是不包含0和pageWidth兩個臨界狀態的。那是因為在pageviewcontroller滑動到臨界狀態,會有view的tihuan和contentoffset的突變,所以必須舍棄。那你可能會說,那還不夠精準啊。不急,且看第三步??
  • pageviewcontroller當我們松手的時候會到達零界點,我們用scrollViewDidEndDecelerating來監聽,并做微小的修正
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        let pageWidth = view.frame.width
        currentPage = Int(round(estimateOffSetX/pageWidth))
        if currentPage < 0 {
            currentPage = (readyViewControllers?.count)! - 1
        }
        estimateOffSetX = CGFloat(currentPage)*pageWidth
//        print("矯正后:\(estimateOffSetX)")
        scrollDidScroll!(estimateOffSetX)
    }

??完事了,如此一來我們能獲取到一個虛擬偏移量(從另一個角度來看,這才是真實的),變化過程是這樣的:
0->10->30->100->200->300->375->380->400->500->600->750->800->990->1040->1170->1200->1400->1600->1800。
天啊嚕,這才是我們想要的啊。也難怪apple的pageviewcontroller并沒有暴露scrollview屬性,因為不可用啊。這樣一改就可用了。

Talk is Cheap,Show you the Code??:
import UIKit
class CYPageViewController: UIPageViewController, UIPageViewControllerDelegate, UIScrollViewDelegate {
   private var tempDelegate: UIPageViewControllerDelegate?


   private var currentPage: Int = 0

   private var scrollDidScroll: ((CGFloat)->Void)?

   private var readyViewControllers: [UIViewController]?

   private var estimateOffSetX: CGFloat = 0

   override func viewDidLoad() {
       super.viewDidLoad()
       for subView: UIView in view.subviews {
           if subView.isKind(of: UIScrollView.classForCoder()) {
               let tempScrollView = subView as? UIScrollView
               tempScrollView?.delegate = self
           }
       }
   }

   func addListenerWithReadyViewControllers(_ readyViewControllers: [UIViewController], didScroll scrollDidScroll: @escaping (CGFloat)->Void){
       self.readyViewControllers = readyViewControllers
       self.scrollDidScroll = scrollDidScroll
   }

   func scrollViewDidScroll(_ scrollView: UIScrollView) {
       let pageWidth = view.frame.width
       for vc in readyViewControllers!{
           let p = vc.view.convert(CGPoint(), to: view)
           if (p.x) > CGFloat(0.0) && (p.x) < pageWidth{
               let estimatePage = (readyViewControllers?.index(of: vc))!
               estimateOffSetX = CGFloat(estimatePage) * pageWidth - (p.x)
           }
       }
       //若不是循環,最后一個找不到左邊距
       if estimateOffSetX >= CGFloat((readyViewControllers?.count)!-1)*pageWidth{
           let p = readyViewControllers?[(readyViewControllers?.count)!-1].view.convert(CGPoint(), to: view)
           estimateOffSetX = CGFloat((readyViewControllers?.count)!-1) * pageWidth - (p?.x)!
       }
//        print("矯正前:\(estimateOffSetX)")
       scrollDidScroll!(estimateOffSetX)
   }

   func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
       let pageWidth = view.frame.width
       currentPage = Int(round(estimateOffSetX/pageWidth))
       if currentPage < 0 {
           currentPage = (readyViewControllers?.count)! - 1
       }
       estimateOffSetX = CGFloat(currentPage)*pageWidth
//        print("矯正后:\(estimateOffSetX)")
       scrollDidScroll!(estimateOffSetX)
   }
}

import UIKit

class ViewController: UIViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {

   private var pageCtrl: CYPageViewController = CYPageViewController.init(transitionStyle: UIPageViewControllerTransitionStyle.scroll, navigationOrientation: UIPageViewControllerNavigationOrientation.horizontal, options: nil)

   lazy var viewControllers: [UIViewController] = {
       var arr = Array<UIViewController>()
       let color = [UIColor.red, UIColor.green, UIColor.brown, UIColor.yellow]
       for i in 0...3{
           let vc = UIViewController()
           vc.view.backgroundColor = color[i]
           arr.append(vc)
       }
       return arr
   }()

   override func viewDidLoad() {
       super.viewDidLoad()
       
       pageCtrl.view.frame = self.view.bounds
       view.addSubview(pageCtrl.view)
       
       pageCtrl.setViewControllers([viewControllers[0]], direction: UIPageViewControllerNavigationDirection.forward, animated: false) { (cp) in
           
       }
       pageCtrl.delegate = self
       pageCtrl.dataSource = self

       pageCtrl.addListenerWithReadyViewControllers(viewControllers) { (x) in
           print("修復后的偏移量___\(x)")
       }
   }

   func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
       let index = viewControllers.index(of: viewController)
       if index == 0 {
//            return viewControllers[viewControllers.count-1]
           return nil;
       }
       return viewControllers[index!-1]
   }
   func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
       let index = viewControllers.index(of: viewController)
       if index == viewControllers.count-1 {
//            return viewControllers[0]
           return nil;
       }
       return viewControllers[index!+1]
   }
}

以上是全部代碼??,代碼量少copy吧????

<b>寫在最后 :</b>
see,其實你只調用了如下一句代碼就獲取到了你想要的,是不是很nice。即便是使用到當前項目中,污染也是極小極小的。如果你說你用UIScrollView替換了,那我告訴你UITableView、UICollectionView你也可以自己寫復用機制用UIScrollView替換,但那樣會很low。存在即有價值。
至于OC版的,這幾句代碼我想你幾分鐘就能翻譯了的

 pageCtrl.addListenerWithReadyViewControllers(viewControllers) { (x) in
            print("修復后的偏移量___\(x)")
        }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容