How To Create Animations with SwiftUI

Introduction

It’s been a couple of months since iOS 13 was released, and although still too soon to be fully supported in our apps, it’s worth taking a look at the two major frameworks that were introduced in this new version: SwiftUI and Combine. No doubt they are the future of iOS development.

Over the last few months, I’ve been playing around with SwiftUI. Even if there are still some things that I feel are missing, I’ve been pleasantly surprised about how extremely easy it is to customize your UI as well as animate it. Something that would take loads of code in UIkit can be accomplished by a couple of lines in SwiftUI. And the best part is it goes smoothly and quickly! It bypasses Core Animation and goes down straight to Metal.

Forget about UIkit and move on, because you’re getting left behind, pal!

BankCards: Quick Overview

BankCards is a small example of what you can do with SwiftUI. I started it out of the blue one of those days I felt inspired. Sometimes, it’s honestly more difficult to come up with a good idea to play around with a new framework than the implementation; I’m sure you got me.

If you take a look at the starter project in my repo, there’s a couple of files you might want to check first, as they’re the ones we’ll be working on: Wallet.swift and WalletView.swift. The first one models a wallet and basically holds a bunch of cards and exposes helper methods. The second one stacks the cards one above each other and applies some UI changes to beautifully display the cards in your wallet:

ZStack() {
    ForEach(self.wallet.cards) { card in
        CardView(card: card)
            .opacity(self.opacity(for: card))
            .offset(x: 0,
                    y: self.offset(for: card))
            .scaleEffect(self.scaleEffect(for: card))
            .rotation3DEffect(self.rotationAngle(for: card),
                                axis: (x: 0.5, y: 1, z: 0))
    }
}.padding(.horizontal, Self.padding)

CardsInWallet.swift

WalletView.swift snippet

The result is this:

This is image title

WalletView.swift

Animate Your Wallet Transition

As I’ve mentioned before, SwiftUI makes it extremely easy for you to build up extraordinary designs. How does it work then? SwiftUI uses Combine behind the scenes and provides three property wrappers (new in Swift 5.1) that will help you with the process. They will basically tell the view that something has changed, which will trigger a view update. These property wrappers are:

  • @State : it represents a view property that holds some state the view relies on to render.
  • @ObservedObject : this is an object whose properties are observed by the View.
  • @EnvironmentObject : similar to @ObservedObject but globally accessible by the View and its subviews.

Let’s get to work! Right now the wallet shows up on the screen with no animation—nothing much is going on. Why don’t we make a transition to present the wallet? Declare a new property isPresented and set it initially to false:

@State var isPresented = false

Change the code inside ZStack to show the cards if isPresented is true. Then, use onAppear to toggle the flag.

ZStack() {
    if self.isPresented {
        ForEach(self.wallet.cards) { card in
            CardView(card: card)
                /*
                  ... Setting up the view ...
                */
        }
    }
}.padding(.horizontal, Self.padding)
.onAppear { self.isPresented.toggle() }

Toggling isPresented to present cards

Now, add an implicit animation to animate CardView transition:

CardView(card: card)
    .opacity(self.opacity(for: card))
    .offset(x: 0,
            y: self.offset(for: card))
    .scaleEffect(self.scaleEffect(for: card))
    .rotation3DEffect(self.rotationAngle(for: card),
                        axis: (x: 0.5, y: 1, z: 0))
    // 1                    
    .transition(.moveUpWardsWhileFadingIn)
    // 2
    .animation(Animation.easeOut.delay(self.transitionDelay(card: card)))

CardViewWithAnimationProperties.swift

Transitioning WalletView subviews

  1. Call transition so that whenever a card is added to WalletView it’s presented moving upwards while fading in.
  2. Use animation to implicitly say how each card and its subviews should animate.

Another way of doing this would be by using explicit animations:

withAnimation {
     self.isPresented.toggle()
}

But for the sake of simplicity, we’ll always use the default animation, although with a different delay (you’ll see later).

Once you’re done, you should have something like this:

This is image title

Drag Your Cards to Sort Your Wallet

Our wallet looks better now, but not good enough. Here’s what we’re going to do now. We’re going to make our CardView draggable so that we can sort the cards in our wallet by dragging the card at the front up or down. To do so, we’ll need to add a DragGesture to our first CardView instance:

CardView(card: card)
    // ...
    .gesture(DragGesture()
        .onChanged({ (value) in
            // some code
        }).onEnded({ (value) in
            // some code
        }))
    .transition(.moveUpWardsWhileFadingIn)
    .disabled(!self.wallet.isFirst(card: card))
    // ...

DraggableCard.swift

Adding DragGesture to the first card

DragGesture comes with two blocks. onChangedis called every time DragGesture updates its translation whereas onEndedis called whenever the gesture finishes. We’ll disable user interaction on cards that are at the back so that just the first one is draggable.

There are already some properties and helper methods defined in the file that we’ll use in this section. Take a look at offset(for:):

private func offset(for card: Card) -> CGFloat {
    guard !wallet.isFirst(card: card) else { return draggingOffset }
    let cardIndex = CGFloat(wallet.index(of: card))
    return cardIndex * Self.cardOffset
}

This is the function that was called to offset the cards depending on its position in the wallet. dragginOffset is declared at the top of the file and initialized to 0 . We’ll use onChanged to update its value:

self.draggingOffset = value.translation.height

Oh, wait! We’re getting an error:

This is image title

WalletView and any other View in SwiftUI is a struct, which means they’re immutable. Luckily for us, SwiftUI lets us modify their properties as long as we wrap them with @State .

@State var draggingOffset: CGFloat = 0

This is going to trigger an animated view update, which we were implicitly delaying. Go to ForEach inside you WalletView and add:

ForEach(self.wallet.cards) {
    // code here
}.onAppear {
    self.shouldDelay = false
}

This will make the helper method transitionDelay(card:) return 0 after ForEach has appeared. Don’t forget to wrap shouldDelay to get rid of the compiler error:

@State var shouldDelay = true

Finally, use onEnded to set draggingOffset back to 0 :

onEnded ({ _ in
    self.draggingOffset = 0
})

This is image title

Now, to sort the cards in your wallet, add the following code to onEnded :

let newCards = [card] + Array(self.wallet.cards.dropLast())
self.wallet.cards = newCards

This is image title

Final Touch: Tap a Card to Bring It to the Front

To finish this tutorial, let’s add a TapGesture to our cards to bring the picked card to the front of the stack. First, get rid of this line

.disabled(!self.wallet.isFirst(card: card))

and modify DragGesture onChanged and onEnded so that just the front card remains draggable:

onChanged ({ _ in
    if self.wallet.isFirst(card: card) {
        // code here
    }
})
.onEnded ({ _ in
    if self.wallet.isFirst(card: card) {
        // code here
    }
})

Now, use onTapGesture to sort the cards again:

.gesture(
    DragGesture()
        // ...
).onTapGesture {
    let newCards = self.wallet.cards.filter { $0 != card } + [card]
    self.wallet.cards = newCards
}

Build and run. Not quite getting the result you were expecting? Try wrapping wallet with @State. Still nothing? Here’s what’s happening. WalletView isn’t aware it has to be updated. We need to add a different wrapper to wallet , @ObservedObject :

@ObservedObject var wallet: Wallet = Wallet(cards: cards)

The compiler should complain at this point: Referencing initializer ‘init(wrappedValue:)’ on ‘ObservedObject’ requires that ‘Wallet’ conform to ‘ObservableObject.’

Go to Wallet.swift and implement ObservableObject :

class Wallet: ObservableObject {
    // ...
}

However, what is it exactly that SwiftUI should be observing? We need to make explicit which properties should be observed, or in other words, which properties will be published:

@Published var cards: [Card]

Build and run. This time we should have the expected result.

Where to Go From Here?

There are so many things you can do to improve your wallet experience. The starter project comes with additional variables isDragging and firstCardScale that you can use to enrich your animation. Try to use these variables (or other ones you may come up with) to change the rotationEffect , offset , … or any other modifier.

Check out the improvements I’ve made in the final version of this tutorial.

This is image title

Final version from the repo

Conclusion

SwiftUI is a very powerful tool that will make your life easier and will take your designs to the next level. Here are some pros:

  • It uses Metal and therefore is extremely fast and smooth.
  • Simple and easy to use.
  • Understandable in contrast to XIB or Storyboards base code. Conflicts can be more easily be addressed.
  • More legible than coding constraints.
  • Fewer lines of codes

On the other hand, here are some cons:

  • It’s pretty new, so it’s constantly changing.
  • Some views that are very used in UIkit don’t have an equivalent in SwiftUI. For instance, UICollectionView or TextView .
  • Integration in your app, as SwiftUI is only supported by iOS 13+.

Resources

#swift #ios #SwiftUI

How To Create Animations with SwiftUI
86.05 GEEK