Home > OS >  SwiftUI Animation - Issue with Tabview
SwiftUI Animation - Issue with Tabview

Time:03-25

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()
    }
}

CodePudding user response:

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
                }
        }
    }
}
  • Related