Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Paywalls V2] Added overflow property to stack #4767

Merged
merged 3 commits into from
Feb 14, 2025
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
8 changes: 5 additions & 3 deletions RevenueCatUI/Templates/V2/Components/Root/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ struct RootView: View {

var body: some View {
VStack(alignment: .center, spacing: 0) {
ScrollView {
StackComponentView(viewModel: viewModel.stackViewModel, onDismiss: onDismiss)
}
StackComponentView(
viewModel: viewModel.stackViewModel,
isScrollableByDefault: true,
onDismiss: onDismiss
)

if let stickyFooterViewModel = viewModel.stickyFooterViewModel {
StackComponentView(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,20 @@ struct StackComponentView: View {
private var screenCondition

private let viewModel: StackComponentViewModel
private let isScrollableByDefault: Bool
private 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.
private let additionalPadding: EdgeInsets

init(
viewModel: StackComponentViewModel,
isScrollableByDefault: Bool = false,
onDismiss: @escaping () -> Void,
additionalPadding: EdgeInsets? = nil
) {
self.viewModel = viewModel
self.isScrollableByDefault = isScrollableByDefault
self.onDismiss = onDismiss
self.additionalPadding = additionalPadding ?? EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
}
Expand Down Expand Up @@ -114,13 +117,52 @@ struct StackComponentView: View {
uiConfigProvider: self.viewModel.uiConfigProvider)
.apply(badge: style.badge, border: style.border, shadow: style.shadow, shape: style.shape)
.padding(style.margin)
.scrollableIfEnabled(
style.dimension,
enabled: style.scrollable ?? self.isScrollableByDefault
)
}

}

private extension Axis {

var scrollViewAxis: Axis.Set {
switch self {
case .horizontal: return .horizontal
case .vertical: return .vertical
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure, we won’t allow scrolling in the opposite direction than the axis of the stack then? I think that can make sense for now (in the android PR I was assuming a direction would be given by the backend to make it more flexible… but we might not need that flexibility)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tonidero Correct correct! That might make it a bit more confusing to use 😅

}
}

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
fileprivate extension View {

@ViewBuilder
// @PublicForExternalTesting
func scrollableIfEnabled(
_ dimension: PaywallComponent.Dimension,
enabled: Bool = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this default value is not needed if we're always passing the parameter

) -> some View {
if enabled {
switch dimension {
case .horizontal:
ScrollView(.horizontal) {
self
}
case .vertical:
ScrollView(.vertical) {
self
}
case .zlayer:
self
}
} else {
self
}
}

// Helper to compute the order or application of border, shadow and badge.
@ViewBuilder
func apply(badge: BadgeModifier.BadgeInfo?,
Expand Down Expand Up @@ -401,6 +443,58 @@ struct StackComponentView_Previews: PreviewProvider {
.previewLayout(.sizeThatFits)
.previewDisplayName("Default - Fill Fit Fixed Fill")

// Scrollable - HStack
HStack(spacing: 0) {
StackComponentView(
// swiftlint:disable:next force_try
viewModel: try! .init(
component: .init(
components: [
.stack(.init(
components: [],
size: .init(width: .fixed(300), height: .fixed(50)),
backgroundColor: .init(light: .hex("#ff0000"))
)),
.stack(.init(
components: [],
size: .init(width: .fixed(300), height: .fixed(50)),
backgroundColor: .init(light: .hex("#00ff00"))
)),
.stack(.init(
components: [],
size: .init(width: .fixed(300), height: .fixed(50)),
backgroundColor: .init(light: .hex("#0000ff"))
)),
.stack(.init(
components: [],
size: .init(width: .fixed(300), height: .fixed(50)),
backgroundColor: .init(light: .hex("#000000"))
))
],
dimension: .horizontal(.center, .start),
size: .init(
width: .fixed(400),
height: .fit
),
spacing: 10,
backgroundColor: .init(light: .hex("#ffcc00")),
padding: .init(top: 80, bottom: 80, leading: 20, trailing: 20),
overflow: .scroll
),
localizationProvider: .init(
locale: Locale.current,
localizedStrings: [
"text_1": .string("Hey")
]
)
),
onDismiss: {}
)
}
.previewRequiredEnvironmentProperties()
.previewLayout(.sizeThatFits)
.previewDisplayName("Scrollable - HStack")

// Fits don't expand
StackComponentView(
// swiftlint:disable:next force_try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ class StackComponentViewModel {
shape: partial?.shape ?? self.component.shape,
border: partial?.border ?? self.component.border,
shadow: partial?.shadow ?? self.component.shadow,
badge: partial?.badge ?? self.component.badge
badge: partial?.badge ?? self.component.badge,
overflow: partial?.overflow ?? self.component.overflow
)

apply(style)
Expand Down Expand Up @@ -135,6 +136,7 @@ struct StackComponentStyle {
let border: ShapeModifier.BorderInfo?
let shadow: ShadowModifier.ShadowInfo?
let badge: BadgeModifier.BadgeInfo?
let scrollable: Bool?

init(
uiConfigProvider: UIConfigProvider,
Expand All @@ -150,7 +152,8 @@ struct StackComponentStyle {
shape: PaywallComponent.Shape?,
border: PaywallComponent.Border?,
shadow: PaywallComponent.Shadow?,
badge: PaywallComponent.Badge?
badge: PaywallComponent.Badge?,
overflow: PaywallComponent.StackComponent.Overflow?
) {
self.visible = visible
self.dimension = dimension
Expand All @@ -167,6 +170,15 @@ struct StackComponentStyle {
stackBorder: self.border,
badgeViewModels: badgeViewModels,
uiConfigProvider: uiConfigProvider)

self.scrollable = overflow.flatMap({ overflow in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a huge nit, but for me it has been a TIL. I usually use the map method for this, and I didn't know the existance of flatMap in this context. It turns out that flatMap is meant to be used when the closure returns an Optional, in order to flatten the 2 optionals. In this case, since the closure returns a nonoptional Bool, map is more appropriate. Again, feel free to not change this, but I still think this is an interesting caveat 😊 (that I didn't know until now)

switch overflow {
case .default:
return false
case .scroll:
return true
}
})
}

var vstackStrategy: StackStrategy {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ private enum Template1Preview {
stack: packageStack
)

static let bodyStack = PaywallComponent.StackComponent(
components: [
.text(body),
.package(package)
],
dimension: .vertical(.center, .start),
size: .init(width: .fill, height: .fit),
spacing: 30,
backgroundColor: nil
)

static let purchaseButton = PaywallComponent.PurchaseButtonComponent(
stack: .init(
components: [
Expand All @@ -117,10 +128,11 @@ private enum Template1Preview {
static let contentStack = PaywallComponent.StackComponent(
components: [
.text(title),
.text(body),
.package(package),
.stack(bodyStack),
.purchaseButton(purchaseButton)
],
dimension: .vertical(.center, .spaceEvenly),
size: .init(width: .fill, height: .fill),
spacing: 30,
backgroundColor: nil,
margin: .init(top: 0,
Expand All @@ -134,8 +146,9 @@ private enum Template1Preview {
.image(catImage),
.stack(contentStack)
],
spacing: 20,
backgroundColor: nil
dimension: .vertical(.center, .start),
size: .init(width: .fill, height: .fill),
spacing: 0
)

static let paywallComponents: Offering.PaywallComponents = .init(
Expand All @@ -161,7 +174,8 @@ private enum Template1Preview {
stack: .init(
components: [
.stack(stack)
]
],
overflow: .default
),
stickyFooter: nil,
background: .color(.init(
Expand Down
17 changes: 16 additions & 1 deletion Sources/Paywalls/Components/PaywallStackComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@
// StackComponent.swift
//
// Created by James Borthwick on 2024-08-20.
// swiftlint:disable missing_docs
// swiftlint:disable missing_docs nesting

import Foundation

public extension PaywallComponent {

final class StackComponent: PaywallComponentBase {

public enum Overflow: PaywallComponentBase {
case `default`
case scroll
}

let type: ComponentType
public let visible: Bool?
public let components: [PaywallComponent]
Expand All @@ -32,6 +37,7 @@ public extension PaywallComponent {
public let border: Border?
public let shadow: Shadow?
public let badge: Badge?
public let overflow: Overflow?
tonidero marked this conversation as resolved.
Show resolved Hide resolved

public let overrides: ComponentOverrides<PartialStackComponent>?

Expand All @@ -49,6 +55,7 @@ public extension PaywallComponent {
border: Border? = nil,
shadow: Shadow? = nil,
badge: Badge? = nil,
overflow: Overflow? = nil,
overrides: ComponentOverrides<PartialStackComponent>? = nil
) {
self.visible = visible
Expand All @@ -65,6 +72,7 @@ public extension PaywallComponent {
self.border = border
self.shadow = shadow
self.badge = badge
self.overflow = overflow
self.overrides = overrides
}
public func hash(into hasher: inout Hasher) {
Expand All @@ -82,6 +90,7 @@ public extension PaywallComponent {
hasher.combine(border)
hasher.combine(shadow)
hasher.combine(badge)
hasher.combine(overflow)
hasher.combine(overrides)
}

Expand All @@ -100,6 +109,7 @@ public extension PaywallComponent {
lhs.border == rhs.border &&
lhs.shadow == rhs.shadow &&
lhs.badge == rhs.badge &&
lhs.overflow == rhs.overflow &&
lhs.overrides == rhs.overrides
}
}
Expand All @@ -117,6 +127,7 @@ public extension PaywallComponent {
public let shape: Shape?
public let border: Border?
public let shadow: Shadow?
public let overflow: PaywallComponent.StackComponent.Overflow?
public let badge: Badge?

public init(
Expand All @@ -131,6 +142,7 @@ public extension PaywallComponent {
shape: Shape? = nil,
border: Border? = nil,
shadow: Shadow? = nil,
overflow: PaywallComponent.StackComponent.Overflow? = nil,
badge: Badge? = nil
) {
self.visible = visible
Expand All @@ -144,6 +156,7 @@ public extension PaywallComponent {
self.shape = shape
self.border = border
self.shadow = shadow
self.overflow = overflow
self.badge = badge
}

Expand All @@ -159,6 +172,7 @@ public extension PaywallComponent {
hasher.combine(shape)
hasher.combine(border)
hasher.combine(shadow)
hasher.combine(overflow)
hasher.combine(badge)
}

Expand All @@ -174,6 +188,7 @@ public extension PaywallComponent {
lhs.shape == rhs.shape &&
lhs.border == rhs.border &&
lhs.shadow == rhs.shadow &&
lhs.overflow == rhs.overflow &&
lhs.badge == rhs.badge
}
}
Expand Down