基于 GeometryReader 结果运行条件代码的视图扩展

问题描述

我创建了一个 View 扩展来读取其偏移量(受 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)
}

我也在使用 Federico 的 readSize 函数

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)
}

两者一起帮助我确定滚动视图中的子视图是否在屏幕上/屏幕外:

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
    }
}

这是检查可见性的 isOffscreen 函数

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

一切正常。但是,我想将代码进一步优化为单个扩展,该扩展基于输入的偏移量和 size.height 来检查可见性,并且还接收有关在可见和不可见时要做什么的参数,即移动 readOffset 的闭包为逻辑与扩展代码共存。

我不知道这是否可行,但认为值得一问。

解决方法

您只需要创建一个需要一些绑定的 View 或 ViewModifier。请注意,下面的代码只是您可以使用的一些模式的示例(例如,可选绑定、转义内容闭包),但采用 Stack 样式包装而不是 ViewModifier 的形式(基于您知道的博客)如何设置)。


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)
    }

}