簡體   English   中英

Swift - 帶有嵌入式 UIStackView 的 UIScrollView 無限滾動

[英]Swift - Infinite Scrolling for UIScrollView with an embedded UIStackView

有人幫助使用以下代碼來實現我在這里需要做的事情:-

UIPageViewController - 檢測滾動到下一個視圖控制器(幾乎可以正常工作)以更改按鈕顏色的一半?

在保持現有行為完整的同時,我需要一些幫助,請修改可能只是 scrollViewDidScroll 方法以允許無限平滑滾動,因此當您向右滑動時到達第四個/最后一個項目時,它將平滑過渡到第一頁,同樣如果您在到達第一個時繼續向左滑動並再次向左滑動,則會顯示最后一個項目,如果您知道我的意思,我嘗試這樣做,但有點播放。 謝謝

class PagedScrollViewController: UIViewController, UIScrollViewDelegate {
    
    let scrollView: UIScrollView = {
        let v = UIScrollView()
        v.isPagingEnabled = true
        v.bounces = false
        return v
    }()
    
    let pageControl: UIPageControl = {
        let v = UIPageControl()
        return v
    }()
    
    let stack: UIStackView = {
        let v = UIStackView()
        v.axis = .horizontal
        v.distribution = .fillEqually
        return v
    }()
    
    var pages: [UIViewController] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        pageControl.translatesAutoresizingMaskIntoConstraints = false
        stack.translatesAutoresizingMaskIntoConstraints = false
        
        scrollView.addSubview(stack)
        view.addSubview(scrollView)
        view.addSubview(pageControl)
        
        let g = view.safeAreaLayoutGuide
        let svCLG = scrollView.contentLayoutGuide
        let svFLG = scrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([
            
            scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -80.0),
            
            stack.topAnchor.constraint(equalTo: svCLG.topAnchor, constant: 0.0),
            stack.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor, constant: 0.0),
            stack.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor, constant: 0.0),
            stack.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor, constant: 0.0),
            
            stack.heightAnchor.constraint(equalTo: svFLG.heightAnchor, constant: 0.0),
            
            pageControl.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 8.0),
            pageControl.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            pageControl.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),

        ])
        
        // if we're loading "page" view controllers from Storyboard
        /*
        if let vc = storyboard?.instantiateViewController(withIdentifier: "psFirst") as? PSFirstViewController {
            pages.append(vc)
        }
        if let vc = storyboard?.instantiateViewController(withIdentifier: "psSecond") as? PSSecondViewController {
            pages.append(vc)
        }
        if let vc = storyboard?.instantiateViewController(withIdentifier: "psThird") as? PSThirdViewController {
            pages.append(vc)
        }
        if let vc = storyboard?.instantiateViewController(withIdentifier: "psFourth") as? PSFourthViewController {
            pages.append(vc)
        }
        pages.forEach { vc in
            self.addChild(vc)
            stack.addArrangedSubview(vc.view)
            vc.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
            vc.didMove(toParent: self)
        }
        */

        // for this example,
        //  create 4 view controllers, with background colors
        let colors: [UIColor] = [
            .red, .brown, .blue, .magenta
        ]
        colors.forEach { c in
            let vc = BasePageController()
            vc.view.backgroundColor = c
            self.addChild(vc)
            stack.addArrangedSubview(vc.view)
            vc.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
            vc.didMove(toParent: self)
            pages.append(vc)
        }
        
        pageControl.numberOfPages = pages.count
        
        scrollView.delegate = self
        
        pageControl.addTarget(self, action: #selector(self.pgControlChange(_:)), for: .valueChanged)
    }
    
    var pgControlScroll: Bool = false
    
    @objc func pgControlChange(_ sender: UIPageControl) {
        pgControlScroll = true
        let w = scrollView.frame.size.width
        guard w != 0 else { return }
        let x = scrollView.contentOffset.x
        let cp = min(Int(round(x / w)), pages.count - 1)
        let np = sender.currentPage
        var r = CGRect.zero
        if np > cp {
            r = CGRect(x: w * CGFloat(np + 1) - 1.0, y: 0, width: 1, height: 1)
        } else {
            r = CGRect(x: w * CGFloat(np), y: 0, width: 1, height: 1)
        }
        scrollView.scrollRectToVisible(r, animated: true)
    }
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        pgControlScroll = false
    }
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let w = scrollView.frame.size.width
        guard w != 0 else { return }
        let x = scrollView.contentOffset.x
        let pg = min(Int(round(x / w)), pages.count - 1)
        let v = stack.arrangedSubviews[pg]
        pageControl.backgroundColor = v.backgroundColor
        if pgControlScroll { return }
        pageControl.currentPage = pg
    }

}

class BasePageController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // add a label at each corner
        for (i, s) in ["top-left", "top-right", "bot-left", "bot-right"].enumerated() {
            let v = UILabel()
            v.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
            v.translatesAutoresizingMaskIntoConstraints = false
            v.text = s
            view.addSubview(v)
            let g = view.safeAreaLayoutGuide
            switch i {
            case 1:
                v.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0).isActive = true
                v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0).isActive = true
            case 2:
                v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0).isActive = true
                v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0).isActive = true
            case 3:
                v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0).isActive = true
                v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0).isActive = true
            default:
                v.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0).isActive = true
                v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0).isActive = true
            }
        }
    }
    
}

這是一種方法來做到這一點......

由於滾動視圖啟用了分頁,因此我們一次只能在滾動視圖中查看 3 個“頁面”。

假設總共有 4 個“頁面”......

  • 從第 4、1、2 頁開始
  • 設置滾動內容偏移 x 以便中心頁面可見
  • 如果我們滾動到下一頁,將視圖移動到 1、2、3 並再次設置滾動內容偏移量 x 以便中心頁面可見
  • 如果我們滾動到上一頁,將視圖移動到 3, 4, 1 並再次設置滾動內容偏移 x 以便中心頁面可見

這種方法將所有“頁面”視圖控制器加載為子視圖控制器。 如果我們最終有 20 個“頁面”——特別是如果它們“重”(大量子視圖、代碼等)——我們只想在需要顯示控制器時加載它們,並在它們出現時卸載它們'已從 3 個“滾動槽”中移除。


struct MyPage定義一個頁面作為視圖控制器和一個索引頁碼:

struct MyPage {
    var vc: UIViewController!
    var pageNumber: Int!
}

PagedScrollViewController類:

class PagedScrollViewController: UIViewController, UIScrollViewDelegate {
    
    let scrollView: UIScrollView = {
        let v = UIScrollView()
        v.isPagingEnabled = true
        v.showsHorizontalScrollIndicator = false
        // set clipsToBounds to false
        //  if we want to see the way the views are being cycled
        v.clipsToBounds = true
        return v
    }()
    
    let pageControl: UIPageControl = {
        let v = UIPageControl()
        return v
    }()
    
    let stack: UIStackView = {
        let v = UIStackView()
        v.axis = .horizontal
        v.distribution = .fillEqually
        return v
    }()
    
    var pages: [MyPage] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        pageControl.translatesAutoresizingMaskIntoConstraints = false
        stack.translatesAutoresizingMaskIntoConstraints = false
        
        scrollView.addSubview(stack)
        view.addSubview(scrollView)
        view.addSubview(pageControl)
        
        let g = view.safeAreaLayoutGuide
        let svCLG = scrollView.contentLayoutGuide
        let svFLG = scrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // cover most of screen (with a little padding on each side)
            //scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            //scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            //scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            //scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -80.0),
            
            // small scroll view at top of screen
            scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
            scrollView.heightAnchor.constraint(equalToConstant: 200.0),
            
            stack.topAnchor.constraint(equalTo: svCLG.topAnchor, constant: 0.0),
            stack.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor, constant: 0.0),
            stack.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor, constant: 0.0),
            stack.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor, constant: 0.0),
            
            stack.heightAnchor.constraint(equalTo: svFLG.heightAnchor, constant: 0.0),
            
            pageControl.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 8.0),
            pageControl.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            pageControl.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            
        ])
        
        /*
        // if we're loading "page" view controllers from Storyboard
        var i = 0
        if let vc = storyboard?.instantiateViewController(withIdentifier: "psFirst") as? PSFirstViewController {
            pages.append(MyPage(vc: vc, pageNumber: i))
            i += 1
        }
        if let vc = storyboard?.instantiateViewController(withIdentifier: "psSecond") as? PSSecondViewController {
            pages.append(MyPage(vc: vc, pageNumber: i))
            i += 1
        }
        if let vc = storyboard?.instantiateViewController(withIdentifier: "psThird") as? PSThirdViewController {
            pages.append(MyPage(vc: vc, pageNumber: i))
            i += 1
        }
        if let vc = storyboard?.instantiateViewController(withIdentifier: "psFourth") as? PSFourthViewController {
            pages.append(MyPage(vc: vc, pageNumber: i))
            i += 1
        }
        
        pages.forEach { pg in
            self.addChild(pg.vc)
            pg.vc.didMove(toParent: self)
        }
        */
        
        // for this example, we will
        //  create 4 "page" view controllers, with background colors
        //  (dark red, dark green, dark blue, brown)
        let colors: [UIColor] = [
            UIColor(red: 0.75, green: 0.00, blue: 0.00, alpha: 1.0),
            UIColor(red: 0.00, green: 0.75, blue: 0.00, alpha: 1.0),
            UIColor(red: 0.00, green: 0.00, blue: 0.75, alpha: 1.0),
            UIColor(red: 0.75, green: 0.50, blue: 0.00, alpha: 1.0),
        ]
        for (c, i) in zip(colors, Array(0..<colors.count)) {
            let vc = BasePageController()
            vc.view.backgroundColor = c
            vc.centerLabel.text = "\(i + 1)"
            self.addChild(vc)
            vc.didMove(toParent: self)
            pages.append(MyPage(vc: vc, pageNumber: i))
        }
        
        // move last page to position Zero
        pages.insert(pages.removeLast(), at: 0)
        // add 3 pages to stack view in scroll view
        pages[0...2].forEach { pg in
            stack.addArrangedSubview(pg.vc.view)
            pg.vc.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
        }
        
        scrollView.delegate = self
        
        pageControl.numberOfPages = pages.count
        
        pageControl.addTarget(self, action: #selector(self.pgControlChange(_:)), for: .valueChanged)
        
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        scrollView.contentOffset.x = scrollView.frame.size.width
    }
    
    // flag so we don't infinite loop on scrolling and setting page control current page
    var pgControlScroll: Bool = false
    
    @objc func pgControlChange(_ sender: UIPageControl) {
        let w = scrollView.frame.size.width
        guard w != 0 else { return }
        // get the middle page
        let pg = pages[1]
        // unwrap current page number in scroll view
        guard let cp = pg.pageNumber else { return }
        // set the flag
        pgControlScroll = true
        // next page based on page control page number
        let np = sender.currentPage
        var r = CGRect.zero
        if np > cp {
            r = CGRect(x: w * 3.0 - 1.0, y: 0.0, width: 1.0, height: 1.0)
            // next page is to the right
        } else {
            // next page is to the left
            r = CGRect(x: 0.0, y: 0, width: 1, height: 1)
        }
        // need to manually animate the scroll, so we can update our page order when scroll finishes
        UIView.animate(withDuration: 0.3, animations: {
            self.scrollView.scrollRectToVisible(r, animated: false)
        }, completion: { _ in
            self.updatePages(self.scrollView)
        })
    }
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        // turn off the flag
        pgControlScroll = false
    }
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let w = scrollView.frame.size.width
        guard w != 0 else { return }
        let x = scrollView.contentOffset.x
        // get the "page" based on scroll offset x
        let pgID = min(Int(round(x / w)), pages.count - 1)
        let pg = pages[pgID]
        guard let v = pg.vc.view else { return }
        pageControl.backgroundColor = v.backgroundColor
        // don't set the pageControl's pageNumber if we scrolled as a result of tapping the page control
        if pgControlScroll { return }
        pageControl.currentPage = pg.pageNumber
    }
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        updatePages(scrollView)
    }
    func updatePages(_ scrollView: UIScrollView) -> Void {
        let w = scrollView.frame.size.width
        guard w != 0 else { return }
        let x = scrollView.contentOffset.x
        if x == 0 {
            // we've scrolled to the left
            // move last page to position Zero
            guard let pg = pages.last,
                  let v = pg.vc.view else { return }
            // remove the last arranged subview
            stack.arrangedSubviews.last?.removeFromSuperview()
            // insert last "page" view as first arranged subview
            stack.insertArrangedSubview(v, at: 0)
            // set its width anchor
            v.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
            // move last page to first position in array
            pages.insert(pages.removeLast(), at: 0)
        } else if x == scrollView.frame.size.width * 2 {
            // we've scrolled right
            // move first page to last position in array
            pages.append(pages.removeFirst())
            // get the next page
            let pg = pages[2]
            guard let v = pg.vc.view else { return }
            // remove first arranged subview
            stack.arrangedSubviews.first?.removeFromSuperview()
            // add next page view to stack
            stack.addArrangedSubview(v)
            // set its width anchor
            v.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
        }
        scrollView.contentOffset.x = scrollView.frame.size.width
    }
    
}

BasePageController示例“頁面”類:

class BasePageController: UIViewController {
    
    let centerLabel: UILabel = {
        let v = UILabel()
        v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        v.font = UIFont.systemFont(ofSize: 48.0, weight: .bold)
        v.textAlignment = .center
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // add a label at each corner
        for (i, s) in ["top-left", "top-right", "bot-left", "bot-right"].enumerated() {
            let v = UILabel()
            v.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
            v.translatesAutoresizingMaskIntoConstraints = false
            v.text = s
            view.addSubview(v)
            let g = view.safeAreaLayoutGuide
            switch i {
            case 1:
                v.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0).isActive = true
                v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0).isActive = true
            case 2:
                v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0).isActive = true
                v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0).isActive = true
            case 3:
                v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0).isActive = true
                v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0).isActive = true
            default:
                v.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0).isActive = true
                v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0).isActive = true
            }
        }
        centerLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(centerLabel)
        NSLayoutConstraint.activate([
            centerLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            centerLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
    }
    
}

筆記:

這只是示例代碼,不應被視為“生產就緒”。

如果您只有 1 或 2 個“頁面”,則會崩潰。

如果你用幾十個“頁面”嘗試這個,你可能會遇到內存問題。


根據評論編輯...

看看你的項目,我看到你正在使用UICollectionView

我認為問題在於您正在混合/匹配您的viewModel.pages - 它有4元素,而您的itemsWithBoundries - 它有6元素。 試圖調和那是相當混亂的。

所以......我將建議一種不同的、更古老的方法。

對於集合視圖的numberOfItemsInSection ,我將返回 10,000。

cellForItemAt ,我將使用indexPath.item % viewModel.pages.count (余數/模運算符)返回viewModel.pages[0...3]范圍內的單元格。

scrollViewDidScroll相同想法...獲取實際單元格項目索引%頁數以從 0 到 3。

為了在兩個方向上實現“無限滾動”,我將首先將集合視圖滾動到第 5,000 項(代碼包括調整,如果頁數不能被等分為 5,000)。 用戶不太可能向任一方向滾動 5,000 頁以到達“終點”。

我用這種方法編輯了您的測試應用程序並將其發布到 GitHub: https : //github.com/DonMag/Test-App,以便您可以看到我所做的更改。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM