ric*_*rdo 3 scrollview snapping swift swiftui
我目前正在开发一个 SwiftUI 项目,我需要创建一个自定义垂直 ScrollView。要求是当用户停止滚动时,此 ScrollView 中的单元格捕捉到视图的中心。我知道 SwiftUI 的 ScrollView 通过修饰符提供了一定程度的自定义.scrollTargetBehavior(_:),但我发现的文档和示例并没有完全涵盖这种特定情况。
我尝试使用基本的scrollTargetBehavior .viewAligned,但捕捉行为不包括我正在寻找的捕捉效果。我知道 UIKit 通过 UICollectionView 和自定义布局属性提供了对滚动行为更精细的控制,但我的目标是纯粹在 SwiftUI 中实现这一点。
任何帮助将不胜感激。
干杯!
为了提供一个有效的示例,我将使用许多这样的实例作为以下内容ScrollView:
struct Card: View {\n let i: Int\n\n var body: some View {\n let suit = ["heart", "spade", "diamond", "club"][i / 13]\n let rank = i == 9 ? "10" : String("A23456789_JQK".dropFirst(i % 13).prefix(1))\n let color = (i / 13).isMultiple(of: 2) ? Color.red : Color.black\n let label = VStack { Text(rank); Image(systemName: "suit.\\(suit).fill") }.padding(12)\n\n RoundedRectangle(cornerRadius: 10, style: .circular)\n .fill(.white)\n .overlay(alignment: .topLeading) { label }\n .overlay(alignment: .bottomTrailing) { label.rotationEffect(.degrees(180)) }\n .foregroundStyle(color)\n .font(.system(size: 40))\n .overlay {\n Canvas { gc, size in\n gc.translateBy(x: 0.5 * size.width, y: 0.5 * size.height)\n gc.stroke(\n Path {\n $0.move(to: .init(x: -10, y: -10))\n $0.addLine(to: .init(x: 10, y: 10))\n $0.move(to: .init(x: 10, y: -10))\n $0.addLine(to: .init(x: -10, y: 10))\n },\n with: .color(color)\n )\n }\n }\n .padding()\n .compositingGroup()\n .shadow(radius: 1.5, x: 0, y: 0.5)\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n这会绘制一张中间有十字的扑克牌。ScrollView当卡片停止滚动时,十字可以很容易地看出卡片是否居中。
让我们从ScrollView包含整副牌的基本设置开始:
struct BasicContentView: View {\n var body: some View {\n ScrollView {\n LazyVStack {\n ForEach(0 ..< 52) { i in\n Card(i: i)\n .frame(height: 500)\n }\n }\n }\n .overlay {\n Rectangle()\n .frame(height: 1)\n .foregroundStyle(.green)\n }\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n它看起来像这样:
\n\n绿线位于 的垂直中心ScrollView。如果卡片的十字线与绿线对齐,我们就可以判断卡片居中。
为了使ScrollView居中卡片停止滚动,我们需要编写ScrollTargetBehavior. 通过阅读文档(特别是ScrollTargetBehaviorContext和的文档ScrollTarget),我们可以推断我们的自定义ScrollTargetBehavior需要访问 s 坐标空间中的卡片视图的框架ScrollView。
为了收集这些帧,我们需要使用SwiftUI 的 \xe2\x80\x9cpreference\xe2\x80\x9d 系统。首先,我们需要一个类型来收集卡框:
\nstruct CardFrames: Equatable {\n var frames: [Int: CGRect] = [:]\n}\nRun Code Online (Sandbox Code Playgroud)\n接下来,我们需要协议PreferenceKey的自定义实现。我们不妨使用CardFrames类型作为键:
extension CardFrames: PreferenceKey {\n static var defaultValue: Self { .init() }\n\n static func reduce(value: inout Self, nextValue: () -> Self) {\n value.frames.merge(nextValue().frames) { $1 }\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n我们需要添加一个@State属性来存储收集到的帧:
struct ContentView: View {\n\n // Add this property to ContentView\n @State var cardFrames: CardFrames = .init()\n\n var body: some View {\n ...\nRun Code Online (Sandbox Code Playgroud)\n我们还需要NamedCoordinateSpace为定义 a ScrollView:
struct ContentView: View {\n\n @State var cardFrames: CardFrames = .init()\n\n // Add this property to ContentView\n private static let geometry = NamedCoordinateSpace.named("geometry")\n\n var body: some View {\n ...\nRun Code Online (Sandbox Code Playgroud)\nScrollView接下来,我们需要通过coordinateSpace向 中添加修饰符将该坐标空间应用于 的内容LazyVStack:
ScrollView {\n LazyVStack {\n ForEach(0 ..< 52) { i in\n Card(i: i)\n .frame(height: 500)\n }\n }\n\n // Add this modifier to LazyVStack\n .coordinateSpace(Self.geometry)\n }\nRun Code Online (Sandbox Code Playgroud)\n要读取 a 的框架Card并设置首选项,我们使用常见的 SwiftUI 模式:使用修饰符添加包含backgrounda的 a :GeometryReaderColor.clearpreference
Card(i: i)\n .frame(height: 500)\n // Add this modifier to LazyVStack\n .background {\n GeometryReader { proxy in\n Color.clear\n .preference(\n key: CardFrames.self,\n value: CardFrames(\n frames: [i: proxy.frame(in: Self.geometry)]\n )\n )\n }\n }\nRun Code Online (Sandbox Code Playgroud)\n现在我们可以使用修饰符读出CardFrames首选项并将其存储在属性中:@StateonPreferenceChange
ScrollView {\n ...\n }\n .overlay {\n Rectangle()\n .frame(height: 1)\n .foregroundStyle(.green)\n }\n // Add this modifier to ScrollView\n .onPreferenceChange(CardFrames.self) { cardFrames = $0 }\nRun Code Online (Sandbox Code Playgroud)\n这就是收集卡框并使它们在cardFrames属性中可用的所有代码。
现在我们准备编写一个自定义的ScrollTargetBehavior. 我们的自定义行为调整 ,ScrollTarget使其中点成为最近卡片的中点:
struct CardFramesScrollTargetBehavior: ScrollTargetBehavior {\n var cardFrames: CardFrames\n\n func updateTarget(_ target: inout ScrollTarget, context: TargetContext) {\n let yProposed = target.rect.midY\n guard let nearestEntry = cardFrames\n .frames\n .min(by: { ($0.value.midY - yProposed).magnitude < ($1.value.midY - yProposed).magnitude })\n else { return }\n target.rect.origin.y = nearestEntry.value.midY - 0.5 * target.rect.size.height\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n最后,我们使用scrollTargetBehavior修饰符将自定义行为应用于ScrollView:
ScrollView {\n ...\n }\n // Add this modifier to ScrollView\n .scrollTargetBehavior(CardFramesScrollTargetBehavior(cardFrames: cardFrames))\n .overlay {\n ...\nRun Code Online (Sandbox Code Playgroud)\n\n我注意到,当向上滚动并落在 3\xe2\x99\xa5\xef\xb8\x8e 卡上时,它并不完全居中。我认为这是 SwiftUI 的一个错误。
\n这是ContentView包含所有添加内容的最终版本:
struct ContentView: View {\n @State var cardFrames: CardFrames = .init()\n\n private static let geometry = NamedCoordinateSpace.named("geometry")\n\n var body: some View {\n ScrollView {\n LazyVStack {\n ForEach(0 ..< 52) { i in\n Card(i: i)\n .frame(height: 500)\n .background {\n GeometryReader { proxy in\n Color.clear\n .preference(\n key: CardFrames.self,\n value: CardFrames(\n frames: [i: proxy.frame(in: Self.geometry)]\n )\n )\n }\n }\n }\n }\n .coordinateSpace(Self.geometry)\n }\n .scrollTargetBehavior(CardFramesScrollTargetBehavior(cardFrames: cardFrames))\n .overlay {\n Rectangle()\n .frame(height: 1)\n .foregroundStyle(.green)\n }\n .onPreferenceChange(CardFrames.self) { cardFrames = $0 }\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n
| 归档时间: |
|
| 查看次数: |
194 次 |
| 最近记录: |