SwiftUI滚动/列表滚动事件 [英] SwiftUI Scroll/List Scrolling Events

查看:173
本文介绍了SwiftUI滚动/列表滚动事件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

最近,我一直在尝试创建受(aref ="https://cocoapods.org/pods/SwiftPullToRefresh" rel ="nofollow noreferrer"启发的(刷新,加载更多)swiftUI滚动视图!!的下拉菜单> https://cocoapods.org/pods/SwiftPullToRefresh

lately I have been trying to create a pull to (refresh, load more) swiftUI Scroll View !!, inspired by https://cocoapods.org/pods/SwiftPullToRefresh

我一直在努力获取内容的偏移量和大小.但是现在,当用户释放滚动视图以完成UI时,我正在努力获取事件.

I was struggling to get the offset and the size of the content. but now I am struggling to get the event when the user releases the scroll view to finish the UI.

这是我当前的代码:

    struct PullToRefresh2: View {
        @State var offset : CGPoint = .zero
        @State var contentSize : CGSize = .zero
        @State var scrollViewRect : CGRect = .zero
        @State var items = (0 ..< 50).map { "Item \($0)" }
        @State var isTopRefreshing = false
        @State var isBottomRefreshing = false


        var top : CGFloat {
            return self.offset.y
        }
        private var bottomLocation : CGFloat {
            if contentSize.height >= scrollViewRect.height {
                return self.contentSize.height + self.top - self.scrollViewRect.height + 32
            }
            return top + 32
        }
        private var shouldTopRefresh : Bool {
            return self.top > 80
        }
        private var shouldBottomRefresh : Bool {
            return self.bottomLocation < -80 + 32
        }
        func watchOffset() -> Binding<CGPoint> {
            return .init(get: {
                return self.offset
            },set: {
                print("watched : offset= \($0)")
                self.offset = $0
            })
        }

        private func computeOffset() -> CGFloat {

            if isTopRefreshing {
                print("OFFSET: isTopRefreshing")
                return 32
            } else if isBottomRefreshing {
                if (contentSize.height+32) < scrollViewRect.height {
                    print("OFFSET: isBottomRefreshing 1")
                    return top
                } else if scrollViewRect.height > contentSize.height  {

                    print("OFFSET: isBottomRefreshing 2")
                    return 32 - (scrollViewRect.height - contentSize.height)
                } else {

                    print("OFFSET: isBottomRefreshing 3")
                    return scrollViewRect.height - contentSize.height - 32
                }
            }

            print("OFFSET: fall back->\(top)")
            return top
        }

        func watchScrollViewRect() -> Binding<CGRect> {
            return .init(get: {
                return self.scrollViewRect
            },set: {
                print("watched : scrollViewRect= \($0)")
                self.scrollViewRect = $0
            })
        }
        func watchContentSize() -> Binding<CGSize> {
            return .init(get: {
                return self.contentSize
            },set: {
                print("watched : contentSize= \($0)")
                self.contentSize = $0
            })
        }
        func newDragGuesture() -> some Gesture {
            return DragGesture()
                .onChanged { _ in
                    print("> drag changed")
                }
            .onEnded { _ in
                DispatchQueue.main.async {
                    print("> drag ended")
                    self.isTopRefreshing = self.shouldTopRefresh
                    self.isBottomRefreshing = self.shouldTopRefresh
                    withAnimation {
                        self.offset = CGPoint.init(x: self.offset.x, y: self.computeOffset())
                    }

                }
            }
        }
        @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

        var body: some View {
            VStack {
                Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
                    Text("Back")
                }
                ZStack {
                    OffsetScrollView(.vertical, showsIndicators: true,
                                     offset: self.watchOffset(),
                                     contentSize: self.watchContentSize(),
                                     scrollViewFrame: self.watchScrollViewRect())
                    {
                        VStack {
                            ForEach(self.items, id: \.self) { item in
                                HStack {
                                    Text("\(item)")
                                        .font(.system(Font.TextStyle.title))
                                        .fontWeight(.regular)
                                        //.frame(width: geo.size.width)
                                        //.background(Color.blue)
                                        .padding(.horizontal, 8)
                                    Spacer()
                                }
                                    //.background(Color.red)
                                    .padding(.bottom, 8)

                            }
                        }//.background(Color.clear)


                    }.edgesIgnoringSafeArea(.horizontal)
                        .background(Color.red)
     //.simultaneousGesture(self.newDragGuesture())
                    VStack {
                        ArrowShape()
                            .stroke(style: StrokeStyle.init(lineWidth: 2, lineCap: .round, lineJoin: .round, miterLimit: 0, dash: [], dashPhase: 0))
                            .fill(Color.black)
                            .frame(width: 12, height: 16)
                            .padding(.all, 2)
                            //.animation(nil)
                            .rotationEffect(.degrees(self.shouldTopRefresh ? -180 : 0))
                            .animation(.linear(duration: 0.2))
                            .transformEffect(.init(translationX: 0, y: self.top - 32))
                            .animation(nil)
                            .opacity(self.isTopRefreshing ? 0 : 1)


                        Spacer()

                        ArrowShape()
                            .stroke(style: StrokeStyle.init(lineWidth: 2, lineCap: .round, lineJoin: .round, miterLimit: 0, dash: [], dashPhase: 0))
                            .fill(Color.black)
                            .frame(width: 12, height: 16)
                            .padding(.all, 2)
                            //.animation(nil)
                            .rotationEffect(.degrees(self.shouldBottomRefresh ? 0 : -180))
                            .animation(.linear(duration: 0.2))
                            .transformEffect(.init(translationX: 0, y: self.bottomLocation))
                            .animation(nil)
                            .opacity(self.isBottomRefreshing ? 0 : 1)
                    }
    //                Color.init(.sRGB, white: 0.2, opacity: 0.7)
    //
    //                    .simultaneousGesture(self.newDragGuesture())
                }

                .clipped()
                .clipShape(Rectangle())


                Text("Offset: \(String(describing: self.offset))")
                Text("contentSize: \(String(describing: self.contentSize))")
                Text("scrollViewRect: \(String(describing: self.scrollViewRect))")

            }
        }
    }


    //https://zacwhite.com/2019/scrollview-content-offsets-swiftui/
    public struct OffsetScrollView<Content>: View where Content : View {

        /// The content of the scroll view.
        public var content: Content

        /// The scrollable axes.
        ///
        /// The default is `.vertical`.
        public var axes: Axis.Set

        /// If true, the scroll view may indicate the scrollable component of
        /// the content offset, in a way suitable for the platform.
        ///
        /// The default is `true`.
        public var showsIndicators: Bool
        /// The initial offset of the view as measured in the global frame
        @State private var initialOffset: CGPoint?

        /// The offset of the scroll view updated as the scroll view scrolls
        @Binding public var scrollViewFrame: CGRect
        @Binding public var offset: CGPoint
        @Binding public var contentSize: CGSize

        public init(_ axes: Axis.Set = .vertical,
                    showsIndicators: Bool = true,
                    offset: Binding<CGPoint> = .constant(.zero),
                    contentSize: Binding<CGSize> = .constant(.zero) ,
                    scrollViewFrame: Binding<CGRect> = .constant(.zero),
                    @ViewBuilder content: () -> Content) {
            self.axes = axes
            self.showsIndicators = showsIndicators
            self._offset = offset
            self._contentSize = contentSize
            self.content = content()
            self._scrollViewFrame = scrollViewFrame

        }
        public var body: some View {
            ZStack {

                GeometryReader { geometry in
                    Run {
                        let frame = geometry.frame(in: .global)
                        self.$scrollViewFrame.wrappedValue = frame
                    }
                }
                ScrollView(axes, showsIndicators: showsIndicators) {
                    ZStack(alignment: .leading) {
                        GeometryReader { geometry in
                            Run {
                                let frame = geometry.frame(in: .global)
                                let globalOrigin = frame.origin
                                self.initialOffset = self.initialOffset ?? globalOrigin
                                let initialOffset = (self.initialOffset ?? .zero)
                                let offset = CGPoint(x: globalOrigin.x - initialOffset.x, y: globalOrigin.y - initialOffset.y)
                                self.$offset.wrappedValue = offset
                                self.$contentSize.wrappedValue = frame.size
                            }
                        }
                        content
                    }
                }

            }
        }
    }


    struct Run: View {
        let block: () -> Void

        var body: some View {
            DispatchQueue.main.async(execute: block)
            return AnyView(EmptyView())
        }
    }



    extension CGPoint {
        func reScale(from: CGRect, to: CGRect) -> CGPoint {
            let x = (self.x - from.origin.x) / from.size.width * to.size.width + to.origin.x
            let y = (self.y - from.origin.y) / from.size.height * to.size.height + to.origin.y
            return .init(x: x, y: y)
        }
        func center(from: CGRect, to: CGRect) -> CGPoint {
            let x = self.x + (to.size.width - from.size.width) / 2 - from.origin.x + to.origin.x
            let y = self.y + (to.size.height - from.size.height) / 2 - from.origin.y + to.origin.y
            return .init(x: x, y: y)
        }
    }
    enum ArrowContentMode {
        case center
        case reScale
    }
    extension ArrowContentMode {
        func transform(point: CGPoint, from: CGRect, to: CGRect) -> CGPoint {
            switch self {
            case .center:
                return point.center(from: from, to: to)
            case .reScale:
                return point.reScale(from: from, to: to)
            }
        }
    }
    struct ArrowShape : Shape {
        let contentMode : ArrowContentMode = .center
        func path(in rect: CGRect) -> Path {
            var path = Path()


            let points = [
                CGPoint(x: 0, y: 8),
                CGPoint(x: 0, y: -8),
                CGPoint(x: 0, y: 8),
                CGPoint(x: 5.66, y: 2.34),
                CGPoint(x: 0, y: 8),
                CGPoint(x: -5.66, y: 2.34)
            ]
            let minX = points.min { $0.x < $1.x }?.x ?? 0
            let minY = points.min { $0.y < $1.y }?.y ?? 0

            let maxX = points.max { $0.x < $1.x }?.x ?? 0
            let maxY = points.max { $0.y < $1.y }?.y ?? 0


            let fromRect = CGRect.init(x: minX, y: minY, width: maxX-minX, height: maxY-minY)
            print("fromRect nx: ",minX,minY,maxX,maxY)
            print("fromRect: \(fromRect), toRect: \(rect)")

            let transformed = points.map { contentMode.transform(point: $0, from: fromRect, to: rect) }

            print("fromRect: transformed=>\(transformed)")

            path.move(to: transformed[0])
            path.addLine(to: transformed[1])
            path.move(to: transformed[2])
            path.addLine(to: transformed[3])
            path.move(to: transformed[4])
            path.addLine(to: transformed[5])


            return path
        }
    }

我需要的一种方法是告诉用户何时释放滚动视图,并且如果刷新刷新"箭头超过了阈值并被旋转,则滚动将移动到某个偏移量(例如32),并隐藏箭头和显示一个ActivityIndi​​cator.

what I need is a way to tell when the user releases the scrollview, and if the pull to refresh arrow passed the threshold and was rotated, the scroll will move to a certain offset (say 32), and hide the arrow and show an ActivityIndicator.

注意:我尝试使用DragGesture,但是:

NOTE: I tried using DragGesture but:

 * it wont work on the scroll view

 * OR block the scrolling on the scrollview content

推荐答案

您可以使用内省获取UIScrollView,然后从中获取UIScrollView.contentOffset和UIScrollView.isDragging的发布者,以获取可用于操纵SwiftUI视图的那些值的更新.

You can use Introspect to get the UIScrollView, then from that get the publisher for UIScrollView.contentOffset and UIScrollView.isDragging to get updates on those values which you can use to manipulate your SwiftUI views.


struct Example: View {
    @State var isDraggingPublisher = Just(false).eraseToAnyPublisher()
    @State var offsetPublisher = Just(.zero).eraseToAnyPublisher()

    var body: some View {
        ...
        .introspectScrollView {
            self.isDraggingPublisher = $0.publisher(for: \.isDragging).eraseToAnyPublisher()
            self.offsetPublisher = $0.publisher(for: \.contentOffset).eraseToAnyPublisher()
        }
        .onReceive(isDraggingPublisher) { 
            // do something with isDragging change
        }
        .onReceive(offsetPublisher) { 
            // do something with offset change
        }
        ...        
}

如果您想看一个例子;我使用这种方法在我的软件包 ScrollViewProxy 中获取偏移量发布者.

If you want to look at an example; I use this method to get the offset publisher in my package ScrollViewProxy.

这篇关于SwiftUI滚动/列表滚动事件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆