|
Hello everyone. So this is my code:
HStack(spacing: 0) {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
HStack(spacing: 10) {
Text("Hello, world!")
.padding(10)
}
.overlay( // apply a rounded border
RoundedRectangle(cornerRadius: 8, style: .continuous)
.strokeBorder(Color(.green), lineWidth: 2)
)
}
Now I get an image and a text with a nice green border like this:
Now this brings me to my question. I would like the left side of the border I made with the overlay to be:
- Now be rounded but be normal square edges. In other words have the cornerRadius: 0
- The left line of the border I made with the overlay be invisible. I dont mean white but simply invisible so you can still see the background color.
For the second issue I know some of you will say simply make the line the same color as the background but the thing is the background will change in some situations so I need it to be invisible..
Thank you all!
EDIT: Not sure why the picture did not appear but I hope you guys get it
|
|
Hi! Without writing custom Shape and using already provided APIs as well as some creativity you may want to try something like this. Definitely not the sleekest solution I would say but does the job. Well, at least if I got your intentions right...
HStack(spacing: 0) {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
HStack(spacing: 10) {
Text("Hello, world!")
.padding(10)
}
.overlay {
GeometryReader { geo in
let fullPath = (geo.size.height * 2) + (geo.size.width * 2)
let endTrim = (geo.size.height / 2 + geo.size.width) / fullPath
UnevenRoundedRectangle(topLeadingRadius: 0,
bottomLeadingRadius: 0,
bottomTrailingRadius: 8,
topTrailingRadius: 8,
style: .continuous)
.trim(from: 0, to: endTrim)
.stroke(Color(.green), lineWidth: 2)
}
}
.overlay {
GeometryReader { geo in
let fullPath = (geo.size.height * 2) + (geo.size.width * 2)
let startTrim = (geo.size.height / 2 + geo.size.width + geo.size.height) / fullPath
UnevenRoundedRectangle(topLeadingRadius: 0,
bottomLeadingRadius: 0,
bottomTrailingRadius: 8,
topTrailingRadius: 8,
style: .continuous)
.trim(from: startTrim, to: 1)
.stroke(Color(.green), lineWidth: 2)
}
}
}
|
|
The way I would prefer to do that is something like this. But maybe you'll find some info how to make those rounded borders calculations youself.
struct MyLabel: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.width
let height = rect.height
let posX = rect.origin.x
let posY = rect.origin.y
path.move(to: CGPoint(x: posX, y: posY))
path.addLine(to: CGPoint(x: posX + width, y: posY)) // after this point add curve
path.addLine(to: CGPoint(x: posX + width, y: posY + height)) // after this point add curve
path.addLine(to: CGPoint(x: posX, y: posY + height))
return path
}
}
and then you can use it simply at it is without much going on on the screen
.overlay {
MyLabel()
.stroke(Color.blue, lineWidth: 2)
}
|
|
Here a solution from @rebeloper | s on twitter/X cornerRaduis
struct RectCorner: OptionSet {
let rawValue: Int
static let topLeading = RectCorner(rawValue: 1 << 0)
static let bottomLeading = RectCorner(rawValue: 1 << 3)
static let bottomTrailing = RectCorner(rawValue: 1 << 2)
static let topTrailing = RectCorner(rawValue: 1 << 1)
static let all: RectCorner = [.topLeading, .bottomLeading, bottomTrailing, .topTrailing]
static let leading: RectCorner = [.topLeading, .bottomLeading]
static let trailing: RectCorner = [.topTrailing, .bottomTrailing]
static let top: RectCorner = [.topLeading, .topTrailing]
static let bottom: RectCorner = [.bottomLeading, .bottomTrailing]
}
struct RoundedCornerShape: InsettableShape {
var radius: Double = .zero
var corners: RectCorner = .all
var style: RoundedCornerStyle = .continuous
var insertAmount = 0.0
func path(in rect: CGRect) -> Path {
var path = Path()
let topLeading: Double = corners.contains(.topLeading) ? radius - insertAmount : 0
let bottomLeading: Double = corners.contains(.bottomLeading) ? radius - insertAmount : 0
let bottomTrailing: Double = corners.contains(.bottomTrailing) ? radius - insertAmount : 0
let topTrailing: Double = corners.contains(.topTrailing) ? radius - insertAmount : 0
let cornerRadii = RectangleCornerRadii(topLeading: topLeading, bottomLeading: bottomLeading, bottomTrailing: bottomTrailing, topTrailing: topTrailing)
path.addRoundedRect(in: rect, cornerRadii: cornerRadii, style: style)
return path
}
func inset(by amount: CGFloat) -> some InsettableShape {
var roundedCornerShape = self
roundedCornerShape.insertAmount += amount
return roundedCornerShape
}
}
extension View {
/// Clips this view to its bounding frame, with the specified corner radius, corners and style.
/// - Parameters:
/// - radius: The corner radius to be applied.
/// - corners: Corners to apply the `radius`on.
/// - style: The shape of the rounded rectangle's corners
/// - Returns: A view that clips this view to its bounding frame with the specified corner radius, corners and style.
func cornerRadius(_ radius: CGFloat, corners: RectCorner, style: RoundedCornerStyle = .continuous) -> some View {
clipShape(
RoundedCornerShape(radius: radius, corners: corners, style: style)
)
}
}
Then you can do this
struct ContentView: View {
var body: some View {
VStack {
Rectangle()
.fill(.blue)
.frame(width: 300, height: 200)
.cornerRadius(20, corners: .trailing)
}
.padding()
}
}
Or as in your example
struct ContentView: View {
var body: some View {
HStack(spacing: 0) {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello World!")
}
.padding()
.overlay(
RoundedCornerShape(radius: 20, corners: .trailing)
.strokeBorder(.green, lineWidth: 2)
)
}
}
|
|
Thank you both very much. Nice solutions but in the end I went with @ygeras first solution.
But I could as an extra explenation as to why we had to use 2 overlays? Couldnt we somehow do 1 overlay to have the same result?
|
|
The point is that when you use .trim function, as you probably noticed, it works with % length of the line. And it starts counting it length for rectangular shape from right side of the rectangular height in the middle of the height. So if you use just one, unfortunately it won't be able to trim part of it then leave it drawn then trim it again. You can just try to remove the second overlay and you will see that only part of your border will be drawn and not the way you want. So I used it twice :) and we have the parts we want to see on the screen.
|
|
Thanks you! Smart solution
|
|
Sorry, forgot to mention. It is not necessary to use two overlays, what I meant you can just put two georeaders in one overlay :) it will do the same job. If you plan to reuse the border in other views, maybe it will make sense to create something like this.
struct MyCustomBorder: ViewModifier {
var cornerRadius: CGFloat
var lineWidth: CGFloat
var lineColor: Color
func body(content: Content) -> some View {
content
.overlay {
GeometryReader { geo in
let fullPath = (geo.size.height * 2) + (geo.size.width * 2)
let startTrim = (geo.size.height / 2 + geo.size.width + geo.size.height) / fullPath
// Upper part of the border
UnevenRoundedRectangle(topLeadingRadius: 0,
bottomLeadingRadius: 0,
bottomTrailingRadius: cornerRadius,
topTrailingRadius: cornerRadius,
style: .continuous)
.trim(from: startTrim, to: 1)
.stroke(lineColor, lineWidth: lineWidth)
// lower part of the border
let endTrim = (geo.size.height / 2 + geo.size.width) / fullPath
UnevenRoundedRectangle(topLeadingRadius: 0,
bottomLeadingRadius: 0,
bottomTrailingRadius: cornerRadius,
topTrailingRadius: cornerRadius,
style: .continuous)
.trim(from: 0, to: endTrim)
.stroke(lineColor, lineWidth: lineWidth)
}
}
}
}
extension View {
func customBorder(cornerRaidus: CGFloat = 8,
lineWidth: CGFloat = 2,
lineColor: Color) -> some View {
modifier(MyCustomBorder(cornerRadius: cornerRaidus,
lineWidth: lineWidth,
lineColor: lineColor))
}
}
and then in every view you are using it just add modifier and that's it.
struct ContentView: View {
var body: some View {
HStack(spacing: 0) {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
HStack(spacing: 10) {
Text("Hello, world!")
.padding(10)
}
// so now what you need to use is only this modifier
.customBorder(lineColor: .blue)
}
}
}
|
|
But no end to perfection. So I think this is the one I would be satisfied with
struct ContentView: View {
var body: some View {
HStack(spacing: 0) {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
HStack(spacing: 10) {
Text("Hello, world!")
.padding(8)
}
.makeRoundedCornerBorder(lineColor: .green)
}
}
}
struct RoundedCorners: Shape {
var cornerRadius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
return Path(path.cgPath)
}
}
struct RoundedCornersBorder: ViewModifier {
var cornerRadius: CGFloat
var lineWidth: CGFloat
var lineColor: Color
func body(content: Content) -> some View {
content
.overlay(
GeometryReader { geo in
let fullPath = (geo.size.height * 2) + (geo.size.width * 2)
let endTrim = (geo.size.width * 2 + geo.size.height) / fullPath
RoundedCorners(cornerRadius: 8, corners: [.topRight, .bottomRight])
.trim(from: 0, to: endTrim)
.stroke(lineColor, lineWidth: lineWidth)
}
)
}
}
extension View {
func makeRoundedCornerBorder(cornerRadius: CGFloat = 8,
lineWidth: CGFloat = 2,
lineColor: Color) -> some View {
modifier(RoundedCornersBorder(cornerRadius: cornerRadius,
lineWidth: lineWidth,
lineColor: lineColor))
}
}
|