Article / 2026/05/29

UIScrollView 完全指南

🚀 UIScrollView 完全指南

目录


1. 简介

1.1 什么是 UIScrollView

UIScrollView 是 iOS 中用于显示大于屏幕尺寸内容的核心视图类。它提供了滚动、缩放、分页等功能,是 UITableViewUICollectionViewUITextView 等的基类。

1.2 基本创建和配置

// 基本创建
let scrollView = UIScrollView()
scrollView.frame = view.bounds
scrollView.contentSize = CGSize(width: view.bounds.width, height: 2000)
view.addSubview(scrollView)

// 使用 Auto Layout
scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
    scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])

2. 基本属性

2.1 框架和内容尺寸属性

属性类型说明默认值
frameCGRectScrollView 在父视图中的位置和大小CGRect.zero
boundsCGRectScrollView 的边界,origin 表示当前滚动位置CGRect.zero
contentSizeCGSize可滚动内容的总大小CGSize.zero
contentOffsetCGPoint当前滚动位置的偏移量CGPoint.zero
contentInsetUIEdgeInsets内容视图的额外滚动区域UIEdgeInsets.zero
adjustedContentInsetUIEdgeInsets系统调整后的内容边距(只读)-
safeAreaInsetsUIEdgeInsets安全区域边距(只读)-
contentInsetAdjustmentBehaviorContentInsetAdjustmentBehavior内容边距自动调整行为.automatic

2.2 使用示例

class ScrollViewBasicExample: UIViewController {
    let scrollView = UIScrollView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 设置 frame
        scrollView.frame = view.bounds
        
        // 设置内容大小(必须大于 frame 才能滚动)
        scrollView.contentSize = CGSize(width: view.bounds.width, height: 2000)
        
        // 设置初始滚动位置
        scrollView.contentOffset = CGPoint(x: 0, y: 100)
        
        // 设置内容边距(常用于适配导航栏或标签栏)
        scrollView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 50, right: 0)
        
        // 内容边距调整行为
        scrollView.contentInsetAdjustmentBehavior = .automatic
        // 可选值:
        // .automatic - 自动调整
        // .scrollableAxes - 仅在可滚动方向上调整
        // .never - 从不调整
        // .always - 始终调整
        
        view.addSubview(scrollView)
    }
}

3. 内容管理

3.1 内容布局属性

属性类型说明
contentLayoutGuideUILayoutGuide表示内容区域的布局指南
frameLayoutGuideUILayoutGuide表示 ScrollView frame 的布局指南
preservesSuperviewLayoutMarginsBool是否保留父视图的布局边距
layoutMarginsUIEdgeInsets布局边距

3.2 Auto Layout 使用

class AutoLayoutScrollView: UIViewController {
    let scrollView = UIScrollView()
    let contentView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupScrollView()
        setupContent()
    }
    
    func setupScrollView() {
        view.addSubview(scrollView)
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    func setupContent() {
        scrollView.addSubview(contentView)
        contentView.translatesAutoresizingMaskIntoConstraints = false
        
        // 使用 contentLayoutGuide 定义可滚动区域
        NSLayoutConstraint.activate([
            contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
            contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
            contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
            
            // 使用 frameLayoutGuide 定义内容宽度(防止水平滚动)
            contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
            
            // 设置内容高度
            contentView.heightAnchor.constraint(equalToConstant: 2000)
        ])
    }
}

4. 滚动行为

4.1 滚动控制属性

属性类型说明默认值
isScrollEnabledBool是否允许用户滚动true
isDirectionalLockEnabledBool是否锁定滚动到单一方向false
isPagingEnabledBool是否启用分页滚动false
scrollsToTopBool点击状态栏是否滚动到顶部true
bouncesBool是否有弹性效果true
alwaysBounceVerticalBool垂直方向始终有弹性false
alwaysBounceHorizontalBool水平方向始终有弹性false
canCancelContentTouchesBool是否可以取消内容触摸true
delaysContentTouchesBool是否延迟内容触摸true
decelerationRateUIScrollView.DecelerationRate减速率.normal

4.2 滚动状态属性(只读)

属性类型说明
isTrackingBool用户是否正在触摸屏幕
isDraggingBool用户是否正在拖动
isDeceleratingBool是否正在减速
isZoomingBool是否正在缩放
isZoomBouncingBool缩放是否正在弹性动画

4.3 滚动方法

class ScrollingMethodsExample {
    let scrollView = UIScrollView()
    
    // 滚动到指定位置
    func scrollToPosition() {
        // 方法1:直接设置 contentOffset
        scrollView.contentOffset = CGPoint(x: 0, y: 500)
        
        // 方法2:带动画的滚动
        scrollView.setContentOffset(CGPoint(x: 0, y: 500), animated: true)
    }
    
    // 滚动到指定矩形可见
    func scrollToRect() {
        let rect = CGRect(x: 100, y: 500, width: 200, height: 200)
        scrollView.scrollRectToVisible(rect, animated: true)
    }
    
    // 停止滚动
    func stopScrolling() {
        scrollView.setContentOffset(scrollView.contentOffset, animated: false)
    }
    
    // 闪烁滚动指示器
    func flashIndicators() {
        scrollView.flashScrollIndicators()
    }
}

4.4 分页滚动示例

class PagingScrollView: UIViewController {
    let scrollView = UIScrollView()
    let pageControl = UIPageControl()
    let numberOfPages = 3
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupPagingScrollView()
        setupPages()
    }
    
    func setupPagingScrollView() {
        scrollView.frame = view.bounds
        scrollView.isPagingEnabled = true
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.delegate = self
        view.addSubview(scrollView)
        
        // 设置内容大小为多个页面
        let pageWidth = scrollView.frame.width
        scrollView.contentSize = CGSize(
            width: pageWidth * CGFloat(numberOfPages),
            height: scrollView.frame.height
        )
        
        // 配置页面控制器
        pageControl.numberOfPages = numberOfPages
        pageControl.currentPage = 0
        pageControl.addTarget(self, action: #selector(pageControlChanged), for: .valueChanged)
        view.addSubview(pageControl)
    }
    
    func setupPages() {
        for i in 0..<numberOfPages {
            let pageView = UIView(frame: CGRect(
                x: scrollView.frame.width * CGFloat(i),
                y: 0,
                width: scrollView.frame.width,
                height: scrollView.frame.height
            ))
            pageView.backgroundColor = [UIColor.red, UIColor.green, UIColor.blue][i]
            scrollView.addSubview(pageView)
        }
    }
    
    @objc func pageControlChanged() {
        let pageWidth = scrollView.frame.width
        let offset = CGPoint(x: pageWidth * CGFloat(pageControl.currentPage), y: 0)
        scrollView.setContentOffset(offset, animated: true)
    }
}

extension PagingScrollView: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let pageWidth = scrollView.frame.width
        let currentPage = Int((scrollView.contentOffset.x + pageWidth / 2) / pageWidth)
        pageControl.currentPage = currentPage
    }
}

5. 滚动指示器

5.1 指示器属性

属性类型说明默认值
showsHorizontalScrollIndicatorBool显示水平滚动条true
showsVerticalScrollIndicatorBool显示垂直滚动条true
indicatorStyleUIScrollView.IndicatorStyle指示器样式.default
scrollIndicatorInsetsUIEdgeInsets滚动指示器边距.zero
horizontalScrollIndicatorInsetsUIEdgeInsets水平滚动指示器边距.zero
verticalScrollIndicatorInsetsUIEdgeInsets垂直滚动指示器边距.zero
automaticallyAdjustsScrollIndicatorInsetsBool自动调整滚动指示器边距true

5.2 使用示例

class ScrollIndicatorExample {
    let scrollView = UIScrollView()
    
    func configureIndicators() {
        // 隐藏滚动指示器
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false
        
        // 设置指示器样式
        scrollView.indicatorStyle = .white // .default, .black, .white
        
        // 设置指示器边距(避免被其他UI遮挡)
        scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 5)
        
        // 闪烁指示器(提示用户可以滚动)
        scrollView.flashScrollIndicators()
        
        // 自动调整指示器边距
        scrollView.automaticallyAdjustsScrollIndicatorInsets = true
    }
}

6. 缩放功能

6.1 缩放属性

属性类型说明默认值
minimumZoomScaleCGFloat最小缩放比例1.0
maximumZoomScaleCGFloat最大缩放比例1.0
zoomScaleCGFloat当前缩放比例1.0
bouncesZoomBool缩放时是否有弹性效果true

6.2 缩放方法

// 设置缩放比例
func setZoomScale(_ scale: CGFloat, animated: Bool)

// 缩放到指定矩形区域
func zoom(to rect: CGRect, animated: Bool)

6.3 完整的图片缩放示例

class ImageZoomScrollView: UIViewController {
    let scrollView = UIScrollView()
    let imageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupScrollView()
        setupImageView()
        setupGestures()
    }
    
    func setupScrollView() {
        scrollView.frame = view.bounds
        scrollView.delegate = self
        scrollView.minimumZoomScale = 0.5
        scrollView.maximumZoomScale = 3.0
        scrollView.bouncesZoom = true
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false
        view.addSubview(scrollView)
    }
    
    func setupImageView() {
        imageView.image = UIImage(named: "sample")
        imageView.contentMode = .scaleAspectFit
        imageView.frame = scrollView.bounds
        scrollView.addSubview(imageView)
        
        // 设置内容大小
        scrollView.contentSize = imageView.frame.size
    }
    
    func setupGestures() {
        // 双击缩放
        let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
        doubleTap.numberOfTapsRequired = 2
        scrollView.addGestureRecognizer(doubleTap)
    }
    
    @objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
        if scrollView.zoomScale > scrollView.minimumZoomScale {
            // 还原
            scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true)
        } else {
            // 放大到点击位置
            let location = gesture.location(in: imageView)
            let rect = CGRect(x: location.x - 50, y: location.y - 50, width: 100, height: 100)
            scrollView.zoom(to: rect, animated: true)
        }
    }
    
    func centerImage() {
        let offsetX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
        let offsetY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
        imageView.center = CGPoint(
            x: scrollView.contentSize.width * 0.5 + offsetX,
            y: scrollView.contentSize.height * 0.5 + offsetY
        )
    }
}

extension ImageZoomScrollView: UIScrollViewDelegate {
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return imageView
    }
    
    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        centerImage()
    }
}

7. 键盘管理

7.1 键盘相关属性

属性类型说明
keyboardDismissModeUIScrollView.KeyboardDismissMode键盘消失模式

7.2 键盘消失模式

// 键盘消失模式
public enum KeyboardDismissMode: Int {
    case none = 0           // 不自动消失
    case onDrag = 1         // 拖动时消失
    case interactive = 2    // 交互式消失(可以跟随手指)
}

7.3 键盘处理示例

class KeyboardHandlingScrollView: UIViewController {
    @IBOutlet weak var scrollView: UIScrollView!
    var activeTextField: UITextField?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupKeyboardHandling()
    }
    
    func setupKeyboardHandling() {
        // 设置键盘消失模式
        scrollView.keyboardDismissMode = .interactive
        
        // 监听键盘通知
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow),
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide),
            name: UIResponder.keyboardWillHideNotification,
            object: nil
        )
    }
    
    @objc func keyboardWillShow(notification: NSNotification) {
        guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
              let animationDuration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
        else { return }
        
        UIView.animate(withDuration: animationDuration) {
            self.scrollView.contentInset.bottom = keyboardFrame.height
            self.scrollView.verticalScrollIndicatorInsets.bottom = keyboardFrame.height
            
            // 滚动到活动文本框
            if let textField = self.activeTextField {
                let rect = textField.convert(textField.bounds, to: self.scrollView)
                self.scrollView.scrollRectToVisible(rect, animated: false)
            }
        }
    }
    
    @objc func keyboardWillHide(notification: NSNotification) {
        guard let animationDuration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
        else { return }
        
        UIView.animate(withDuration: animationDuration) {
            self.scrollView.contentInset.bottom = 0
            self.scrollView.verticalScrollIndicatorInsets.bottom = 0
        }
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

8. 刷新控制

8.1 刷新控件属性

属性类型说明
refreshControlUIRefreshControl?下拉刷新控件

8.2 下拉刷新实现

class RefreshableScrollView: UIViewController {
    let scrollView = UIScrollView()
    let refreshControl = UIRefreshControl()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupRefreshControl()
    }
    
    func setupRefreshControl() {
        // 配置刷新控件
        refreshControl.tintColor = .systemBlue
        refreshControl.attributedTitle = NSAttributedString(
            string: "下拉刷新",
            attributes: [.foregroundColor: UIColor.gray]
        )
        
        // 添加刷新事件
        refreshControl.addTarget(
            self,
            action: #selector(handleRefresh),
            for: .valueChanged
        )
        
        // 添加到 ScrollView
        scrollView.refreshControl = refreshControl
    }
    
    @objc func handleRefresh() {
        // 更新标题
        refreshControl.attributedTitle = NSAttributedString(
            string: "加载中...",
            attributes: [.foregroundColor: UIColor.gray]
        )
        
        // 模拟网络请求
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            // 更新数据
            self.loadData()
            
            // 结束刷新
            self.refreshControl.endRefreshing()
            
            // 恢复标题
            self.refreshControl.attributedTitle = NSAttributedString(
                string: "下拉刷新",
                attributes: [.foregroundColor: UIColor.gray]
            )
        }
    }
    
    func loadData() {
        // 加载数据的逻辑
    }
}

9. UIScrollViewDelegate 代理方法

9.1 滚动相关代理方法

protocol UIScrollViewDelegate: NSObjectProtocol {
    
    // MARK: - 滚动事件
    
    // 滚动时持续调用(高频调用,注意性能)
    optional func scrollViewDidScroll(_ scrollView: UIScrollView)
    
    // 将要开始拖动
    optional func scrollViewWillBeginDragging(_ scrollView: UIScrollView)
    
    // 将要结束拖动
    optional func scrollViewWillEndDragging(
        _ scrollView: UIScrollView,
        withVelocity velocity: CGPoint,
        targetContentOffset: UnsafeMutablePointer<CGPoint>
    )
    
    // 已经结束拖动
    optional func scrollViewDidEndDragging(
        _ scrollView: UIScrollView,
        willDecelerate decelerate: Bool
    )
    
    // 将要开始减速
    optional func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView)
    
    // 已经结束减速
    optional func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
    
    // 滚动动画结束(仅程序化滚动)
    optional func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView)
    
    // MARK: - 缩放事件
    
    // 返回要缩放的视图(必须实现才能缩放)
    optional func viewForZooming(in scrollView: UIScrollView) -> UIView?
    
    // 将要开始缩放
    optional func scrollViewWillBeginZooming(
        _ scrollView: UIScrollView,
        with view: UIView?
    )
    
    // 缩放时持续调用
    optional func scrollViewDidZoom(_ scrollView: UIScrollView)
    
    // 缩放结束
    optional func scrollViewDidEndZooming(
        _ scrollView: UIScrollView,
        with view: UIView?,
        atScale scale: CGFloat
    )
    
    // MARK: - 滚动到顶部
    
    // 是否允许点击状态栏滚动到顶部
    optional func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool
    
    // 已经滚动到顶部
    optional func scrollViewDidScrollToTop(_ scrollView: UIScrollView)
    
    // MARK: - 内容偏移和边距
    
    // 内容偏移改变时调用(iOS 11+)
    optional func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView)
}

9.2 代理方法使用示例

class ScrollViewDelegateExample: UIViewController {
    let scrollView = UIScrollView()
    var lastContentOffset: CGPoint = .zero
    
    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.delegate = self
    }
}

extension ScrollViewDelegateExample: UIScrollViewDelegate {
    
    // 检测滚动方向
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let currentOffset = scrollView.contentOffset
        
        if currentOffset.y > lastContentOffset.y {
            print("向上滚动")
        } else if currentOffset.y < lastContentOffset.y {
            print("向下滚动")
        }
        
        lastContentOffset = currentOffset
        
        // 计算滚动进度
        let progress = scrollView.contentOffset.y / 
            (scrollView.contentSize.height - scrollView.frame.height)
        print("滚动进度: \(progress * 100)%")
    }
    
    // 自定义减速目标位置(实现自定义分页)
    func scrollViewWillEndDragging(
        _ scrollView: UIScrollView,
        withVelocity velocity: CGPoint,
        targetContentOffset: UnsafeMutablePointer<CGPoint>
    ) {
        let pageWidth: CGFloat = 200 // 自定义页面宽度
        let currentPage = round(targetContentOffset.pointee.x / pageWidth)
        targetContentOffset.pointee.x = currentPage * pageWidth
    }
    
    // 判断是否停止滚动
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if !decelerate {
            print("滚动停止(无减速)")
            scrollingDidStop()
        }
    }
    
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        print("滚动停止(减速结束)")
        scrollingDidStop()
    }
    
    func scrollingDidStop() {
        // 滚动停止后的处理
    }
    
    // 缩放相关
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return scrollView.subviews.first
    }
    
    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        print("当前缩放比例: \(scrollView.zoomScale)")
    }
}

10. 手势识别

10.1 内置手势识别器

// UIScrollView 内置的手势识别器
let panGesture = scrollView.panGestureRecognizer           // 拖动手势
let pinchGesture = scrollView.pinchGestureRecognizer       // 缩放手势(可选)

// 访问手势状态
if scrollView.panGestureRecognizer.state == .changed {
    let translation = scrollView.panGestureRecognizer.translation(in: scrollView)
    let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
}

10.2 手势冲突处理

class GestureHandlingScrollView: UIScrollView {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupGestures()
    }
    
    func setupGestures() {
        // 添加自定义手势
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
        tapGesture.delegate = self
        addGestureRecognizer(tapGesture)
        
        // 设置手势优先级
        tapGesture.require(toFail: panGestureRecognizer)
    }
    
    @objc func handleTap(_ gesture: UITapGestureRecognizer) {
        print("ScrollView 被点击")
    }
}

extension GestureHandlingScrollView: UIGestureRecognizerDelegate {
    
    // 允许多个手势同时识别
    func gestureRecognizer(
        _ gestureRecognizer: UIGestureRecognizer,
        shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
    ) -> Bool {
        return true
    }
    
    // 控制手势是否应该开始
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        // 可以根据条件决定是否响应手势
        if gestureRecognizer is UIPanGestureRecognizer {
            let velocity = panGestureRecognizer.velocity(in: self)
            // 只响应垂直滑动
            return abs(velocity.y) > abs(velocity.x)
        }
        return true
    }
    
    // 处理手势冲突
    func gestureRecognizer(
        _ gestureRecognizer: UIGestureRecognizer,
        shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer
    ) -> Bool {
        // 如果是嵌套的 ScrollView,优先处理内部的
        if otherGestureRecognizer.view is UIScrollView {
            return true
        }
        return false
    }
}

---

## 11. 实战示例

### 11.1 图片浏览器(支持缩放和分页)

```swift
class PhotoBrowser: UIViewController {
    private var scrollView: UIScrollView!
    private var pageControl: UIPageControl!
    private var images: [UIImage] = []
    private var imageScrollViews: [UIScrollView] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupMainScrollView()
        setupPageControl()
        loadImages()
    }
    
    private func setupMainScrollView() {
        scrollView = UIScrollView(frame: view.bounds)
        scrollView.isPagingEnabled = true
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.delegate = self
        view.addSubview(scrollView)
        
        // 设置内容大小
        let numberOfImages = 3
        scrollView.contentSize = CGSize(
            width: view.bounds.width * CGFloat(numberOfImages),
            height: view.bounds.height
        )
    }
    
    private func setupPageControl() {
        pageControl = UIPageControl()
        pageControl.numberOfPages = images.count
        pageControl.currentPage = 0
        pageControl.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(pageControl)
        
        NSLayoutConstraint.activate([
            pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            pageControl.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20)
        ])
    }
    
    private func loadImages() {
        for (index, image) in images.enumerated() {
            let imageScrollView = createImageScrollView(image: image, index: index)
            scrollView.addSubview(imageScrollView)
            imageScrollViews.append(imageScrollView)
        }
    }
    
    private func createImageScrollView(image: UIImage, index: Int) -> UIScrollView {
        let frame = CGRect(
            x: view.bounds.width * CGFloat(index),
            y: 0,
            width: view.bounds.width,
            height: view.bounds.height
        )
        
        let imageScrollView = UIScrollView(frame: frame)
        imageScrollView.minimumZoomScale = 1.0
        imageScrollView.maximumZoomScale = 3.0
        imageScrollView.delegate = self
        
        let imageView = UIImageView(image: image)
        imageView.contentMode = .scaleAspectFit
        imageView.frame = imageScrollView.bounds
        imageView.tag = index + 1000
        
        imageScrollView.addSubview(imageView)
        
        // 双击缩放
        let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
        doubleTap.numberOfTapsRequired = 2
        imageScrollView.addGestureRecognizer(doubleTap)
        
        return imageScrollView
    }
    
    @objc private func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
        guard let scrollView = gesture.view as? UIScrollView else { return }
        
        if scrollView.zoomScale > 1.0 {
            scrollView.setZoomScale(1.0, animated: true)
        } else {
            let location = gesture.location(in: scrollView)
            let rect = CGRect(x: location.x - 50, y: location.y - 50, width: 100, height: 100)
            scrollView.zoom(to: rect, animated: true)
        }
    }
}

extension PhotoBrowser: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView == self.scrollView {
            let pageIndex = round(scrollView.contentOffset.x / view.bounds.width)
            pageControl.currentPage = Int(pageIndex)
        }
    }
    
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        if scrollView != self.scrollView {
            return scrollView.subviews.first
        }
        return nil
    }
}

11.2 无限滚动列表

class InfiniteScrollView: UIViewController {
    @IBOutlet weak var scrollView: UIScrollView!
    private var items: [String] = []
    private var isLoading = false
    private let itemHeight: CGFloat = 100
    
    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.delegate = self
        loadInitialData()
    }
    
    private func loadInitialData() {
        // 加载初始数据
        for i in 0..<20 {
            items.append("Item \(i)")
        }
        updateScrollViewContent()
    }
    
    private func loadMoreData() {
        guard !isLoading else { return }
        
        isLoading = true
        
        // 模拟网络加载
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
            let currentCount = self.items.count
            for i in currentCount..<(currentCount + 20) {
                self.items.append("Item \(i)")
            }
            self.updateScrollViewContent()
            self.isLoading = false
        }
    }
    
    private func updateScrollViewContent() {
        // 清除旧视图
        scrollView.subviews.forEach { $0.removeFromSuperview() }
        
        // 添加新视图
        for (index, item) in items.enumerated() {
            let itemView = createItemView(text: item, index: index)
            scrollView.addSubview(itemView)
        }
        
        // 更新内容大小
        scrollView.contentSize = CGSize(
            width: scrollView.frame.width,
            height: CGFloat(items.count) * itemHeight
        )
    }
    
    private func createItemView(text: String, index: Int) -> UIView {
        let frame = CGRect(
            x: 0,
            y: CGFloat(index) * itemHeight,
            width: scrollView.frame.width,
            height: itemHeight
        )
        
        let view = UIView(frame: frame)
        view.backgroundColor = index % 2 == 0 ? .systemGray6 : .systemGray5
        
        let label = UILabel(frame: view.bounds.insetBy(dx: 20, dy: 0))
        label.text = text
        view.addSubview(label)
        
        return view
    }
}

extension InfiniteScrollView: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let offsetY = scrollView.contentOffset.y
        let contentHeight = scrollView.contentSize.height
        let scrollViewHeight = scrollView.frame.height
        
        // 当滚动到底部附近时加载更多
        if offsetY > contentHeight - scrollViewHeight - 200 {
            loadMoreData()
        }
    }
}

11.3 视差滚动效果(Parallax Effect)

class ParallaxScrollViewController: UIViewController {
    private var scrollView: UIScrollView!
    private var headerImageView: UIImageView!
    private var contentView: UIView!
    private let headerHeight: CGFloat = 300
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupScrollView()
        setupHeader()
        setupContent()
    }
    
    private func setupScrollView() {
        scrollView = UIScrollView(frame: view.bounds)
        scrollView.contentInsetAdjustmentBehavior = .never
        scrollView.delegate = self
        view.addSubview(scrollView)
    }
    
    private func setupHeader() {
        headerImageView = UIImageView(frame: CGRect(
            x: 0,
            y: 0,
            width: view.bounds.width,
            height: headerHeight
        ))
        headerImageView.image = UIImage(named: "header")
        headerImageView.contentMode = .scaleAspectFill
        headerImageView.clipsToBounds = true
        view.addSubview(headerImageView) // 注意:添加到 view 而不是 scrollView
    }
    
    private func setupContent() {
        contentView = UIView(frame: CGRect(
            x: 0,
            y: 0,
            width: view.bounds.width,
            height: 1000
        ))
        contentView.backgroundColor = .white
        
        // 添加内容到 contentView
        let label = UILabel(frame: CGRect(x: 20, y: headerHeight + 20, width: view.bounds.width - 40, height: 40))
        label.text = "Scroll for parallax effect"
        label.font = .boldSystemFont(ofSize: 24)
        contentView.addSubview(label)
        
        scrollView.addSubview(contentView)
        scrollView.contentSize = contentView.frame.size
        
        // 设置初始 contentInset
        scrollView.contentInset = UIEdgeInsets(top: headerHeight, left: 0, bottom: 0, right: 0)
        scrollView.contentOffset = CGPoint(x: 0, y: -headerHeight)
    }
}

extension ParallaxScrollViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let offsetY = scrollView.contentOffset.y
        
        if offsetY < -headerHeight {
            // 下拉放大效果
            let scaleFactor = abs(offsetY + headerHeight) / headerHeight
            let scale = 1 + scaleFactor
            
            headerImageView.transform = CGAffineTransform(scaleX: scale, y: scale)
                .concatenating(CGAffineTransform(translationX: 0, y: offsetY))
        } else {
            // 视差滚动效果
            let parallaxFactor: CGFloat = 0.5
            let translateY = offsetY * parallaxFactor
            
            headerImageView.transform = CGAffineTransform(translationX: 0, y: translateY)
        }
    }
}

11.4 聊天界面(自动滚动到底部)

class ChatScrollView: UIViewController {
    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var inputTextField: UITextField!
    @IBOutlet weak var bottomConstraint: NSLayoutConstraint!
    
    private var messages: [String] = []
    private let messageHeight: CGFloat = 60
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupKeyboardObservers()
        setupScrollView()
    }
    
    private func setupScrollView() {
        scrollView.keyboardDismissMode = .interactive
        
        // 添加点击手势隐藏键盘
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
        scrollView.addGestureRecognizer(tapGesture)
    }
    
    private func setupKeyboardObservers() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow),
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide),
            name: UIResponder.keyboardWillHideNotification,
            object: nil
        )
    }
    
    @objc private func keyboardWillShow(notification: NSNotification) {
        guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
              let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
        else { return }
        
        bottomConstraint.constant = keyboardFrame.height
        
        UIView.animate(withDuration: duration) {
            self.view.layoutIfNeeded()
            self.scrollToBottom()
        }
    }
    
    @objc private func keyboardWillHide(notification: NSNotification) {
        guard let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
        else { return }
        
        bottomConstraint.constant = 0
        
        UIView.animate(withDuration: duration) {
            self.view.layoutIfNeeded()
        }
    }
    
    @objc private func dismissKeyboard() {
        view.endEditing(true)
    }
    
    @IBAction func sendMessage(_ sender: Any) {
        guard let text = inputTextField.text, !text.isEmpty else { return }
        
        messages.append(text)
        addMessageView(text: text)
        inputTextField.text = ""
        
        scrollToBottom()
    }
    
    private func addMessageView(text: String) {
        let messageView = createMessageView(text: text, index: messages.count - 1)
        scrollView.addSubview(messageView)
        
        // 更新内容大小
        scrollView.contentSize = CGSize(
            width: scrollView.frame.width,
            height: CGFloat(messages.count) * messageHeight
        )
    }
    
    private func createMessageView(text: String, index: Int) -> UIView {
        let frame = CGRect(
            x: 10,
            y: CGFloat(index) * messageHeight + 10,
            width: scrollView.frame.width - 20,
            height: messageHeight - 10
        )
        
        let messageView = UIView(frame: frame)
        messageView.backgroundColor = .systemBlue
        messageView.layer.cornerRadius = 10
        
        let label = UILabel(frame: messageView.bounds.insetBy(dx: 10, dy: 5))
        label.text = text
        label.textColor = .white
        messageView.addSubview(label)
        
        return messageView
    }
    
    private func scrollToBottom() {
        let bottomOffset = CGPoint(
            x: 0,
            y: max(scrollView.contentSize.height - scrollView.bounds.height, 0)
        )
        scrollView.setContentOffset(bottomOffset, animated: true)
    }
}

12. 性能优化

12.1 视图复用机制

class ReusableScrollView: UIScrollView {
    private var visibleViews: Set<UIView> = []
    private var reusableViews: Set<UIView> = []
    private let viewHeight: CGFloat = 100
    private var totalItems: Int = 1000
    
    override func layoutSubviews() {
        super.layoutSubviews()
        recycleViews()
        displayVisibleViews()
    }
    
    private func recycleViews() {
        // 计算可见区域
        let visibleRect = CGRect(
            x: contentOffset.x,
            y: contentOffset.y,
            width: bounds.width,
            height: bounds.height
        )
        
        // 回收不可见的视图
        for view in visibleViews {
            if !visibleRect.intersects(view.frame) {
                view.removeFromSuperview()
                visibleViews.remove(view)
                reusableViews.insert(view)
            }
        }
    }
    
    private func displayVisibleViews() {
        // 计算可见的索引范围
        let firstVisibleIndex = max(0, Int(contentOffset.y / viewHeight))
        let lastVisibleIndex = min(totalItems - 1, Int((contentOffset.y + bounds.height) / viewHeight))
        
        // 显示可见范围内的视图
        for index in firstVisibleIndex...lastVisibleIndex {
            let viewFrame = CGRect(
                x: 0,
                y: CGFloat(index) * viewHeight,
                width: bounds.width,
                height: viewHeight
            )
            
            // 检查是否已经显示
            let isDisplayed = visibleViews.contains { $0.frame == viewFrame }
            
            if !isDisplayed {
                let view = dequeueReusableView() ?? createNewView()
                view.frame = viewFrame
                configureView(view, at: index)
                addSubview(view)
                visibleViews.insert(view)
            }
        }
    }
    
    private func dequeueReusableView() -> UIView? {
        guard let view = reusableViews.first else { return nil }
        reusableViews.remove(view)
        return view
    }
    
    private func createNewView() -> UIView {
        let view = UIView()
        // 配置视图
        return view
    }
    
    private func configureView(_ view: UIView, at index: Int) {
        // 配置视图内容
        view.backgroundColor = index % 2 == 0 ? .systemGray6 : .white
    }
}

12.2 优化滚动性能

class OptimizedScrollViewDelegate: NSObject, UIScrollViewDelegate {
    private var lastUpdateTime: TimeInterval = 0
    private let updateInterval: TimeInterval = 0.1 // 限制更新频率
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let currentTime = CACurrentMediaTime()
        
        // 限制更新频率
        guard currentTime - lastUpdateTime > updateInterval else { return }
        lastUpdateTime = currentTime
        
        // 异步处理重操作
        DispatchQueue.global(qos: .userInitiated).async {
            // 执行计算密集型操作
            let result = self.performHeavyCalculation(scrollView.contentOffset)
            
            DispatchQueue.main.async {
                // 更新UI
                self.updateUI(with: result)
            }
        }
    }
    
    private func performHeavyCalculation(_ offset: CGPoint) -> String {
        // 模拟耗时操作
        return "Offset: \(offset)"
    }
    
    private func updateUI(with result: String) {
        // 更新界面
    }
}

13. 常见问题与解决方案

13.1 ScrollView 不能滚动

// 问题诊断和解决
class ScrollViewTroubleshooting {
    
    func diagnoseScrollingIssue(scrollView: UIScrollView) {
        print("=== ScrollView 诊断 ===")
        print("Frame: \(scrollView.frame)")
        print("ContentSize: \(scrollView.contentSize)")
        print("ContentOffset: \(scrollView.contentOffset)")
        print("IsScrollEnabled: \(scrollView.isScrollEnabled)")
        print("ContentInset: \(scrollView.contentInset)")
        print("Subviews count: \(scrollView.subviews.count)")
        
        // 常见问题检查
        if scrollView.contentSize.height <= scrollView.frame.height &&
           scrollView.contentSize.width <= scrollView.frame.width {
            print("❌ 问题: ContentSize 小于或等于 frame,无法滚动")
            print("✅ 解决: 确保 contentSize 大于 frame")
        }
        
        if !scrollView.isScrollEnabled {
            print("❌ 问题: isScrollEnabled 为 false")
            print("✅ 解决: scrollView.isScrollEnabled = true")
        }
        
        if scrollView.isUserInteractionEnabled == false {
            print("❌ 问题: 用户交互被禁用")
            print("✅ 解决: scrollView.isUserInteractionEnabled = true")
        }
    }
    
    // 解决方案示例
    func fixCommonIssues(scrollView: UIScrollView) {
        // 1. 确保 contentSize 正确
        var maxRect = CGRect.zero
        for subview in scrollView.subviews {
            maxRect = maxRect.union(subview.frame)
        }
        scrollView.contentSize = maxRect.size
        
        // 2. 启用滚动
        scrollView.isScrollEnabled = true
        
        // 3. 启用用户交互
        scrollView.isUserInteractionEnabled = true
        
        // 4. 设置正确的 contentInset(如果需要)
        if #available(iOS 11.0, *) {
            scrollView.contentInsetAdjustmentBehavior = .automatic
        }
    }
}

13.2 Auto Layout 中的 ScrollView

class AutoLayoutScrollViewFix {
    
    func setupScrollViewWithAutoLayout(in viewController: UIViewController) {
        let scrollView = UIScrollView()
        let contentView = UIView()
        
        viewController.view.addSubview(scrollView)
        scrollView.addSubview(contentView)
        
        // 关闭 autoresizing mask
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        contentView.translatesAutoresizingMaskIntoConstraints = false
        
        // ScrollView 约束
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: viewController.view.safeAreaLayoutGuide.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor)
        ])
        
        // ContentView 约束 - 关键!
        NSLayoutConstraint.activate([
            // 定义可滚动区域
            contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
            contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
            contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
            
            // 定义内容宽度(防止水平滚动)
            contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
            
            // 定义内容高度(或让内容决定高度)
            contentView.heightAnchor.constraint(equalToConstant: 2000)
        ])
    }
}

13.3 嵌套 ScrollView 处理

class NestedScrollViewHandler: UIViewController {
    @IBOutlet weak var outerScrollView: UIScrollView!
    @IBOutlet weak var innerScrollView: UIScrollView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupNestedScrollViews()
    }
    
    func setupNestedScrollViews() {
        outerScrollView.delegate = self
        innerScrollView.delegate = self
        
        // 设置手势识别优先级
        outerScrollView.panGestureRecognizer.require(toFail: innerScrollView.panGestureRecognizer)
    }
}

extension NestedScrollViewHandler: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView == innerScrollView {
            // 内部滚动到顶部或底部时,允许外部滚动
            let isAtTop = scrollView.contentOffset.y <= 0
            let isAtBottom = scrollView.contentOffset.y >= 
                (scrollView.contentSize.height - scrollView.frame.height)
            
            outerScrollView.isScrollEnabled = isAtTop || isAtBottom
        }
    }
}

14. 最佳实践

14.1 设计模式

// MARK: - ScrollView 工厂模式
class ScrollViewFactory {
    
    static func createVerticalScrollView(in view: UIView) -> UIScrollView {
        let scrollView = UIScrollView()
        scrollView.showsVerticalScrollIndicator = true
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.alwaysBounceVertical = true
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(scrollView)
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        return scrollView
    }
    
    static func createPagedScrollView(numberOfPages: Int, in view: UIView) -> UIScrollView {
        let scrollView = UIScrollView(frame: view.bounds)
        scrollView.isPagingEnabled = true
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.contentSize = CGSize(
            width: view.bounds.width * CGFloat(numberOfPages),
            height: view.bounds.height
        )
        return scrollView
    }
}

14.2 扩展和工具方法

// MARK: - 有用的 ScrollView 扩展
extension UIScrollView {
    
    // 滚动到顶部
    func scrollToTop(animated: Bool = true) {
        let topOffset = CGPoint(x: 0, y: -contentInset.top)
        setContentOffset(topOffset, animated: animated)
    }
    
    // 滚动到底部
    func scrollToBottom(animated: Bool = true) {
        let bottomOffset = CGPoint(
            x: 0,
            y: contentSize.height - bounds.height + contentInset.bottom
        )
        if bottomOffset.y > 0 {
            setContentOffset(bottomOffset, animated: animated)
        }
    }
    
    // 是否已滚动到底部
    var isAtBottom: Bool {
        return contentOffset.y >= (contentSize.height - frame.height - 10)
    }
    
    // 是否已滚动到顶部
    var isAtTop: Bool {
        return contentOffset.y <= -contentInset.top
    }
    
    // 可见区域
    var visibleRect: CGRect {
        return CGRect(
            x: contentOffset.x,
            y: contentOffset.y,
            width: bounds.width,
            height: bounds.height
        )
    }
    
    // 更新内容大小以适应所有子视图
    func updateContentSizeToFitSubviews(padding: UIEdgeInsets = .zero) {
        var contentRect = CGRect.zero
        
        for view in subviews {
            contentRect = contentRect.union(view.frame)
        }
        
        contentSize = CGSize(
            width: contentRect.width + padding.left + padding.right,
            height: contentRect.height + padding.top + padding.bottom
        )
    }
    
    // 缩放到适应内容
    func zoomToFit(animated: Bool = true) {
        guard let delegate = delegate,
              let view = delegate.viewForZooming?(in: self) else { return }
        
        let widthScale = bounds.width / view.bounds.width
        let heightScale = bounds.height / view.bounds.height
        let scale = min(widthScale, heightScale)
        
        setZoomScale(scale, animated: animated)
    }
}

14.3 调试助手

// MARK: - ScrollView 调试
#if DEBUG
extension UIScrollView {
    
    func enableDebugMode() {
        layer.borderWidth = 2
        layer.borderColor = UIColor.red.cgColor
        
        // 显示内容区域
        let contentView = UIView(frame: CGRect(origin: .zero, size: contentSize))
        contentView.layer.borderWidth = 2
        contentView.layer.borderColor = UIColor.blue.cgColor
        contentView.backgroundColor = UIColor.blue.withAlphaComponent(0.1)
        contentView.isUserInteractionEnabled = false
        insertSubview(contentView, at: 0)
        
        // 添加调试信息视图
        addDebugInfoView()
    }
    
    private func addDebugInfoView() {
        let debugLabel = UILabel()
        debugLabel.backgroundColor = UIColor.black.withAlphaComponent(0.7)
        debugLabel.textColor = .white
        debugLabel.font = .systemFont(ofSize: 12)
        debugLabel.numberOfLines = 0
        debugLabel.translatesAutoresizingMaskIntoConstraints = false
        
        addSubview(debugLabel)
        NSLayoutConstraint.activate([
            debugLabel.topAnchor.constraint(equalTo: topAnchor, constant: 50),
            debugLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
            debugLabel.widthAnchor.constraint(equalToConstant: 200)
        ])
        
        // 更新调试信息
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
            debugLabel.text = """
            Offset: \(String(format: "%.1f, %.1f", self.contentOffset.x, self.contentOffset.y))
            Content: \(String(format: "%.0f x %.0f", self.contentSize.width, self.contentSize.height))
            Frame: \(String(format: "%.0f x %.0f", self.frame.width, self.frame.height))
            Zoom: \(String(format: "%.2f", self.zoomScale))
            """
        }
    }
    
    func printDebugInfo() {
        print("""
        === UIScrollView Debug Info ===
        Frame: \(frame)
        Bounds: \(bounds)
        ContentSize: \(contentSize)
        ContentOffset: \(contentOffset)
        ContentInset: \(contentInset)
        AdjustedContentInset: \(adjustedContentInset)
        ZoomScale: \(zoomScale) (min: \(minimumZoomScale), max: \(maximumZoomScale))
        IsScrollEnabled: \(isScrollEnabled)
        IsPagingEnabled: \(isPagingEnabled)
        IsScrolling: \(isDragging || isDecelerating)
        Subviews Count: \(subviews.count)
        ==============================
        """)
    }
}
#endif

14.4 内存管理最佳实践

class MemoryEfficientScrollView: UIViewController {
    weak var scrollView: UIScrollView!
    private var imageViews: [UIImageView] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupScrollView()
    }
    
    private func setupScrollView() {
        let scrollView = UIScrollView()
        self.scrollView = scrollView
        view.addSubview(scrollView)
        
        // 设置内存警告处理
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleMemoryWarning),
            name: UIApplication.didReceiveMemoryWarningNotification,
            object: nil
        )
    }
    
    @objc private func handleMemoryWarning() {
        // 清理不可见的图片
        let visibleRect = scrollView.visibleRect
        
        for imageView in imageViews {
            if !visibleRect.intersects(imageView.frame) {
                imageView.image = nil // 释放图片内存
            }
        }
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
        // 清理资源
        imageViews.forEach { $0.image = nil }
        imageViews.removeAll()
    }
}

14.5 响应式设计

class ResponsiveScrollView: UIViewController {
    @IBOutlet weak var scrollView: UIScrollView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupResponsiveLayout()
    }
    
    private func setupResponsiveLayout() {
        // 监听设备方向变化
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(orientationChanged),
            name: UIDevice.orientationDidChangeNotification,
            object: nil
        )
    }
    
    @objc private func orientationChanged() {
        // 重新计算布局
        updateScrollViewLayout()
    }
    
    private func updateScrollViewLayout() {
        let isLandscape = UIDevice.current.orientation.isLandscape
        
        // 根据方向调整内容
        if isLandscape {
            // 横屏布局
            scrollView.contentSize = CGSize(
                width: view.bounds.width * 2,
                height: view.bounds.height
            )
            scrollView.isPagingEnabled = true
        } else {
            // 竖屏布局
            scrollView.contentSize = CGSize(
                width: view.bounds.width,
                height: view.bounds.height * 2
            )
            scrollView.isPagingEnabled = false
        }
        
        // 重新排列子视图
        rearrangeSubviews()
    }
    
    private func rearrangeSubviews() {
        // 实现子视图重新排列逻辑
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        
        coordinator.animate(alongsideTransition: { _ in
            self.updateScrollViewLayout()
        }, completion: nil)
    }
}

15. 高级技巧

15.1 自定义滚动行为

class CustomScrollBehavior: UIScrollView {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupCustomBehavior()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupCustomBehavior()
    }
    
    private func setupCustomBehavior() {
        decelerationRate = UIScrollView.DecelerationRate(rawValue: 0.992) // 自定义减速率
    }
    
    // 自定义滚动到最近的页面
    func scrollToNearestPage() {
        let pageWidth: CGFloat = 100 // 自定义页面宽度
        let currentPage = round(contentOffset.x / pageWidth)
        let targetX = currentPage * pageWidth
        
        setContentOffset(CGPoint(x: targetX, y: contentOffset.y), animated: true)
    }
    
    // 实现弹性边界
    override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
        var adjustedOffset = contentOffset
        
        // 添加弹性限制
        let maxX = max(0, contentSize.width - bounds.width)
        let maxY = max(0, contentSize.height - bounds.height)
        
        adjustedOffset.x = min(max(0, adjustedOffset.x), maxX)
        adjustedOffset.y = min(max(0, adjustedOffset.y), maxY)
        
        super.setContentOffset(adjustedOffset, animated: animated)
    }
}

15.2 自定义滚动指示器

class CustomScrollIndicator: UIView {
    weak var scrollView: UIScrollView?
    private let indicatorView = UIView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupIndicator()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupIndicator()
    }
    
    private func setupIndicator() {
        // 设置指示器样式
        indicatorView.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.7)
        indicatorView.layer.cornerRadius = 2
        addSubview(indicatorView)
        
        // 隐藏系统指示器
        scrollView?.showsVerticalScrollIndicator = false
    }
    
    func updateIndicator() {
        guard let scrollView = scrollView else { return }
        
        let contentHeight = scrollView.contentSize.height
        let scrollViewHeight = scrollView.bounds.height
        
        guard contentHeight > scrollViewHeight else {
            indicatorView.isHidden = true
            return
        }
        
        indicatorView.isHidden = false
        
        // 计算指示器高度
        let indicatorHeight = (scrollViewHeight / contentHeight) * bounds.height
        
        // 计算指示器位置
        let progress = scrollView.contentOffset.y / (contentHeight - scrollViewHeight)
        let indicatorY = progress * (bounds.height - indicatorHeight)
        
        // 更新指示器 frame
        indicatorView.frame = CGRect(
            x: bounds.width - 6,
            y: indicatorY,
            width: 4,
            height: indicatorHeight
        )
    }
}

15.3 智能加载和预加载

class SmartLoadingScrollView: UIViewController {
    @IBOutlet weak var scrollView: UIScrollView!
    private var loadedIndexes: Set<Int> = []
    private let itemHeight: CGFloat = 100
    private let preloadOffset: CGFloat = 200 // 预加载距离
    
    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.delegate = self
    }
}

extension SmartLoadingScrollView: UIScrollViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        loadVisibleContent()
        preloadContent()
    }
    
    private func loadVisibleContent() {
        let visibleRect = scrollView.visibleRect
        let firstIndex = max(0, Int(visibleRect.minY / itemHeight))
        let lastIndex = Int(visibleRect.maxY / itemHeight)
        
        for index in firstIndex...lastIndex {
            loadItemIfNeeded(at: index)
        }
    }
    
    private func preloadContent() {
        let preloadRect = CGRect(
            x: scrollView.contentOffset.x,
            y: scrollView.contentOffset.y - preloadOffset,
            width: scrollView.bounds.width,
            height: scrollView.bounds.height + preloadOffset * 2
        )
        
        let firstIndex = max(0, Int(preloadRect.minY / itemHeight))
        let lastIndex = Int(preloadRect.maxY / itemHeight)
        
        for index in firstIndex...lastIndex {
            preloadItemIfNeeded(at: index)
        }
    }
    
    private func loadItemIfNeeded(at index: Int) {
        guard !loadedIndexes.contains(index) else { return }
        
        // 加载内容
        let itemView = createItemView(at: index)
        scrollView.addSubview(itemView)
        loadedIndexes.insert(index)
    }
    
    private func preloadItemIfNeeded(at index: Int) {
        // 预加载逻辑(如预下载图片)
        DispatchQueue.global(qos: .utility).async {
            // 预加载操作
        }
    }
    
    private func createItemView(at index: Int) -> UIView {
        let frame = CGRect(
            x: 0,
            y: CGFloat(index) * itemHeight,
            width: scrollView.bounds.width,
            height: itemHeight
        )
        
        let view = UIView(frame: frame)
        // 配置视图
        return view
    }
}

16. SwiftUI 集成

16.1 UIScrollView 在 SwiftUI 中使用

import SwiftUI

struct ScrollViewWrapper: UIViewRepresentable {
    @Binding var contentOffset: CGPoint
    let content: () -> UIView
    
    func makeUIView(context: Context) -> UIScrollView {
        let scrollView = UIScrollView()
        scrollView.delegate = context.coordinator
        
        let contentView = content()
        scrollView.addSubview(contentView)
        scrollView.contentSize = contentView.bounds.size
        
        return scrollView
    }
    
    func updateUIView(_ scrollView: UIScrollView, context: Context) {
        scrollView.contentOffset = contentOffset
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, UIScrollViewDelegate {
        var parent: ScrollViewWrapper
        
        init(_ parent: ScrollViewWrapper) {
            self.parent = parent
        }
        
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            parent.contentOffset = scrollView.contentOffset
        }
    }
}

// SwiftUI 中使用
struct ContentView: View {
    @State private var scrollOffset = CGPoint.zero
    
    var body: some View {
        ScrollViewWrapper(contentOffset: $scrollOffset) {
            // 创建 UIView 内容
            let view = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 1000))
            view.backgroundColor = .systemBlue
            return view
        }
        .overlay(
            Text("Offset: \(Int(scrollOffset.y))")
                .padding()
                .background(Color.white.opacity(0.8))
                .cornerRadius(8)
                .padding(),
            alignment: .topTrailing
        )
    }
}

17. PagedHorizontalScrollView 可以水平分页滚动,可以禁止触摸滚动


import SwiftUI
//
//  PagedHorizontalScrollView.swift
//  wscanner
//
//  Created by yunshen on 2025/9/16.
//
import UIKit

public struct PagedHorizontalScrollView: UIViewRepresentable {
    var selectedTab: Binding<Int>
    let views: [AnyView]
    let isScrollEnabled: Bool
    private var onPageSelected: ((Int) -> Void)? = nil
    
    public init(selectedTab: Binding<Int>, views: [AnyView], isScrollEnabled: Bool, onPageSelected: ((Int) -> Void)? = nil) {
        self.selectedTab = selectedTab
        self.views = views
        self.isScrollEnabled = isScrollEnabled
        self.onPageSelected = onPageSelected
    }

    public func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    public func makeUIView(context: Context) -> UIScrollView {
        let scrollView = UIScrollView()
        scrollView.isPagingEnabled = true
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.bounces = true
        scrollView.backgroundColor = .clear
        scrollView.isScrollEnabled = isScrollEnabled
        scrollView.delegate = context.coordinator

        // 创建容器视图
        let stackView = UIStackView()
        stackView.axis = .horizontal
        stackView.distribution = .fillEqually
        stackView.alignment = .fill
        stackView.spacing = 0
        stackView.backgroundColor = .clear
        stackView.translatesAutoresizingMaskIntoConstraints = false

        scrollView.addSubview(stackView)

        // 添加约束
        NSLayoutConstraint.activate([
            stackView.leadingAnchor.constraint(
                equalTo: scrollView.leadingAnchor
            ),
            stackView.trailingAnchor.constraint(
                equalTo: scrollView.trailingAnchor
            ),
            stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor),
        ])

        // 保存引用
        context.coordinator.stackView = stackView
        context.coordinator.scrollView = scrollView

        // 添加视图
        context.coordinator.setupViews()
        return scrollView
    }

    public func updateUIView(_ scrollView: UIScrollView, context: Context) {
        // 检查是否需要更新滚动位置
        let width = scrollView.bounds.width
        if width > 0 {
            let targetOffset = CGFloat(selectedTab.wrappedValue) * width
            let currentOffset = scrollView.contentOffset.x
            
            // 只有当差异大于阈值时才滚动(避免浮点误差)
            if abs(currentOffset - targetOffset) > 1 && !context.coordinator.isUserDragging {
                // 标记为程序化滚动
                context.coordinator.isProgrammaticScrolling = true
                
                scrollView.setContentOffset(
                    CGPoint(x: targetOffset, y: 0),
                    animated: true
                )
            }
        }

        scrollView.isScrollEnabled = isScrollEnabled
    }

    public class Coordinator: NSObject, UIScrollViewDelegate {
        var parent: PagedHorizontalScrollView
        var stackView: UIStackView?
        var scrollView: UIScrollView?
        var hostingControllers: [UIHostingController<AnyView>] = []
        
        // 添加标志来跟踪滚动状态
        var isUserDragging = false
        var isProgrammaticScrolling = false
        var lastReportedPage = -1

        init(_ parent: PagedHorizontalScrollView) {
            self.parent = parent
            self.lastReportedPage = parent.selectedTab.wrappedValue
            parent.onPageSelected?(lastReportedPage)
        }

        func setupViews() {
            guard let stackView = stackView, let scrollView = scrollView else {
                return
            }

            // 清除旧的视图
            stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
            hostingControllers.removeAll()

            // 添加新的视图
            for view in parent.views {
                let hostingController = UIHostingController(rootView: view)
                hostingController.view
                    .translatesAutoresizingMaskIntoConstraints = false
                hostingController.view.backgroundColor = .clear

                stackView.addArrangedSubview(hostingController.view)

                // 设置宽度约束
                NSLayoutConstraint.activate([
                    hostingController.view.widthAnchor.constraint(
                        equalTo: scrollView.widthAnchor
                    )
                ])

                hostingControllers.append(hostingController)
            }
        }

        public func scrollViewDidScroll(_ scrollView: UIScrollView) {
            // 如果是程序化滚动,不更新 selectedTab
            if isProgrammaticScrolling {
                return
            }
            
            // 只在用户拖动时更新
            if isUserDragging {
                updateFinalPosition(scrollView)
            }
        }
        
        public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
            isUserDragging = true
            isProgrammaticScrolling = false
            print("User started dragging")
        }
        
        public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
            if !decelerate {
                isUserDragging = false
                // 确保最终位置正确
                updateFinalPosition(scrollView)
            }
        }
        
        public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
            isUserDragging = false
            // 确保最终位置正确
            updateFinalPosition(scrollView)
        }
        
        public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
            isProgrammaticScrolling = false
            parent.onPageSelected?(parent.selectedTab.wrappedValue)
            // 程序化滚动结束
            print("Programmatic scrolling ended")
        }
        
        private func updateFinalPosition(_ scrollView: UIScrollView) {
            let width = scrollView.bounds.width
            guard width > 0 else { return }
            
            let currentPage = Int(round(scrollView.contentOffset.x / width))
            
            if currentPage != lastReportedPage &&
               currentPage >= 0 &&
               currentPage < parent.views.count {
                lastReportedPage = currentPage
                parent.selectedTab.wrappedValue = currentPage
                parent.onPageSelected?(currentPage)
            }
        }
    }
}

18. 总结

关键要点

  1. 正确配置基础属性

    • 始终设置正确的 contentSize
    • 理解 frameboundscontentOffset 的关系
    • 合理使用 contentInset 适配界面
  2. 性能优化原则

    • 实现视图复用机制
    • 避免在 scrollViewDidScroll 中执行重操作
    • 使用异步加载和预加载策略
    • 及时释放不可见视图的资源
  3. 用户体验优化

    • 提供流畅的滚动体验
    • 实现合适的加载指示器
    • 处理好键盘交互
    • 支持下拉刷新等常见交互
  4. 常见陷阱规避

    • Auto Layout 中正确使用 contentLayoutGuideframeLayoutGuide
    • 处理好嵌套 ScrollView 的手势冲突
    • 注意内存管理,避免循环引用

最佳实践检查清单

  • ✅ ContentSize 设置正确
  • ✅ 启用了必要的滚动方向
  • ✅ 合理设置了弹性效果
  • ✅ 实现了必要的代理方法
  • ✅ 处理了键盘遮挡问题
  • ✅ 优化了滚动性能
  • ✅ 支持了旋转和多任务
  • ✅ 添加了适当的无障碍支持

相关资源


版本历史

  • iOS 2.0: UIScrollView 首次引入
  • iOS 3.0: 添加分页支持
  • iOS 5.0: 改进缩放功能
  • iOS 7.0: 引入 contentInsetAdjustmentBehavior
  • iOS 11.0: 添加 contentLayoutGuideframeLayoutGuide
  • iOS 13.0: 改进暗黑模式支持
  • iOS 15.0: 优化性能和手势识别

本文档持续更新,最后更新时间:2024年

Giscus 未启用:请在 src/site.config.ts 中配置 repoId 与 categoryId。