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

Hel*_*imo 11 scrollview ios13 swiftui

问题

如何修改scrollView的滚动目标?我正在寻找一种替代“经典” scrollView委托方法的方法

override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)

......在这里我们可以modfify目标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)
Run Code Online (Sandbox Code Playgroud)

尝试

这是我尝试过的方法(除了自定义的scrollView方法):

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

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

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

  4. 仅在拖动结束时才有条件地添加动画。

  5. 将手势附加到scrollView。

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

有什么解决办法吗?期待任何投入。谢谢!

guj*_*jci 21

我设法通过@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 }
            }))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
  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()
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

TODOView我的自定义视图在哪里指示要实现的视图。

我希望我的问题是正确的,如果不是,请说明我应该关注哪个部分。我也欢迎任何删除isGestureActive状态的建议。


Ala*_*laq 6

@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 }
            }))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

并可以用作:

    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)
        }
    }
Run Code Online (Sandbox Code Playgroud)

在一些实用程序的帮助下:

    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) }
        }
    }
Run Code Online (Sandbox Code Playgroud)


gla*_*oss 1

据我所知,滚动swiftUI不支持任何潜在有用的东西,例如scrollViewDidScrollscrollViewWillEndDragging尚未。我建议使用经典的 UIKit 视图来实现非常自定义的行为,并使用酷炫的 SwiftUI 视图来实现更简单的操作。我已经尝试了很多而且它确实有效!看看这个指南。希望有帮助