In SwiftUI, where are the control events, i.e. scrollViewDidScroll to detect the bottom of list data

后端 未结 5 1749
南笙
南笙 2020-12-09 18:57

In SwiftUI, does anyone know where are the control events such as scrollViewDidScroll to detect when a user reaches the bottom of a list causing an event to retrieve additio

相关标签:
5条回答
  • 2020-12-09 19:25

    The OnAppear workaround works fine on a LazyVStack nested inside of a ScrollView, e.g.:

    ScrollView {
       LazyVStack (alignment: .leading) {
          TextField("comida", text: $controller.searchedText)
          
          switch controller.dataStatus {
             case DataRequestStatus.notYetRequested:
                typeSomethingView
             case DataRequestStatus.done:
                bunchOfItems
             case DataRequestStatus.waiting:
                loadingView
             case DataRequestStatus.error:
                errorView
          }
          
          bottomInvisibleView
             .onAppear {
                controller.loadNextPage()
             }
       }
       .padding()
    }
    

    The LazyVStack is, well, lazy, and so only create the bottom when it's almost on the screen

    0 讨论(0)
  • 2020-12-09 19:34

    You can to check that the latest element is appeared inside onAppear.

    struct ContentView: View {
        @State var items = Array(1...30)
    
        var body: some View {
            List {
                ForEach(items, id: \.self) { item in
                    Text("\(item)")
                    .onAppear {
                        if let last == self.items.last {
                            print("last item")
                            self.items += last+1...last+30
                        }
                    }
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-09 19:40

    In case you need more precise info on how for the scrollView or list has been scrolled, you could use the following extension as a workaround:

    extension View {
    
        func onFrameChange(_ frameHandler: @escaping (CGRect)->(), 
                        enabled isEnabled: Bool = true) -> some View {
    
            guard isEnabled else { return AnyView(self) }
    
            return AnyView(self.background(GeometryReader { (geometry: GeometryProxy) in
    
                Color.clear.beforeReturn {
    
                    frameHandler(geometry.frame(in: .global))
                }
            }))
        }
    
        private func beforeReturn(_ onBeforeReturn: ()->()) -> Self {
            onBeforeReturn()
            return self
        }
    }
    

    The way you can leverage the changed frame like this:

    struct ContentView: View {
    
        var body: some View {
    
            ScrollView {
    
                ForEach(0..<100) { number in
    
                    Text("\(number)").onFrameChange({ (frame) in
    
                        print("Origin is now \(frame.origin)")
    
                    }, enabled: number == 0)
                }
            }
        }
    }
    

    The onFrameChange closure will be called while scrolling. Using a different color than clear might result in better performance.

    edit: I've improved the code a little bit by getting the frame outside of the beforeReturn closure. This helps in the cases where the geometryProxy is not available within that closure.

    0 讨论(0)
  • 2020-12-09 19:41

    I tried the answer for this question and was getting the error Pattern matching in a condition requires the 'case' keyword like @C.Aglar .

    I changed the code to check if the item that appears is the last of the list, it'll print/execute the clause. This condition will be true once you scroll and reach the last element of the list.

    struct ContentView: View {
        @State var items = Array(1...30)
    
        var body: some View {
            List {
                ForEach(items, id: \.self) { item in
                    Text("\(item)")
                    .onAppear {
                        if item == self.items.last {
                            print("last item")
                            fetchStuff()
                        }
                    }
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-09 19:49

    Plenty of features are missing from SwiftUI - it doesn't seem to be possible at the moment.

    But here's a workaround.

    TL;DR skip directly at the bottom of the answer

    An interesting finding whilst doing some comparisons between ScrollView and List:

    struct ContentView: View {
    
        var body: some View {
    
            ScrollView {
                ForEach(1...100) { item in
                    Text("\(item)")
                }
                Rectangle()
                    .onAppear { print("Reached end of scroll view")  }
            }
        }
    
    }
    

    I appended a Rectangle at the end of 100 Text items inside a ScrollView, with a print in onDidAppear.

    It fired when the ScrollView appeared, even if it showed the first 20 items.

    All views inside a Scrollview are rendered immediately, even if they are offscreen.

    I tried the same with List, and the behaviour is different.

    struct ContentView: View {
    
        var body: some View {
    
            List {
                ForEach(1...100) { item in
                    Text("\(item)")
                }
                Rectangle()
                    .onAppear { print("Reached end of scroll view")  }
            }
        }
    
    }
    

    The print gets executed only when the bottom of the List is reached!

    So this is a temporary solution, until SwiftUI API gets better.

    Use a List and place a "fake" view at the end of it, and put fetching logic inside onAppear { }

    0 讨论(0)
提交回复
热议问题