Swift - 带有嵌入式 UIStackView 的 UIScrollView 无限滚动 [英] Swift - Infinite Scrolling for UIScrollView with an embedded UIStackView

查看:35
本文介绍了Swift - 带有嵌入式 UIStackView 的 UIScrollView 无限滚动的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

有人帮助使用以下代码来实现我在这里需要做的事情:-

Someone helped with the below code to achieve what I needed to do here:-

UIPageViewController - 检测滚动到一半进入下一个视图控制器(几乎可以工作)以更改按钮颜色?

在保持现有行为不变的同时,我需要一些帮助,可能只是修改 scrollViewDidScroll 方法以允许无限平滑滚动,因此当您向右滑动时到达第四个/最后一个项目时,它将平滑过渡到第一个页面,同样,如果您在到达第一个时继续向左滑动并再次向左滑动,则会显示最后一个项目,如果您知道我的意思,我尝试这样做,但有点播放.谢谢

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

推荐答案

这是一种方法...

由于滚动视图启用了分页,我们只能有 3 个视图页面";一次在滚动视图中.

Since the scroll view has paging enabled, we are able to have only 3 views "pages" in the scroll view at a time.

假设总共有 4 个页面"...

Assuming 4 total "pages"...

  • 从第 4、1、2 页开始
  • 设置滚动内容偏移 x 使中心页面可见
  • 如果我们滚动到下一页,将视图移动到 1、2、3 并再次设置滚动内容偏移量 x 以便中心页面可见
  • 如果我们滚动到上一页,将视图移动到 3、4、1 并再次设置滚动内容偏移 x 以便中心页面可见

这种方法加载所有页面"视图控制器作为子视图控制器.如果我们最终得到,比如说,20 个页面"- 特别是如果它们是重的"(许多子视图、代码等) - 我们希望仅在需要显示控制器时加载它们,并在它们从 3 个滚动槽"中移除时卸载它们.

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."

struct MyPage 将页面定义为视图控制器和索引页码:

struct MyPage Defines a page as a view controller and an index page number:

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),
        ])
    }
    
}

注意事项:

这只是示例代码,不应被视为生产就绪".

This is example code only and should not be considered "production ready."

如果您只有 1 或 2 个页面"这会崩溃.

If you have only 1 or 2 "pages" this will crash.

如果你用几十个页面"试试这个您可能会遇到内存问题.

If you try this with a couple dozen "pages" you'll likely hit memory problems.

编辑 根据评论...

查看您的项目,我发现您使用的是 UICollectionView.

Took a look at your project, and I see you're using a UICollectionView instead.

我认为问题在于您正在混合/匹配您的 viewModel.pages - 它具有 4 元素,以及您的 itemsWithBoundries -其中有 6 元素.试图调和那是相当混乱的.

I think the issue is that you're mixing / matching your viewModel.pages - which has 4 elements, and your itemsWithBoundries - which has 6 elements. Trying to reconcile that is pretty messy.

所以...我将建议一种不同的、较旧的方法.

So... I'm going to suggest a different, older approach.

对于集合视图的 numberOfItemsInSection,我将返回 10,000.

For the collection view's numberOfItemsInSection, I'm going to return 10,000.

cellForItemAt 中,我将使用 indexPath.item % viewModel.pages.count(余数/模运算符)返回 viewModel 中的单元格.pages[0...3] 范围.

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.

scrollViewDidScroll 中的相同想法 ... 获取实际的单元格项目索引 % 页数以从 0 到 3.

Same idea in scrollViewDidScroll ... get the actual cell item index % number of pages to get Zero thru 3.

实现无限滚动"在两个方向上,我将首先将集合视图滚动到第 5,000 项(代码包括调整,如果页数不能被等分为 5,000).用户不太可能向任一方向滚动 5,000 页以到达结尾".

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). It's pretty unlikely a user would scroll 5,000 pages in either direction to reach the "end."

我用这种方法编辑了您的测试应用程序并将其发布到 GitHub:https://github.com/DonMag/Test-App 以便您可以看到我所做的更改.

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.

这篇关于Swift - 带有嵌入式 UIStackView 的 UIScrollView 无限滚动的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆