问题描述
我创建了一个 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)
}
}