How to align elements on ZStack in SwiftUI? - swift

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
}

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.

SwiftUI Fixed constraint from bottom of view to top of screen

Question: How to set fixed "constraint" from buttom of view to top of the screen?
My problem explanation:
I'm trying to implement such element on screen:
Rounded shape which has stable height in all screens
I use Oval to draw it like
public var body: some View {
VStack {
ZStack(alignment: .bottom) {
Ellipse()
.fill(Color.surfacePrimary)
.frame(width: 546, height: 364)
.offset(x: 0, y: -20)
Text("Text")
.padding(.bottom, 60)
.foregroundColor(.onPrimary)
}
.frame(minWidth: 0, idealWidth: .infinity, maxHeight: 20)
.edgesIgnoringSafeArea(.top)
Text("This text should be right below yellow shape, and it is, but on screens with not rectangle forms, on screens with rectangle form, it pops up on yellow zone")
Spacer()
}
}
Problem: that it works good on iphones 11, 12. But on 8, 8+, SE (where screens have right rectangle form) frame of ZStack move on top but Ellipse left on the same place. And Views which should be below yellow zone move to yellow zone.
UPD: I solved problem with different heights of StatusBar, but now I have another problem which was not observable before.
My new code:
public var body: some View {
ZStack(alignment: .bottom) {
Ellipse()
.fill(.yellow)
Text("Text")
.padding(.bottom, 42)
.foregroundColor(.red)
}
.frame(width: 546, height: 364)
.position(x: UIScreen.main.bounds.size.width / 2, y: Spacing.padding_0_5)
.edgesIgnoringSafeArea(.top)
.background(Color.red)
}
This ZStack fills almost all height of the screen. But I expect it should has fixed height. It looks like:
(hmmm ... some problem with uploading image)

How can I implement a slowly sliding image that has been clipped to shape using SwiftUI?

So far I have this circle and image inside it on my view:
and here is the code for it:
Image(chosenImage)
.resizable()
.scaledToFit()
.clipShape(Circle())
.shadow(radius: 20)
.overlay(Circle().stroke(Color.white, lineWidth: 5).shadow(radius: -20))
.frame(width: width, height: width, alignment: .center)
.offset(y: -height * 0.05)
How can I make the image slide slowly to the left within the circle?
The circle should not move, only the image within it should move.
As the image ends another copy of it should be displayed and the action repeated. Or the image could quickly jump back to its previous position and start moving again. Another way to do this is when the image reaches its end it starts slowly moving to the right.
Any ideas on how to do this or is there any libraries that can help me achieve this?
The answer Jeeva Tamil gave is almost correct however it moves the image while staying in the shape of a circle (shown bellow).
Whereas I need it to "show different parts of the image as it moves".
Use .mask before the .overlay modifier to move the image within the circle. Here, I've used Draggesture to demonstrate the behaviour.
#State private var horizontalTranslation: CGFloat = .zero
.
.
.
Image(chosenImage)
.resizable()
.scaledToFit()
.clipShape(Circle())
.offset(x: horizontalTranslation)
.gesture(
DragGesture()
.onChanged({ (value) in
withAnimation {
horizontalTranslation = value.translation.width
print(horizontalTranslation)
}
})
.onEnded({ (value) in
withAnimation {
horizontalTranslation = .zero
}
})
)
.mask(Circle())
.shadow(radius: 20)
.overlay(Circle().stroke(Color.white, lineWidth: 5).shadow(radius: -20))
.frame(width: width, height: width, alignment: .center)

SwiftUI: Rounded Rectangle Overlay Corners Cut Off

I'm trying to have a consistent-width broder with an overlay, and I keep getting the wrong result. My overlay looks like this:
With this code:
VStack {
HStack {
Text("Test")
}
HStack {
Text("test2")
}
}
.padding()
.frame(width: UIScreen.main.bounds.width*0.95)
.frame(minHeight: 50)
.overlay (
RoundedRectangle(cornerRadius: 20)
.stroke(RandomColor(), lineWidth: 3)
)
As you can see the corners are thicker than all other parts of the overlay. How can I fix this?
The thing you are missing is the difference between stroke and strokeBorder, if you changed your code to strokeBorder, it help you.
RoundedRectangle(cornerRadius: 20)
.strokeBorder(RandomColor(), lineWidth: 3)
The lineWidth makes it stretch beyond its bounds slightly. You can balance this with inset(by:) to keep it inside the original frame:
RoundedRectangle(cornerRadius: 20)
.inset(by: 3)
.stroke(RandomColor(), lineWidth: 3)
Note: you may only need to inset by 2 -- haven't done enough experimentation to see what the exact number is

SwiftUI - Add Border to One Edge of an Image

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.