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.