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

ScrollViewDidScroll偏移问题:indicator超出indicatorBackground可视区域

解决方案

问题出在scrollViewDidScroll方法中对indicator偏移量的计算逻辑——原来的计算基于view.frame.width的一半,没有考虑indicatorBackground添加左右内边距后宽度缩小的情况,导致indicator的移动范围超出了背景视图的边界。

修改scrollViewDidScroll方法,改为基于滚动进度和视图实际宽度的计算方式:

override func scrollViewDidScroll(_ scrollView: UIScrollView) {
    // 计算滚动进度:0(第一个页面)到1(第二个页面)
    let progress = scrollView.contentOffset.x / view.frame.width
    // 计算indicator可移动的最大距离:背景视图宽度减去indicator自身宽度
    let maxOffset = indicatorBackground.bounds.width - indicator.bounds.width
    // 根据进度计算当前需要移动的偏移量
    let currentOffset = progress * maxOffset
    indicator.transform = CGAffineTransform(translationX: currentOffset, y: 0)
}

逻辑说明:

  • progress:表示当前滚动的相对进度,从第一个页面到第二个页面时,值从0平滑过渡到1。
  • maxOffset:是indicator在背景视图内可以移动的最大水平距离,确保indicator移动到最右端时,不会超出背景视图的边界。
  • currentOffset:将滚动进度映射到indicator的可移动范围内,实现平滑且边界正确的跟随效果。

修改后的完整代码

class CollectionPager: UICollectionViewController, UICollectionViewDelegateFlowLayout, CollectionDelegate {
    
    func didTapHeaderCell(index: Int) {
        collectionView.isPagingEnabled = false
        collectionView.scrollToItem(
            at: IndexPath(item: index, section: 0),
            at: .centeredHorizontally,
            animated: true
        )
        collectionView.isPagingEnabled = true
    }

    let indicator: UIView = {
        let v = UIView()
        v.backgroundColor = .white
        v.layer.cornerRadius = 26
       return v
    }()
    let indicatorBackground: UIView = {
        let v = UIView()
        v.backgroundColor = .secondarySystemGroupedBackground
        v.layer.cornerRadius = 30
       return v
    }()
    
    fileprivate let collectionHeader = CollectionHeader(collectionViewLayout: UICollectionViewFlowLayout())

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .systemGroupedBackground
        collectionHeader.delegate = self
        collectionHeader.collectionView.selectItem(at: [0,0], animated: true, scrollPosition: .centeredHorizontally)
        
        if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
            layout.scrollDirection = .horizontal
            layout.minimumLineSpacing = 0
         
        }
        
        setupLayout()
        
        collectionView.delegate = self
        collectionView.register(PagerCell.self, forCellWithReuseIdentifier: "PagerCell")
        collectionView.isPagingEnabled = true
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.allowsSelection = true
    }

    fileprivate func setupLayout(){

        view.addSubview(indicatorBackground)
        view.addSubview(indicator)
        view.addSubview(collectionHeader.view)
        
        indicatorBackground.anchor(
            top: view.safeAreaLayoutGuide.topAnchor,
            leading: view.leadingAnchor,
            bottom: collectionView.topAnchor,
            trailing: view.trailingAnchor,
            padding: .init(top: 24, left: 16, bottom: 4, right: 16),
            size: .init(width: 0, height: 60)
        )
        
        indicator.anchor(
            top: indicatorBackground.topAnchor,
            leading: indicatorBackground.leadingAnchor,
            bottom: indicatorBackground.bottomAnchor,
            trailing: nil,
            padding: .init(top: 4, left: 4, bottom: 4, right: 4)
        )
        indicator.widthAnchor.constraint(equalTo: indicatorBackground.widthAnchor, multiplier: 0.5).isActive = true
        
        collectionHeader.view.anchor(
            top: indicatorBackground.topAnchor,
            leading: indicatorBackground.leadingAnchor,
            bottom: indicatorBackground.bottomAnchor,
            trailing: indicatorBackground.trailingAnchor,
            padding: .init(top: 0, left: 0, bottom: 0, right: 0),
            size: .init(width: 0, height: 60)
        )

        collectionView.anchor(
            top: indicatorBackground.bottomAnchor,
            leading: view.leadingAnchor,
            bottom: view.bottomAnchor,
            trailing: view.trailingAnchor,
            padding: .init(top: 0, left: 0, bottom: 0, right: 0)
        )
    }
    
    override func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // 计算滚动进度:0(第一个页面)到1(第二个页面)
        let progress = scrollView.contentOffset.x / view.frame.width
        // 计算indicator可移动的最大距离:背景视图宽度减去indicator自身宽度
        let maxOffset = indicatorBackground.bounds.width - indicator.bounds.width
        // 根据进度计算当前需要移动的偏移量
        let currentOffset = progress * maxOffset
        indicator.transform = CGAffineTransform(translationX: currentOffset, y: 0)
    }

    override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        let x = targetContentOffset.pointee.x
        print(x)
        let item = x / view.frame.width
        let indexPath = IndexPath(item: Int(item), section: 0)
        collectionHeader.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
        collectionHeader.selectedIndex = indexPath.item
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 2
    }
        override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PagerCell", for: indexPath) as! PagerCell
        
        if indexPath.item == 0 {
            cell.label.text = "Today"
            cell.backgroundColor = .red
        }else if indexPath.item == 1 {
            cell.label.text = "News"
            cell.backgroundColor = .yellow
        }
        
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let height = view.frame.height
        let width = view.frame.width
        return .init(width: width, height: height)
    }
}


class PagerCell: UICollectionViewCell {
    
    let label: UILabel = {
        let l = UILabel()
        l.textAlignment = .center
        return l
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(label)
        label.fillSuperview()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

protocol CollectionDelegate {
    
    func didTapHeaderCell(index: Int)
}

class CollectionHeader: UICollectionViewController, UICollectionViewDelegateFlowLayout {
    
    fileprivate let titles = ["Today", "News"]
    
    var delegate: CollectionDelegate?
    
    let menuBar: UIView = {
        let v = UIView()
        v.backgroundColor = .systemBlue
        v.layer.cornerRadius = 30
       return v
    }()
    var selectedIndex: Int = 0 {
        didSet {
            collectionView.reloadData()
        }
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        
        collectionView.backgroundColor = .clear
        collectionView.layer.cornerRadius = 30
        
        collectionView.register(HeaderCell.self, forCellWithReuseIdentifier: "HeaderCell")
        
        if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
            layout.scrollDirection = .horizontal
            layout.minimumLineSpacing = 0
            layout.minimumInteritemSpacing = 0
        }
        selectedIndex = 0
    }
    
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        delegate?.didTapHeaderCell(index: indexPath.item)
        selectedIndex = indexPath.item
    }
    
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        
        return titles.count
    }
    
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HeaderCell", for: indexPath) as! HeaderCell
        cell.label.text = titles[indexPath.item]
        cell.label.textColor = indexPath.item == selectedIndex ? .black : .lightGray
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        
        let width = view.frame.width
        return .init(width: width / 2, height: view.frame.height)
    }
}

class HeaderCell: UICollectionViewCell {
    
    let label: UILabel = {
        let l = UILabel()
        l.textAlignment = .center
        l.textColor = .lightGray
        l.font = .systemFont(ofSize: 20, weight: .bold)
        return l
    }()
    
    override var isSelected: Bool {
        didSet {
            label.textColor = isSelected ? .black : .lightGray
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(label)
        label.fillSuperview()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 19:48:17