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.
The code works when (either or)
I want to retain the functionality of both the ignore safe are and tabview, how can I workaround this?
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
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 {
NavigationStack {
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))
}
.ignoresSafeArea(edges: .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
}
Here is the complete working solution done by KavSoft. I recommend everyone checking out his YouTube, it's great.
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 {
NavigationStack {
ScrollView(.init()){
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))
}
}
.ignoresSafeArea()
}
}
@ViewBuilder
func scrollBody() -> some View {
ScrollView {
Rectangle()
.fill(.clear)
.frame(height: 90 + top_Inset())
.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)
}
}
LazyVStack {
ForEach(0..<30){ i in
RoundedRectangle(cornerRadius: 15).frame(width: 100, height: 100)
}
}
}
.coordinateSpace(name: "SCROLL")
}
@ViewBuilder
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)
}
.frame(height: 90)
.padding(.top, top_Inset())
.background(.ultraThinMaterial)
}
}
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
}