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

Added new individual corner radius and border modifier #4328

Merged
merged 12 commits into from
Oct 16, 2024
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 {
Copy link
Member

Choose a reason for hiding this comment

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

Typo 🙂

-    struct RaidusInfo {
+    struct RadiusInfo {

Copy link
Member Author

Choose a reason for hiding this comment

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

This is how I leave my mark everywhere I go but... I'll change it just for you 🫶

Copy link
Member

Choose a reason for hiding this comment

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

Haha I'm honored ❤️


let topLeft: CGFloat?
let topRight: CGFloat?
let bottomLeft: CGFloat?
let bottomRight: CGFloat?
Comment on lines +36 to +39
Copy link
Member

Choose a reason for hiding this comment

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

How does this work with RTL languages? Not sure how SwiftUI handles that in general, but should we rename them like so?

-        let topLeft: CGFloat?
-        let topRight: CGFloat?
-        let bottomLeft: CGFloat?
-        let bottomRight: CGFloat?
+        let topStart: CGFloat?
+        let topEnd: CGFloat?
+        let bottomStart: CGFloat?
+        let bottomEnd: CGFloat?

Copy link
Member Author

Choose a reason for hiding this comment

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

I should put a todo in here but we would have to manually flip it in this case for RTL languages since there isn't native support for this. Thanks for catching!


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?
Copy link
Member

Choose a reason for hiding this comment

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

Is my understanding correct that we now have 2 ways to define corner radiuses, RadiusInfo and CornerRadiuses? Is that necessary, or can we consolidate the 2? I think it would be great if we could have a single concept of "corner radiuses".

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeaaaahhh, this is kind of confusing 🤔 I'll take another look to see if they can combined in a sensible way 💪


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?
Comment on lines +108 to +111
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if this makes sense, but we could also make these non-nil and default to 0? That way you can apply rounding to some corners, without having to specify all corners.

) -> 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))
Comment on lines +164 to +185
Copy link
Member

Choose a reason for hiding this comment

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

Beautiful! 😚👌


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