简体   繁体   English

Swift - 带有嵌入式 UIStackView 的 UIScrollView 无限滚动

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

Someone helped with the below code to achieve what I needed to do here:-有人帮助使用以下代码来实现我在这里需要做的事情:-

UIPageViewController - Detect scrolling halfway into the next view controller (almost working) to change button color? UIPageViewController - 检测滚动到下一个视图控制器(几乎可以正常工作)以更改按钮颜色的一半?

Whilst keeping the existing behaviour intact, I would like some assistance please to modify probably just the scrollViewDidScroll method to allow for an infinite smooth scrolling so when you reach the fourth / last item as you swipe right it will smoothly transition to the first page and likewise if you keep swiping back to the left as you reach the first and swipe left again the last item will be displayed, if you know what I mean, I tried doing it but was playing up a bit.在保持现有行为完整的同时,我需要一些帮助,请修改可能只是 scrollViewDidScroll 方法以允许无限平滑滚动,因此当您向右滑动时到达第四个/最后一个项目时,它将平滑过渡到第一页,同样如果您在到达第一个时继续向左滑动并再次向左滑动,则会显示最后一个项目,如果您知道我的意思,我尝试这样做,但有点播放。 Thank you谢谢

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
            }
        }
    }
    
}

Here is one way to do this...这是一种方法来做到这一点......

Since the scroll view has paging enabled, we are able to have only 3 views "pages" in the scroll view at a time.由于滚动视图启用了分页,因此我们一次只能在滚动视图中查看 3 个“页面”。

Assuming 4 total "pages"...假设总共有 4 个“页面”......

  • Start with pages 4, 1, 2从第 4、1、2 页开始
  • set scroll content offset x so the center page is visible设置滚动内容偏移 x 以便中心页面可见
  • if we scroll to the next page, shift the views to 1, 2, 3 and again set scroll content offset x so the center page is visible如果我们滚动到下一页,将视图移动到 1、2、3 并再次设置滚动内容偏移量 x 以便中心页面可见
  • if we scroll to the previous page, shift the views to 3, 4, 1 and again set scroll content offset x so the center page is visible如果我们滚动到上一页,将视图移动到 3, 4, 1 并再次设置滚动内容偏移 x 以便中心页面可见

This approach loads all "page" view controllers as child view controllers.这种方法将所有“页面”视图控制器加载为子视图控制器。 If there we end up with, say, 20 "pages" - particularly if they are "heavy" (lots of subviews, code, etc) - we would want to load controllers only when we need to show them, and unload them when they're removed from the 3 "scroll slots."如果我们最终有 20 个“页面”——特别是如果它们“重”(大量子视图、代码等)——我们只想在需要显示控制器时加载它们,并在它们出现时卸载它们'已从 3 个“滚动槽”中移除。


struct MyPage Defines a page as a view controller and an index page number: struct MyPage定义一个页面作为视图控制器和一个索引页码:

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

PagedScrollViewController class: 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 example "page" class: 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),
        ])
    }
    
}

Notes:笔记:

This is example code only and should not be considered "production ready."这只是示例代码,不应被视为“生产就绪”。

If you have only 1 or 2 "pages" this will crash.如果您只有 1 或 2 个“页面”,则会崩溃。

If you try this with a couple dozen "pages" you'll likely hit memory problems.如果你用几十个“页面”尝试这个,你可能会遇到内存问题。


Edit based on comments...根据评论编辑...

Took a look at your project, and I see you're using a UICollectionView instead.看看你的项目,我看到你正在使用UICollectionView

I think the issue is that you're mixing / matching your viewModel.pages - which has 4 elements, and your itemsWithBoundries - which has 6 elements.我认为问题在于您正在混合/匹配您的viewModel.pages - 它有4元素,而您的itemsWithBoundries - 它有6元素。 Trying to reconcile that is pretty messy.试图调和那是相当混乱的。

So... I'm going to suggest a different, older approach.所以......我将建议一种不同的、更古老的方法。

For the collection view's numberOfItemsInSection , I'm going to return 10,000.对于集合视图的numberOfItemsInSection ,我将返回 10,000。

In cellForItemAt , I'll use indexPath.item % viewModel.pages.count (the remainder / modulo operator) to return a cell in the viewModel.pages[0...3] range.cellForItemAt ,我将使用indexPath.item % viewModel.pages.count (余数/模运算符)返回viewModel.pages[0...3]范围内的单元格。

Same idea in scrollViewDidScroll ... get the actual cell item index % number of pages to get Zero thru 3. scrollViewDidScroll相同想法...获取实际单元格项目索引%页数以从 0 到 3。

To achieve "infinite scrolling" in both directions, I'll start with scrolling the collection view to item 5,000 (code includes adjusting that if the number of pages is not equally divisible into 5,000).为了在两个方向上实现“无限滚动”,我将首先将集合视图滚动到第 5,000 项(代码包括调整,如果页数不能被等分为 5,000)。 It's pretty unlikely a user would scroll 5,000 pages in either direction to reach the "end."用户不太可能向任一方向滚动 5,000 页以到达“终点”。

I edited your Test App with that approach and posted it to GitHub: https://github.com/DonMag/Test-App so you can see the changes I made.我用这种方法编辑了您的测试应用程序并将其发布到 GitHub: https : //github.com/DonMag/Test-App,以便您可以看到我所做的更改。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM