SwiftUI ScrollView:如何修改.content.offset又名Paging?

时间:2019-07-14 14:14:36

标签: scrollview swiftui ios13

问题

如何修改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方法之外):

  1. scrollView的内容区域大于屏幕空间,因此根本无法滚动。

  2. 我创建了一个DragGesture(),以检测是否有拖动在继续。在.onChanged和.onEnded闭包中,我修改了@State值以创建所需的scrollTarget。

  3. 有条件地将原始未更改的值和新的修改后的值馈入.content.offset(x:y :)修饰符-取决于dragState来代替缺少的scrollDelegate方法。

  4. 添加了仅在拖动结束后才有条件执行的动画。

  5. 将手势附加到scrollView。

长话短说。没用 我希望我能解决我的问题。

有解决方案吗?期待任何投入。谢谢!

5 个答案:

答案 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 }
            }))
        }
    }
}
  1. 您可能只包装了内容,我将其用于“教程视图”。
  2. 这是在外部状态更改和内部状态更改之间切换的一种技巧
  3. .leading是强制性的,如果您不想将所有偏移量转换为居中。
  4. 将状态设置为本地状态更改
  5. 计算手势增量(* -1)加上前一个索引状态的完整偏移量
  6. 最后根据手势预测的末端设置最终索引,同时向上或向下舍入偏移量
  7. 重置状态以处理对索引的外部更改

我已经在以下环境中对其进行了测试

    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中的滚动还不支持任何可能有用的东西,例如scrollViewDidScrollscrollViewWillEndDragging。我建议使用经典的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的所有功能。