以编程方式使用 UIScrollView 的 Teaser 轮播视图

Teaser Carousel View using UIScrollView programatically

提问人:Aniket Prakash 提问时间:7/27/2023 更新时间:8/2/2023 访问量:107

问:

我想使用 UIScrollView 实现一个预告片轮播视图(即视口中显示的部分左右视图以及居中的主视图)。

视图层次结构:

ViewController -> ScrollView -> Horizontal Stack View -> 和嵌入其中的 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. 视图不可滚动。
  2. 左右部分视图未显示。请指导我!!包括仅处理 1、2 和 3 个视图的案例
iOS Swift UIscrollView 自动布局 轮播界面

评论

0赞 AntiVIRUZ 7/27/2023
为什么选择 UIScrollView,而不是具有自定义布局的 UICollectionView?永远只有 3 个屏幕,没有更多?
0赞 AntiVIRUZ 7/27/2023
这样的事情可能会帮助你 stackoverflow.com/questions/41207199/......

答:

2赞 DonMag 7/28/2023 #1

对于当前代码,无法滚动的原因是 的实例没有高度。scrollCarouselView

默认情况下,a 具有 -- 因此您可以看到在视图框架之外延伸的任何子视图。UIView.clipsToBounds = false

如果按原样设置并运行代码:scroll.clipsToBounds = true

    let scroll = CarouselView(views: [])
    scroll.clipsToBounds = true

你不会看到任何东西

超出其超视图框架的视图无法接收触摸。因此,即使你可以看到它们,你也无法与它们互动

您在约束设置中遗漏了重要的一行:

scrollView.topAnchor.constraint(equalTo: topAnchor),
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
scrollView.heightAnchor.constraint(equalToConstant: 150.0),

// you need this line to give a height to "self"
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),

现在,您可以滚动(并且仍然使用 )。scroll.clipsToBounds = true

下一步是使轮播界面子视图小于整个视图宽度。但是,将页面设置为滚动视图的整个宽度scrollscrollView.isPagingEnabled = true

许多不同的方法可以解决这个问题,包括禁用和自行处理视图定位、减速等;使用带有计算内容插图的插图;等。.isPagingEnabledUICollectionView

或者,我们可以变得有点棘手......

让我们将 scrollView 的 Width 设置为所需的“部分”宽度,如下所示 - 浅灰色是 Carousel 视图背景,滚动视图周围有一个黑色轮廓):

enter image description here

现在,当我们滚动时,我们看到这个:

enter image description here

启用分页后,它将“捕捉”到下一个子视图:

enter image description here

听起来我们是其中的一部分......但是,我们也希望看到部分上一个/下一个子视图。

因此,让我们设置:scrollView.clipsToBounds = false

enter image description here

enter image description here

enter image description here

但是,在尝试时,我们很快就会发现问题。

由于子视图在滚动视图的“可见但在框架之外”,因此我们只能通过在滚动视图本身(黑色轮廓内)拖动来滚动。

为了允许从视图的任何部分拖动,我们可以像这样实现(在 Carouself 视图类中):hitTest(...)

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    if self.bounds.contains(point) {
        return scrollView
    }
    return super.hitTest(point, with: event)
}

现在,如果触摸在视图内部,我们告诉 scrollView 使用该触摸。如果它位于视图之外,我们将返回以允许触摸对任何其他 UI 元素执行操作。super...

这是一个完整的例子......

  • 我正在控制器中设置“虚拟”视图,以便更轻松地测试 1、2、3 等视图。
  • 我对您的约束进行了一些更改,以使用滚动视图的和.contentLayoutGuide.frameLayoutGuide
  • 为了避免混淆,我还重命名了您的视图。scrollmyCarouselView

View Controller 类

class CarouselViewController: UIViewController {

    override func viewDidLoad() {

        let colors: [UIColor] = [
            .red, .green, .blue,
            .cyan, .magenta, .yellow,
        ]
        
        let numViews: Int = 3
        
        var views: [UIView] = []
        
        for i in 0..<numViews {
            let v = UIView()
            v.backgroundColor = colors[i % colors.count]
            views.append(v)
        }

        let myCarouselView = CarouselView(views: views)

        view.addSubview(myCarouselView)
        myCarouselView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            myCarouselView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            myCarouselView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            myCarouselView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
        ])
    
        // so we can see the view framing
        myCarouselView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        
    }
    
}

CarouselView 类

class CarouselView: UIView, UIScrollViewDelegate {
    var views: [UIView] = []
    
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if self.bounds.contains(point) {
            return scrollView
        }
        return super.hitTest(point, with: event)
    }
    
    init(views: [UIView]) {
        super.init(frame: .zero)
        
        self.views = views
        
        scrollView.delegate = self
        
        setupSubviews()
        setupLayoutConstraints()
        
        for view in views {
            stackView.addArrangedSubview(view)
            NSLayoutConstraint.activate([
                view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
            ])
        }
        
        // so we can see the views that are outside the frame of the scroll view
        scrollView.clipsToBounds = false
        
        // let's give the scroll view a border to make it clear
        scrollView.layer.borderWidth = 2
        scrollView.layer.borderColor = UIColor.black.cgColor
        
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setupSubviews() {
        addSubview(scrollView)
        scrollView.addSubview(stackView)
    }
    
    private func setupLayoutConstraints() {
        
        let cg = scrollView.contentLayoutGuide
        let fg = scrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([
            
            scrollView.topAnchor.constraint(equalTo: topAnchor),
            scrollView.heightAnchor.constraint(equalToConstant: 150.0),
            scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),

            scrollView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.60),
            scrollView.centerXAnchor.constraint(equalTo: centerXAnchor),
            
            stackView.topAnchor.constraint(equalTo: cg.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor),
            stackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor),
            
            stackView.heightAnchor.constraint(equalTo: fg.heightAnchor),
            
        ])
    }
    
    // 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
    }()
}

编辑

如果您希望在轮播视图之间保持间距,请不要更改堆栈视图间距。

相反,请将子视图设计为包含间距。

例如,我们可以添加一个白色作为“可见框架”,并添加一个居中的标签作为子框。我们将用顶部和底部的 8 个点来约束白色视图,在前导和后跟上限制 4 个点。UIView

因此,单个视图将如下所示:

enter image description here

其中两个在 Zero-spacing 堆栈视图中并排显示,如下所示:

enter image description here

现在,子视图之间有 8 个点间距的视觉效果。

我们也可以稍微“设置”子视图的样式,如下所示:

enter image description here

enter image description here

enter image description here

因此,上面发布的类没有变化CarouselView

我们将创建一个类的开头:CarouselCardView

class CarouselCardView: UIView {
    
    let label: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        v.font = .systemFont(ofSize: 48.0, weight: .regular)
        return v
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        
        // add a view with rounded corners be the "visible frame"
        let rv = UIView()
        rv.backgroundColor = .white
        
        rv.layer.cornerRadius = 12
        
        // let's give it a very light shadow
        rv.layer.shadowOffset = .init(width: 0.0, height: 1.0)
        rv.layer.shadowColor = UIColor.black.cgColor
        rv.layer.shadowRadius = 2.0
        rv.layer.shadowOpacity = 0.5
        
        rv.translatesAutoresizingMaskIntoConstraints = false
        label.translatesAutoresizingMaskIntoConstraints = false
        addSubview(rv)
        addSubview(label)
        
        NSLayoutConstraint.activate([
            
            rv.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
            rv.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4.0),
            rv.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4.0),
            rv.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),

            label.centerXAnchor.constraint(equalTo: centerXAnchor),
            label.centerYAnchor.constraint(equalTo: centerYAnchor),
            
        ])
        
    }

}

然后在示例视图控制器中,我们将创建以下实例,而不是具有不同颜色背景的“虚拟”视图:CarouselCardView

class CarouselViewController: UIViewController {
    
    override func viewDidLoad() {
        
        let numViews: Int = 5
        
        var views: [UIView] = []
        
        for i in 0..<numViews {
            let v = CarouselCardView()
            v.label.text = "\(i)"
            views.append(v)
        }
        
        let myCarouselView = CarouselView(views: views)
        
        view.addSubview(myCarouselView)
        myCarouselView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            myCarouselView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            myCarouselView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            myCarouselView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
        ])
        
        // so we can see the view framing
        myCarouselView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        
    }
    
}

评论

0赞 Aniket Prakash 7/31/2023
如果我想在左右局部视图之间保持 8px 的间距,需要进行哪些更新?此外,当第一个视图显示在视口中时,不应显示部分左视图,而应显示主视图和 8px 间距和 8px 部分右视图。向堆栈视图添加间距会导致不规则的偏部分左视图和右视图大小调整@DonMag
0赞 DonMag 8/2/2023
@AniketPrakash - 不要向堆栈视图添加间距。相反,请设计子视图以添加间距。请参阅编辑我的答案。