-
Notifications
You must be signed in to change notification settings - Fork 338
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
Changes from all commits
1a7a1b6
1b98e44
757cc5c
485b817
30c1c25
ca1b927
05456f8
1c688df
bdf0ca1
8fb89a4
690d572
cf68c07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
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? | ||
Comment on lines
+36
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo 🙂
There was a problem hiding this comment.
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 🫶
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha I'm honored ❤️