Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get onAppear behaviour from list in ScrollView in SwiftUI

Tags:

swiftui

When creating a List view onAppear triggers for elements in that list the way you would expect: As soon as you scroll to that element the onAppear triggers. However, I'm trying to implement a horizontal list like this

ScrollView(.horizontal) { 
   HStack(spacing: mySpacing) {
      ForEach(items) { item in 
         MyView(item: item)
            .onAppear { \\do something }
      } 
   }
}

Using this method the onAppear triggers for all items at once, that is to say: immediately, but I want the same behavior as for a List view. How would I go about doing this? Is there a manual way to trigger onAppear, or control when views load?

Why I want to achieve this: I have made a custom Image view that loads an image from an URL only when it appears (and substitutes a placeholder in the mean time), this works fine for a List view, but I'd like it to also work for my horizontal 'list'.

like image 449
M. Koot Avatar asked Jul 28 '19 14:07

M. Koot


2 Answers

As per SwiftUI 2.0 (XCode 12 beta 1) this is finally natively solved: In a LazyHStack (or any other grid or stack with the Lazy prefix) elements will only initialise (and therefore trigger onAppear) when they appear on screen.

like image 132
M. Koot Avatar answered Oct 03 '22 23:10

M. Koot


Here is possible approach how to do this (tested/worked with Xcode 11.2 / iOS 13.2)

Demo: (just show dynamically first & last visible cell in scrollview)

demo

A couple of important View extensions

extension View {
    func rectReader(_ binding: Binding<CGRect>, in space: CoordinateSpace) -> some View {
        self.background(GeometryReader { (geometry) -> AnyView in
            let rect = geometry.frame(in: space)
            DispatchQueue.main.async {
                binding.wrappedValue = rect
            }
            return AnyView(Rectangle().fill(Color.clear))
        })
    }
}

extension View {
    func ifVisible(in rect: CGRect, in space: CoordinateSpace, execute: @escaping (CGRect) -> Void) -> some View {
        self.background(GeometryReader { (geometry) -> AnyView in
            let frame = geometry.frame(in: space)
            if frame.intersects(rect) {
                execute(frame)
            }
            return AnyView(Rectangle().fill(Color.clear))
        })
    }
}

And a demo view of how to use them with cell views being in scroll view

struct TestScrollViewOnVisible: View {
    @State private var firstVisible: Int = 0
    @State private var lastVisible: Int = 0
    @State private var visibleRect: CGRect = .zero
    var body: some View {
        VStack {
            HStack {
                Text("<< \(firstVisible)")
                Spacer()
                Text("\(lastVisible) >> ")
            }
            Divider()
            band()
        }
    }

    func band() -> some View {
        ScrollView(.horizontal) {
            HStack(spacing: 10) {
                ForEach(0..<50) { i in
                    self.cell(for: i)
                        .ifVisible(in: self.visibleRect, in: .named("my")) { rect in
                            print(">> become visible [\(i)]")

                            // do anything needed with visible rects, below is simple example
                            // (w/o taking into account spacing)
                            if rect.minX <= self.visibleRect.minX && self.firstVisible != i {
                                DispatchQueue.main.async {
                                    self.firstVisible = i
                                }
                            } else
                            if rect.maxX >= self.visibleRect.maxX && self.lastVisible != i {
                                DispatchQueue.main.async {
                                    self.lastVisible = i
                                }
                            }
                        }
                }
            }
        }
        .coordinateSpace(name: "my")
        .rectReader(self.$visibleRect, in: .named("my"))
    }

    func cell(for idx: Int) -> some View {
        RoundedRectangle(cornerRadius: 10)
            .fill(Color.yellow)
            .frame(width: 80, height: 60)
            .overlay(Text("\(idx)"))
    }
}
like image 38
Asperi Avatar answered Oct 03 '22 23:10

Asperi