0

I’ve created a View extension to read its offset (inspired by https://fivestars.blog/swiftui/swiftui-share-layout-information.html):

func readOffset(in coordinateSpace: String? = nil, onChange: @escaping (CGFloat) -> Void) -> some View {
    background(
        GeometryReader { 
            Color.clear.preference(key: ViewOffsetKey.self,
               value: -$0.frame(in: coordinateSpace == nil ? .global : .named(coordinateSpace)).origin.y)
    })
    .onPreferenceChange(ViewOffsetKey.self, perform: onChange)
}

I’m also using Federico’s readSize function:

func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
    background(
        GeometryReader { geo in
            Color.clear
                .preference(key: SizePreferenceKey.self, value: geo.size)
    })
    .onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}

The two work together to help me determine whether a child view within a scrollview is on/off-screen:

struct TestInfinityList: View {
    
    @State var visibleItems: Set<Int> = []
    @State var items: [Int] = Array(0...20)
    @State var size: CGSize = .zero
    
    var body: some View {
        ScrollView(.vertical) {
            ForEach(items, id: \.self) { item in
                
                GeometryReader { geo in
                    VStack {
                        Text("Item \(item)")
                    }.id(item)
                    .readOffset(in: "scroll") { newOffset in
                        if !isOffscreen(when: newOffset, in: size.height) {
                            visibleItems.insert(item)
                        }
                        else {
                            visibleItems.remove(item)
                        }
                    }
                }.frame(height: 300)
                
            }
        }.coordinateSpace(name: "scroll")
    }
    .readSize { newSize in
        self.size = newSize
    }
}

This is the isOffscreen function that checks for visibility:

func isOffscreen(when offset: CGFloat, in height: CGFloat) -> Bool {
    if offset <= 0 && offset + height >= 0 {
        return false
    }
    return true
} 

Everything works fine. However, I’d like to optimise the code further into a single extension that checks for visibility based on the offset and size.height inputted, and also receives parameters for what to do if visible and when not i.e. move readOffset’s closure to be logic that co-exists with the extension code.

I’ve no idea whether this is feasible but thought it’s worth an ask.

Barrrdi
  • 902
  • 13
  • 33

1 Answers1

0

You just need to create a View or ViewModifier that demands some Bindings. Note, the code below is just an example of some of the patterns you can use (e.g., an optional binding, escaping content closure), but in the form of a Stack style wrap rather than a ViewModifier (which based on the blog you know how to setup).


struct ScrollableVStack<Content: View>: View {

    let content: Content
    @Binding var useScrollView: Bool
    @Binding var scroller: ScrollViewProxy?
    @State private var staticGeo = ViewGeometry()
    @State private var scrollContainerGeo = ViewGeometry()
    let topFade: CGFloat
    let bottomFade: CGFloat

    init(_ useScrollView: Binding<Bool>,
         topFade: CGFloat = 0.09,
         bottomFade: CGFloat = 0.09,
         _ scroller: Binding<ScrollViewProxy?> = .constant(nil),
         @ViewBuilder _ content: @escaping () -> Content ) {
        _useScrollView = useScrollView
        _scroller = scroller
        self.content = content()
        self.topFade = topFade
        self.bottomFade = bottomFade
    }

    var body: some View {
        if useScrollView { scrollView }
        else { VStack { staticContent } }
    }

    var scrollView: some View {
        ScrollViewReader { scroller in
            ScrollView(.vertical, showsIndicators: false) {
                staticContent
                    .onAppear { self.scroller = scroller }
            }
            .geometry($scrollContainerGeo)
            .fadeInOut(topFade: staticGeo.size.height * topFade,
                       bottomFade: staticGeo.size.height * bottomFade)
        }
        .onChange(of: staticGeo.size.height) { newStaticHeight in
            useScrollView = newStaticHeight > scrollContainerGeo.size.height * 0.85
        }

    }

    var staticContent: some View {
        content
            .geometry($staticGeo)
            .padding(.top, staticGeo.size.height * topFade * 1.25)
            .padding(.bottom, staticGeo.size.height * bottomFade)
    }

}


Ryan
  • 1,252
  • 6
  • 15