掌握 SwiftUI 中的 ScrollView:滚动几何

2024-09-21 23:57:39 浏览数 (3)

前言

本文探讨了如何使用 onScrollGeometryChange 视图修饰符有效地监控和管理滚动位置和几何。通过详细的代码示例和解释,你将学习如何利用这些工具创建动态和响应迅速的用户界面。

SwiftUI 是一个强大的框架,它简化了在苹果平台上构建用户界面的过程。SwiftUI 中的一个基本组件是 ScrollView,它允许用户通过滚动导航内容。然而,管理滚动位置和理解滚动交互可能是一个挑战。ScrollGeometry 和 onScrollGeometryChange 视图修饰符的引入解决了这些挑战,为开发者提供了更多的控制和对滚动行为的深入了解。

什么是 ScrollPosition

ScrollPosition 是一种类型,允许开发者以编程方式读取或更改滚动位置。虽然有用,但当用户使用手势与滚动视图交互时,它显得不够全面。以下是一个展示 ScrollPosition 使用的示例:

代码语言:swift复制
struct ContentView: View {
    @State private var position = ScrollPosition(edge: .top)
    
    var body: some View {
        ScrollView {
            Button("Scroll to offset") {
                position.scrollTo(point: CGPoint(x: 0, y: 100))
            }
            
            ForEach(1..<100) { index in
                Text(verbatim: index.formatted())
                    .id(index)
            }
        }
        .scrollPosition($position)
        .animation(.default, value: position)
    }
}

在这个示例中,我们将滚动视图绑定到一个状态属性。当按下按钮时,滚动视图会将其内容偏移移动到指定点。然而,我们无法读取用户通过手势交互设置的具体内容偏移。

引入 ScrollGeometry

SwiftUI 的新 ScrollGeometry 类型以及 onScrollGeometryChange 视图修饰符提供了一个解决方案。这些工具允许开发者在用户交互期间准确读取内容偏移。

使用 onScrollGeometryChange

让我们探索如何使用 onScrollGeometryChange 视图修饰符与 ScrollGeometry:

代码语言:swift复制
struct ContentView: View {
    @State private var scrollPosition = ScrollPosition(y: 0)
    @State private var offsetY: CGFloat = 0
    
    var body: some View {
        ScrollView {
            ForEach(1..<100, id: .self) { number in
                Text(verbatim: number.formatted())
                    .id(number)
            }
        }
        .scrollPosition($scrollPosition)
        .onScrollGeometryChange(for: CGFloat.self) { geometry in
            geometry.contentOffset.y
        } action: { oldValue, newValue in
            if oldValue != newValue {
                offsetY = newValue
            }
        }
        .onChange(of: offsetY) {
            print(offsetY)
        }
    }
}

onScrollGeometryChange 视图修饰符接受三个参数:

  1. 类型参数:指定要跟踪的滚动几何类型。在此示例中,我们使用 CGFloat 来跟踪内容偏移的 Y 轴。
  2. 转换闭包:从 ScrollGeometry 实例中提取所需信息。
  3. 动作闭包:处理滚动几何的变化,通过比较旧值和新值,允许我们相应地更新状态属性。

高级滚动几何跟踪

ScrollGeometry 提供了许多有价值的属性,如内容偏移、边界、容器大小、可见矩形、内容插入和内容大小。开发者可以提取单个属性或组合多个属性以获得全面的见解。

以下是一个结合内容大小和可见矩形跟踪的示例:

代码语言:swift复制
struct ContentView: View {
    struct ScrollData: Equatable {
        let size: CGSize
        let visible: CGRect
    }
    
    @State private var scrollPosition = ScrollPosition(y: 0)
    @State private var scrollData = ScrollData(size: .zero, visible: .zero)
    
    var body: some View {
        ScrollView {
            ForEach(1..<100, id: .self) { number in
                Text(verbatim: number.formatted())
                    .id(number)
            }
        }
        .scrollPosition($scrollPosition)
        .onScrollGeometryChange(for: ScrollData.self) { geometry in
            ScrollData(size: geometry.contentSize, visible: geometry.visibleRect)
        } action: { oldValue, newValue in
            if oldValue != newValue {
                scrollData = newValue
            }
        }
        .onChange(of: scrollData) {
            print(scrollData)
        }
    }
}

在这个示例中,我们定义了一个 ScrollData 结构来保存大小和可见矩形属性。在使用 onScrollGeometryChange 视图修饰符时,我们将 ScrollData 作为转换闭包的返回类型,从 ScrollGeometry 实例中提取所有所需的数据。

完整代码示例分析

下面是一个完整的 SwiftUI Demo,其中包含了我们刚刚讨论的 ScrollView、ScrollGeometry 和 onScrollGeometryChange 的使用示例。你可以在 Xcode 中运行这个项目来观察其效果。

完整代码示例

代码语言:swift复制
import SwiftUI

struct ContentView: View {
    @State private var scrollPosition = ScrollPosition(y: 0)
    @State private var offsetY: CGFloat = 0
    
    var body: some View {
        VStack {
            Text("Scroll Offset: (offsetY, specifier: "%.2f")")
                .padding()
            
            ScrollView {
                ForEach(1..<100, id: .self) { number in
                    Text(verbatim: number.formatted())
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background(Color(.secondarySystemBackground))
                        .cornerRadius(8)
                        .padding(.horizontal)
                        .id(number)
                }
            }
            .scrollPosition($scrollPosition)
            .onScrollGeometryChange(for: CGFloat.self) { geometry in
                geometry.contentOffset.y
            } action: { oldValue, newValue in
                if oldValue != newValue {
                    offsetY = newValue
                }
            }
            .onChange(of: offsetY) {
                print(offsetY)
            }
        }
    }
}

struct ScrollData: Equatable {
    let size: CGSize
    let visible: CGRect
}

struct AdvancedContentView: View {
    @State private var scrollPosition = ScrollPosition(y: 0)
    @State private var scrollData = ScrollData(size: .zero, visible: .zero)
    
    var body: some View {
        VStack {
            Text("Content Size: (scrollData.size.width, specifier: "%.2f") x (scrollData.size.height, specifier: "%.2f")")
                .padding()
            Text("Visible Rect: (scrollData.visible.origin.x, specifier: "%.2f"), (scrollData.visible.origin.y, specifier: "%.2f") - (scrollData.visible.width, specifier: "%.2f") x (scrollData.visible.height, specifier: "%.2f")")
                .padding()
            
            ScrollView {
                ForEach(1..<100, id: .self) { number in
                    Text(verbatim: number.formatted())
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background(Color(.secondarySystemBackground))
                        .cornerRadius(8)
                        .padding(.horizontal)
                        .id(number)
                }
            }
            .scrollPosition($scrollPosition)
            .onScrollGeometryChange(for: ScrollData.self) { geometry in
                ScrollData(size: geometry.contentSize, visible: geometry.visibleRect)
            } action: { oldValue, newValue in
                if oldValue != newValue {
                    scrollData = newValue
                }
            }
            .onChange(of: scrollData) {
                print(scrollData)
            }
        }
    }
}

@main
struct ScrollViewDemoApp: App {
    var body: some Scene {
        WindowGroup {
            TabView {
                ContentView()
                    .tabItem {
                        Label("Basic", systemImage: "1.square.fill")
                    }
                
                AdvancedContentView()
                    .tabItem {
                        Label("Advanced", systemImage: "2.square.fill")
                    }
            }
        }
    }
}

如何运行

  1. 打开 Xcode 并创建一个新的 SwiftUI 项目。
  2. 将默认生成的 ContentView.swift 文件替换为上面的完整代码。
  3. @main 注释下的应用程序入口点中,确保你的主视图是 ScrollViewDemoApp
  4. 运行项目。

功能解释

  • ContentView: 展示基本的滚动偏移追踪功能,通过 onScrollGeometryChange 视图修饰符追踪 Y 轴的内容偏移。
  • AdvancedContentView: 展示更高级的滚动几何追踪功能,追踪内容大小和可见矩形的变化。
  • ScrollViewDemoApp: 包含 TabView,方便在基本和高级示例之间切换。

总结

今天,我们探讨了 SwiftUI 中的新 ScrollGeometry 类型和 onScrollGeometryChange 视图修饰符。这些工具为开发者提供了对滚动位置和交互的精确控制和洞察,增强了动态和响应迅速的用户界面的开发。通过利用这些功能,你可以创建更具吸引力和直观的应用程序。

0 人点赞