diff --git a/Flipcash/Core/Screens/Profile/ChatProfileScreen.swift b/Flipcash/Core/Screens/Profile/ChatProfileScreen.swift index 388706726..2d768c561 100644 --- a/Flipcash/Core/Screens/Profile/ChatProfileScreen.swift +++ b/Flipcash/Core/Screens/Profile/ChatProfileScreen.swift @@ -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. @@ -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 diff --git a/Flipcash/Core/Screens/Profile/ProfilePinnedBackdrop.swift b/Flipcash/Core/Screens/Profile/ProfilePinnedBackdrop.swift index 4f2531061..f1c808aa6 100644 --- a/Flipcash/Core/Screens/Profile/ProfilePinnedBackdrop.swift +++ b/Flipcash/Core/Screens/Profile/ProfilePinnedBackdrop.swift @@ -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 { @@ -50,7 +51,6 @@ extension View { startPoint: .top, endPoint: .bottom ) - .padding(.top, -ProfilePinnedBackdrop.fadeHeight) .ignoresSafeArea(edges: .bottom) .allowsHitTesting(false) } @@ -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 } } diff --git a/Flipcash/Core/Screens/Profile/UserProfileScreen.swift b/Flipcash/Core/Screens/Profile/UserProfileScreen.swift index 2f9e16809..260cdc4c6 100644 --- a/Flipcash/Core/Screens/Profile/UserProfileScreen.swift +++ b/Flipcash/Core/Screens/Profile/UserProfileScreen.swift @@ -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. diff --git a/FlipcashTests/ProfileScrollFitTests.swift b/FlipcashTests/ProfileScrollFitTests.swift index 3e05274b9..02f4f2053 100644 --- a/FlipcashTests/ProfileScrollFitTests.swift +++ b/FlipcashTests/ProfileScrollFitTests.swift @@ -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) - } }