问题
如何修改scrollView的滚动目标?我正在寻找一种替代“经典” scrollView委托方法的方法
override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
...例如,我们可以通过scrollView.contentOffset
修改目标targetContentOffset.pointee
来创建自定义分页行为。
或者换句话说:我确实想在(水平)scrollView中创建分页效果。
我尝试过的就是。是这样的:
ScrollView(.horizontal, showsIndicators: true, content: {
HStack(alignment: VerticalAlignment.top, spacing: 0, content: {
card(title: "1")
card(title: "2")
card(title: "3")
card(title: "4")
})
})
// 3.
.content.offset(x: self.dragState.isDragging == true ? self.originalOffset : self.modifiedOffset, y: 0)
// 4.
.animation(self.dragState.isDragging == true ? nil : Animation.spring())
// 5.
.gesture(horizontalDragGest)
尝试
这是我尝试过的方法(除了自定义的scrollView方法之外):
scrollView的内容区域大于屏幕空间,因此根本无法滚动。
我创建了一个DragGesture()
,以检测是否有拖动在继续。在.onChanged和.onEnded闭包中,我修改了@State
值以创建所需的scrollTarget。
有条件地将原始未更改的值和新的修改后的值馈入.content.offset(x:y :)修饰符-取决于dragState来代替缺少的scrollDelegate方法。
添加了仅在拖动结束后才有条件执行的动画。
将手势附加到scrollView。
长话短说。没用 我希望我能解决我的问题。
有解决方案吗?期待任何投入。谢谢!
答案 0 :(得分:6)
我设法通过@Binding
索引实现了分页行为。解决方案可能看起来很脏,我将解释我的解决方法。
我弄错的第一件事是要对齐.leading
而不是默认的.center
,否则偏移量会异常。然后,我将绑定和局部偏移状态结合在一起。这有点违反“单一事实来源”的原则,但是否则我不知道如何处理外部索引更改和修改偏移量。
所以,我的代码如下
struct SwiftUIPagerView<Content: View & Identifiable>: View {
@Binding var index: Int
@State private var offset: CGFloat = 0
@State private var isGestureActive: Bool = false
// 1
var pages: [Content]
var body: some View {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 0) {
ForEach(self.pages) { page in
page
.frame(width: geometry.size.width, height: nil)
}
}
}
// 2
.content.offset(x: self.isGestureActive ? self.offset : -geometry.size.width * CGFloat(self.index))
// 3
.frame(width: geometry.size.width, height: nil, alignment: .leading)
.gesture(DragGesture().onChanged({ value in
// 4
self.isGestureActive = true
// 5
self.offset = value.translation.width + -geometry.size.width * CGFloat(self.index)
}).onEnded({ value in
if -value.predictedEndTranslation.width > geometry.size.width / 2, self.index < self.pages.endIndex - 1 {
self.index += 1
}
if value.predictedEndTranslation.width > geometry.size.width / 2, self.index > 0 {
self.index -= 1
}
// 6
withAnimation { self.offset = -geometry.size.width * CGFloat(self.index) }
// 7
DispatchQueue.main.async { self.isGestureActive = false }
}))
}
}
}
.leading
是强制性的,如果您不想将所有偏移量转换为居中。我已经在以下环境中对其进行了测试
struct WrapperView: View {
@State var index: Int = 0
var body: some View {
VStack {
SwiftUIPagerView(index: $index, pages: (0..<4).map { index in TODOView(extraInfo: "\(index + 1)") })
Picker(selection: self.$index.animation(.easeInOut), label: Text("")) {
ForEach(0..<4) { page in Text("\(page + 1)").tag(page) }
}
.pickerStyle(SegmentedPickerStyle())
.padding()
}
}
}
其中TODOView
是我的自定义视图,表示要实现的视图。
我希望我的问题正确,如果没有,请指定我应该关注的部分。另外,我也欢迎删除isGestureActive
状态的任何建议。
答案 1 :(得分:0)
据我所知,swiftUI
中的滚动还不支持任何可能有用的东西,例如scrollViewDidScroll
或scrollViewWillEndDragging
。我建议使用经典的UIKit视图进行非常自定义的行为,并使用SwiftUI视图进行更简单的处理。我已经尝试了很多,它实际上有效!看看这个guide。希望有帮助
答案 2 :(得分:0)
@gujci,感谢您提供有趣的示例。我已经玩过它并删除了isGestureActive
状态。完整的示例可以在我的gist中找到。
struct SwiftUIPagerView<Content: View & Identifiable>: View {
@State private var index: Int = 0
@State private var offset: CGFloat = 0
var pages: [Content]
var body: some View {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 0) {
ForEach(self.pages) { page in
page
.frame(width: geometry.size.width, height: nil)
}
}
}
.content.offset(x: self.offset)
.frame(width: geometry.size.width, height: nil, alignment: .leading)
.gesture(DragGesture()
.onChanged({ value in
self.offset = value.translation.width - geometry.size.width * CGFloat(self.index)
})
.onEnded({ value in
if abs(value.predictedEndTranslation.width) >= geometry.size.width / 2 {
var nextIndex: Int = (value.predictedEndTranslation.width < 0) ? 1 : -1
nextIndex += self.index
self.index = nextIndex.keepIndexInRange(min: 0, max: self.pages.endIndex - 1)
}
withAnimation { self.offset = -geometry.size.width * CGFloat(self.index) }
})
)
}
}
}
答案 3 :(得分:0)
@gujci您的解决方案是完美的,对于更通用的用法,使其接受模型并按如下所示查看构建器(请注意,我在构建器中传递了几何尺寸):
struct SwiftUIPagerView<TModel: Identifiable ,TView: View >: View {
@Binding var index: Int
@State private var offset: CGFloat = 0
@State private var isGestureActive: Bool = false
// 1
var pages: [TModel]
var builder : (CGSize, TModel) -> TView
var body: some View {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 0) {
ForEach(self.pages) { page in
self.builder(geometry.size, page)
}
}
}
// 2
.content.offset(x: self.isGestureActive ? self.offset : -geometry.size.width * CGFloat(self.index))
// 3
.frame(width: geometry.size.width, height: nil, alignment: .leading)
.gesture(DragGesture().onChanged({ value in
// 4
self.isGestureActive = true
// 5
self.offset = value.translation.width + -geometry.size.width * CGFloat(self.index)
}).onEnded({ value in
if -value.predictedEndTranslation.width > geometry.size.width / 2, self.index < self.pages.endIndex - 1 {
self.index += 1
}
if value.predictedEndTranslation.width > geometry.size.width / 2, self.index > 0 {
self.index -= 1
}
// 6
withAnimation { self.offset = -geometry.size.width * CGFloat(self.index) }
// 7
DispatchQueue.main.async { self.isGestureActive = false }
}))
}
}
}
,可以用作:
struct WrapperView: View {
@State var index: Int = 0
@State var items : [(color:Color,name:String)] = [
(.red,"Red"),
(.green,"Green"),
(.yellow,"Yellow"),
(.blue,"Blue")
]
var body: some View {
VStack(spacing: 0) {
SwiftUIPagerView(index: $index, pages: self.items.identify { $0.name }) { size, item in
TODOView(extraInfo: item.model.name)
.frame(width: size.width, height: size.height)
.background(item.model.color)
}
Picker(selection: self.$index.animation(.easeInOut), label: Text("")) {
ForEach(0..<4) { page in Text("\(page + 1)").tag(page) }
}
.pickerStyle(SegmentedPickerStyle())
}.edgesIgnoringSafeArea(.all)
}
}
借助某些实用程序:
struct MakeIdentifiable<TModel,TID:Hashable> : Identifiable {
var id : TID {
return idetifier(model)
}
let model : TModel
let idetifier : (TModel) -> TID
}
extension Array {
func identify<TID: Hashable>(by: @escaping (Element)->TID) -> [MakeIdentifiable<Element, TID>]
{
return self.map { MakeIdentifiable.init(model: $0, idetifier: by) }
}
}
答案 4 :(得分:0)
另一种解决方案是使用UIViewRepresentative将UIKit集成到SwiftUI中,该UIViewRepresentative将UIKit组件与SwiftUI链接在一起。有关其他线索和资源,请参阅Apple建议您如何使用UIKit:Interfacing with UIKit。他们有一个很好的例子,可以显示图像和曲目选择索引之间的页面。
编辑:在他们(苹果公司)实施某种影响屏幕滚动而不是整个视图的内容偏移之前,这是他们的建议解决方案,因为他们知道SwiftUI的初始发行版并不包含UIKit的所有功能。>