Skip to content

Commit

Permalink
[Paywalls V2] Add masking (concave, convex, circle) and padding/margi…
Browse files Browse the repository at this point in the history
…n to image (#4674)

* Image mask for convex and concave and replace pill with circle

* Added padding and margin

* Fixed

* Made size consistent
  • Loading branch information
joshdholtz authored Jan 15, 2025
1 parent 6f82e59 commit e28f028
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,16 @@ struct ImageComponentView: View {
.frame(maxWidth: .infinity)
// WIP: Fix this later when accessibility info is available
.accessibilityHidden(true)
// WIP: Need to replace this gradient with the better one
.applyIfLet(style.colorOverlay, apply: { view, colorOverlay in
view.overlay(
Color.clear.backgroundStyle(.color(colorOverlay))
)
})
// WIP: this needs more shapes and borders
// WIP: this might also need dropshadow
.shape(border: nil,
shape: style.shape)
.padding(style.padding)
// WIP: Add border still
.padding(style.margin)
}

}
Expand Down Expand Up @@ -229,6 +229,96 @@ struct ImageComponentView_Previews: PreviewProvider {
.previewRequiredEnvironmentProperties()
.previewLayout(.fixed(width: 400, height: 400))
.previewDisplayName("Light - Rounded Corner")

// Light - Fit with Circle
VStack {
ImageComponentView(
// swiftlint:disable:next force_try
viewModel: try! .init(
localizationProvider: .init(
locale: Locale.current,
localizedStrings: [:]
),
uiConfigProvider: .init(uiConfig: PreviewUIConfig.make()),
component: .init(
source: .init(
light: .init(
width: 750,
height: 530,
original: catUrl,
heic: catUrl,
heicLowRes: catUrl
)
),
fitMode: .fit,
maskShape: .circle
)
)
)
}
.previewRequiredEnvironmentProperties()
.previewLayout(.fixed(width: 400, height: 400))
.previewDisplayName("Light - Circle")

// Light - Fit with Convex
VStack {
ImageComponentView(
// swiftlint:disable:next force_try
viewModel: try! .init(
localizationProvider: .init(
locale: Locale.current,
localizedStrings: [:]
),
uiConfigProvider: .init(uiConfig: PreviewUIConfig.make()),
component: .init(
source: .init(
light: .init(
width: 750,
height: 530,
original: catUrl,
heic: catUrl,
heicLowRes: catUrl
)
),
fitMode: .fit,
maskShape: .convex
)
)
)
}
.previewRequiredEnvironmentProperties()
.previewLayout(.fixed(width: 400, height: 400))
.previewDisplayName("Light - Fit with Convex")

// Light - Fit with Concave
VStack {
ImageComponentView(
// swiftlint:disable:next force_try
viewModel: try! .init(
localizationProvider: .init(
locale: Locale.current,
localizedStrings: [:]
),
uiConfigProvider: .init(uiConfig: PreviewUIConfig.make()),
component: .init(
source: .init(
light: .init(
width: 750,
height: 530,
original: catUrl,
heic: catUrl,
heicLowRes: catUrl
)
),
fitMode: .fit,
maskShape: .concave
)
)
)
}
.previewRequiredEnvironmentProperties()
.previewLayout(.fixed(width: 400, height: 400))
.previewDisplayName("Light - Fit with Concave")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ struct ImageComponentStyle {
let size: PaywallComponent.Size
let shape: ShapeModifier.Shape?
let colorOverlay: DisplayableColorScheme?
let padding: PaywallComponent.Padding?
let margin: PaywallComponent.Padding?
let padding: EdgeInsets
let margin: EdgeInsets
let border: PaywallComponent.Border?
let shadow: PaywallComponent.Shadow?
let contentMode: ContentMode
Expand Down Expand Up @@ -172,8 +172,8 @@ struct ImageComponentStyle {
self.size = size
self.shape = maskShape?.shape
self.colorOverlay = colorOverlay?.asDisplayable(uiConfigProvider: uiConfigProvider)
self.padding = padding
self.margin = margin
self.padding = (padding ?? .zero).edgeInsets
self.margin = (margin ?? .zero).edgeInsets
self.border = border
self.shadow = shadow
self.contentMode = fitMode.contentMode
Expand All @@ -196,8 +196,8 @@ private extension PaywallComponent.MaskShape {
)
}
return .rectangle(corners)
case .pill:
return .pill
case .circle:
return .circle
case .concave:
return .concave
case .convex:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@ private enum Template1Preview {
)
),
fitMode: .fit,
colorOverlay: .init(light: .linear(0, [
.init(color: "#ffffff", percent: 0),
.init(color: "#ffffff00", percent: 40)
]))
maskShape: .convex
)

static let title = PaywallComponent.TextComponent(
Expand Down
116 changes: 115 additions & 1 deletion RevenueCatUI/Templates/V2/ViewHelpers/Shape.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,10 @@ struct ShapeModifier: ViewModifier {
case .concave:
// WIP: Need to implement
content
.modifier(ConcaveMaskModifier(curveHeightPercentage: 0.2))
case .convex:
// WIP: Need to implement
content
.modifier(ConvexMaskModifier(curveHeightPercentage: 0.2))
}
}

Expand Down Expand Up @@ -154,6 +155,119 @@ struct ShapeModifier: ViewModifier {

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
private struct ConcaveMaskModifier: ViewModifier {

let curveHeightPercentage: CGFloat

@State
private var size: CGSize = .zero

func body(content: Content) -> some View {
content
.onSizeChange { self.size = $0 }
.clipShape(
ConcaveShape(curveHeightPercentage: curveHeightPercentage, size: size)
)
}
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
private struct ConcaveShape: Shape {

let curveHeightPercentage: CGFloat
let size: CGSize

func path(in rect: CGRect) -> Path {
var path = Path()

// Start at the top-left corner
path.move(to: CGPoint(x: rect.minX, y: rect.minY))

// Top-right corner
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))

// Bottom-right corner
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))

// Create the upward-facing concave curve
path.addQuadCurve(
to: CGPoint(x: rect.minX, y: rect.maxY),
control: CGPoint(x: rect.midX, y: rect.maxY - self.curveHeight)
)

// Bottom-left corner
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))

path.closeSubpath()

return path
}

private var curveHeight: CGFloat {
// Calculate the curve height as a percentage of the view's height
max(0, size.height * curveHeightPercentage)
}

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
private struct ConvexMaskModifier: ViewModifier {

let curveHeightPercentage: CGFloat

@State
private var size: CGSize = .zero

func body(content: Content) -> some View {
content
.onSizeChange { self.size = $0 }
.clipShape(
ConvexShape(curveHeightPercentage: curveHeightPercentage, size: size)
)
}
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
private struct ConvexShape: Shape {

let curveHeightPercentage: CGFloat
let size: CGSize

func path(in rect: CGRect) -> Path {
var path = Path()

// Start at the top-left corner
path.move(to: CGPoint(x: rect.minX, y: rect.minY))

// Top-right corner
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))

// Bottom-right corner
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - curveHeight))

// Create the concave curve
path.addQuadCurve(
to: CGPoint(x: rect.minX, y: rect.maxY - curveHeight),
control: CGPoint(x: rect.midX, y: rect.maxY + curveHeight)
)

// Bottom-left corner
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY - curveHeight))

path.closeSubpath()

return path
}

private var curveHeight: CGFloat {
// Calculate the curve height as a percentage of the view's height
max(0, size.height * curveHeightPercentage)
}

}

// Type-erasing wrapper for InsettableShape protocol
struct AnyInsettableShape: InsettableShape {
private var base: any InsettableShape
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ public extension PaywallComponent {
enum MaskShape: Codable, Sendable, Hashable, Equatable {

case rectangle(CornerRadiuses?)
case pill
case circle
case concave
case convex

Expand All @@ -251,8 +251,8 @@ public extension PaywallComponent {
case .rectangle(let corners):
try container.encodeIfPresent(MaskShapeType.rectangle.rawValue, forKey: .type)
try container.encode(corners, forKey: .corners)
case .pill:
try container.encode(MaskShapeType.pill.rawValue, forKey: .type)
case .circle:
try container.encode(MaskShapeType.circle.rawValue, forKey: .type)
case .concave:
try container.encode(MaskShapeType.concave.rawValue, forKey: .type)
case .convex:
Expand All @@ -268,8 +268,8 @@ public extension PaywallComponent {
case .rectangle:
let value: CornerRadiuses? = try container.decodeIfPresent(CornerRadiuses.self, forKey: .corners)
self = .rectangle(value)
case .pill:
self = .pill
case .circle:
self = .circle
case .concave:
self = .concave
case .convex:
Expand All @@ -289,7 +289,7 @@ public extension PaywallComponent {
private enum MaskShapeType: String, Decodable {

case rectangle
case pill
case circle
case concave
case convex

Expand Down

0 comments on commit e28f028

Please sign in to comment.