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 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:
WalletView.swift
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
transition
so that whenever a card is added to WalletView
it’s presented moving upwards while fading in.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:
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. onChanged
is called every time DragGesture
updates its translation whereas onEnded
is 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:
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
})
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
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.
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.
Final version from the repo
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:
On the other hand, here are some cons:
UICollectionView
or TextView
.#swift #ios #SwiftUI