Skip to content

Commit

Permalink
Added new individual corner radius and border modifier (#4328)
Browse files Browse the repository at this point in the history
* Added scaffolding for paywall components, view models, and views

* Fixed lint

* Improvements from PR review

* Fixed lint

* Made intro offer optional

* Views be viewing

* Fixed lint

* Add swift flag check

* Fixed compile issue from rebase

* Made view models internal

* Added new individual corner radius and border modifier
  • Loading branch information
joshdholtz authored Oct 16, 2024
1 parent 8a4645b commit e772e93
Show file tree
Hide file tree
Showing 15 changed files with 524 additions and 68 deletions.
134 changes: 101 additions & 33 deletions RevenueCat.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

301 changes: 301 additions & 0 deletions RevenueCatUI/Templates/Components/CornerBorder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// CornerBorder.swift
//
// Created by Josh Holtz on 9/30/24.

import Foundation
import SwiftUI

#if PAYWALL_COMPONENTS

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct CornerBorderModifier: ViewModifier {

struct BorderInfo {

let color: Color
let width: CGFloat

init(color: Color, width: Double) {
self.color = color
self.width = width
}

}

struct RaidusInfo {

let topLeft: CGFloat?
let topRight: CGFloat?
let bottomLeft: CGFloat?
let bottomRight: CGFloat?

init(topLeft: Double? = nil, topRight: Double? = nil, bottomLeft: Double? = nil, bottomRight: Double? = nil) {
self.topLeft = topLeft.flatMap { CGFloat($0) }
self.topRight = topRight.flatMap { CGFloat($0) }
self.bottomLeft = bottomLeft.flatMap { CGFloat($0) }
self.bottomRight = bottomRight.flatMap { CGFloat($0) }
}

}

var border: BorderInfo?
var radiuses: RaidusInfo?

func body(content: Content) -> some View {
content
.conditionalClipShape(topLeft: self.radiuses?.topLeft,
topRight: self.radiuses?.topRight,
bottomLeft: self.radiuses?.bottomLeft,
bottomRight: self.radiuses?.bottomRight)
.conditionalOverlay(color: self.border?.color,
width: self.border?.width,
topLeft: self.radiuses?.topLeft,
topRight: self.radiuses?.topRight,
bottomLeft: self.radiuses?.bottomLeft,
bottomRight: self.radiuses?.bottomRight)
}
}

// Helper extensions to conditionally apply clipShape and overlay without AnyView

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

func conditionalClipShape(
topLeft: CGFloat?,
topRight: CGFloat?,
bottomLeft: CGFloat?,
bottomRight: CGFloat?
) -> some View {
Group {
if let topLeft = topLeft,
let topRight = topRight,
let bottomLeft = bottomLeft,
let bottomRight = bottomRight,
topLeft > 0 || topRight > 0 || bottomLeft > 0 || bottomRight > 0 {
self
.applyIf(topLeft > 0) {
$0.clipShape(SingleRoundedCornerShape(radius: topLeft, corners: [.topLeft]))
}
.applyIf(topRight > 0) {
$0.clipShape(SingleRoundedCornerShape(radius: topLeft, corners: [.topRight]))
}
.applyIf(bottomLeft > 0) {
$0.clipShape(SingleRoundedCornerShape(radius: topLeft, corners: [.bottomLeft]))
}
.applyIf(bottomRight > 0) {
$0.clipShape(SingleRoundedCornerShape(radius: topLeft, corners: [.bottomRight]))
}
} else {
self
}
}
}

// swiftlint:disable:next function_parameter_count
func conditionalOverlay(
color: Color?,
width: CGFloat?,
topLeft: CGFloat?,
topRight: CGFloat?,
bottomLeft: CGFloat?,
bottomRight: CGFloat?
) -> some View {
Group {
if let color = color, let width = width, width > 0 {
if let topLeft = topLeft,
let topRight = topRight,
let bottomLeft = bottomLeft,
let bottomRight = bottomRight,
topLeft > 0 || topRight > 0 || bottomLeft > 0 || bottomRight > 0 {
self.overlay(
BorderRoundedCornerShape(
topLeft: topLeft,
topRight: topRight,
bottomLeft: bottomLeft,
bottomRight: bottomRight
)
.stroke(color, lineWidth: width)
)
} else {
self
.border(color, width: width)
}
} else {
self
}
}
}

}

private struct SingleRoundedCornerShape: Shape {
var radius: CGFloat
var corners: UIRectCorner

func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return Path(path.cgPath)
}
}

private struct BorderRoundedCornerShape: Shape {
var topLeft: CGFloat
var topRight: CGFloat
var bottomLeft: CGFloat
var bottomRight: CGFloat

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

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

// Top edge and top-right corner
path.addLine(to: CGPoint(x: rect.maxX - topRight, y: rect.minY))
path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.minY + topRight),
control: CGPoint(x: rect.maxX, y: rect.minY))

// Right edge and bottom-right corner
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - bottomRight))
path.addQuadCurve(to: CGPoint(x: rect.maxX - bottomRight, y: rect.maxY),
control: CGPoint(x: rect.maxX, y: rect.maxY))

// Bottom edge and bottom-left corner
path.addLine(to: CGPoint(x: rect.minX + bottomLeft, y: rect.maxY))
path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.maxY - bottomLeft),
control: CGPoint(x: rect.minX, y: rect.maxY))

// Left edge and top-left corner
path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + topLeft))
path.addQuadCurve(to: CGPoint(x: rect.minX + topLeft, y: rect.minY),
control: CGPoint(x: rect.minX, y: rect.minY))

return path
}
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension View {
func cornerBorder(
border: CornerBorderModifier.BorderInfo?,
radiuses: CornerBorderModifier.RaidusInfo?
) -> some View {
self.modifier(
CornerBorderModifier(
border: border,
radiuses: radiuses
)
)
}
}

#if DEBUG

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct CornerBorder_Previews: PreviewProvider {

static var previews: some View {
// Equal Radius - No Border
VStack {
Text("Hello")
.padding(.vertical, 10)
.padding(.horizontal, 20)
.background(.yellow)
.cornerBorder(
border: nil,
radiuses: .init(topLeft: 8,
topRight: 8,
bottomLeft: 8,
bottomRight: 8))
.padding()
}
.previewLayout(.sizeThatFits)
.previewDisplayName("Equal Radius - No Border")

// No - Blue Border
VStack {
Text("Hello")
.padding(.vertical, 10)
.padding(.horizontal, 20)
.background(.yellow)
.cornerBorder(
border: .init(color: .blue,
width: 4),
radiuses: nil)
.padding()
}
.previewLayout(.sizeThatFits)
.previewDisplayName("No Right - Blue Border")

// Top Left and Bottom Right Radius - No Border
VStack {
Text("Hello")
.padding(.vertical, 10)
.padding(.horizontal, 20)
.background(.yellow)
.cornerBorder(
border: nil,
radiuses: .init(topLeft: 8,
topRight: 0,
bottomLeft: 0,
bottomRight: 8))
.padding()
}
.previewLayout(.sizeThatFits)
.previewDisplayName("Top Left and Bottom Right Radius - No Border")

// Equal Radius - Blue Border
VStack {
Text("Hello")
.padding(.vertical, 10)
.padding(.horizontal, 20)
.background(.yellow)
.cornerBorder(
border: .init(color: .blue,
width: 6),
radiuses: .init(topLeft: 8,
topRight: 8,
bottomLeft: 8,
bottomRight: 8))
.padding()
}
.previewLayout(.sizeThatFits)
.previewDisplayName("Equal Radius - Blue Border")

// Top Left and Bottom Right Radius - Blue Border
VStack {
Text("Hello")
.padding(.vertical, 10)
.padding(.horizontal, 20)
.background(.yellow)
.cornerBorder(
border: .init(color: .blue,
width: 6),
radiuses: .init(topLeft: 8,
topRight: 0,
bottomLeft: 0,
bottomRight: 8))
.padding()
}
.previewLayout(.sizeThatFits)
.previewDisplayName("Top Left and Bottom Right - Blue Border")
}
}

#endif

#endif
34 changes: 29 additions & 5 deletions RevenueCatUI/Templates/Components/Image/ImageComponentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,8 @@ struct ImageComponentView: View {
endPoint: .bottom
)
)
.roundedCorner(viewModel.cornerRadiuses.topLeading, corners: .topLeft)
.roundedCorner(viewModel.cornerRadiuses.topTrailing, corners: .topRight)
.roundedCorner(viewModel.cornerRadiuses.bottomLeading, corners: .bottomLeft)
.roundedCorner(viewModel.cornerRadiuses.bottomTrailing, corners: .bottomRight)
.cornerBorder(border: nil,
radiuses: viewModel.cornerRadiuses)
}

}
Expand Down Expand Up @@ -139,7 +137,33 @@ struct ImageComponentView_Previews: PreviewProvider {
)
}
.previewLayout(.fixed(width: 400, height: 400))
.previewDisplayName("Light - Fill")
.previewDisplayName("Light - Gradient")

// Light - Fit with Rounded Corner
VStack {
ImageComponentView(
// swiftlint:disable:next force_try
viewModel: try! .init(
localizedStrings: [:],
component: .init(
source: .init(
light: .init(
original: catUrl,
heic: catUrl,
heicLowRes: catUrl
)
),
fitMode: .fit,
cornerRadiuses: .init(topLeading: 40,
topTrailing: 40,
bottomLeading: 40,
bottomTrailing: 40)
)
)
)
}
.previewLayout(.fixed(width: 400, height: 400))
.previewDisplayName("Light - Rounded Corner")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,15 @@ class ImageComponentViewModel {
self.imageInfo.light.heic
}

var cornerRadiuses: PaywallComponent.CornerRadiuses {
component.cornerRadiuses
var cornerRadiuses: CornerBorderModifier.RaidusInfo? {
component.cornerRadiuses.flatMap { cornerRadiuses in
CornerBorderModifier.RaidusInfo(
topLeft: cornerRadiuses.topLeading,
topRight: cornerRadiuses.topTrailing,
bottomLeft: cornerRadiuses.bottomLeading,
bottomRight: cornerRadiuses.bottomLeading
)
}
}

var gradientColors: [Color] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,8 @@ struct PurchaseButtonComponentView: View {
.padding(viewModel.padding)
.background(viewModel.backgroundColor)
.shape(viewModel.clipShape)
.applyIfLet(viewModel.cornerRadiuses, apply: { view, value in
view
.roundedCorner(value.topLeading, corners: .topLeft)
.roundedCorner(value.topTrailing, corners: .topRight)
.roundedCorner(value.bottomLeading, corners: .bottomLeft)
.roundedCorner(value.bottomTrailing, corners: .bottomRight)
})
.padding(viewModel.margin)
.cornerBorder(border: nil,
radiuses: viewModel.cornerRadiuses) .padding(viewModel.margin)
}
}

Expand Down
Loading

0 comments on commit e772e93

Please sign in to comment.