Skip to content

Commit

Permalink
Fix #12
Browse files Browse the repository at this point in the history
  • Loading branch information
wtmoose committed Sep 17, 2024
1 parent 3042a80 commit bb97ec4
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file.

## 2.0.3

* Fix #12 Scroll doesn't reset when switching the tabs while scrolled to top
* Fix the bottom padding on `MaterialTabsScroll` in iOS 18

## 2.0.2
Expand Down
48 changes: 33 additions & 15 deletions Sources/SwiftUIMaterialTabs/Internal/HeaderModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,24 +73,42 @@ class HeaderModel<Tab>: ObservableObject where Tab: Hashable {

// MARK: - Height



// MARK: - Scroll tracking

func scrolled(tab: Tab, offset: CGFloat, deltaOffset: CGFloat) {
/// Adjust the header offset as the scroll view's offset changes. As discussed elsewhere in this library, the header view itself doesn't shrink—instead its vertical
/// position is shifted up until it reaches a maximum corresponding to a fully collapsed header state. Scroll effects can be applied to subviews within the
/// header to give the appearance of shrinking, among other things.
///
/// In the basic case, the header offset matches the scroll view up calculated max offset. However, in a multi-tab environment, scrolling on another tab
/// can change the header offset, introducing edge cases that need to be handled.
func scrolled(tab: Tab, contentOffset: CGFloat, deltaContentOffset: CGFloat) {
guard tab == state.headerContext.selectedTab else { return }
//print("tab=\(tab), offset=\(offset), deltaOffset=\(deltaOffset)")
state.headerContext.contentOffset = offset
// Any time the offset is less than the max offset, the header offset exactly tracks the offset.
if offset < state.headerContext.maxOffset {
state.headerContext.offset = offset
}
// However, for greater offsets, the header offset only gets adjusted for positive changes in the offset.
// Once we scroll too far, the header offset hits the limit, so we can't just track the offset. Instead, we
// use the change in offset.
else if deltaOffset > 0 {
let unconstrainedOffset = state.headerContext.offset + deltaOffset
state.headerContext.offset = min(unconstrainedOffset, state.headerContext.maxOffset)
state.headerContext.contentOffset = contentOffset
// When scrolling down (a.k.a. swiping up), the header offset matches the scroll view until it reaches the
// max offset, at which point it is fully collapsed.
if deltaContentOffset > 0 {
// If the scroll view offset is less than the max offset, then the scroll and header offsets should
// match.
if contentOffset < state.headerContext.maxOffset {
state.headerContext.offset = contentOffset
}
// However, if the scroll view is past the max offset, the header must move by the same amount until
// it reaches the max offset.
else {
state.headerContext.offset = min(
state.headerContext.offset + deltaContentOffset,
state.headerContext.maxOffset
)
}
// When scrolling up (a.k.a. swiping down), the header offset remains fixed unless it needs to change to
// prevent the header from separating from the scroll view content. This threshold is reached when the
// top of the scroll view reaches the bottom of the header.
} else {
// If the scroll view's offset is less than the header's offset, the header must match the scroll view
// offset to avoid separation.
if contentOffset < state.headerContext.offset {
state.headerContext.offset = contentOffset
}
}
}
}
2 changes: 1 addition & 1 deletion Sources/SwiftUIMaterialTabs/Internal/ScrollModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class ScrollModel<Item, Tab>: ObservableObject where Item: Hashable, Tab: Hashab
// This is how we're detecting programatic scroll for lack of a better idea. We don't want to report
// the programatic sync of content offset with the header because it could result in the header moving.
guard scrollItem != reservedItem else { return }
headerModel?.scrolled(tab: tab, offset: contentOffset, deltaOffset: deltaOffset)
headerModel?.scrolled(tab: tab, contentOffset: contentOffset, deltaContentOffset: deltaOffset)
}

func appeared(headerModel: HeaderModel<Tab>?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ public struct StickyHeaderScroll<Content, Item>: View where Content: View, Item:
}
}
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
headerModel.scrolled(tab: .none, offset: -offset, deltaOffset: -(offset - contentOffset))
headerModel.scrolled(
tab: .none,
contentOffset: -offset,
deltaContentOffset: -(offset - contentOffset)
)
contentOffset = -offset
}
content(
Expand Down

0 comments on commit bb97ec4

Please sign in to comment.