Hello I am making a simple view with content in a scroll view and a top header. When the user scrolls down I want to hide the header and when the user scrolls up I want to show it. I have three different tabs and if I manually swipe between them everything works fine. If I try and click the buttons in the header to switch tabs the scroll view adjusts the scroll view a bit and does nothing then I get a ton of the error below. Ive been working at this for hours and have had no luck Id appreciate any help, thanks.
The behavior of the UICollectionViewFlowLayout is not defined because: the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values. The relevant UICollectionViewFlowLayout instance is and it is attached to <SwiftUIPagingCollectionView: 0x12d0f6a00; baseClass = UICollectionView; frame = (0 0; 390 844); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x600000888a20>; backgroundColor = UIExtendedGrayColorSpace 0 0; layer = <CALayer: 0x600000670580>; contentOffset: {0.33333333333333331, 0}; contentSize: {1170, 763}; adjustedContentInset: {47, 0, 34, 0}; layout: Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.
import SwiftUI
struct FeedViewSec: View {
@Environment(.colorScheme) var colorScheme
@State private var selection = 0
@State var headerHeight: CGFloat = 130
@State var headerOffset: CGFloat = 0
@State var lastHeaderOffset: CGFloat = 0
@State var direction: SwipeDirection = .none
@State var shiftOffset: CGFloat = 0
var body: some View {
ZStack(alignment: .bottomTrailing){
TabView(selection: $selection) {
scrollBody().tag(0)
scrollBody().tag(1)
scrollBody().tag(2)
}.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
.overlay(alignment: .top) {
headerView().offset(y: -headerOffset < headerHeight ? headerOffset : (headerOffset < 0 ? headerOffset : 0))
}
.edgesIgnoringSafeArea(.top)
}
func scrollBody() -> some View {
ScrollView {
LazyVStack {
Color.clear.frame(height: 130)
ForEach(0..<30){ i in
RoundedRectangle(cornerRadius: 15).frame(width: 100, height: 100)
}
}
.offsetY { previous, current in
if previous > current {
if direction != .up && current < 0{
shiftOffset = current - headerOffset
direction = .up
lastHeaderOffset = headerOffset
}
let offset = current < 0 ? (current - shiftOffset) : 0
headerOffset = (-offset < headerHeight ? (offset < 0 ? offset : 0) : -headerHeight * 2.0)
} else {
if direction != .down{
shiftOffset = current
direction = .down
lastHeaderOffset = headerOffset
}
let offset = lastHeaderOffset + (current - shiftOffset)
headerOffset = (offset > 0 ? 0 : offset)
}
}
}.coordinateSpace(name: "SCROLL")
}
func headerView() -> some View {
VStack(spacing: 0){
HStack {
HStack(spacing: 1){
Text("Explore").font(.title).bold()
Image(systemName: "chevron.down").font(.body).bold()
}
Spacer()
}
.padding(.leading)
HStack(alignment: .center, spacing: 0) {
Button {
withAnimation(.easeInOut){
selection = 0
}
} label: {
Text("New")
.foregroundColor(.black).bold()
.frame(width: 80, height: 25)
}
.background((selection == 0) ? colorScheme == .dark ? .gray.opacity(0.3) : .gray : colorScheme == .dark ? .gray : .gray.opacity(0.3))
Button {
withAnimation(.easeInOut){
selection = 1
}
} label: {
Text("LeaderBoard")
.foregroundColor(.black).bold()
.frame(width: 120, height: 25)
}
.background((selection == 1) ? colorScheme == .dark ? .gray.opacity(0.3) : .gray : colorScheme == .dark ? .gray : .gray.opacity(0.3))
Button {
withAnimation(.easeInOut){
selection = 2
}
} label: {
Text("Hot")
.foregroundColor(.black).bold()
.frame(width: 80, height: 25)
}
.background((selection == 2) ? colorScheme == .dark ? .gray.opacity(0.3) : .gray : colorScheme == .dark ? .gray : .gray.opacity(0.3))
}
.mask {
RoundedRectangle(cornerRadius: 5)
}
.padding(.top, 8)
Color.clear.frame(height: 13)
}
.padding(.top, top_Inset())
.background(.ultraThinMaterial)
}
}
func top_Inset() -> CGFloat {
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
return window?.safeAreaInsets.top ?? 0
}
extension View{
@ViewBuilder
func offsetY(completion: @escaping (CGFloat,CGFloat)->())->some View{
self.modifier(OffsetHelper(onChange: completion))
}
func safeArea()->UIEdgeInsets{
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else{return .zero}
guard let safeArea = scene.windows.first?.safeAreaInsets else{return .zero}
return safeArea
}
}
struct OffsetHelper: ViewModifier{
var onChange: (CGFloat,CGFloat)->()
@State var currentOffset: CGFloat = 0
@State var previousOffset: CGFloat = 0
func body(content: Content) -> some View {
content
.overlay {
GeometryReader{proxy in
let minY = proxy.frame(in: .named("SCROLL")).minY
Color.clear
.preference(key: OffsetKeyNew.self, value: minY)
.onPreferenceChange(OffsetKeyNew.self) { value in
previousOffset = currentOffset
currentOffset = value
onChange(previousOffset,currentOffset)
}
}
}
}
}
struct OffsetKeyNew: PreferenceKey{
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct HeaderBoundsKey: PreferenceKey{
static var defaultValue: Anchor<CGRect>?
static func reduce(value: inout Anchor<CGRect>?, nextValue: () -> Anchor<CGRect>?) {
value = nextValue()
}
}
enum SwipeDirection{
case up
case down
case none
}
2
Answers
Tou have an error from the
UICollectionViewFlowLayout
which is related to the height of the items in the collection view.However, you are using SwiftUI for your interface and not UIKit‘s
UICollectionView
. The error should mean that the item height in a collection view is too tall for the available space. It could be related to how SwiftUI is being bridged to UIKit under the hood, or how the UIKit’sUIScrollView
is being managed within SwiftUI’sScrollView
.You are trying to manage the header’s visibility and the tab selection with quite a complex setup using offsets, preferences, and geometry readers. That setup might be making it hard to debug or identify the actual issue.
I would recommend first to simplify the layout: Temporarily remove the header and see if the error persists. That can help isolate whether the header is the cause of the issue. You might want to simplify the layout to the bare minimum to see if the error still occurs, and then gradually reintroduce elements to identify the cause.
Then check the layout calculations: Verify all the offset and height calculations, especially where you are manipulating
headerOffset
andshiftOffset
, to ensure they are producing the intended values.You might want to add some print statements or use a debugger to check the values of your offsets and other variables at runtime to see if anything unexpected is happening. The error message suggests setting a symbolic breakpoint at
UICollectionViewFlowLayoutBreakForInvalidSizes
to catch this in the debugger. That might provide more insight into the issue.There might be a simpler way to achieve the effect you’re after. In the revised example below, the following techniques are used:
ScrollView
has aGeometryReader
in the background and this is used to monitor for a change of scroll position.GeometryReader
in.onAppear
. By placing the.ignoresSafeArea
more selectively, it is not necessary to add padding to compensate for the safe area insets.ScrollViewPoxy
. The header is also brought into view, if it was previously hidden.ScrollView
was causing the header to be hidden or to become visible when it was not supposed to. To prevent this from happening, a change of scroll direction is only accepted if there has been a delay of at least 0.5s since the last scroll.Here you go: