Due to API changes, part of this article has been invalid, please check the latest complete Chinese tutorial and codeGithub.com/WillieWangW…

Wechat Technology Group

SwiftUI represents the direction of building App in the future. Welcome to join us to exchange technology and solve problems.

Add group needs to apply now, you can add my wechat first, note “SwiftUI”, I will pull you into the group.

Animate View and transition

With SwiftUI, we can animate the View individually, or animate the state of the View, regardless of where it is used. SwiftUI handles the complexity of all animation combinations, overlaps and interruptions for us.

In this article, we will animate the view that contains the diagram to track the user’s behavior while using the Landmarks App. We’ll see how easy it is to animate a view by using the animation(_:) method.

Download the project file and follow the steps below, or open the completed project and browse the code yourself.

  • Estimated completion time: 20 minutes
  • Project files: Download

1. Animate a single View

When we use animation(_:) on a view, SwiftUI dynamically modifies the view’s animatable properties. A view’s color, transparency, rotation, size, and other properties are animatable.

1.1 In hikeview.swift, open live Preview to test showing and hiding charts.

Make sure you have live preview turned on throughout this article so you can test the results of each step.

1.2 Add animation(.basic()) method to turn on the rotation animation of the button.

HikeView.swift

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    self.showDetail.toggle()
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .padding()
                        .animation(.basic())
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}
Copy the code

1.3 Add an animation that makes the buttons bigger when the chart is displayed.

Animation (_:) applies to all animatable changes wrapped by the view.

HikeView.swift

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    self.showDetail.toggle()
                }) {
                    Image(systemName: "chevron.right.circle"ImageScale (.large).rotationEffect(.degrees(showDetail? 90:0)).scaleEffect(showDetail? 1.5: 1) .padding() .animation(.basic()) } }if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}
Copy the code

1.4 Change the animation type from.basic() to.spring().

SwiftUI includes basic animations with preset or custom easing, as well as elastic and fluid animations. We can adjust the speed of the animation, set a delay before the animation starts, or specify the repetition of the animation.

HikeView.swift

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    self.showDetail.toggle()
                }) {
                    Image(systemName: "chevron.right.circle"ImageScale (.large).rotationEffect(.degrees(showDetail? 90:0)).scaleEffect(showDetail? 1.5: 1) .padding() .animation(.spring()) } }if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}
Copy the code

Try adding another animation method above the scaleEffect method to turn off the rotation animation.

Try combining different animation effects around SwiftUI and see what they have.

HikeView.swift

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    self.showDetail.toggle()
                }) {
                    Image(systemName: "chevron.right.circle"ImageScale (.large).rotationEffect(.degrees(showDetail? 90:0)).animation(nil).scaleeffect (showDetail? 1.5: 1) .padding() .animation(.spring()) } }if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}
Copy the code

1.6 delete the two animation(_:) methods before proceeding to the next section.

HikeView.swift

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    self.showDetail.toggle()
                }) {
                    Image(systemName: "chevron.right.circle"ImageScale (.large).rotationEffect(.degrees(showDetail? 90:0)).scaleEffect(showDetail? 1.5:1).padding()}}if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}
Copy the code

2. Animate state changes

Now that we’ve learned how to animate individual Views, it’s time to animate changes in state values.

In this section, we’ll animate all the changes that occur when the user clicks the button and toggles the showDetail state property.

2.1 Wrap the call to showdetail.toggle () in the withAnimation function.

The public button and the HikeDetail View that are affected by the showDetail property now have animated transitions.

HikeView.swift

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle"ImageScale (.large).rotationEffect(.degrees(showDetail? 90:0)).scaleEffect(showDetail? 1.5:1).padding()}}if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}
Copy the code

Slow down the animation to see how SwiftUI animation can be interrupted.

2.2 Pass a 4-second base animation to the withAnimation method.

We can pass the same type of animation to the withAnimation function of animation(_:).

HikeView.swift

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation(.basic(duration: 4)) {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle"ImageScale (.large).rotationEffect(.degrees(showDetail? 90:0)).scaleEffect(showDetail? 1.5:1).padding()}}if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}
Copy the code

2.3 Try opening and closing the diagram View during animation.

2.4 Remove the slow animation from the withAnimation function before moving on to the next section.

HikeView.swift

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle"ImageScale (.large).rotationEffect(.degrees(showDetail? 90:0)).scaleEffect(showDetail? 1.5:1).padding()}}if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}
Copy the code

3. Customize View transitions

By default, views transition to on-screen and off-screen by fading in and out. We can customize the transition using the transition(_:) method.

3.1 add a transition(_:) method to the HikeView displayed when the condition is met.

The icon will now slide and disappear.

HikeView.swift

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle"ImageScale (.large).rotationEffect(.degrees(showDetail? 90:0)).scaleEffect(showDetail? 1.5:1).padding()}}if showDetail {
                HikeDetail(hike: hike)
                    .transition(.slide)
            }
        }
    }
}
Copy the code

3.2 Extract the transition as the static attribute of AnyTransition.

This keeps the code clean when you expand your custom transitions. For custom transitions we can use the same. As SwiftUI uses. Symbols.

HikeView.swift

import SwiftUI

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        AnyTransition.slide
    }
}

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle"ImageScale (.large).rotationEffect(.degrees(showDetail? 90:0)).scaleEffect(showDetail? 1.5:1).padding()}}if showDetail {
                HikeDetail(hike: hike)
                    .transition(.moveAndFade)
            }
        }
    }
}
Copy the code

3.3 use a move(edge:) transition instead, so that the chart slides in and out from the same side.

HikeView.swift

import SwiftUI

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        AnyTransition.move(edge: .trailing)
    }
}

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle"ImageScale (.large).rotationEffect(.degrees(showDetail? 90:0)).scaleEffect(showDetail? 1.5:1).padding()}}if showDetail {
                HikeDetail(hike: hike)
                    .transition(.moveAndFade)
            }
        }
    }
}
Copy the code

3.4 Use the Asymmetric (insertion:removal) method to provide different transitions for view display and disappearance.

HikeView.swift

import SwiftUI

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        let insertion = AnyTransition.move(edge: .trailing)
            .combined(with: .opacity)
        let removal = AnyTransition.scale()
            .combined(with: .opacity)
        return .asymmetric(insertion: insertion, removal: removal)
    }
}

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle"ImageScale (.large).rotationEffect(.degrees(showDetail? 90:0)).scaleEffect(showDetail? 1.5:1).padding()}}if showDetail {
                HikeDetail(hike: hike)
                    .transition(.moveAndFade)
            }
        }
    }
}
Copy the code

Animate complex effect combinations

When you click the button below the bar, the graph switches between three different sets of data. In this section, we’ll use composite animations to provide dynamic, fluctuating transitions to the Capsule that makes up the graphics.

4.1 Change the default value of showDetail to true and fix the HikeView preview to canvas.

This allows us to see the diagram in context when animating in other files.

4.2 In GraphCapsule.swift, add a new computational animation property and apply it to Capsule’s shape.

GraphCapsule.swift

import SwiftUI struct GraphCapsule: View { var index: Int var height: Length var range: Range<Double> var overallRange: Range<Double> var heightRatio: Length { max(Length(magnitude(of: range) / magnitude(of: LowerBound: Length {Length((range. LowerBound - overallrange.lowerbound)/magnitude(of: overallRange)) } var animation: Animation { Animation.default } var body: some View { Capsule() .fill(Color.gray) .frame(height: height * heightRatio, alignment: .bottom) .offset(x: 0, y: height * -offsetRatio) .animation(animation) ) } }Copy the code

4.3 Change the animation to elastic animation and use the initial speed to make the bar graph jump.

GraphCapsule.swift

import SwiftUI struct GraphCapsule: View { var index: Int var height: Length var range: Range<Double> var overallRange: Range<Double> var heightRatio: Length { max(Length(magnitude(of: range) / magnitude(of: LowerBound: Length {Length((range. LowerBound - overallrange.lowerbound)/magnitude(of: overallRange)) } var animation: Animation { Animation.spring(initialVelocity: 5) } var body: some View { Capsule() .fill(Color.gray) .frame(height: height * heightRatio, alignment: .bottom) .offset(x: 0, y: height * -offsetRatio) .animation(animation) ) } }Copy the code

4.4 Speed up the animation and shorten the time required for each bar to move to a new position.

GraphCapsule.swift

import SwiftUI struct GraphCapsule: View { var index: Int var height: Length var range: Range<Double> var overallRange: Range<Double> var heightRatio: Length { max(Length(magnitude(of: range) / magnitude(of: LowerBound: Length {Length((range. LowerBound - overallrange.lowerbound)/magnitude(of: overallRange)) } var animation: Animation { Animation.spring(initialVelocity: 5) .speed(2) } var body: some View { Capsule() .fill(Color.gray) .frame(height: height * heightRatio, alignment: .bottom) .offset(x: 0, y: height * -offsetRatio) .animation(animation) ) } }Copy the code

4.5 Add a delay for each animation based on the position of Capsule on the diagram.

GraphCapsule.swift

import SwiftUI struct GraphCapsule: View { var index: Int var height: Length var range: Range<Double> var overallRange: Range<Double> var heightRatio: Length { max(Length(magnitude(of: range) / magnitude(of: LowerBound: Length {Length((range. LowerBound - overallrange.lowerbound)/magnitude(of: overallRange)) } var animation: Animation { Animation.spring(initialVelocity: 5).speed(2).delay(0.03 * Double(index))} var body: some View {Capsule().fill(color.gray).frame(height: height * heightRatio, alignment: .bottom) .offset(x: 0, y: height * -offsetRatio) .animation(animation) ) } }Copy the code

4.6 Observe how custom animations create ripple effects as they transition from chart to chart.