如何通过代码实现带左右部分视图的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
相关产品推荐
相关产品推荐

