Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions Flipcash/Core/Screens/Profile/ChatProfileScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,18 +148,19 @@ struct ChatProfileScreen: View {
.padding(.top, 24)
}

if origin == .chat, isMember {
if endsWithLeaveButton {
// Holds Leave Chat at the bottom of the screen when the content is short.
Spacer(minLength: 24)
leaveButton
.padding(.horizontal, ProfileHeaderMetrics.inset)
}
}
.frame(minHeight: max(scrollFit.visibleHeight - 24, 0), alignment: .top)
.padding(.bottom, 24)
.frame(minHeight: max(scrollFit.visibleHeight - contentBottomPadding, 0), alignment: .top)
.padding(.bottom, contentBottomPadding)
}
.profilePinnedBackdropClearance(isActive: origin != .chat || scrollFit.overflows)
.profilePinnedBackdropClearance(isActive: origin != .chat)
.profileScrollFit($scrollFit)
.scrollIndicators(.hidden)
// The banner runs under the status bar.
.ignoresSafeArea(edges: .top)
// The blur only belongs once the banner has scrolled up under the bar.
Expand Down Expand Up @@ -333,6 +334,18 @@ struct ChatProfileScreen: View {
return holding(stated, mint: mint)
}

/// Whether Leave Chat ends the scroll content rather than sitting in the pinned bar.
private var endsWithLeaveButton: Bool {
origin == .chat && isMember
}

/// Space under the scroll content. Leave Chat ending it sits where the pinned bar puts it: a
/// text-only button is a full button tall, so its frame already leaves room under the title and
/// can overlap the home indicator's inset.
private var contentBottomPadding: CGFloat {
endsWithLeaveButton ? -12 : 24
}

// MARK: - Pinned -

@ViewBuilder
Expand Down
15 changes: 6 additions & 9 deletions Flipcash/Core/Screens/Profile/ProfilePinnedBackdrop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ extension View {
}

/// Fades content into the bottom of a profile that pins no bar, the way the chat's transcript
/// dissolves under its composer: a ramp from the clearance's top edge to the screen's bottom edge.
/// `isActive` false draws nothing, for a screen whose content doesn't scroll.
/// dissolves under its composer: a ramp across the home indicator's inset only, so the row that
/// ends the scroll stays clear of it. `isActive` false draws nothing, for a screen whose content
/// doesn't scroll.
func profileBarlessFade(isActive: Bool) -> some View {
background {
if isActive {
Expand All @@ -50,7 +51,6 @@ extension View {
startPoint: .top,
endPoint: .bottom
)
.padding(.top, -ProfilePinnedBackdrop.fadeHeight)
.ignoresSafeArea(edges: .bottom)
.allowsHitTesting(false)
}
Expand Down Expand Up @@ -82,17 +82,14 @@ struct ProfileScrollFit: Equatable {
/// The height the scroll view shows content in, which short content stretches to.
private(set) var visibleHeight: CGFloat = 0

/// Whether the content runs past the screen, which is when the bottom fade and its clearance
/// belong.
/// Whether the content runs past the screen, which is when a barless profile's bottom fade
/// belongs.
private(set) var overflows = false

/// Records the scroll view's visible height and its content's height.
mutating func update(visibleHeight: CGFloat, contentHeight: CGFloat) {
self.visibleHeight = visibleHeight
// The clearance shrinks the visible height while it is on; compare against the height
// without it, or turning it on would keep it on.
let clearance = overflows ? ProfilePinnedBackdrop.fadeHeight : 0
overflows = contentHeight > visibleHeight + clearance + 0.5
overflows = contentHeight > visibleHeight + 0.5
}
}

Expand Down
3 changes: 2 additions & 1 deletion Flipcash/Core/Screens/Profile/UserProfileScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,9 @@ private struct UserProfileContent: View {
.frame(minHeight: max(scrollFit.visibleHeight - 24, 0), alignment: .top)
.padding(.bottom, 24)
}
.profilePinnedBackdropClearance(isActive: !hidesPinnedBar || scrollFit.overflows)
.profilePinnedBackdropClearance(isActive: !hidesPinnedBar)
.profileScrollFit($scrollFit)
.scrollIndicators(.hidden)
// The banner runs under the status bar.
.ignoresSafeArea(edges: .top)
// The blur only belongs once the banner has scrolled up under the bar.
Expand Down
16 changes: 3 additions & 13 deletions FlipcashTests/ProfileScrollFitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,13 @@ struct ProfileScrollFitTests {
#expect(fit.overflows)
}

@Test("Content that shrinks to fit releases the clearance it turned on")
func clearance_releases() {
@Test("Content that shrinks to fit stops overflowing")
func shrinks_to_fit() {
var fit = ProfileScrollFit()
fit.update(visibleHeight: 700, contentHeight: 1200)
#expect(fit.overflows)

// The 56pt clearance leaves 644 visible; 690 overflows that but fits the 700 the screen
// has without it.
fit.update(visibleHeight: 644, contentHeight: 690)
fit.update(visibleHeight: 700, contentHeight: 700)
#expect(fit.overflows == false)
}

@Test("Overflowing content stays overflowing once the clearance is on")
func clearance_holds() {
var fit = ProfileScrollFit()
fit.update(visibleHeight: 700, contentHeight: 1200)
fit.update(visibleHeight: 644, contentHeight: 1200)
#expect(fit.overflows)
}
}
Loading