diff --git a/RevenueCatUI/Templates/Components/Root/RootView.swift b/RevenueCatUI/Templates/Components/Root/RootView.swift index 4d59077f9f..3db99b6343 100644 --- a/RevenueCatUI/Templates/Components/Root/RootView.swift +++ b/RevenueCatUI/Templates/Components/Root/RootView.swift @@ -19,6 +19,8 @@ import SwiftUI @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct RootView: View { + @State private var additionalFooterPaddingBottom: CGFloat = 0 + private let viewModel: RootViewModel private let onDismiss: () -> Void @@ -31,13 +33,61 @@ struct RootView: View { ScrollView { StackComponentView(viewModel: viewModel.stackViewModel, onDismiss: onDismiss) }.applyIfLet(viewModel.stickyFooterViewModel) { stackView, stickyFooterViewModel in - stackView.safeAreaInset(edge: .bottom) { - StackComponentView(viewModel: stickyFooterViewModel.stackViewModel, onDismiss: onDismiss) - } + stackView + .safeAreaInset(edge: .bottom) { + StackComponentView( + viewModel: stickyFooterViewModel.stackViewModel, + onDismiss: onDismiss, + additionalPadding: EdgeInsets( + top: 0, + leading: 0, + bottom: additionalFooterPaddingBottom, + trailing: 0 + ) + ) + } + // First we ensure our footer draws in the bottom safe area. Then we add additional padding, so its + // background shows in that same bottom safe area. + .ignoresSafeArea(edges: .bottom) + .onBottomSafeAreaPaddingChange { bottomPadding in + self.additionalFooterPaddingBottom = bottomPadding + } } } } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private struct OnBottomSafeAreaPaddingChangeModifier: ViewModifier { + private let callback: (CGFloat) -> Void + + init(_ callback: @escaping (CGFloat) -> Void) { + self.callback = callback + } + + func body(content: Content) -> some View { + content + .background( + GeometryReader { geometry in + Color.clear + .onAppear { + callback(geometry.safeAreaInsets.bottom) + } + .onChange(of: geometry.safeAreaInsets.bottom) { newValue in + callback(newValue) + } + } + ) + } +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +fileprivate extension View { + /// Sort-of backported safeAreaPadding (iOS 17+), for as much as we need. + func onBottomSafeAreaPaddingChange(_ callback: @escaping (CGFloat) -> Void) -> some View { + self.modifier(OnBottomSafeAreaPaddingChangeModifier(callback)) + } +} + #endif diff --git a/RevenueCatUI/Templates/Components/Stack/StackComponentView.swift b/RevenueCatUI/Templates/Components/Stack/StackComponentView.swift index 36056fe873..19922033c7 100644 --- a/RevenueCatUI/Templates/Components/Stack/StackComponentView.swift +++ b/RevenueCatUI/Templates/Components/Stack/StackComponentView.swift @@ -21,6 +21,15 @@ struct StackComponentView: View { let viewModel: StackComponentViewModel let onDismiss: () -> Void + /// Used when this stack needs more padding than defined in the component, e.g. to avoid being drawn in the safe + /// area when displayed as a sticky footer. + let additionalPadding: EdgeInsets + + init(viewModel: StackComponentViewModel, onDismiss: @escaping () -> Void, additionalPadding: EdgeInsets? = nil) { + self.viewModel = viewModel + self.onDismiss = onDismiss + self.additionalPadding = additionalPadding ?? EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) + } var body: some View { Group { @@ -42,6 +51,7 @@ struct StackComponentView: View { } } .padding(viewModel.padding) + .padding(additionalPadding) .width(viewModel.width) .background(viewModel.backgroundColor) .cornerBorder(border: viewModel.border,