1

I have the following code for displaying images in LazyHStack:

ScrollView(.horizontal, showsIndicators: false) {
    LazyHStack(alignment: .top) {
        let photos: [PhotoObject] = []//some array of photo objects
        ForEach(photos, id: \.self) { item in
            KFImage.url(URL(string: item.url))
                    .resizable()
                    .frame(width: 200, height: 200)
                    .cornerRadius(13)
        }
    }
}

I would like to execute a function every time when the first visible item in the stack changes (e.g. when user scrolls to the left so much, that the left-most image is no longer visible and the image to the right is now first visible image). Android's Jetpack compose has very simple solution for this. I've played with custom TrackableScrollView but this only returns offset which I'm not sure what it is, as it starts with e.g. 1500 and when you scroll to the end the value is ~ -1200 - not sure how should I calculate the index from the offset that is half positive and half negative.

Simon
  • 1,657
  • 11
  • 16

1 Answers1

7

While there are the same sort of built in functions in UIView, as you are showing, this isn't built in to SwiftUI yet, but you can do it yourself. Essentially what you do is get the coordinates of the outer view, in this case your ScrollView, and see if the inner coordinates of you image view are at least partially within the outer view. This is done with two GeometryReaders and a .coordinateSpaces() designator so we can reliably be in the same coordinate space. If the image exists within the outer coordinate space, then it is added to a @State variable which is a set. if not, it is removed from the set. This gives you are running list of what views are currently partially or fully visible.

Since you did not give a Minimal, Reproducible Example, I took your code and used Rectangles to represent your images, but you should be able to simply reinsert your code. Below is a working example:

struct ImageIsInView: View {
    
    @State var visibleIndex: Set<Int> = [0,1]
       
    var body: some View {
        VStack {
            Text(visibleIndex.map( { $0.description }).sorted().joined(separator: ", "))
            // The outer GeometryReader has to go directly around your ScrollView
            GeometryReader { outerProxy in
                ScrollView(.horizontal, showsIndicators: false) {
                    LazyHStack(alignment: .top) {
                        ForEach(0..<10, id: \.self) { item in
                            GeometryReader { geometry in
                                Rectangle()
                                    .fill(Color.orange)
                                    .cornerRadius(13)
                                    .overlay(
                                        Text("Item: \(item)")
                                    )
                                    // every time the ScrollView moves, the inner geometry changes and is
                                    // picked up here:
                                    .onChange(of: geometry.frame(in: .named("scrollView"))) { imageRect in
                                        if isInView(innerRect: imageRect, isIn: outerProxy) {
                                            visibleIndex.insert(item)
                                        } else {
                                            visibleIndex.remove(item)
                                        }
                                    }
                            }
                            .frame(width: 200, height: 200)
                        }
                    }
                }
                .coordinateSpace(name: "scrollView")
            }
        }
    }
    
    private func isInView(innerRect:CGRect, isIn outerProxy:GeometryProxy) -> Bool {
        let innerOrigin = innerRect.origin.x
        let imageWidth = innerRect.width
        let scrollOrigin = outerProxy.frame(in: .global).origin.x
        let scrollWidth = outerProxy.size.width
        if innerOrigin + imageWidth < scrollOrigin + scrollWidth && innerOrigin + imageWidth > scrollOrigin ||
            innerOrigin + imageWidth > scrollOrigin && innerOrigin < scrollOrigin + scrollWidth {
            return true
        }
        return false
    }
}
Yrb
  • 8,103
  • 2
  • 14
  • 44
  • Thanks a ton. I used this as a workaround for an issue I can't solve. If it would help or anyone is interested that issue is described here: https://stackoverflow.com/questions/71649370/lazyhstack-nested-inside-lazyvgrid-views-dont-align-horizontally-on-diagonal-s – braden Mar 28 '22 at 18:36