This might help in case you need to listen on changes in font sizes, context changes etc. Simply just change the viewIndex
to you needed identifier for changes.
This view will inform you about if it's scrolled or not, and also if the original content fits inside the scrollview or if it's scrollable.
Hope it helps someone :)
import Combine
import SwiftUI
struct FeedbackScrollView<Content: View>: View {
/// Used to inform the FeedbackScrollView if the view changes (mainly used in 'flows')
var viewIndex: Double
/// Notifies if the scrollview is scrolled
@Binding var scrollViewIsScrolled: Bool
/// Notifies if the scrollview has overflow in it's content, to indicate if it can scroll or now
@Binding var scrollViewCanScroll: Bool
/// The content you want to put into the scrollview.
@ViewBuilder private let content: () -> Content
public init(
viewIndex: Double = 0,
scrollViewIsScrolled: Binding<Bool> = .constant(false),
scrollViewCanScroll: Binding<Bool>,
@ViewBuilder content: @escaping () -> Content
) {
self.viewIndex = viewIndex
self._scrollViewIsScrolled = scrollViewIsScrolled
self._scrollViewCanScroll = scrollViewCanScroll
self.content = content
}
var body: some View {
GeometryReader { geometry in
ScrollView {
offsetReader
content()
.frame(
minHeight: geometry.size.height,
alignment: .topLeading
)
.background(
GeometryReader { contentGeometry in
Color.clear
.onAppear {
scrollViewCanScroll = contentGeometry.size.height > geometry.size.height
}
.onChange(of: viewIndex) { _ in
scrollViewCanScroll = contentGeometry.size.height > geometry.size.height
}
}
)
}
.dismissKeyboardOnDrag()
.coordinateSpace(name: "scrollSpace")
.onPreferenceChange(OffsetPreferenceKey.self, perform: offsetChanged(offset:))
}
}
var offsetReader: some View {
GeometryReader { proxy in
Color.clear
.preference(
key: OffsetPreferenceKey.self,
value: proxy.frame(in: .named("scrollSpace")).minY
)
}
.frame(height: 0)
}
private func offsetChanged(offset: CGFloat) {
withAnimation {
scrollViewIsScrolled = offset < 0
}
}
}
private struct OffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}
struct FeedbackScrollView_Previews: PreviewProvider {
static var previews: some View {
FeedbackScrollView(
viewIndex: 0,
scrollViewIsScrolled: .constant(false),
scrollViewCanScroll: .constant(true)
) { }
}
}
Use it like this:
...
@State var scrollViewIsScrolled: Bool
@State var scrollViewCanScroll: Bool
FeedbackScrollView(
viewIndex: numberOfCompletedSteps,
scrollViewIsScrolled: $scrollViewIsScrolled,
scrollViewCanScroll: $scrollViewCanScroll
) {
// Your (scrollable) content goes here..
}