You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过代码实现带左右部分视图的UIScrollView预告轮播视图

带侧边预览的UIScrollView轮播视图问题解决

需求与问题

需要实现轮播视图,要求视口中除居中主视图外,还显示左右部分视图。当前代码存在以下问题:

  • 视图无法滚动
  • 左右部分视图未显示
  • 需兼容1个、2个、3个视图的场景

原代码

class ViewController: UIViewController {
    override func viewDidLoad() {
        let scroll = CarouselView(views: [])
        view.addSubview(scroll)
        scroll.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            scroll.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scroll.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scroll.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
        ])
    }
}


class CarouselView: UIView, UIScrollViewDelegate {
    var views: [UIView] = []
    
    init(views: [UIView]) {
        super.init(frame: .zero)
        
        self.views = views
        
        scrollView.delegate = self
        
        setupSubviews()
        setupLayoutConstraints()
        setupDummyViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setupSubviews() {
        addSubview(scrollView)
        scrollView.addSubview(stackView)
    }
    
    private func setupLayoutConstraints() {
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
            scrollView.heightAnchor.constraint(equalToConstant: 150.0),
            
            stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
        ])
    }
    
    private func setupDummyViews() {
        let view1 = UIView()
        view1.backgroundColor = .red
        
        let view2 = UIView()
        view2.backgroundColor = .yellow
        
        let view3 = UIView()
        view3.backgroundColor = .green
        
        let views = [view1, view2, view3]
        
        for view in views {
            stackView.addArrangedSubview(view)
            NSLayoutConstraint.activate([
                view.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
            ])
        }
    }
    
    // MARK: - UI Components
    let scrollView: UIScrollView = {
        let scrollView = UIScrollView()
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.isPagingEnabled = true
        scrollView.showsHorizontalScrollIndicator = false
        return scrollView
    }()
    
    let stackView: UIStackView = {
        let stackView = UIStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .horizontal
        stackView.spacing = 0.0
        stackView.distribution = .fillEqually
        stackView.alignment = .fill
        
        return stackView
    }()
}

问题分析与解决方案

1. 解决无法滚动的问题

原代码中stackView的leading/trailing直接与scrollView绑定,导致scrollView无法计算超出自身范围的内容宽度,因此无法滚动。需要移除这两个约束,让stackView的宽度由子视图自动计算,scrollView会根据stackView的尺寸自动设置contentSize。

2. 实现左右预览效果

  • 设置子视图宽度小于scrollView宽度,比如给scrollView左右各留40pt的预览空间,子视图宽度设为scrollView.width - 80。
  • 给scrollView设置contentInset = UIEdgeInsets(top: 0, left: 40, bottom: 0, right: 40),让初始状态下第一个视图居中,左右露出部分视图。

3. 兼容不同视图数量的场景

场景1:仅1个视图

  • 禁用scrollView滚动,子视图宽度等于scrollView宽度,contentInset设为0,直接居中显示。

场景2:2个视图

  • 在stackView开头添加第二个视图的副本,结尾添加第一个视图的副本,实现循环滚动效果。
  • 初始滚动位置设置为第一个真实视图的位置(即偏移量为子视图宽度)。

场景3:3个及以上视图

  • 在stackView开头添加最后一个视图的副本,结尾添加第一个视图的副本。
  • 通过UIScrollViewDelegate的scrollViewDidEndDecelerating方法,当滚动到边界的副本视图时,无缝跳转到对应的真实视图位置。

修改后完整代码

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 测试不同场景:1个、2个、3个视图
        let view1 = UIView()
        view1.backgroundColor = .red
        let view2 = UIView()
        view2.backgroundColor = .yellow
        let view3 = UIView()
        view3.backgroundColor = .green
        
        let carousel = CarouselView(views: [view1, view2, view3])
        view.addSubview(carousel)
        carousel.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            carousel.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            carousel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            carousel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            carousel.heightAnchor.constraint(equalToConstant: 150)
        ])
    }
}

class CarouselView: UIView, UIScrollViewDelegate {
    private var originalViews: [UIView] = []
    private let previewWidth: CGFloat = 40 // 单侧预览宽度
    private var itemWidth: CGFloat {
        scrollView.bounds.width - previewWidth * 2
    }
    
    init(views: [UIView]) {
        self.originalViews = views
        super.init(frame: .zero)
        scrollView.delegate = self
        setupSubviews()
        setupLayoutConstraints()
        configureCarousel()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupSubviews() {
        addSubview(scrollView)
        scrollView.addSubview(stackView)
    }
    
    private func setupLayoutConstraints() {
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
            scrollView.heightAnchor.constraint(equalToConstant: 150.0),
            
            // 移除stackView与scrollView的leading/trailing绑定,让stackView宽度由子视图决定
            stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor),
            // 让stackView在scrollView中垂直居中
            stackView.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor)
        ])
    }
    
    private func configureCarousel() {
        guard !originalViews.isEmpty else { return }
        
        let viewsToAdd: [UIView]
        
        switch originalViews.count {
        case 1:
            // 单视图场景:禁用滚动,无预览
            viewsToAdd = originalViews
            scrollView.isScrollEnabled = false
            scrollView.contentInset = .zero
        case 2:
            // 双视图场景:首尾添加对方副本
            viewsToAdd = [originalViews[1]] + originalViews + [originalViews[0]]
            scrollView.contentInset = UIEdgeInsets(top: 0, left: previewWidth, bottom: 0, right: previewWidth)
            // 初始滚动到第一个真实视图
            DispatchQueue.main.async {
                self.scrollView.setContentOffset(CGPoint(x: self.itemWidth, y: 0), animated: false)
            }
        default:
            // 3个及以上:首尾添加首尾视图副本
            viewsToAdd = [originalViews.last!] + originalViews + [originalViews.first!]
            scrollView.contentInset = UIEdgeInsets(top: 0, left: previewWidth, bottom: 0, right: previewWidth)
            // 初始滚动到第一个真实视图
            DispatchQueue.main.async {
                self.scrollView.setContentOffset(CGPoint(x: self.itemWidth, y: 0), animated: false)
            }
        }
        
        for view in viewsToAdd {
            stackView.addArrangedSubview(view)
            NSLayoutConstraint.activate([
                view.widthAnchor.constraint(equalToConstant: itemWidth),
                view.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
            ])
        }
    }
    
    // MARK: - UIScrollViewDelegate
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        let currentOffsetX = scrollView.contentOffset.x
        let totalItems = stackView.arrangedSubviews.count
        
        // 处理循环滚动
        if originalViews.count >= 2 {
            if currentOffsetX <= 0 {
                // 滚动到最左边的副本,跳转到倒数第二个真实视图
                scrollView.setContentOffset(CGPoint(x: itemWidth * CGFloat(originalViews.count), y: 0), animated: false)
            } else if currentOffsetX >= itemWidth * CGFloat(totalItems - 1) {
                // 滚动到最右边的副本,跳转到第二个真实视图
                scrollView.setContentOffset(CGPoint(x: itemWidth, y: 0), animated: false)
            }
        }
    }
    
    // MARK: - UI Components
    let scrollView: UIScrollView = {
        let scrollView = UIScrollView()
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.isPagingEnabled = true
        scrollView.showsHorizontalScrollIndicator = false
        return scrollView
    }()
    
    let stackView: UIStackView = {
        let stackView = UIStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .horizontal
        stackView.spacing = 0.0
        stackView.alignment = .fill
        return stackView
    }()
}

关键修改说明

  • 拆分originalViews存储原始视图,避免与副本混淆
  • 动态计算itemWidth,确保左右预览空间一致
  • 根据视图数量动态调整stackView的子视图集合,实现循环滚动
  • 通过scrollViewDidEndDecelerating处理边界跳转,实现无缝循环
  • 针对不同视图数量设置对应的滚动属性和初始偏移量

内容的提问来源于stack exchange,提问作者Aniket Prakash

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 19:42:34