SwiftUI - Add Border to One Edge of an Image - swift

It's a pretty straight-forward question - How does one apply a border effect to only the wanted edges of an Image with SwiftUI?
For example, I only want to apply a border to the top and bottom edges of an image because the image is taking up the entire width of the screen.
Image(mission.missionImageString)
.resizable()
.aspectRatio(contentMode: .fit)
.border(Color.white, width: 2) //Adds a border to all 4 edges
Any help is appreciated!

Demo
Implementation
You can use this modifier on any View:
.border(width: 5, edges: [.top, .leading], color: .yellow)
With the help of this simple extension:
extension View {
func border(width: CGFloat, edges: [Edge], color: Color) -> some View {
overlay(EdgeBorder(width: width, edges: edges).foregroundColor(color))
}
}
And here is the magic struct behind this:
struct EdgeBorder: Shape {
var width: CGFloat
var edges: [Edge]
func path(in rect: CGRect) -> Path {
var path = Path()
for edge in edges {
var x: CGFloat {
switch edge {
case .top, .bottom, .leading: return rect.minX
case .trailing: return rect.maxX - width
}
}
var y: CGFloat {
switch edge {
case .top, .leading, .trailing: return rect.minY
case .bottom: return rect.maxY - width
}
}
var w: CGFloat {
switch edge {
case .top, .bottom: return rect.width
case .leading, .trailing: return width
}
}
var h: CGFloat {
switch edge {
case .top, .bottom: return width
case .leading, .trailing: return rect.height
}
}
path.addRect(CGRect(x: x, y: y, width: w, height: h))
}
return path
}
}

If somebody ever needs to just add a quick 1 (or more) sided border to a view (e.g., the top edge, or any random combination of edges), I've found this works well and is tweakable:
top edge:
.overlay(Rectangle().frame(width: nil, height: 1, alignment: .top).foregroundColor(Color.gray), alignment: .top)
leading edge:
.overlay(Rectangle().frame(width: 1, height: nil, alignment: .leading).foregroundColor(Color.gray), alignment: .leading)
etc.
Just tweak the height, width, and edge to produce the combination of borders you want.

If you don't need to control thickness, you can do this:
.overlay(Divider(), alignment: .top)
.overlay(Divider(), alignment: .bottom)
Set the color of the divider using:
.overlay(Divider().background(.red), alignment: .left)

Add a top border aka Divider:
.overlay( Divider()
.frame(maxWidth: .infinity, maxHeight:1)
.background(Color.green), alignment: .top)
Example Usage:
Image("YouImageName")
.resizable()
.scaledToFit()
.frame(height: 40)
.padding(.top, 6) // padding above you Image, before your border
.overlay( Divider()
.frame(maxWidth: .infinity, maxHeight:1)
.background(Color.green), alignment: .top) // End Overlay
.padding(.top, 0) // padding above border
Explanation:
For a horizontal border aka Divider, frame width is the length of the border and height is the thickness of the border. Vertical border the frame width is thickness and frame height is the length.
The .background will set the color of the border.
Alignment will set, where the border will draw. For example "alignment: .bottom" will place the border on the bottom of the Image and "alignment: .top" on the top of the Image.
".leading & .trailing" will draw the border on the left and right of the Image correspondingly.
For a vertical border:
.overlay( Divider()
.frame(maxWidth: 1, maxHeight: .infinity)
.background(Color.green), alignment: .leading )

I find the most elegant solution is to create a custom shape and add it as an overlay. It works nicely with the SwiftUI image.
struct BottomBorder: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
return path
}
}
struct ContentView: View {
var body: some View {
Image("image")
.overlay(BottomBorder().stroke(Color.red, lineWidth: 8))
}
}

a very easy way to accomplish this
VStack(spacing: 0) {
ViewThatNeedsABorder()
Divider() // or use rectangle
}
you could swap the divider for a rectangle as well and get the same effect. Style the divider or rectangle as needed using frame, or background etc.
if you need left/right borders use an HStack instead of a VStack, or even combine HStack and VStack to get borders on multiple sides ie: left and top.

Related

Alignment for GeometryReader and .frame

I'm using GeometryReader() and .frame() to show their comparability and playing with their width.
However the object wrapped in GeometryReader() doesn't have center alignment when width is less than minimum.
Please refer to attached gifs for demo.
Could you please help me with alignment?
Ideal:
Current:
struct ExampleView: View {
#State private var width: CGFloat = 50
var body: some View {
VStack {
SubView()
.frame(width: self.width, height: 120)
.border(Color.blue, width: 2)
Text("Offered Width is \(Int(width))")
Slider(value: $width, in: 0...200, step: 5)
}
}
}
struct SubView: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.yellow.opacity(0.6))
.frame(width: max(geometry.size.width, 120), height: max(geometry.size.height, 120))
}
}
}
That's because GeometryReader doesn't center its children.
You have to manually position the Rectangle by adding either a .position modifier or a .offset.
.position will move the origin of the rectangle's frame relative to the parent's center.
.offset will not change the frame, but rather change the rendering (working like a CGAffineTransform with translation).
The following modifiers to your Rectangle will yield the same results visually (though different under the hood):
Rectangle()
.fill(Color.yellow.opacity(0.6))
.frame(width: max(geometry.size.width, 120), height: max(geometry.size.height, 120))
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
or
let rectWidth = max(geometry.size.width, 120)
Rectangle()
.fill(Color.yellow.opacity(0.6))
.frame(width: rectWidth, height: max(geometry.size.height, 120))
.offset(x: ((geometry.size.width - rectWidth) / 2), y: 0)
Note that your rectangle's frame exceeds the bounds of its parent. I'd suggest to avoid that because it will cause all sorts of difficulties laying out other UI elements on the screen.
You could build it the other way around (unless your practical goal is to just understand how GeometryReader works):
struct ExampleView: View {
#State private var width: CGFloat = 50
var body: some View {
VStack {
let minWidth: CGFloat = 120
let subViewWidth: CGFloat = max(minWidth, width)
SubView(desiredWidth: width)
.frame(width: subViewWidth, height: 120)
.background(.yellow.opacity(0.6))
Text("Offered Width is \(Int(width))")
Slider(value: $width, in: 0...200, step: 5)
}
}
}
struct SubView: View {
let desiredWidth: CGFloat
var body: some View {
Rectangle()
.fill(.clear)
.border(Color.blue, width: 2)
.frame(width: desiredWidth, height: nil)
}
}
In this example the SubView has a yellow fill and a minimal frame width while the inner rectangle just takes whatever width is set on the slider. It also doesn't need a GeometryReader anymore. It looks and behaves the same but none of the views exceeds its parent's bounds anymore.

How to align elements on ZStack in SwiftUI?

Problem
I am trying to create a view of a card with a symbol in the middle.
I tried to achieve this by creating a ZStack.
However, despite using .center alignment, the symbol always show in the top left.
Code
In the following code, the contentShape shows on the top-left despite alignment setting.
ZStack(alignment: .center) {
let baseShape = RoundedRectangle(cornerRadius: 10)
let contentShape = Rectangle()
.size(width: width, height: height)
.foregroundColor(getContentColor(color: card.color))
baseShape.fill().foregroundColor(.white)
baseShape.strokeBorder(lineWidth: 3, antialiased: true)
contentShape
}
Question
How do I properly align the contentShape at the center of the ZStack?
You need to use frame instead of size, because size is just for path drawing within provided rect, but rect here is entire area, so to fix use
ZStack(alignment: .center) {
let baseShape = RoundedRectangle(cornerRadius: 10)
let contentShape = Rectangle()
.frame(width: width, height: height) // << here !!
.foregroundColor(getContentColor(color: card.color))
baseShape.fill().foregroundColor(.white)
baseShape.strokeBorder(lineWidth: 3, antialiased: true)
contentShape
}

Crop Rectangle view with shape SwiftUI

I am trying to make the center portion of the view transparent like a camera overlay.
As shown in the below picture where the center portion is transparent while the remaining area is black colored with opacity.
I tried the below code.
Rectangle()
.fill(Color.red.opacity(0.5))
.mask(
Ellipse()
.fill(
Color.green.opacity(0.5)
)
.padding()
)
}
but the output is this.
Any help will be appreciated.
Thanks
This is adapted from #Asperi's answer here: https://stackoverflow.com/a/59659733/560942
struct MaskShape : Shape {
var inset : UIEdgeInsets
func path(in rect: CGRect) -> Path {
var shape = Rectangle().path(in: rect)
shape.addPath(Ellipse().path(in: rect.inset(by: inset)))
return shape
}
}
struct ContentView : View {
var body: some View {
ZStack {
Image("2")
.resizable()
.aspectRatio(contentMode: .fill)
GeometryReader { geometry in
Color.black.opacity(0.6)
.mask(
MaskShape(
inset: UIEdgeInsets(top: geometry.size.height / 6,
left: geometry.size.width / 6,
bottom: geometry.size.height / 6,
right: geometry.size.width / 6)
).fill(style: FillStyle(eoFill: true))
)
}
}
}
}
The ZStack sets up the image and then a semi-transparent black overlay on the top. The overlay is cut away with a mask with eoFill set to true. I added some code to provide insets for the mask, as I'm assuming this might be a variably-sized mask.
Lots of minutiae that can be changed (like the image aspect ratio, the insets, etc), but it should get you started.

How to create a NavigationLink or Button from a shape in SwiftUI that is only triggered by tapping on the shape but not its frame

I have a simple shape I want to use as a button but I would like to have the link/button only triggered when clicked on the shape itself but not on its frame. With the following code the link is also triggered when clicking inside the frame of the triangle:
struct Triangle: Shape {
func path(in rect: CGRect)-> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
return path
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: Text("DetailView")){
Triangle()
.fill(Color.green)
.frame(width: 200, height: 200, alignment: .center)
}.navigationBarTitle(Text("Triangle"))
}
}
}
How can I adjust the code to have only tapping the triangle but not the frame triggering the link?
You can add a contentShape modifier to tell the system how to do hit testing for the NavigationLink:
NavigationLink(destination: Text("DetailView")){
Triangle()
.fill(Color.green)
.frame(width: 100, height: 100, alignment: .center)
}
.contentShape(Triangle())
There's a caveat here, which is SwiftUI includes some slop in the hit testing, so this will still accept some clicks/touches that are close to the borders of the shape, but still outside. I don't have a great solution for getting rid of that slop. But, you can see that the same thing happens on the basic Rectangle that normally makes up the navigation view as well -- e.g. put a border around it and note that you can still tap slightly outside of that border.
A secondary way you could experiment with is render the Path on its own, attach an onTapGesture to it and turn on/off a boolean connected to the NavigationLink that you'd then want to move outside your hierarchy. Something like:
struct ContentView: View {
#State var navigationLinkActive = false
var body: some View {
NavigationView {
Group {
if navigationLinkActive {
NavigationLink(destination: Text("Detail"), isActive: $navigationLinkActive) {
EmptyView()
}
}
Triangle()
.fill(Color.green)
.frame(width: 100, height: 100, alignment: .center)
.onTapGesture {
navigationLinkActive = true
}
}.navigationBarTitle(Text("Triangle"))
EmptyView()
}
}
}
Note that in this solution, you still have the hit slop issue, plus you lose the touch down/up highlighting that the Button has.

SwiftUI, shadow only for container

For example, I have this view:
import SwiftUI
struct TarifsScreen: View {
var body: some View {
GeometryReader { geometry in
VStack {
VStack {
Spacer()
Text("Text1")
Spacer()
Text("Text2")
Spacer()
Text("Text3")
Spacer()
}
}
.frame(width: geometry.size.width, height: geometry.size.height)
.shadow(color: Color.white, radius: 10, x: 0, y: 0)
}
}
}
How can I apply shadow only for VStack, not for all elements inside VStack? May be I can do it with ZStack and two containers?
Add background and apply shadow to it, like in below example
VStack {
...
}
.background(Color.white // any non-transparent background
.shadow(color: Color.red, radius: 10, x: 0, y: 0)
)
.frame(width: geometry.size.width, height: geometry.size.height)
Also for a specific view:
var body: some View {
Text("SwiftUI is Awesome").padding().background(
Rectangle()
.fill(Color.white)
.cornerRadius(12)
.shadow(
color: Color.gray.opacity(0.7),
radius: 8,
x: 0,
y: 0
)
)
}
Reference from here.
Result:
If you don't want to add fills or backgrounds, you can better override the .shadow modifier on the child views with a .clear color, to "stop" the shadow modifier from propagating down the hierarchy.
VStack {
// View with shadow
...
VStack {
// View without shadow
...
}
.shadow(color: .clear, radius: 0) // Override shadow
}
.shadow(color: .black.opacity(0.3), radius: 10)
For example if you are using a png image, the other answers would render the alpha channel with a solid color, which is not ideal.