在 WWDC 2023 上,Apple 为 ScrollViews 引入了新的 API,让我们可以轻松地将视图对齐到位。当用户在滚动(拖动)手势后抬起手指时,以下代码示例将
Rectangle
捕捉到位。
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack(spacing: 8) {
ForEach(1..<100) { number in
Rectangle()
.fill(.mint)
.frame(height: 200)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
}
}
现在,如果我只想捕捉每一秒
Rectangle
并滚动其他秒怎么办?或者更一般地说,我如何准确控制 ScrollView
应捕捉到哪些项目(子视图)以及捕捉时应忽略哪些项目?
这似乎是一件非常微不足道的事情,但我不知道如何实现这一点。
scrollTargetLayout(isEnabled:)
修饰符的文档指出:
滚动目标布局可以方便地将
修饰符应用于布局中的每个视图。View/scrollTarget(isEnabled:)
这告诉我,还有另一个名为
scrollTarget(isEnabled:)
的修改器,我可以将其附加到各个项目以精细地启用或禁用捕捉。然而,这个修饰符似乎并不存在。 🤷u200d♂️
文档根本就是错误的还是我误解了它?我怎样才能完成这项工作(不回退到 UIKit)?
事实证明,使用新的 iOS 17 ScrollViews API 可以做到这一点。为此,您可以在滚动视图上使用新的
.scrollPosition(id: Hashable)
修饰符。我对此感到有点乐趣。这是我到目前为止所取得的成就:
这是我使用的代码。要使其正常工作,您将需要 8 张名为“Pic (index)”的图像。
struct ScrollTransition: View {
// MARK: - UI
@State private var pickerType: TripPicker = .normal
@State private var activeID: Int?
var body: some View {
VStack {
Picker("", selection: $pickerType) {
ForEach(TripPicker.allCases, id: \.rawValue) {
Text($0.rawValue)
.tag($0)
} //: LOOP TRIP PICKER
} //: PICKER
.pickerStyle(.segmented)
.padding()
Button("Move of two") {
withAnimation {
if let activeID, (activeID + 2) < 9 {
self.activeID! += 2
} else {
self.activeID = 1
}
print("Index \(self.activeID ?? 0)")
}
}
Text("Active ID: \(self.activeID ?? 0)")
.padding(.top, 20)
Spacer(minLength: 0)
GeometryReader {
let size = $0.size
let padding = (size.width - 70) / 2
/// Circular Slider
ScrollView(.horizontal) {
HStack(spacing: 35) {
ForEach(1...8, id: \.self) { index in
Image("Pic \(index)")
.resizable()
.scaledToFill()
.frame(width: 70, height: 70)
.clipShape(.circle)
.shadow(color: .black.opacity(0.15), radius: 5, x: 5, y: 5)
.visualEffect { view, proxy in
view
.offset(y: offset(proxy))
.offset(y: scale(proxy) * 15)
}
.scrollTransition(.interactive, axis: .horizontal) {view, phase in
view
//.offset(y: phase.isIdentity && activeID == index ? 15 : 0)
.scaleEffect(phase.isIdentity && activeID == index && pickerType == .scaled ? 1.5 : 1, anchor: .bottom)
}
} //: LOOP IMAGES
} //: HSTACK
.frame(height: size.height)
.offset(y: -30)
.scrollTargetLayout()
} //: SCROLL
.background {
if pickerType == .normal {
Circle()
.fill(.white.shadow(.drop(color: .black.opacity(0.2), radius: 5)))
.frame(width: 85, height: 85)
.offset(y: -15)
}
}
.safeAreaPadding(.horizontal, padding) // <-- With this kind of padding the view's minX starts from 0
.scrollIndicators(.hidden)
// Snapping
.scrollTargetBehavior(.viewAligned)
.scrollPosition(id: $activeID)
.frame(height: size.height)
.onAppear {
// Place carousel at the middle item
activeID = 8/2
}
} //: GEOMETRY
.frame(height: 200)
} //: VSTACK
.ignoresSafeArea(.container, edges: .bottom)
}
//MARK: - Functions
func offset(_ proxy: GeometryProxy) -> CGFloat {
let progress = progress(proxy)
/// Simply moving view up/down based on progress
return progress < 0 ? progress * -30 : progress * 30
}
func scale(_ proxy: GeometryProxy) -> CGFloat {
let progress = min(max(progress(proxy), -1), 1)
return progress < 0 ? 1 + progress : 1 - progress
}
func progress(_ proxy: GeometryProxy) -> CGFloat {
let viewWidth = proxy.size.width
let minX = (proxy.bounds(of: .scrollView)?.minX ?? 0)
return minX / viewWidth
}
}
enum TripPicker: String, CaseIterable {
case scaled = "Scaled"
case normal = "Normal"
}
也许所有这些代码都有点矫枉过正,但我对 SwiftUI 新功能很感兴趣。顺便说一句,修改 activeID 整数后,滚动视图将自动导航到该索引处的元素。希望我能帮到你,请告诉我!