Learn How to Bring Your SwiftUI App to Life With Advanced Animations

11 min read

Providing onboarding animations is a great way to show new users how to use and interact with your app. In this tutorial, you’ll learn how to animate emojis using SwiftUI so your users can express how they feel during chat conversations, resulting in an improved user experience.

Amos G.
Amos G.
Published February 4, 2022 Updated February 18, 2022
Prototyping in SwiftUI part 3

Part three of this tutorial will guide you through creating a splash screen animation, an onboarding animation for an empty messages screen, turn-taking animations in chat messaging, and animating emojis. You’ll use our iOS Chat SDK sample application to get you up and running.

Since this is the final installment of our prototyping in SwiftUI series, you should check out part one and part two before diving in.

If you're unfamiliar with SwiftUI, also be sure to check out our SwiftUI Chat App tutorial.

Let’s get started.

Project Files to Get Started

The code for this tutorial is hosted on GitHub. You can clone the project’s repository, open it with Xcode( if you already have Xcode installed on your Mac), or download it as a ZIP file.

With the project files downloaded, you’re ready to start creating your launch screen animation.

Creating the Launch Screen Animation

The way your application launches impacts how users feel about the app. This section teaches you how to create a fast and seamless animated launch experience. When users tap your app icon, it will display the launch screen first as it starts. The launch screen is then replaced immediately with your app’s home screen.

The launch screen SwiftUI animation you will build in this section uses assets exported from Sketch. (Check out our seamless looping animations video on YouTube to learn how theses assets were structured in Sketch.)

In the Xcode assets library, you’ll find all the assets required to build this animation in the folder called LogoAnimation. Create a new Swift file called SplashAnimation.swift and replace its content with the following code.

//
//  SplashAnimation.swift
//  StreamiOSChatSDKPrototyping
//
//  Created by Amos from getstream.io on 14.10.2021.
//

import SwiftUI

struct SplashAnimation: View {
    let StreamBlue = Color(#colorLiteral(red: 0, green: 0.368627451, blue: 1, alpha: 1))
    // 1. Initial Animation States
    @State private var move = false
    @State private var swing = false
    @State private var splash = false
    
    var body: some View {
        if #available(iOS 15.0, *) {
            ZStack {
                StreamBlue
                    .opacity(0.25)
                    .ignoresSafeArea()
                ZStack {
                    
                    // 4. Logo: Splash Animation (scale and opacity)
                    Image("streamLogo")
                        .scaleEffect(0.6)
                        .scaleEffect(splash ? 40 : 1)
                        .opacity(splash ? 0 : 1)
                        .rotationEffect(.degrees(swing ? -10 : 10), anchor: swing ? .bottomLeading : .bottomTrailing)
                        .offset(y: -15)
                        
                    VStack(spacing: -46) {
                        
                        // 3. Top Wave: Horizontal Motion
                        Image("stream_wave")
                            .opacity(splash ? 0 : 1)
                            .offset(y: 20)
                            .offset(x: move ? -180 : 180)
                        
                        // 2. Bottom Wave: Horizontal Motion
                        Image("stream_wave")
                            .opacity(splash ? 0 : 1)
                            .offset(y: 10)
                            .offset(x: move ? -150 : 150)
                            .onAppear {
                                withAnimation(.easeInOut(duration: 1).repeatCount(8, autoreverses: true)){
                                    swing.toggle()
                                }
                                
                                withAnimation(.linear(duration: 4).speed(0.5)){
                                    move.toggle()
                                }
                                
                                withAnimation(.easeInOut(duration: 0.25).delay(8)){
                                    splash.toggle()
                                }
                            }
                    }
                    .mask(Image("wave_top").opacity(0.9))
                }
                // Change size here
                .scaleEffect(2)
            }
    
       
    }
}

struct SplashAnimation_Previews: PreviewProvider {
    static var previews: some View {
        SplashAnimation()
    }
}

}

To animate anything in SwiftUI, you have to change objects over time. To change objects over time, they must be driven by a state variable. When a state’s instance changes, it affects the layout, behavior, and contents of the view that has access to it.

To build the splash animation, we’ll use the move, swing, and splash states. The move state variable is used to create the horizontal motion of the waves (the two blue, wave-like rectangles). The swing state is used to create the swinging effect of the logo, and splash scales the logo up and makes all the views disappear at the end of the animation.

Follow the steps below to build the splash animation:

  1. At the beginning of your struct, declare the following three states as private and set their initial states to false.

  2. After using SwiftUI layout views to arrange the Stream logo and the waves as shown in the Swift file called SplashAnimation.swift, define how you want the animation to be triggered using the .onAppear modifier. The .onAppear modifier is used to trigger animations automatically when the views it attaches to appear.

    • In the .onAppear modifier, add an explicit animation using withAnimation and specify
      the easing functions as easeInOut and linear. In SwiftUI, you can create implicit animations by attaching an animation modifier to the view you want to animate. Another way to add animations to a view is to use an explicit animation which animates views without attaching the animation to the views themselves. Visit Hacking with Swift to learn more about how to create implicit and explicit animations.
  3. Set the duration and speed parameters for the easing equations. Then, add the state variable inside each animation and use .toggle to switch them between the initial and final animation states.

  4. Next, use a ternary conditional operation along with each state variable to animate the scale, opacity, rotation, offset, and anchor properties of the logo as well as the waves.

Using the sample code from the above, .scaleEffect(splash ? 40 : 1), you can see that the ternary operator takes three operands. The first, splash, is a condition, followed by a question mark ?, then a true expression 40 to execute, followed by a colon :, and finally a false condition 1 to execute. The result of this sample code will scale the logo from its original size of 1 to its final size of 40 before it disappears since the initial state is false.

Following all steps above will create the splash animation. To preview the animation, click the play icon at the top of the Xcode preview.

Building the No Messages Onboarding Animation

Onboarding experiences help first-time users to get started with apps quickly. Your app’s onboarding animation should be purposeful and goal oriented. In this section, you will create an onboarding animation experience for chat participants when it is their first time to message each other.

After downloading the project from GitHub, open it with Xcode and look for the ChannelList folder. You will find the Swift file ChannelListEmptyView.swift, which contains the chat icon animation and the writing effect seen above. Copy all the content in the file and paste into a new Swift file.

//
//  SelectUserListView.swift
//  StreamiOSChatSDKPrototyping
//
//  Created by Amos from getstream.io on 14.10.2021.
//

import SwiftUI

struct ChannelListEmptyView: View {
    
    // 1. Animate From: Chaticon animations
    @State private var blinkLeftEye = true
    @State private var blinkRightEye = true
    @State private var trimMouth = false
    @State private var shake = false
    
    // 1. Animate From: Writing animation
    @State private var writing = false
    @State private var movingCursor = false
    @State private var blinkingCursor = false
    
    let cursorColor = Color(#colorLiteral(red: 0, green: 0.368627451, blue: 1, alpha: 1))
    
    let emptyChatColor = Color(#colorLiteral(red: 0.2997708321, green: 0.3221338987, blue: 0.3609524071, alpha: 1))
    var body: some View {
        VStack {
            HeaderView()
            Spacer()
            VStack {
                    ZStack {
                        Image("emptyChatDark")
                            .rotationEffect(.degrees(shake ? -5 : 5), anchor: .bottomTrailing)
                        VStack {
                            HStack(spacing: 16) {
                                RoundedRectangle(cornerRadius: 2)
                                    .frame(width: 8, height: 4)
                                    .scaleEffect(y: blinkRightEye ? 0.1 : 1)
                                    .opacity(blinkRightEye ? 0.1 : 1)
                                RoundedRectangle(cornerRadius: 2)
                                    .frame(width: 8, height: 4)
                                    .scaleEffect(y: blinkLeftEye ? 0.05 : 1)
                            }
                            Circle()
                                .trim(from: trimMouth ? 0.5 : 0.6, to: trimMouth ? 0.9 : 0.8)
                                .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
                                .frame(width: 16, height: 16)
                                .rotationEffect(.degrees(200))
                        }.foregroundColor(emptyChatColor)
                            .rotationEffect(.degrees(shake ? -5 : 5), anchor: .bottomLeading)
                    } // 2. Animate To
                    .onAppear {
                        withAnimation(.easeInOut(duration: 1).repeatForever()){
                            blinkRightEye.toggle()
                        }
                        
                        withAnimation(.easeOut(duration: 1).repeatForever()){
                            blinkLeftEye.toggle()
                        }
                        withAnimation(.easeOut(duration: 1).repeatForever()){
                            trimMouth.toggle()
                        }
                        
                        withAnimation(.easeOut(duration: 1).repeatForever()){
                            shake.toggle()
                        }
                        
                        // Writing Animation
                        withAnimation(.easeOut(duration: 2).delay(1).repeatForever(autoreverses: true)) {
                           writing.toggle()
                            movingCursor.toggle()
                        }
                        
                        // Cursor Blinking Animation
                        withAnimation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true)) {
                        
                            blinkingCursor.toggle()
                        }
                    }
               
                ZStack(alignment: .leading) {
                    Text("Let’s start chatting!")
                        .font(.body)
                        .mask(Rectangle().offset(x: writing ? 0 : -150))
                    Rectangle()
                        .fill(cursorColor)
                        .opacity(blinkingCursor ? 0 : 1)
                        .frame(width: 2, height: 24)
                        .offset(x: movingCursor ? 148 : 0)
                }
                
                Text("How about sending your first message to a friend?")
                    .font(.body)
                    .foregroundColor(.secondary)
                    .lineLimit(2)
                    .multilineTextAlignment(.center)
                    
            }
            Spacer()
            TabView {
                MentionsView()
                        .tabItem {
                            Label("Chats", image: "message_icon")
                        }
                        
                    MentionsView()
                        .tabItem {
                        Label("Mentions", systemImage: "at")
                    }
            }.frame(width: .infinity, height: 73)
        } // All Views
    }
}

struct ChannelListEmptyView_Previews: PreviewProvider {
    static var previews: some View {
        ChannelListEmptyView()
            .preferredColorScheme(.dark)
    }
}

Follow the steps below to create any of the animations in this section. For example, to create the writing effect:

  1. Define the animation’s From value as @State private var writing = false. This is the same as the initial state of the animation.
  2. Next, specify how the animation should be initiated using the .onAppear modifier: .onAppear {withAnimation(.easeOut(duration: 2).delay(1).repeatForever(autoreverses: true)) { writing.toggle() }}.
  3. Then, place withAnimation inside the .onAppear modifier and set the animation parameters such as the easing, duration, and delay. Add the .repeatForever modifier to loop the animation infinitely. In addition, you should set the To value (final state) of the animation using writing.toggle(). The toggle() method is used here to switch between the false and true states of the writing variable. For each iteration, after the animation reaches its end value, it should return to the initial state. This is achieved by setting autoreverses to true.
  4. Finally, use the writing state variable to animate the offset (x-coordinate) of the rectangular mask which is overlaid on the text .mask(Rectangle().offset(x: writing ? 0 : -150)). The text reveals and hides itself using the ternary conditional operation.

Building Animated Typing Indicators

In this section, you will animate turn-taking for chat messaging, one of the most common conversational cues used in face-to-face conversations. Turn-taking is easily discoverable for users because there are visual cues that signal to each participant that it is time to listen while the other participant speaks. In digital chat interfaces, this mechanism is known as a typing indication (“a user is typing…”).

You will create visual and animated cues that allow chat participants to take turns when making statements, asking questions and giving replies.

When using a chat messaging application, you will normally see an animation similar to the animations above which indicates the other user is about to send a message. Since the above animations are similar, you will create only one, which is the one that animates with opacity.

Create a new Swift file IsTypingOpacityView.swift or open the finished animation from the Animations folder after you download the project from GitHub to explore the code. The content of the animation is the same as the code below.

//
//  IsTypingOpacityView.swift
//  StreamiOSChatSDKPrototyping
//
//  Created by Amos from getstream.io on 14.10.2021.
//

import SwiftUI

struct IsTypingOpacityView: View {
    let notificationColor = Color(#colorLiteral(red: 1, green: 0.2156862745, blue: 0.2588235294, alpha: 1))
    let onlineColor = Color(#colorLiteral(red: 0.1254901961, green: 0.8784313725, blue: 0.4392156863, alpha: 1))
    @State private var isTyping = false
    
    var body: some View {
        HStack {
            ZStack(alignment: .topTrailing) {
                Image("user_chew")
                //User status: Online or offline
                Circle()
                    .frame(width: 12, height: 12)
                    .foregroundColor(onlineColor)
            }
            
            VStack(alignment: .leading){
                Text("Opacity")
                    .font(.body)
                HStack {
                        HStack(spacing: 4) {
                            Circle()
                                .frame(width: 6, height: 6)
                                .opacity(isTyping ? 1 : 0.1)
                                .animation(.easeOut(duration: 1).repeatForever(autoreverses: true), value: isTyping)
                            Circle()
                                .frame(width: 6, height: 6)
                                .opacity(isTyping ? 1 : 0.1)
                                .animation(.easeInOut(duration: 1).repeatForever(autoreverses: true), value: isTyping)
                            Circle()
                                .frame(width: 6, height: 6)
                                .opacity(isTyping ? 1 : 0.1)
                                .animation(.easeIn(duration: 1).repeatForever(autoreverses: true), value: isTyping)
                        }
                        .onAppear{
                            isTyping.toggle()
                        }
                    
                    Text("is typing")
                        .font(.footnote)
                        .lineLimit(1)
                    .foregroundColor(.secondary)
                }
            }
            
            Spacer()
            
            VStack(alignment: .trailing) {
                // Number of unread messages
                Image(systemName: "2.circle")
                    .foregroundColor(notificationColor)
                    .font(.footnote)
                HStack(spacing: 4) {
                    Image("readReceipt")
                    Text("Now")
                        .font(.footnote)
                        .foregroundColor(.secondary)
                }
            }
        }
    }
}

struct IsTypingOpacityView_Previews: PreviewProvider {
    static var previews: some View {
        IsTypingOpacityView()
            .preferredColorScheme(.dark)
    }
}

Follow these steps to create the is typing opacity animation:

  1. Draw three circles and position them horizontally using an HStack layout container.
  2. Define the initial animation state as @State private var isTyping = false, which is a boolean data type.
  3. Attach the .onAppear modifier to the container holding all the circles to cue the animation when the circles appear. Also, toggle the isTyping state variable so that the animation can switch between the false and true states.
  4. Use the state variable along with the ternary conditional operation .opacity(isTyping ? 1 : 0.1) to animate the opacity of the circles.
  5. Lastly, set the easing of the first circle to easeOut, the second circle to easeInOut, and the third circle to easeIn. Setting a different easing equation for each of the circles results in creating a sequential animation since the easing functions accelerate and decelerate at different times although they all have the same duration of one second. To make the animation loop back-and-forth indefinitely, append .repeatForever(autoreverses: true), value: isTyping) to each of the easing equations.

Note: You can see from the code that each of the circles animates using the .animation modifier. Attaching the animation modifier directly to the circles this way will cause SwiftUI to animate any changes to the animatable properties of the circles. This is called an implicit animation.

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

Creating the Clapping Hands Animated Emoji

In this section, you will animate a round of applause. Popular messaging applications such as Skype and Telegram call it a “clapping hands” emoji.

Create a Swift file called ClappingHandsEmojiView.swift and replace its content with the following code or explore the code from the file in the folder AnimatedEmojis after you download the project from GitHub to see how it works.

//
//  ClappingHandsEmojiView.swift
//  ClappingHands
//
//  Created by Amos Gyamfi from getstream.io on 5.11.2021.
//

import SwiftUI

struct ClappingHandsEmojiView: View {
    
    // Initial Animation States
    @State private var blinking = false
    @State private var openingClosing = true
    @State private var clapping = true
    let StreamBlue = Color(#colorLiteral(red: 0, green: 0.368627451, blue: 1, alpha: 1))
    var body: some View {
        
        VStack(alignment: .trailing) {
            ZStack {
                Image("head")
                
                VStack {
                    ZStack {
                        Image("eyelid")
                        
                        Image("eye_blink")
                        // 1. Eye Blink Animation
                            .scaleEffect(y: blinking ? 0 : 1)
                            .animation(.timingCurve(0.68, -0.6, 0.32, 1.6).delay(1).repeatForever(autoreverses: false), value: blinking)
                    }
                    
                    ZStack {
                        Image("mouth")
                        // 2. Mouth Opening Animation
                            .scaleEffect(x: openingClosing ? 0.7 : 1)
                            .animation(.timingCurve(0.68, -0.6, 0.32, 1.6).delay(1).repeatForever(autoreverses: true), value: openingClosing)
                        
                        
                        HStack {
                            Image("left_hand")
                            // 3. Clapping Animation: Left Hand
                                .rotationEffect(.degrees(clapping ? 15 : -5), anchor: .bottom)
                                .offset(x: clapping ? 20 : -40)
                                .animation(.easeInOut(duration: 0.2).repeatForever(autoreverses: true), value: clapping)
                            
                            Image("right_hand")
                            // 4. Clapping Animation: Right Hand
                                .rotationEffect(.degrees(clapping ? -15 : 5), anchor: .bottom)
                                .offset(x: clapping ? -20 : 40)
                                .animation(.easeInOut(duration: 0.2).repeatForever(autoreverses: true), value: clapping)
                        }
                        
                    }
                    
                }
                .onAppear{
                    // Final Animation States
                    clapping.toggle()
                    blinking.toggle()
                    openingClosing.toggle()
                }
                
            }.frame(width: 58, height: 58)
             .scaleEffect(0.25)
            
            HStack(spacing: 4) { // Timestamp and read receipt
                Text("82")
                    .foregroundColor(StreamBlue)
                Image("readReceipt")
                Text("18.37 PM")
                    .foregroundColor(.secondary)
            }
            .font(.footnote)
        }
        
    }
    
}

struct ClappingHandsEmojiView_Previews: PreviewProvider {
    static var previews: some View {
        ClappingHandsEmojiView()
            .preferredColorScheme(.dark)
    }
}

To create the clapping hand emoji animation, begin with the eye-blink animation:

  1. Find all the assets in the assets library as shown below. They are contained in the folder, ClappingHands.
    Clapping Hands design components

  2. Define the following three initial animation states. The state variable blinking creates the eyeblink animation, openingClosing is used for creating the mouth animation, and clapping creates the clapping animation.

  3. Use SwiftUI’s depth stack (ZStack), vertical stack (VStack) and horizontal stack (HStack) to compose the emoji as shown in the finished code.

  4. Next, add the Final Animation States code snippet to the root ZStack container housing all the animatable views so that the animation triggers automatically, and each final animation state can be toggled between true and false states:

  5. For the eye-blink animation, use the state variable blinking to scale the y-coordinate parameter of the scaleEffect() modifier.

Note: The eyeblink animation uses a custom timing curve. To use custom cubic bezier curves for SwiftUI animations, visit easinggs.net. You can copy custom cubic bezier values and paste them directly into your SwiftUI animations’ timing curve.

The animation of the mouth is the same as the eye-blink animation, but opposite. Use the state variable openingClosing to animate the x-coordinate parameter of the scale effect modifier.

To animate the hands, wrap them in a row container using HStack. The right hand animation is opposite to the animation of the left hand. Here, you need to animate the angle rotation of each hand using the rotation effect modifier. Set the rotation to take effect from the bottom part of the hands by specifying the center gravity (anchor point) of the hands as bottom. To move the hands back-and-forth in the horizontal direction, animate the x-offset of the .offset modifier using the clapping state variable.

Creating the Revolving Hearts Emoji Animation

Animated emojis (emoticons) enrich chat messaging by adding affection, humor, and personal touches to key moments. In this section, you will learn how to create the revolving hearts emoji animation with SwiftUI. The revolving hearts emoji depicts a small heart and a large heart spinning (orbiting each other) on a circular motion path.

The revolving hearts emoji can be found in the folder ‘AnimatedEmojis’ with the file name RevolvingHeartsView. Its content is shown in the code below.

//
//  RevolvingHeartsView.swift
//  RevolvingHearts
//
//  Created by Amos Gyamfi from getstream on 23.11.2021.
//

import SwiftUI

struct RevolvingHeartsView: View {
    
    @State private var revolving = false
    
    var body: some View {
       
        VStack {
            VStack(spacing: 50) {
                ZStack {
                    ZStack {
                        Image("circular")
                        Image("heart_top")
                            // Rotation Mode: Do not rotate on path
                            .rotationEffect(.degrees(revolving ? -360 : 360))
                            .offset(x: 10, y: -20)
                            
                        Image("heart_bottom")
                            // Rotation Mode: Do not rotate
                            .rotationEffect(.degrees(revolving ? -360 : 360))
                            .offset(x: -25, y: 20)
                                
                    } // Circular
                    .rotationEffect(.degrees(revolving ? 360 : -360))
                    //.rotation3DEffect(.degrees(15), axis: (x: 3, y: 1, z: 0))
                    .animation(.easeInOut(duration: 5).repeatForever(autoreverses: false), value: revolving)
                    .offset(x: 12.5, y: -20)
                    .onAppear {
                            revolving.toggle()
                    }
                }
            }
        }
            
    }
}

struct RevolvingHeartsView_Previews: PreviewProvider {
    static var previews: some View {
        RevolvingHeartsView()
            .preferredColorScheme(.dark)
    }
}

It consists of an image of a circular ring, a small heart image, and a large heart image. All the images can be found in the assets library in the folder called Emojis. Use column and depth stack containers to layout the views as shown in the code above.

You need only one animation state to create the revolving hearts emoji animation. Follow the steps below to create this animation:

  1. Define an initial animation state called revolving and set it as a boolean data type using @State private var revolving = false.

  2. Find the ZStack that is the direct parent of the image views circular, heart_top, and heart_bottom. Attach the .onAppear modifier to it as shown below. Then, add the state variable you defined and toggle its state so that it can switch between true and false.

  3. Add the rotation effect modifier and use the state variable to change the angle of rotation parameter with a ternary conditional operator:
    .rotationEffect(.degrees(revolving ? 360 : -360))

  4. Animate the views implicitly by attaching the .animation modifier, set duration, and how the animation should be repeated. The value parameter indicates the animation values to monitor for changes over time. It differs and it is dependent on the property you want to animate. In this case, the values to monitor are the angles of rotation 360° and -360°.

Following all the steps above will animate the revolving hearts emoji successfully.

Creating the “Like” Animation

Message reactions provide users in chat more playful and fun ways to react to how they feel about messages. In this section, you will create a heart burst animation and trigger it as a reaction. This animation could be used and triggered whenever users in a chat respond to messages to show that they like, love, or acknowledge something.

There are two heart icons used for this animation. A gray heart icon, which can be found in the assets library in the folder called ReactionIcons, and a pink icon, which is an SF Symbol. The other reaction icons beside the heart icon (which are not used in the animation) can be found in the same folder. The heart icons are displayed using the if and else condition called notLiked. The if statement shows the gray heart when it is not tapped. The pink heart only shows after the gray one has been tapped.

You can find the code for this animation in the Xcode project, after downloading it from GitHub. It is contained in the file ReactionsView.swift under the folder called LongPressGesture. The content of the file is the same as the code below.

//
//  ReactionsView.swift
//  Stream iOS Chat SDK Prototyping
//
//  Created by Amos from getstream.io on 09.01.2022.
//

import SwiftUI

struct ReactionsView: View {
    
    let reactionsBGColor = Color(#colorLiteral(red: 0.07058823529, green: 0.07843137255, blue: 0.0862745098, alpha: 1))
    
    // Like Animation States
    @State private var notLiked = true
    @State private var removeInnerStroke = 14
    @State private var chromaRotate = 0
    @State private var animateTopPlus = 1
    @State private var animateMiddlePlus = 1
    @State private var animateBottomPlus = 1

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 28)
                .frame(width: 216, height: 40)
                .foregroundColor(reactionsBGColor)
                
            HStack(spacing: 20) {
                ZStack{
                    // When the heart icon is not tapped
                    if notLiked {
                        Image("like")
                            
                    } else {
                        Image(systemName: "heart.fill")
                            .font(.system(size: 24))
                            .frame(width: 24, height: 21)
                            .foregroundColor(Color(.systemPink))
                        
                        ZStack {
                            Circle()
                                .strokeBorder(lineWidth: CGFloat(removeInnerStroke))
                                .frame(width: 28, height: 28)
                                .foregroundColor(Color(.systemPink))
                                .hueRotation(.degrees(Double(chromaRotate)))
                            VStack {
                                Image(systemName: "heart.fill")
                                    .scaleEffect(CGFloat(animateTopPlus))
                                    .foregroundColor(Color(.systemPink))
                                Image(systemName: "plus")
                                    .scaleEffect(CGFloat(animateMiddlePlus))
                                Image(systemName: "heart.fill")
                                    .scaleEffect(CGFloat(animateBottomPlus))
                                    .foregroundColor(Color(.systemPink))
                            }
                        }
                    }
                }
                .onTapGesture {
                    withAnimation(.easeInOut(duration: 0.25)){
                        notLiked.toggle()
                    }
                    
                    withAnimation(.easeOut(duration: 0.5)){
                        removeInnerStroke = 0
                        chromaRotate = 270
                    }
                    
                    withAnimation(.easeOut(duration: 0.5).delay(0.1)){
                        animateTopPlus = 0
                    }
                    
                    withAnimation(.easeInOut(duration: 0.5).delay(0.2)){
                        animateMiddlePlus = 0
                    }
                    
                    withAnimation(.spring()){
                        animateBottomPlus = 0
                    }
                }
                
                Image("thumbs_up")
                Image("thumbs_down")
                Image("lol")
                Image("wut_reaction")
            }
           
        } // All reaction views
    }
}

struct ReactionsView_Previews: PreviewProvider {
    static var previews: some View {
        ReactionsView()
            .preferredColorScheme(.dark)
    }
}

To build the heart icon animation, start by defining the following animation start values. Each of the states (except notLiked) is initialized with an integer. You need to change these integer values in the final animation state so that the animation can interpolate between the two values.

Like Animation States:
@State private var notLiked = true
@State private var removeInnerStroke = 14
@State private var chromaRotate = 0
@State private var animateTopPlus = 1
@State private var animateMiddlePlus = 1
@State private var animateBottomPlus = 1

  • The noLiked state is used to swap the gray heart icon for the pink heart icon when you tap the gray one.
  • When the gray heart icon is tapped, the appearing and disappearing circle’s filling is removed. This is achieved using the state removeInnerStroke.
  • The chromaRotate state variable changes the hue values of the circle described above.
  • The animateTopPlus state displays and hides a plus icon on the top of the heart icon when it is tapped.
  • The animateMiddlePlus state displays a plus icon in front of the heart icon when it is tapped.
  • The animateBottomPlus shows a plus icon below the heart when the animation is triggered.

The heart animation should only start when you tap the gray heart icon. To create the tap action, add the .onTapGesture modifier to the row layout container (HStack) that houses all the reaction icons. Set the final animation states inside the .onTapGesture modifier and define the following easing equations.

In the final animation states, you should set each state variable with an integer value. The individual animations for this animation do not occur at the same time. To make an animation start later in time, change its Begin Time to a value greater than zero using the .delay modifier.

All the views that animate when you tap the gray heart are embedded in an else statement.

The final step to create this animation is to use all the animation states you have already defined to change the foreground color, size, and inner stroke of the views inside the else statement as demonstrated in the code above.

Note: You need to convert the state variable used as the angle of rotation of the .hueRotation modifier to a Double data type and that of the .scaleEffect modifier to CGFloat for the animation to work. When you use the state state variable without converting it to the required data type, Xcode may throw an error with an option to fix it. Clicking on the fix button will change the state variable to the required data type.

Congratulations

This tutorial guided you through creating SwiftUI animations for chat messaging. You learned how to onboard users with animations, how to emulate turn-taking in chat with animations, and how to create animated emojis.

Download the GitHub project and explore the code used to create the animations in this tutorial, and be sure to show us what you're working on. If you'd like to incorporate our SwiftUI SDK into your own chat app, be sure to sign up for a free maker account to get started!

If you have any feedback regarding this tutorial, feel free to reach out at @getstream_io.

Happy coding!

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->