I have a view struct that renders ok in preview, but not when embedded in an app. The view used to display image files, and worked ok when the dice changed value. Now it has changed to drawing dynamically, it has stopped working correctly. It stays displaying the same number of dots as the first values, and misplaces the dots when it redraws.
Below is the code for the view:
import SwiftUI
struct DiceView: View {
var dice: NubbleGame.Dice
var color: Color = Color.red
let dotSizeDivisor = 5
var body: some View {
GeometryReader
{ viewSize in
let smallestSide = min(viewSize.size.width, viewSize.size.height)
let dotSize = smallestSide/CGFloat(dotSizeDivisor)
ZStack()
{
RoundedRectangle(cornerRadius: smallestSide/10)
.stroke(Color.black, lineWidth: viewSize.size.width/50)
ForEach(0..<dice.currentValue)
{ i in
ZStack()
{
Circle()
.fill(color)
.position(getPositionForDot(dotNumber: i+1, diceValue: dice.currentValue, diceSize:smallestSide))
.frame(width: dotSize, height: dotSize, alignment: .center)
Circle()
.stroke(Color.black, lineWidth: 2)
.position(getPositionForDot(dotNumber: i+1, diceValue: dice.currentValue, diceSize:smallestSide))
.frame(width: dotSize, height: dotSize, alignment: .center)
}
}
}
}
}
func getPositionForDot(dotNumber: Int, diceValue: Int, diceSize: CGFloat) -> CGPoint
{
var tempDiceValue = diceValue
var tempDotNumber = dotNumber
let zero = CGFloat(diceSize/CGFloat(dotSizeDivisor))
let offSet = CGFloat((diceSize * 0.9) / 2)
let center = CGFloat(diceSize/2)
var thisDot: CGPoint = CGPoint(x: 0, y: 0)
if diceValue%2 != 0
{
if dotNumber == 1
{
return CGPoint(x: (diceSize/2) - offSet, y: (diceSize/2) - offSet)
}
else
{
tempDiceValue = diceValue - 1
tempDotNumber = dotNumber - 1
}
}
for i in 1..<tempDotNumber+1
{
if tempDiceValue % 2 == 0
{
if i == 1
{
thisDot = CGPoint(x: zero - offSet, y: zero - offSet)
}
else if i == 2
{
thisDot = CGPoint(x: diceSize - zero - offSet, y: (diceSize - zero) - offSet)
}
else if i == 3
{
thisDot = CGPoint(x: (diceSize - zero) - offSet, y: zero - offSet)
}
else if i == 4
{
thisDot = CGPoint(x: zero - offSet, y: (diceSize - zero) - offSet)
}
else if i == 5
{
thisDot = CGPoint(x: zero - offSet, y: diceSize/2 - offSet)
}
else if i == 6
{
thisDot = CGPoint(x: (diceSize - zero) - offSet, y: center - offSet)
}
else if i == 7
{
thisDot = CGPoint(x: center - offSet, y: center-diceSize/(CGFloat(dotSizeDivisor)) - offSet)
}
else if i == 8
{
thisDot = CGPoint(x: center - offSet, y: center+diceSize/(CGFloat(dotSizeDivisor)) - offSet)
}
}
}
return thisDot
}
}
This is the code used in the app. The model has an array of Dice in it - either with 3 or 4 depending on the specific version of the game being played:
ForEach(viewModel.dice){dice in
DiceView(dice: dice)
.frame(width: 80, height: 80)
.onTapGesture(perform: {
if !viewModel.diceRolled
{
Sounds.playSounds(soundName: "diceroll")
}
viewModel.rollDice()
})
...and this is the Dice struct from the model:
struct Dice: Identifiable
{
var id: Int
var numberOfSides: Int
var currentValue: Int
}
Any help will be much appreciated! Thank you!
The issue was in the ForeEach loop that drew the dots... This answer had the solution. It needed to be identifiable.
View is not rerendered in Nested ForEach loop
Related
I try to move an Image along a path in a vein. I didn't find how to do it. Here my present code:
struct ContentView: View {
#State var chemin = Path {chemin in
chemin.move(to: CGPoint(x: 620, y: 0))
chemin.addLine(to: CGPoint(x:460, y:120))
chemin.addQuadCurve(to: CGPoint(x: 480, y: 200), control: CGPoint(x: 380, y: 220))
chemin.addLine(to: CGPoint(x:590, y:120))
chemin.addQuadCurve(to: CGPoint(x: 680, y: 320), control: CGPoint(x: 890, y: 160))
chemin.addLine(to: CGPoint(x: 300, y: 330))
chemin.addLine(to: CGPoint(x: 0, y: 160))
}
var body: some View {
ZStack{
Image("carte").resizable().ignoresSafeArea()
VStack{
Image("corona").resizable().frame(width: 150, height: 100, alignment: .center).animation(Animation.linear(duration: 3), value: true)
}
chemin.stroke(Color.green, lineWidth: 3).ignoresSafeArea()
}
}
}
I finally found how to do it, here's the code with some extra-functions:
import SwiftUI
struct FollowEffect: GeometryEffect {
var pct: CGFloat = 0
let path: Path
var rotate = true
var animatableData: CGFloat {
get { return pct }
set { pct = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
if !rotate {
let pt = percentPoint(pct)
return ProjectionTransform(CGAffineTransform(translationX: pt.x, y: pt.y))
} else {
// Calculate rotation angle, by calculating an imaginary line between two points
// in the path: the current position (1) and a point very close behind in the path (2).
let pt1 = percentPoint(pct)
let pt2 = percentPoint(pct - 0.01)
let a = pt2.x - pt1.x
let b = pt2.y - pt1.y
let angle = a < 0 ? atan(Double(b / a)) : atan(Double(b / a)) - Double.pi
let transform = CGAffineTransform(translationX: pt1.x, y: pt1.y).rotated(by: CGFloat(angle))
return ProjectionTransform(transform)
}
}
func percentPoint(_ percent: CGFloat) -> CGPoint {
let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
let f = pct > 0.999 ? CGFloat(1-0.001) : pct
let t = pct > 0.999 ? CGFloat(1) : pct + 0.001
let tp = path.trimmedPath(from: f, to: t)
return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY)
}
}
struct ContentView: View {
#State var chemin: Path = Path {path in
path.move(to: CGPoint(x: 620, y: 0))
path.addLine(to: CGPoint(x:460, y:120))
path.addQuadCurve(to: CGPoint(x: 480, y: 200), control: CGPoint(x: 380, y: 220))
path.addLine(to: CGPoint(x:590, y:120))
path.addQuadCurve(to: CGPoint(x: 680, y: 320), control: CGPoint(x: 890, y: 160))
path.addLine(to: CGPoint(x: 300, y: 330))
path.addLine(to: CGPoint(x: 0, y: 160))
}
#State private var flag = false
var body: some View {
GeometryReader { proxy in
ZStack(alignment: .topLeading) {
Image("carte").resizable().ignoresSafeArea()
// Animate movement of Image
Image("corona").resizable().foregroundColor(Color.red).frame(width: 50, height: 50).offset(x: -25, y: -25).modifier(FollowEffect(pct: self.flag ? 1 : 0, path: chemin)).onAppear {
withAnimation(Animation.linear(duration: 4.0).repeatForever(autoreverses: false)) {
self.flag.toggle()
}
}
}.frame(alignment: .topLeading)
}
}
}
I am having an issue with animations in a tabview. I have a tabview with 2 views.
The first view has a shape with an animation. The second view is a simple text.
When I launch the application, View1 appears and the animation is correct. When I swipe to View2 and come back to View1, the animation no longer appear as intended and is somewhat random. Anyone might know what the issue might be ? Thank you.
ContentView
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
View1()
View2()
} //: TAB
.tabViewStyle(PageTabViewStyle())
.padding(.vertical, 20)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
View1
import SwiftUI
struct FollowEffect: GeometryEffect {
var pct: CGFloat = 0
let path: Path
var rotate = true
var animatableData: CGFloat {
get { return pct }
set { pct = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
if !rotate {
let pt = percentPoint(pct)
return ProjectionTransform(CGAffineTransform(translationX: pt.x, y: pt.y))
} else {
// Calculate rotation angle, by calculating an imaginary line between two points
// in the path: the current position (1) and a point very close behind in the path (2).
let pt1 = percentPoint(pct)
let pt2 = percentPoint(pct - 0.01)
let a = pt2.x - pt1.x
let b = pt2.y - pt1.y
let angle = a < 0 ? atan(Double(b / a)) : atan(Double(b / a)) - Double.pi
let transform = CGAffineTransform(translationX: pt1.x, y: pt1.y).rotated(by: CGFloat(angle))
return ProjectionTransform(transform)
}
}
func percentPoint(_ percent: CGFloat) -> CGPoint {
let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
let f = pct > 0.999 ? CGFloat(1-0.001) : pct
let t = pct > 0.999 ? CGFloat(1) : pct + 0.001
let tp = path.trimmedPath(from: f, to: t)
return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY)
}
}
struct Solar2Grid: Shape {
func path(in rect: CGRect) -> Path {
return Solar2Grid.createArcPath(in: rect)
}
static func createArcPath(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.width, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height - 20))
path.addArc(center: CGPoint(x: rect.width - 20, y: rect.height - 20), radius: CGFloat(20), startAngle: .degrees(0), endAngle: .degrees(90), clockwise: false)
path.addLine(to: CGPoint(x: 0, y: rect.height))
return path
}
}
struct AnimRecView: View {
#State var flag: Bool = false
var body: some View {
ZStack {
Solar2Grid()
.stroke(Color.purple, style: StrokeStyle( lineWidth: 2, dash: [3]))
Circle()
.foregroundColor(Color.red)
.blur(radius: 3.0)
.frame(width: 8, height: 8).offset(x: -40, y: -40)
.modifier(FollowEffect(pct: self.flag ? 1 :0, path: Solar2Grid.createArcPath(in: CGRect(x: 0, y: 0, width: 80, height: 80)), rotate: false))
.onAppear {
withAnimation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false)) {
self.flag.toggle()
}
}
}
}
}
struct View1: View {
#State var flag: Bool = false
var body: some View {
VStack() {
Text("View1")
Spacer()
HStack() {
AnimRecView()
}
.frame(width: 80, height: 80, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
Spacer()
}
.frame(minWidth: /*#START_MENU_TOKEN#*/0/*#END_MENU_TOKEN#*/, maxWidth: /*#START_MENU_TOKEN#*/.infinity/*#END_MENU_TOKEN#*/, minHeight: /*#START_MENU_TOKEN#*/0/*#END_MENU_TOKEN#*/, maxHeight: /*#START_MENU_TOKEN#*/.infinity/*#END_MENU_TOKEN#*/, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.background(LinearGradient(gradient: Gradient(colors: [Color.blue, Color.black]), startPoint: .top, endPoint: .bottom))
.cornerRadius(20)
.padding(.horizontal, 20)
}
}
struct View1_Previews: PreviewProvider {
static var previews: some View {
View1()
}
}
View2
import SwiftUI
struct View2: View {
var body: some View {
Text("View2")
}
}
struct View2_Previews: PreviewProvider {
static var previews: some View {
View2()
}
}
The problem is that .onAppear() is only called once, so the next time the view is shown, the animation doesn't know what to do. The fix is to put an explicit animation on the Circle() itself. Then, when the view comes back on screen, it has the appropriate animation. Like this:
struct AnimRecView: View {
#State var flag: Bool = false
var body: some View {
ZStack {
Solar2Grid()
.stroke(Color.purple, style: StrokeStyle( lineWidth: 2, dash: [3]))
Circle()
.foregroundColor(Color.red)
.blur(radius: 3.0)
.frame(width: 8, height: 8).offset(x: -40, y: -40)
.modifier(FollowEffect(pct: self.flag ? 1 : 0, path: Solar2Grid.createArcPath(in: CGRect(x: 0, y: 0, width: 80, height: 80)), rotate: false))
// Put the explicit animation here
.animation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false), value: flag)
.onAppear {
self.flag = true
}
}
}
}
My problem is simple I think but I can't figure how solve it.
I've this :
struct ArcSelectionView: View {
#Binding var isShowing: Bool
#Binding var curColor: Color
#Binding var colorToPress: Color
#Binding var score: Int
#State var colors = [Color.blue, Color.red, Color.green, Color.yellow]
var body: some View {
ZStack {
ForEach(1 ..< 5, id: \.self) { item in
Circle()
.trim(from: self.isShowing ? CGFloat((Double(item) * 0.25) - 0.25) : CGFloat(Double(item) * 0.25),
to: CGFloat(Double(item) * 0.25))
.stroke(self.colors[item - 1], lineWidth: 50)
.frame(width: 300, height: 300)
.onTapGesture {
if colors[item - 1] == colorToPress {
score += 1
}
isShowing.toggle()
colorToPress = colors.randomElement() ?? Color.offWhite
colors.shuffle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
self.isShowing.toggle()
}
}
}
}
.opacity(self.isShowing ? 1 : 0)
.rotationEffect(.degrees(self.isShowing ? 0 : 180))
.animation(.linear(duration: 0.35))
}
}
If I didn't shuffle colors in the .onTapGesture, everything is ok. But If I do, I've a strange plain Circle that appears in the middle and disappear after. It's ugly. Ugly Circle
Thank you for your help !
The issue is with the animation of the Circles. The better solution is to use arc shapes. Here is a working solution:
struct ArcSelectionView: View {
#Binding var curColor: Color
#Binding var colorToPress: Color
#Binding var score: Int
#State private var colors = [Color.blue, Color.red, Color.green, Color.yellow]
#State private var pct: CGFloat = 0.25
#State private var originalPCT: CGFloat = 0.25
let duration: Double = 0.35
var body: some View {
ZStack {
CircleView(wedge: originalPCT)
// I am not sure why, but at there is a difference of 10 in the sizes of the
// circle and the modifier. This corrects for it so the touch is accurate.
.frame(width: 310, height: 310)
PercentageArc(Color.clear, colors: colors, pct: pct) {
// With this solution you must have the callback sent to
// the main thread. This was unnecessary with AnimatbleModifier.
DispatchQueue.main.async {
pct = originalPCT
}
}
.animation(.linear(duration: duration), value: pct)
.frame(width: 300, height: 300)
// This forces the view to ignore taps.
.allowsHitTesting(false)
}
.onAppear {
pct = 1.0 / CGFloat(colors.count)
originalPCT = pct
}
}
func CircleView(wedge: CGFloat) -> some View {
ZStack {
// Array(zip()) is a cleaner and safe way of using indices AND you
// have the original object to use as well.
ForEach(Array(zip(colors, colors.indices)), id: \.0) { color, index in
Circle()
.trim(from: CGFloat((Double(index) * wedge)),
to: CGFloat(Double(index + 1) * wedge))
// The color of the stroke should match your background color.
// Clear won't work.
.stroke(.white, lineWidth: 50)
.onTapGesture {
if color == colorToPress {
score += 1
print("score!")
}
pct = 0
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
colorToPress = colors.randomElement() ?? .white
colors.shuffle()
}
}
}
}
}
}
struct PercentageArc<Content>: View, Animatable where Content: View {
private var content: Content
private var colors: [Color]
private var pct: CGFloat
private var target: CGFloat
private var onEnded: () -> ()
init(_ content: Content, colors: [Color], pct: CGFloat, onEnded: #escaping () -> () = {}) {
self.content = content
self.colors = colors
self.pct = pct
self.target = pct
self.onEnded = onEnded
}
var animatableData: CGFloat {
get { pct }
set { pct = newValue
// newValue here is interpolating by engine, so changing
// from previous to initially set, so when they got equal
// animation ended
if newValue == target {
onEnded()
}
}
}
var body: some View {
content
.overlay(
ForEach(Array(zip(colors, colors.indices)), id: \.0) { color, index in
ArcPortionShape(pct: pct, startAngle: .degrees(1.0 / CGFloat(colors.count) * CGFloat(index) * 360.0))
.foregroundColor(color)
}
)
}
struct ArcPortionShape: InsettableShape {
let pct: CGFloat
let startAngle: Angle
var insetAmount = 0.0
init(pct: CGFloat, startAngle: Angle) {
self.pct = pct
self.startAngle = startAngle
}
var portion: CGFloat {
pct * 360.0
}
var endAngle: Angle {
.degrees(startAngle.degrees + portion)
}
func path(in rect: CGRect) -> Path {
var p = Path()
p.addArc(center: CGPoint(x: rect.width / 2.0, y:rect.height / 2.0),
radius: rect.height / 2.0 + 5.0,
startAngle: startAngle,
endAngle: endAngle,
clockwise: false)
return p.strokedPath(.init(lineWidth: 50))
}
func inset(by amount: CGFloat) -> some InsettableShape {
var arc = self
arc.insetAmount += amount
return arc
}
}
}
Originally, I made this with an AnimatableModifier, but it is deprecated, and the solution using it fails if it is placed in ANY stack or NavigationView. I can see why AnimatableModifier is deprecated.
This solution draws inspiration from this answer from Asperi, for the callback idea, though the solution will not work in iOS 15.2.
I have a view that contains an HStack that contains two views. The right view contains a graph. The left view contains labels for the graphs YAxis. I would like to center the graph in its parent view. If someone could point me in the right direction to accomplish this it would be appreciated. Below is my code.
Regards,
Chris
import SwiftUI
struct MainView: View {
#EnvironmentObject var viewModel : ViewModel
var body: some View {
VStack (alignment: .center) {
let maxYValue = viewModel.data.max { $0.Value < $1.Value }?.Value
let minYValue = viewModel.data.max { $0.Value > $1.Value }?.Value
let deltaY = maxYValue! - minYValue!
let maxYVal = Int(round(maxYValue!))
let minYVal = Int(round(minYValue!))
HStack{
VStack{
Text(String("\(maxYVal)"))
Spacer()
Text("2")
Spacer()
Text("3")
Spacer()
Text("3")
Spacer()
Text(String("\(minYVal)"))
}.frame(width: 100, height: 510, alignment: .trailing)
GeometryReader { geometry in
Path { path in
for index in viewModel.data.indices {
let xPosition = ((geometry.size.width) / (CGFloat(viewModel.data.count - 1))) * CGFloat(index + 0)
let yPosition = (1 - (CGFloat(viewModel.data[index].Value - minYValue!)) / CGFloat(deltaY) ) * (geometry.size.height)
if index == 0 {
path.move(to: CGPoint(x : xPosition, y : yPosition))
}
path.addLine(to: CGPoint(x : xPosition, y: yPosition))
}
} .stroke(Color.blue, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
Path { path in
path.move(to: CGPoint(x: 0, y: geometry.size.height))
path.addLine(to: CGPoint(x: 0, y: 0))
path.move(to: CGPoint(x: 0, y: geometry.size.height))
path.addLine(to: CGPoint(x: geometry.size.width, y: geometry.size.height))
}.stroke(Color.black, style: StrokeStyle(lineWidth: 2))
Path { path in
for index in 0...3 {
path.move(to: CGPoint(x: 0 , y: geometry.size.height * CGFloat(index) / 4))
path.addLine(to: CGPoint(x: geometry.size.width , y: geometry.size.height * CGFloat(index) / 4))
}
}.stroke(Color.gray, style: StrokeStyle(lineWidth: 0.5))
} // end geometry reader
.frame(width: 700, height: 500)// end inner geometry reader
}
} // end of vstack
}
}
Thank you for the response. After posting my query I did some research and came to the conclusion that my code was too complex. So, I rewrote it and I think simplified it. I create a geometry reader and use it to create two rectangles that combined take up the entire parent view. In the upper rectangle I place the graph title and in the lower rectangle I place the graph and am in the process of putting labels on the X and Y axis. Using this approach has allowed me to exactly place items within the rectangle including centering the graph. Problem solved.
The only thing I need to figure out is how to change the stroke for a given line without having to create multiple paths as I have done.
Below is the updated code.
Thanks again for responding.
Chris
import SwiftUI
struct MainView: View {
var body: some View {
GeometryReader { geometry in
VStack (spacing: 0){
let frameWidth = geometry.size.width
let frameHeight = geometry.size.height
Rectangle()
.fill(Color.clear)
// .border(Color.red)
.frame(width: frameWidth, height: frameHeight * 1/8 )
.overlay(
Text("Principal Values Over the Last Year")
.font(.largeTitle)
)
Rectangle()
.fill(Color.clear)
.frame(width: frameWidth, height: frameHeight * 7/8 )
// .border(Color.black, width: 1)
.overlay(
PrincipalChart(frameWidth: frameWidth, frameHeight: frameHeight * 7/8)
)
.padding(0)
} // end Vstack
} // end geometry reader
} // end body var
} // end of MainView
struct PrincipalChart: View {
let frameWidth : CGFloat
let frameHeight : CGFloat
#EnvironmentObject var viewModel : ViewModel
var body: some View {
ZStack {
let maxYValue = viewModel.data.max { $0.Value < $1.Value }?.Value
let minYValue = viewModel.data.max { $0.Value > $1.Value }?.Value
let deltaY = maxYValue! - minYValue!
let maxYVal = Int(round(maxYValue!))
let minYVal = Int(round(minYValue!))
let yAxisStep = Int((maxYVal - minYVal) / 4)
Path { path in
for index in viewModel.data.indices {
let xPosition = (frameWidth * 1/10) + ((frameWidth * 8/10) / (CGFloat(viewModel.data.count - 1))) * CGFloat(index + 0)
let yPosition = (1 - (CGFloat(viewModel.data[index].Value - minYValue!)) / CGFloat(deltaY) ) * (frameHeight * 9/10)
if index == 0 {
path.move(to: CGPoint(x : xPosition, y : yPosition))
}
path.addLine(to: CGPoint(x : xPosition, y: yPosition))
}
} .stroke(Color.blue, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
Path { path in
path.move(to: CGPoint(x: frameWidth * 1/10, y: 0))
path.addLine(to: CGPoint(x: frameWidth * 1/10 , y: frameHeight * 9/10 ))
path.addLine(to: CGPoint(x: frameWidth * 9/10, y: frameHeight * 9/10))
}.stroke(Color.black, style: StrokeStyle(lineWidth: 2))
Path { path in
for index in 0...3 {
path.move(to: CGPoint(x: frameWidth * 1/10, y: (frameHeight * 9/10) * (CGFloat (index) / 4) ))
path.addLine(to: CGPoint(x: frameWidth * 9/10, y: (frameHeight * 9/10) * (CGFloat (index) / 4)))
}
}.stroke(Color.gray, style: StrokeStyle(lineWidth: 0.5))
Text("\(maxYVal)").position(x: (frameWidth * 1/10) - 20, y: 1)
let stepValue3 = maxYVal - yAxisStep
Text("\(stepValue3)").position(x: (frameWidth * 1/10) - 20, y: -2 + (frameHeight * 9/10) * 1 / 4 )
let stepValue2 = stepValue3 - yAxisStep
Text("\(stepValue2)").position(x: (frameWidth * 1/10) - 20, y: -2 + (frameHeight * 9/10) * 2 / 4 )
let stepValue1 = stepValue2 - yAxisStep
Text("\(stepValue1)").position(x: (frameWidth * 1/10) - 20, y: -2 + (frameHeight * 9/10) * 3 / 4 )
Text("\(minYVal)").position(x: (frameWidth * 1/10) - 20, y: -2 + (frameHeight * 9/10) * 4 / 4 )
} // end z stack
} // end body var
} // end Principal Chart view
i want to do this: my Text ("Hello 0") should rotate 90 degrees around the y axis then increase (hidden) its number and then fullfill the circle rotation with the increased number.
unfortuantely the rotation starts at 90 degrees and before the text is hidden?
what am i doing wrong?
here is my code (runnable and reproducable)
struct ContentView: View {
#State var number : Int = 0
#State var degrees : Angle = .degrees(0)
var body: some View {
VStack {
Text("Hello \(number)")
.rotation3DEffect(degrees, axis: (x: 0, y: 1, z: 0))
.animation(.easeInOut(duration:2.5))
Button(action: {
withAnimation(Animation.easeInOut(duration:2.5)) {
self.degrees = .degrees(90)
}
withAnimation(Animation.easeInOut(duration: 2.5 * 3).delay(2.5)) {
self.number = self.number + 1
self.degrees = .degrees(359)
}
}) {
Text("Animate")
}
}
}
}
If I correctly understood the goal here is possible solution. Tested with Xcode 11.4 / iOS 13.4
struct ContentView: View {
#State var number : Int = 0
#State var degrees : Angle = .degrees(0)
var body: some View {
VStack {
Text("Hello \(number)").font(.largeTitle)
.rotation3DEffect(degrees, axis: (x: 0, y: 1, z: 0))
Button(action: {
withAnimation(.easeInOut(duration:2.5)) {
self.degrees = .degrees(90) // << animating
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2.51) {
self.number = self.number + 1 // << don't
withAnimation(.easeInOut(duration:2.5)) {
self.degrees = .degrees(360) // << animating
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2.51) {
self.degrees = .degrees(0) // << don't - reset !!
}
}
}) {
Text("Animate")
}
}
}
}