Using geometry reader to Center the Rectangle - swiftui

I'm trying to use GeometryReader to make a Card View (to use in a card game).
This Card is going to have 3 Shapes on it.
I'm trying to center these 3 Shapes using GeometryReader (starting with the first shape, Rectangle), but it doesn't work.
What am I doing wrong?
Here's how I WANT it to look like: Here's how I want it to look like
Here's how it ACTUALLY looks like: Here's how it actually looks like
struct Card: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10.0).stroke(lineWidth: 3)
VStack {
GeometryReader { geometry in
Rectangle()
.size(
width: geometry.size.width * 0.75,
height: geometry.size.height * 0.75
)
.position(
x: geometry.size.width / 2,
y: geometry.size.height / 2
)
}
GeometryReader { geometry in
Circle()
.size(
width: geometry.size.width * 0.75,
height: geometry.size.height * 0.75
)
.position(
x: geometry.size.width / 2,
y: geometry.size.height / 2
)
}
GeometryReader { geometry in
Rectangle()
.size(
width: geometry.size.width * 0.75,
height: geometry.size.height * 0.75
)
.position(
x: geometry.size.width / 2,
y: geometry.size.height / 2
)
}
}
}
.foregroundColor(Color.orange)
}
}

Here is possible variant of layout. Tested with Xcode 12 / iOS 14
struct Card3: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10.0).stroke(lineWidth: 3)
GeometryReader { geometry in
VStack {
Color.clear.overlay(Rectangle()
.frame(
width: geometry.size.width * 0.75,
height: geometry.size.height / 3 * 0.75
))
Color.clear.overlay(Circle()
.frame(
width: geometry.size.width * 0.75,
height: geometry.size.height / 3 * 0.75
))
Color.clear.overlay(Rectangle()
.frame(
width: geometry.size.width * 0.75,
height: geometry.size.height / 3 * 0.75
))
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.foregroundColor(Color.orange)
}
}

Related

Shape resizing animation on orientation change

I have an issue with the shadow shape not being animated at the same time as the discs.
Here's the corresponding code. You can test this on an iPad by changing the orientation.
This is a direct consequence of the ContentView. Removing the GeometryReader and using a fixed frame fixes it.
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
ZStack {
PlanetView()
.frame(width: geometry.size.width * 0.25, height: geometry.size.width * 0.25)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
struct PlanetView: View {
var body: some View {
GeometryReader { geometry in
ZStack {
Circle()
.fill(.blue)
Circle()
.foregroundColor(.green)
.overlay(PlanetShadowView())
.frame(width: geometry.size.width * 0.8, height: geometry.size.width * 0.8)
}
}
}
}
struct PlanetShadowView: View {
var offsetRatio: CGFloat = 15/100.0
func control1(_ size: CGSize) -> CGPoint {
let xOffset = offsetRatio * size.width
let yOffset = offsetRatio * size.height
return .init(x: xOffset, y: size.height - yOffset)
}
func control2(_ size: CGSize) -> CGPoint {
let xOffset = offsetRatio * size.width
let yOffset = offsetRatio * size.height
return .init(x: size.width - xOffset, y: size.height - yOffset)
}
var body: some View {
GeometryReader { geometry in
Path { path in
path.move(to: CGPoint(x: 0, y: geometry.size.height / 2))
path.addCurve(to: CGPoint(x: geometry.size.width, y: geometry.size.height / 2),
control1: control1(geometry.size),
control2: control2(geometry.size))
path.addArc(center: CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2),
radius: geometry.size.width / 2,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 180),
clockwise: false)
path.closeSubpath()
}
.fill(Color.black.opacity(0.25))
.rotationEffect(.degrees(-30))
}
}
}
struct PlanetView_Previews: PreviewProvider {
static var previews: some View {
PlanetView()
}
}
A solution for this is to make planet shadow as a shape, so it would render in the same GeometryReader transformation context as everything else.
Tested with Xcode 13.4 / iOS 15.5
Circle()
.foregroundColor(.green)
.overlay(
PlanetShadow() // << here !!
.fill(Color.black.opacity(0.25))
.rotationEffect(.degrees(-30)))
.frame(width: geometry.size.width * 0.8, height: geometry.size.width * 0.8)
}
}
}
}
struct PlanetShadow: Shape { // << here !!
var offsetRatio: CGFloat = 15/100.0
func path(in rect: CGRect) -> Path {
// calc here ...
Test module on GitHub
Add .drawingGroup(). This should render the composition as a whole before displaying it.

SwiftUI, Circular progress bar with image on progress

I'm trying to implement a circular progress bar in SwiftUI with an image on the progress and animation. Any help or idea would be appreciated.
Here is what I plan to implement:imageLink
This is what I could implement so far: image2
And this is my code:
struct CircularView: View {
#State var progress:Double = 0
var total:Double = 100
let circleHeight:CGFloat = 217
#State var xPos:CGFloat = 0.0
#State var yPos:CGFloat = 0.0
var body: some View {
let pinHeight = circleHeight * 0.1
VStack {
Circle()
.trim(from: 0.0, to: 0.6)
.stroke(Color(.systemGray5),style: StrokeStyle(lineWidth: 8.0, lineCap: .round, dash: [0.1]))
.frame(width: 278, height: 217, alignment: .center)
.rotationEffect(.init(degrees: 162))
.overlay(
Circle()
.trim(from: 0.0, to: CGFloat(progress) / CGFloat(total))
.stroke(Color.purple,style: StrokeStyle(lineWidth: 8.0, lineCap: .round, dash: [0.1]))
.rotationEffect(.init(degrees: 162))
.rotation3DEffect(
.init(degrees: 1),
axis: (x: 1.0, y: 0.0, z: 0.0)
)
.animation(.easeOut)
.overlay(
Circle()
.frame(width: pinHeight, height: pinHeight)
.foregroundColor(.blue)
.offset(x: 107 - xPos, y: 0 + yPos)
.animation(.easeInOut)
.rotationEffect(.init(degrees: 162))
)
)
.foregroundColor(.red)
Button(action: {
progress += 5
if progress >= 65 {
progress = 0
}
}, label: {
Text("Button")
})
}
}
}
Without changing much of your initial code, the idea is to rotate also the image when the circular progress bar value changes
Your code would look like this:
struct CircularView: View {
#State var progress:Double = 0
var total:Double = 100
let circleHeight:CGFloat = 217
#State var xPos:CGFloat = 0.0
#State var yPos:CGFloat = 0.0
var body: some View {
let pinHeight = circleHeight * 0.1
VStack {
Circle()
.trim(from: 0.0, to: 0.6)
.stroke(Color(.systemGray5),style: StrokeStyle(lineWidth: 8.0, lineCap: .round, dash: [0.1]))
.frame(width: 278, height: 217, alignment: .center)
.rotationEffect(.init(degrees: 162))
.overlay(
Circle()
.trim(from: 0.0, to: CGFloat(progress) / CGFloat(total))
.stroke(Color.purple,style: StrokeStyle(lineWidth: 8.0, lineCap: .round, dash: [0.1]))
.rotationEffect(.init(degrees: 162))
.rotation3DEffect(
.init(degrees: 1),
axis: (x: 1.0, y: 0.0, z: 0.0)
)
// .animation(.easeOut)
.overlay(
Circle()
.frame(width: pinHeight, height: pinHeight)
.foregroundColor(.blue)
.offset(x: 107 - xPos, y: 0 + yPos)
// .animation(.easeInOut)
.rotationEffect(Angle(degrees: progress * 3.6))
.rotationEffect(.init(degrees: 162))
)
)
.foregroundColor(.red)
.animation(.easeOut) // Animation added here
Button(action: {
progress += 5
if progress >= 65 {
progress = 0
}
}, label: {
Text("Button")
})
}
}
}
1. Apply one common animation to have a better transition
I Have removed .animation modifiers in 2 places and place only one on the parent Circle View to allow a smooth animation ( You can still change and adapt it to the output you desire )
2. Calculate Image Angle rotation Degree
Perform simple calculation to determine rotation Angle, how much degree should I rotate the Image view
So, basically it is: .rotationEffect(Angle(degrees: (progress / 60) * (0.6 * 360)) => .rotationEffect(Angle(degrees: progress * 3.6))

Position of ScrollView not consistent in main view

I have a scrollview with an HStack inside it that is sized according to how many hexagons are to be placed in it. There are three possible values - 25, 64 and 100. The scrollview is positioned correctly for 25 and 64 hexagons, but is shifted to the right for 100. How can I make sure that is centred correctly too?
Incidentally, I have already tried manually setting the position of the scrollview - it worked the same as without.
GeometryReader
{ scrollViewGeometry in
ScrollView([.vertical, .horizontal], showsIndicators: true)
{
Spacer()
HStack(spacing:-(max(minHexagonSize, smallSide)))
{
ForEach(viewModel.hexagons){hexagon in
let hexagonFrame = getFrameFor(hexagon: hexagon, minWidth: smallSide)
let colorArray = getColorsFor(hexagon: hexagon)
ZStack()
{
let thisIndex = hexagon.id
HexagonView(strokeColor: colorArray[0]).onTapGesture(perform: {
viewModel.chooseHexagon(hexagon: hexagon)
})
.scaleEffect(hexagon.pressed ? 1.2 : 1.0)
.animation(Animation.easeInOut(duration: 0.2))
if hexagon.isSelected
{
Circle().fill(playerColorArray[hexagon.playerIndex]).frame(width: hexagonFrame.width * fontScalingFactor, height: hexagonFrame.height * fontScalingFactor, alignment: .center)
Circle().stroke(Color.black).frame(width: hexagonFrame.width * fontScalingFactor, height: hexagonFrame.height * fontScalingFactor, alignment: .center)
}
else
{
Text(verbatim: String(thisIndex+1)).font(Font.system(size: hexagonFrame.size.width * fontScalingFactor)).foregroundColor(colorArray[1])
.onTapGesture(perform: {
Sounds.playSounds(soundName: "pop")
viewModel.chooseHexagon(hexagon: hexagon)
})
.scaleEffect(hexagon.pressed ? 1.2 : 1.0)
.animation(Animation.easeIn(duration: 0.5))
}
if hexagon.isSelected && hexagon.pressed
{
Text(String(viewModel.currentScore))
.foregroundColor(Color.black)
.font(Font.system(size: hexagonFrame.size.width * fontScalingFactor)).foregroundColor(colorArray[1])
.transition(AnyTransition.asymmetric(insertion: .identity, removal: AnyTransition.move(edge: .top).combined(with: .opacity).animation(.easeIn(duration: 1.0))))
.animation(.easeIn(duration: 1.0))
.zIndex(hexagon.isSelected ? 1 : 0)
}
}
.position(x: hexagonFrame.origin.x - (scrollViewGeometry.size.width/2), y: scrollViewGeometry.size.height/2 - hexagonFrame.origin.y)
.frame(width: hexagonFrame.size.width, height: hexagonFrame.size.height)
.zIndex(hexagon.isSelected ? 1 : 0)
}
}
.frame(width: (max(minHexagonSize, smallSide)) * (sqrt(CGFloat(viewModel.hexagons.count)) * 2), height: (max(minHexagonSize, smallSide) * (sqrt(CGFloat(viewModel.hexagons.count)) + 2)))
//.position(x: scrollViewGeometry.size.width, y: scrollViewGeometry.size.height)
}
.padding()
}
OK... so it turns out that the reason it was not centred, is because I was looking at the wrong part of the view. I still don't understand why it worked fine for the smaller scrollable areas in the scrollview, but not for the larger one!

How to move View in HStack to the top of the other views

I have a set of subviews that are arranged within an HStack. I have a transition that is run whenever a view is selected successfully, but it is occluded by neighbouring views "above" it.
I found this article here, but I can't see how to integrate it into my solution:
How to bring a view on top of VStack containing ZStacks
Is there a way of getting the selected view to pop on top of the others?
HStack(spacing: -75)
{
ForEach(viewModel.hexagons){hexagon in
let hexagonFrame = getFrameFor(hexagon: hexagon, minWidth: smallSide)
let colorArray = getColorsFor(hexagon: hexagon)
ZStack()
{
let thisIndex = hexagon.id
HexagonView(strokeColor: colorArray[0]).onTapGesture(perform: {
viewModel.chooseHexagon(hexagon: hexagon)
})
.scaleEffect(hexagon.pressed ? 1.2 : 1.0)
.animation(Animation.easeInOut(duration: 0.2))
if hexagon.isSelected
{
Circle().fill(playerColorArray[hexagon.playerIndex]).frame(width: hexagonFrame.width * fontScalingFactor, height: hexagonFrame.height * fontScalingFactor, alignment: .center)
Circle().stroke(Color.black).frame(width: hexagonFrame.width * fontScalingFactor, height: hexagonFrame.height * fontScalingFactor, alignment: .center)
}
else
{
Text(verbatim: String(thisIndex+1)).font(Font.system(size: hexagonFrame.size.width * fontScalingFactor)).foregroundColor(colorArray[1])
.onTapGesture(perform: {
viewModel.chooseHexagon(hexagon: hexagon)
})
.scaleEffect(hexagon.pressed ? 1.2 : 1.0)
.animation(Animation.easeIn(duration: 0.5))
}
This is the view that animates up from the haxagon that has been selected
if hexagon.isSelected && hexagon.pressed
{
Text(String(viewModel.currentScore))
.foregroundColor(Color.gray)
.font(Font.system(size: hexagonFrame.size.width * fontScalingFactor)).foregroundColor(colorArray[1])
.transition(AnyTransition.asymmetric(insertion: .identity, removal: AnyTransition.move(edge: .top)))
.animation(.easeIn(duration: 2.0))
.zIndex(.greatestFiniteMagnitude)
}
}
.position(x: hexagonFrame.origin.x + hexagonFrame.size.width / 3 - (scrollViewGeometry.size.width/2), y: (scrollViewGeometry.size.height/2) - hexagonFrame.origin.y)
.frame(width: hexagonFrame.size.width, height: hexagonFrame.size.height, alignment:.center)
}
}.frame(width: (max(75, smallSide) * 8), height: (max(75, smallSide) * 5))

Changing the anchor point for a View when placing using .position()

In the following example, a view is placed relative to an Image, in this case in the bottom-right corner:
struct RelativePositionExampleView: View {
var body: some View {
Image("cookies")
.resizable()
.aspectRatio(contentMode: .fit)
.overlay(
GeometryReader { geometry in
Text("Hello")
.background(Color.red)
.position(x: geometry.size.width * 1.0, y: geometry.size.height * 1.0)
}
)
}
}
The "anchor point" for .position is the center of the Text view:
I tried using an alignment guide like described in Change Button Anchor Point SwiftUI, this didn't do the trick.
Is there a way to express "place based on the top/left or bottom/right corner" in such a case? (not based on Spacer - the position doesn't need to be necessarily in the corner, it could be 30% of the width f.e., 30% referring to the left edge of the positioned view).
If the goal is to just place text into image corner then there is simpler solution.
var body: some View {
Image("cookies")
.resizable()
.aspectRatio(contentMode: .fit)
.overlay(
ZStack {
Text("Hello")
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
)
}
Here is a very general way to relatively position overlay views anyway you like. Try it in Playground.
import SwiftUI
import PlaygroundSupport
extension View {
func overlay<V: View>(alignment: Alignment, relativePos: UnitPoint = .center, _ other: V) -> some View {
self
.alignmentGuide(alignment.horizontal, computeValue: { $0.width * relativePos.x })
.alignmentGuide(alignment.vertical, computeValue: { $0.height * relativePos.y })
.overlay(other, alignment: alignment)
}
}
struct ContentView: View {
var body: some View {
Circle()
.foregroundColor(.yellow)
.frame(width: 200, height: 200)
.overlay(alignment: .center, relativePos: UnitPoint(x: 0.3, y: 0.3), Capsule().fill(Color.green).frame(width: 40, height: 20))
.overlay(alignment: .center, relativePos: UnitPoint(x: 0.7, y: 0.3), Capsule().fill(Color.green).frame(width: 40, height: 20))
.overlay(alignment: .center, relativePos: UnitPoint(x: 0.5, y: 0.75), Text("Hello"))
.overlay(alignment: .center, relativePos: .bottom, Divider())
}
}
PlaygroundPage.current.setLiveView(ContentView())