(gentle music)
- [Narrator] Stanford University
- [Instructor] CS193P Spring of 2020.
This is lecture six
and today I'm gonna
cover two brief topics,
either brief because they're minor topic
like property observer,
or I'm only gonna get into
so much detail in them
like @State.
And then we're gonna dive
into our main topic today,
which is animation, which
is what our demo is gonna be
all about.
So let's get started.
Property observers, only
do a quick word on this.
The main thing I want you to understand
about property observers,
it's just a way to watch a var
and see when it changes
and then take some action.
Now, a lot of times people get confused
between property observers
and computed vars.
They're completely different things,
nothing to do with each other.
Here, I have this var isFaceUp
which has a property observer on it.
That var is stored in memory,
it's not a computed var in any way.
In the property observer there
willSet I'm just watching
for when isFaceUp is gonna be set
and if the new value
it's gonna get is set,
so in other words thing is gonna face-up,
then for example, in this case,
like maybe in our Memorize game,
I'm gonna start using bonus
time, start the little pie
behind our emoji started ticking.
And if the isFaceUp is just
about to be set to false,
I'm gonna stop using the bonus time.
So the newValue in here that purple thing
that's a special var that only appears
in the will sets here.
And of course, there's a didSet
I'll use that in the demo
just to be different,
inside that one oldValue is
what the value used to be
before it changed.
Alright, for this next topic,
I want to explain something
that not obvious about what's going on
and I haven't really
talked about this before,
but your View like CardView,
and your EmojiGameView,
they're all read-only.
In other words, if you
look at the top View
and all the SwiftUI on your
device, there's a let there.
And you might think that's impossible.
How can we not be changing the Views
views are changing all the time.
But we are not, the read-only,
and that means that
having a var that stored
in our View is kinda useless.
We can't set its value anyway
the only exception is vars that are set
when you create
the View.
So like in CardView we have isFaceUp,
it's set by people who create it,
so that kind of makes
sense for that to be a var.
But anything else that's not set that way,
it might as well be a let.
Or if it's a computed var definitely
just wants to be a read-only computed var,
which is the only kind of
computed vars we've had so far.
Why?
Why are they all read-only?
Well, functional programming,
one of the awesome things about it,
is that it's very clear about mutability,
when can this be changed?
And there's a huge premium on good designs
to having things be immutable.
When things are immutable,
nobody's changing it behind the scenes
or doing something that is
messing up the provability
that your code actually works.
And also, when things
are mutable or immutable,
when they change, you know they change,
and you can do something about it.
That's how we can use
these property observers on
value types so effectively.
In the case of SwiftUI,
it wants to be knowing
when things are changing,
and when they're changing, It
wants to do the minimum amount
of work to replace the View
hierarchy with the right Views.
And in fact, that is what's happening,
if isFaceUp
changes,
it makes a new CardView that has that.
That is how this is all working.
This is actually a wonderful thing for you
you might not think so.
The fact you can't have
read-write vars is really good
because remember that your Views
are supposed to be stateless.
They're supposed to always be reflecting
what the state of the Model is.
That's where the state
is, is in the Model.
So you shouldn't need any
state of their own, right?
No need for them to be not read-only.
Well, mostly,
and not 100% true.
So let's talk about when
Views do need some state.
And there are a few rare times,
when it does need state,
it's always temporary.
Views never have any long-term storage
that's always gonna be in your Model.
This is just temporary situations
where you might need a little storage.
What are some examples of
temporary storage you might need?
Well, you might enter an "editing mode",
where you're gonna collect
some data from the user
and gather it up and then call
an Intent to change the Model
with that data you've
collected or whatever.
So just temporarily, you're collecting it.
You might have been
displaying another View,
some other View that
is collecting some data
like a TextField, or a Picker,
like we saw on that post in Piazza
or have a View that's an Alert,
you're giving the user an Alert,
you just wanna keep track
of the fact that it's up
until the user hits cancel,
then you're like, "argh it's gone."
And so you're just having a Bool,
whether that thing is up or not
that kinda really tiny
temporary data, or an animation.
Animation only reflects
things in the past.
So if you wanna have an
animation that's kinda
going along with the present,
you have to have a little
var, which is the future.
So you can set that var to the future
and animation will start
animating towards the future.
Hopefully, you're setting the future to be
the same thing that's gonna eventually be
in your permanent state.
But you're only using that var
during the time the animation is happening
and we know that animations are short,
they're little temporary things.
So animations, another case
of these temporary storage,
and we'll see doing that in the demo.
We can, in fact, create
storage in our read-only Views
and we do it by marking a var
that stores the information
we want with @State.
So @State you know,
like @Published, @ObservedObject,
it's a property wrapper.
Note that I've marked this state private.
That's because state is private to you
It's just temporary storage
you're using in your View,
nobody else is gonna be looking at it.
So by marking it private,
just kind of remind yourself,
yeah, yeah this is a little private state
temporary state right there.
And of course, the type of
this thing can be any type,
you're just declaring
a var basically here,
that's gonna be read-write.
But it can be any type that you want.
An important thing to
understand about this State var,
something temporary
here is if I change it,
my View might get redrawn.
If my View depends on something temporary,
and it changes in a way that
makes my body draw differently
it'll get redrawn.
It's the same as ObservedObject, right?
We do observed object on our ViewModel.
If the ViewModel changes in a way
that would make our View look different.
It's gonna get redrawn
same thing with state
which is kind of cool
feature of it actually.
The space for this @State var
is gonna be allocated in the heap.
It has to, it can't make
the space in your View,
your View is read-only
so it's basically making a pointer
so your View has a pointer in it
and it points into the heap.
And when your View gets rebuilt,
cause your View's getting
rebuilt all the time,
when isFaceUp changes in your CardView,
you gotta make a new CardView.
And when that happens,
this pointer gets moved to the new version
so you're still pointing at
that same thing in the heap.
So if isFaceUp changes,
your CardView is not gonna
lose its temporary storage.
Basically your temporary
storage will stay around even
as other things that are causing your View
to even be completely rebuilt
you get to keep your @State.
We're gonna learn what
these @ sign things are,
you know, @Published,
@ObservedObject, @State
they're called property wrappers.
You can read about it, in your reading.
I think it's assigned for next week.
I'm gonna explain it
eventually in lecture.
Not quite yet, though, almost there.
For now, just know that you
can mark a var with @State
and now you can write to it.
But use this sparingly and certainly never
put anything that's
not temporary in there.
All right, main topic
of the day is animation.
What is animation?
It's essentially a kind of
a smoothed out portrayal
in your UI, over a period of time,
which is configurable by the way,
of a change that has
happened in your UI already.
When the user looked at an animation,
they're seeing something
that has already changed
in the Model, okay,
or it's already changed somewhere
at the very least has
changed in an @State.
Something that's already happened,
it can't do it any other way.
Otherwise, all your variables
in your Model would have
to be constantly changing
as the animation went on
that's just untenable architecture, right?
So your Model changes,
your View changes and that change gets
animated in front of the user's eyes.
So it's showing you
their very recent past.
The point of animations is
to make the user experience
less herky-jerky, obviously,
but also to draw attention
to things change.
We wanna use the user
peripheral vision to notice
Oh, that number over there change,
oh, that View moved over here, whatever.
That is what we use animation for
and that's why we have animation.
It also just makes more pleasant,
less stressful experience
to have something
jumping on screen at you all the time.
So in SwiftUI, what can get animated?
Well, I'm gonna talk about
what can get animated
but first, I wanna make it clear
that animation only works for Views
that are in a container
that is already on screen.
So if they're in a container
that's already on screen
and a change happens to them,
what kind of change can be animated?
Well, the appearance and
disappearance of Views again
only if they're in a container
this already on screen
also changes to the arguments
of animatable ViewModifiers,
like opacity and
rotation, things like this
that boost ViewModifiers know
how to animate themselves.
And we're gonna create
one of our own today,
those changes to those
arguments can be animated.
And also the changes to arguments
are the creation of Shapes.
If you create a Shape,
with certain arguments
configured in some way,
and then you change those,
then it can be animated
to go to a new state.
So how do we kick off an animation?
By the way, that's it for changes,
there's nothing else gonna be changed
just ViewModifiers, Shapes,
and the appearance and
disappearance of Views,
wanna make that clear.
Alright, so how do we
make an animation "go"?
Well, there's two ways to do it.
One is an implicit animation
where we're going to
just mark a View and say,
whenever one of the modifiers
on this View changes,
we're going to animate that change.
So that's implicit animation
is going to automatically
every time that modifiers
on that View change,
it's going to animate it.
The second one is explicitly,
where we are going to call some
code that is going to result
in some changes to
ViewModifiers, or Shapes,
or Views are gonna be coming and going.
And we're gonna wrap that code
by calling this function withAnimation.
And inside the curly braces there,
we're gonna put the code
and that's gonna cause all
the things that would change
all those ViewModifier
arguments that change,
all the Views come and go.
They're all gonna happen together
in one concurrent animation.
So we're explicitly animating right there,
we're saying, animate
this and then we usually
do something like call an
Intent in our ViewModel.
And we know that's gonna
make a lot of changes
we want the result of that
to all be animated together.
So that's the explicit animation.
So let's talk about the
implicit animation first.
Some people call this automatic animation
essentially just declares or tags a View
so that all ViewModifier arguments
are always animated
for this View.
You get to specify how
these things happen,
like how long it takes for them to happen.
And also a curve, which I'll talk about,
you do get to control it a little bit.
You do it by calling this
function animation on any View.
So here I've called it
on opacity or rotation,
modified Text of a ghost.
I've said animation,
provided the argument there,
which is the how to do the animation,
how long and all that stuff.
And now forevermore, whenever
the scary var changes,
and thus the opacity changes,
or the upsideDown var changes there
rotation effect that's
gonna change the rotation.
Anytime those changes happen,
it's going to animate it,
because this View this
combination, things here in green,
is now implicitly animated.
That's always gonna be the
case that that happens.
Warning here, little red
word warning so pay attention
.animation on a container View
does not work how you
would generally think
you might imagine, it's just gonna animate
the whole container like one
big, somehow blob of change.
But it doesn't all of doing
animation on a container does,
it just applies that
animation to all the things
inside the container.
In other words, .animation
is not like .padding, right.
.padding puts padding
around the whole ZStack
or the whole VStack or whatever.
It's more like .font or if
you say .font on the ZStack,
all the texts in the
ZStack, get that font, okay.
Animation is more like .font
if you say animation on ZStack,
all the things inside are
gonna get that, and that's
rarely what you actually want surprising.
And so I'm just giving you a warning
that we don't usually put
.animation on container Views,
they're usually put on,
if not on leaf Views,
at least on very small,
self-contained Views.
So that animation argument
that you're passing there you
saw in the previous slide,
it was ease in and ease out, it's called,
that lets you control the
animation like its duration.
How long is this gonna take ?
Two seconds or whatever, a delay,
wait a half a second before
you start this animation.
It can repeat a certain number of times,
or even repeat forever,
do the animation to make this change,
and then just keep doing
the animation over and over.
The change, of course, has
already been made in the past,
but just keep doing the animation
sometimes you wanna do that.
And also you can set
the animation's curve.
So what is the animation curve?
This is actually determined by
what kind of animation you choose.
And the animation curve controls the rate
at which the animation plays out
in linear animation, for example,
the rate is constant
throughout the whole time
the whole animation from, you
know, one opacity to another,
or from one rotation or another
happens, linear constant rate.
Then this ease in, ease out
that I mentioned on the
previous one, it's different.
It starts out slow, slowly
changing the opacity
of the rotation and then speeds up,
and then as it's almost
there, it slows back down.
And why do you want something like that?
Well, if you're, for example,
moving a card across screen
from one place to another,
it's kind of abrupt, if it
just picks up moves over.
It's much nicer for it to
start moving slow and then
move over and then slow down
as it's arriving, kind of
like an airplane, right.
Starts on the runway stop
and it's slowly taking off
and then go in the air and
it goes to 500 miles an hour
then it gets to the destination airport,
and then slows down to
100, 150 miles an hour,
and then it lands.
So it's that kind of curve of the rate
is ease in and ease out.
You almost always want things to moving
to at least do ease in, ease out
if not the next one, which is spring.
So spring is the thing
gets to its destination
and then it kind of bounces a little bit
has a little soft landing there
overshoots the mark a
little bit and springs back
a little bit like it's
got a spring connecting it
to the destination.
And these kinds of
animation curves make the UI
a little more comfortable
for users less, you know,
jarring, less abrupt.
So let's talk about implicit animation
when I think I was just
describing an explicit animation,
which is what I'm gonna describe next.
So these automatic, implicit animations,
they're not really even the
primary way we do animation.
It probably depends on
the app a little bit
but mostly, we don't use them that much
because when you have a
kind of a group of Views,
they wanna work together to animate.
That's why putting .animation
on a container like a ZStack,
not just, we just don't do it very much.
Because if a ZStack has a whole
bunch of Views inside of it
maybe Views inside those
and this big construction,
those Views want to animate all together,
all the same duration, all the same curve,
they wanna be in sync.
Explicit animation is where we
cause an animation to happen
all with the same duration,
all with the same curve
for a whole bunch of Views.
And the way we do it is we just take this
withAnimation function, call it
with this argument, which is a closure,
Inside the closure, we're
just gonna do something
like calling Intent on our ViewModel,
that's a classic thing we do there.
And Model can change like crazy,
our View might change like crazy,
and all those crazy changes
are all gonna happen together
with this Animation that
we passed to withAnimation.
Here I'm doing two
second linear Animation.
So all of the changes are
gonna happen at a linear rate
over the course of two seconds.
Now, I'm calling a
function here withAnimation
one has two arguments, an
animation, and then a closure,
calling functions like this,
that's more of an imperative
approach to programming.
We know the SwiftUI is mostly declarative,
we just saying the state of everything
and the implicit
animations are declarative.
You're just declaring that
when these modifiers on this thing change,
you're gonna animate it.
But this is actually imperative.
Here we are telling somebody
do this and animate it
so it's imperative.
So there's not a lot of
places in SwiftUI code,
where you're doing imperative programming.
Remember, all this code
has to be in your View,
cause ViewModels don't see the View
so they can't be doing this.
This is all in your View,
it's in the spot in your SwiftUI code,
where you do imperative code
which is like onTapGesture,
Views are tapped, boom,
you're gonna do something imperative,
you're gonna say choose the
Intent, choose card or whatever.
And those are the places
on things happening
like on tap, just other gestures
we're gonna learn about next week.
That's when we're gonna
call withAnimation.
Now, explicitly animations, as I said,
are usually wrapped around
things like Intents,
a ViewModel Intent.
But you might also wrap it
around something that happens
only in the UI.
For example, that editing
mode I was talking about,
let's say you're going into editing mode
and little icons will appear
to delete things or whatever
those things want to
kind of animate smoothly
and appear on screen.
And so you might be doing
withAnimation when the user
hits the button to to say
enter edit mode or whatever.
Another imperative place
is the action of a Button.
And I'll show Buttons
today in the demo as well
and you'll see the action
of a Button is another place
we do imperative code
and where We would likely
do something like withAnimation.
This is red again, the second red thing.
First one was the red about doing
.animation on container Views.
This one is to remind you
that explicit animations
do not override implicit animations.
Implicit animations are
assumed to be on Views
they are self-contained,
they work independently,
whatever animation makes sense for them
should always make sense
to them no matter what.
So if there's a View,
and it has an implicit
animation attached to it,
then it's gonna be doing
that implicit animation
whenever its things change.
Even if there's an explicit animation
going on at the same time.
It's gonna have no effect on them.
Implicitly animations always win.
Now transitions specify
how to animate the arrival
and departure of Views.
Remember, those Views
have to be in containers
that are already on screen.
But whenever you arrives, you
want to be able to animate it
fade in or flies in from
outer space or something
you want some sort of animation
for that View arriving.
Now a transition
is only
a pair of ViewModifiers, that's all it is
one of the ViewModifiers is modifying
the View for what is supposed
to look like when it's there
and the other one is
modifying the View for what
it's supposed to look
like when it's not there.
In other words, it hasn't
arrived or it just left,
the one that's on there's
probably gonna have,
let's say it's a fade, it'll
start with the one, with
the ViewModifier, where it's
on there of opacity one,
and then the ViewModifier for
the other one is opacity zero.
And system is gonna animate
between those two ViewModifiers
to make that thing appear or disappear.
So a transition is really just a version
of changes to the
arguments of ViewModifier
cause a transition is just
this pair of modifiers.
the two modifiers can
have different arguments
and so that's it, that's all it is.
So transitioning is not really
a different kind of animation,
it's just a way of specifying
the two ViewModifiers
for when Views appear and disappear.
So how do we specify
what transition we want
the system to use when it's
animating the appearance
or disappearance of a given View?
Remember a transition
just a pair of modifiers,
so we're essentially just
gonna attach the two modifiers
we want modifier for when it's
on screen and the modifier
when it's not to a View.
And we attach this
transition using .transition,
very simple ViewModifier.
I'm gonna show you by example here,
and I'm gonna use two
built-in transitions.
One is called .scale,
that's the blue one there,
and the other is called .identity,
which is the purple one.
So a .scale transition,
its two ViewModifiers are frame modifiers.
And the off-screen one
has a frame of zero,
and the on-screen one has
a frame of like full size
whenever its normal size is.
So scale transition zooms the
View in and out from tiny,
zero size up to full size
as it goes out or in.
And the identity transition
is an interesting one,
it's ViewModifier does nothing.
So since nothing is changing
between when it's gone,
and when it's there, it
just instantly appears
and instantly disappears.
In other words, there's
no animation because
there's no differences
between the two ViewModifiers
are exactly the same
and they don't actually modify the View
so there's no animation to happen.
So bloop, it appears and disappears.
And it is occasionally the case
that when you're doing an animation,
and you have Views coming and going,
possibly you might want a View
to just bloop appear and bloop disappear
and not be animated.
That's not the default
so the default transition
is called .opacity,
which is a fade, fades in and out, right.
It's just taking the opacity
which we learned about last time,
making go from one to
zero when it goes out
and from zero to one when it goes in.
So we moderated that in that case
is the opacity ViewModifier.
Let's look at this ZStack right here,
it's basically our card kind of
simplified here.
There's only so much room on the slides,
and you can see that from my
face-up the front of my card,
I got my RoundedRectangles,
let's say don't have
them all specified there,
but you know what they are.
And I didn't put a .transition modifier
on the RoundedRectangles.
So they're gonna get
the default transition,
which is opacity. So the
RoundedRectangles on the front
are gonna fade in and
out as they come and go.
But I did put a transition on the Text
the little ghosts there.
That is going to zoom up out of nowhere
and then zoom back down to
zero size when it goes away.
That's what .scale means.
And then the RoundedRectangle on the back
I put the transition identity that means
when the back appears
it's gonna bloop appear
it's not gonna fade in or
grow out, it's bloop appear.
Now it'll bloop appear and meanwhile,
the front will be fading out
and zooming shrinking down
at the same time.
So it's not gonna be
a very nice animation.
This is kind of a kitchen
sink animation here,
you would never do it actually like this
but I just want you to understand
what all these transitions do.
By the way, you see here how isFaceUp,
which is just a conditional
inside of a ViewBuilder, right?
This is the ZStack,
we know the content
ZStack is a ViewBuilder.
And inside ViewBuilder,
it's a list of Views
but we can use if-thens to
include or not include some,
well, of course, as we include them,
they appear on screen as
we don't include them,
they disappear on screen.
And these kinds of if-thens
inside of ViewBuilders
is probably the number one way
the Views are coming and going.
So when you have
animations of the contents
of some ViewBuilder thing like the ZStack,
and it's got conditionals in there,
you wanna think about transitions,
because those things are
gonna be coming and going
and if you don't think about it,
you're gonna get fade-in and fade-out.
That's the default, but
you might want something
that looks a little better than that.
Another way that Views
come and go is for example,
in a ForEach.
So ForEach takes an Array
of identifiable things,
and makes Views for them.
Well, if that Array changes,
like new things got added to it,
or some of the identifiers got pulled out,
it's going to either add new
Views or take some of the Views
it made in the past out of there.
And those Views are gonna
be coming and going.
You're gonna see this
definitely in your homework
number three, you're gonna have your cards
somehow displayed on
screen through some ForEach
maybe inside grade or something.
And it's a different game to the memorize
the cards kind of come and
go in the game you're doing.
Whereas memorize the cards are
just really always on screen,
even when they're match
they're just hidden.
There's kind of a space for them there,
but wouldn't have to be that way.
So those are probably number
one and number two ways that
Views come and go: conditional
things inside ViewBuilders
and things like ForEach that
are conditionally essentially
building Views for you.
All right, back to the screen code.
So let's just walk through,
if we change isFaceUp from
one thing to the other,
what would happen?
So if we change isFaceUp to false,
in other words we wanna show the back,
the back would instantly appear
because we have said its
transition to be identity,
which means don't do any modifications.
So there's no animation
so it just bloop instantly appears.
The Text would shrink down to nothing,
because it's using a scale transition,
and the front RoundedRectangles
would fade out
because they have no transition
so they're getting the default opacity.
And if isFaceUp changed
to true, by the way,
the isFaceUp has to be changing here,
while an explicit
animation is in progress.
If we're not actually animating,
then these transitions
mean nothing, right.
These transitions don't do
the animation themselves
they just specify what
ViewModifiers to use,
when an animation is happening.
So if isFaceUp changes from false to true,
now the back would disappear
instantly just gone
because again, its transition
is the identity ViewModifier.
So there's no difference between
the identity ViewModifier
when it's there or not there. So it just,
it's still going off screen
though, so it disappears.
The Text would grow in from nothing
would start at zero size and
grow up to its normal size.
And the front
RoundedRectangles since again,
no transition specified,
they would fade in.
This would all be
happening simultaneously.
Now one thing to consider
about transition,
unlike animation, transition
does not get redistributed
to a container's content Views.
Remember, I told you
that we don't usually put
implicit animations on container Views,
because it just ends up
giving that animation
to each of the things inside.
Well, transition does not do that.
Transition, when you put
it on a ZStack or a VStack
it is now talking about the transition
when the entire ZStack comes on screen
or goes off screen.
So it does not distribute it
off into its things inside.
It's actually talking about the animation
of the ZStack itself
when it comes and goes.
Now Group and ForEach,
those are container Views
but since they don't put
anything on screen anywhere,
they're just essentially
grouping or creating Views
from a list of Identifiables,
they do distribute .transition
to their content View.
So if you say group and a bunch of things,
and you say .transition,
it's going to be talking about
each one of those things,
getting that transition.
This .transition function,
remember is doing nothing
more than the specifying
what the two ViewModifiers are.
Think of the word
transition there as a noun.
This is the transitions to use.
It's not a verb, like
this View transitions now,
transitions only happen
when an explicit animation
is going on.
That is the only time that
transition animations happen.
Transitions do not really
work with implicit animations.
If you try to do implicit animations,
on Views with transitions,
it's gonna get a little confused
and you can understand why this
is remember that, you know,
explicit animations are for animations
that are coordinating a
lot of different Views
that when Views are coming and going.
Implicit animations
are for self-contained,
independent working Views, that
their animations make sense.
That doesn't sound to me
like Views coming and going.
Transitions are not intended to be used
with implicit animations,
they're to be used with
explicit animations.
And that's the only time a
transition animation will happen
is when you are animating it.
Transitions are just saying
what ViewModifiers to use,
you still have to animate.
By the way, if you do
an implicit animation on
something that has a transition
and the View comes or goes,
it's gonna do some sort of animation,
but it's probably not
gonna be what you expect.
The transition API,
you know, like creating
an actual transition
is a little bit interesting.
And it's type-erased.
That means that the
actual type of transition,
which the real type of a
transition is going to have
don't cares is in there that
are the two ViewModifiers
that you're using and all that.
They can be quite complicated.
And we're trying to pass them
as arguments to .transition
we don't want that so
we want to be simple.
The argument to the .transition function
is something called an AnyTransition.
And this AnyTransition is
a type-erased transition.
Imagine, kind of like this,
that AnyTransition is just a
struct that has an initializer
that takes a don't care, kind of,
which is a transition we had
modifiers and all that stuff.
And it just knows how to do
the transition thing with it.
And what you get back
is just an AnyTransition
with no don't cares or
any of that business.
So erasing types like this,
so we simplify and lose.
We don't really lose it,
but we can't see all the details
like what kind of
ViewModifiers it's using,
we do that in Swift on a number of cases,
you can even do it with a View,
there is a View called AnyView,
and its initializer will
take any kind of View
no matter how complicated
and return you AnyView
and erase all that information,
you'll now you'll have a
View it's called an AnyView,
it's of type AnyView,
you know nothing about what's inside
or what's modified or none of that.
If you didn't understand
what I'm saying there
about type-erased, don't
worry about it too much
we're gonna see it again
later in the quarter.
But the important thing to realize is
that AnyTransition is just a struct.
It has some static vars on it
for the built in transitions
like opacity, which animates the opacity,
scale which animates the frame modifier
to make the frame go
down to zero and back up.
There's a really important
transition for your homework
called offset CGSize,
which causes a View to
move across the screen
by some offset when it comes and goes.
Alright, and in your
homework you're required
to make your cards be dealt
fly off from off the
screen to on the screen
so you're gonna be wanting
to use this offset transition
for those Views.
And you can of course
create your own transition
by just specifying the two ViewModifiers.
The modifier to use when
things are on screen
and to use when it's not, right, so
identity is when it's on and
active there is when it's not.
You can also override
the animation that's used
for a transition if you
always want a transition
to be really fast, for
example, or really slow.
You can use this .animation
that you attach to the transition, okay,
you know, attach it like a .animation.
Don't get confused by this little thing
this is not implicit animation,
implicit animation transitions,
they don't go together.
This is just a way to
override the duration
and curve and all that
of a transition animation
so that always does it this way.
Transitions can be thorny,
and a little bit frustrating
sometimes when you're first using them,
because of this restriction
that the container
that has the View has
to already be on screen.
So for example, in your
homework assignment three,
you are required to have the
cards deal out in animated
fashion onto the screen. It
can't be like Memorize where
Memorize launches and oh,
there's the cards already on-screen.
No, has to launch just momentarily blank
and then the cards fly
and automatic, you know,
kind of a deal animation
to come on the screen.
How do you get out of this conundrum of
a View that contains the cards
has to come on screen first
and then once it's on-screen,
then you can do something that
causes the cards to happen.
There's a great function in
View for helping with this
is called onAppear, very simple.
It's kind of like onTapGesture, right,
onTapGesture, when a tap
happens, it calls this closure,
executes some code, this
is kind of the same,
when a View appears on screen,
then it calls this code.
So we're gonna use this
nice little feature
on our container View.
On our container View,
we're gonna add onAppear
and in that code, we're going to change
something in our Model,
probably, that makes it
so those cards which weren't there,
when my container View first appeared,
something about them changed
now they need to be there.
Now they'll get animated coming on screen.
We're using the fact that
we know you know onAppear
that our container View
is finally on screen
to go ahead and do some Intent
to the Model that says, okay,
you can deal the Cards now.
So the Model has to change
in some way where the Cards
now are suddenly being
thrown out into the View,
cause the View is just
reflecting what's in the Model.
So if the Model says that cards are there,
when the app launches,
they're gonna be there.
So the Cards have to not be
there when the app launches,
and then after the onAppear
happens on the container,
then something happens
where the Cards are there.
So you've gotten the message,
I'm sure in all this,
that the actual animations
are done by ViewModifiers
and Shapes.
They're the things that actually animate.
How do they participate in
this whole animation system?
ViewModifiers and Shapes,
how do they get animated?
Well, essentially,
the animation system
divides up the duration
of the animation into little tiny pieces,
depending on the curve.
And then it just asks all
the shapes and ViewModifiers
that are Animatable,
here, draw this piece, draw
this, draw this piece, right,
it's just drawing them
over and over and over
and then piecing it together
into like a little movie,
which is the animation.
That's it.
That's how this thing works
it's incredibly elegant and
simple, to make it work.
The communication between
the animation system
and ViewModifiers and Shapes
is just one single var,
this var animatableData.
This animatableData is in
the Animatable protocol,
it's the only var in there.
And all you have to do is implement this.
And if you're a Shape or a ViewModifier,
you can participate in this
little piecewise animation.
The type of animatableData
is a don't care.
Actually, it's a care a little bit
because that type has to
implement the protocol
VectorArithmetic, which makes sense
because we're gonna be
taking this animatableData,
whatever it is maybe the
rotation of the angles
of the Pie thing or, you
know, something like that
we're gonna be cutting
up into little pieces
so we have to be able
to do some math on it
to cut it up into pieces
using that nice curve.
So type is almost always a Float,
either a Float or a Double
or a CGFloat lots of the time
because we're doing a lot
of drawing going on here.
But there's another struct that
implements VectorArithmetic
called AnimatablePair that's really cool.
It combines two
VectorArithmetic things into one
Animatable VectorArithmetic thing.
And of course, you can
have AnimatablePairs
of AnimatablePairs.
So you can have any number
of Animatable things.
Also, if you had your own
complicated structure,
that encapsulated animation data,
you could make it
implement VectorArithmetic,
that's just a protocol.
And it could be directly animated.
animatableData just has to be something
that can be sliced up into little pieces.
So it has to implement VectorArithmetic.
This var, as I said, is
communicating both ways
between the animation system
and the Shapes and ViewModifiers.
So the setting of this var,
that's the animation system saying,
here's a little piece, draw it,
here's a little piece, draw it,
So it's just basically
saying where in the curve,
that your a little piece
of animatable data is
during this animation.
Now the getting of the
var also matters for the
animation system to know the
start and end of animation.
Now, this animatableData by the way,
is usually a computed var
not because it has to be
it definitely doesn't have to be.
It's just that in our
code like in our Pie,
or you know, our Cardify ViewModifier,
which we're gonna modify so
that the card flips over,
it's rotated.
So in there, I really
want to call the rotation
of my flipping card "rotation",
I don't wanna call it animatableData,
it's not very good name for a var.
So a lot of times we're
gonna create a computed var
just to essentially
rename some internal var
so that the animation system can see it.
So let's get right to that demo.
We're gonna be doing so
many animation things here.
We are gonna do implicit animation,
but obviously doing explicit animation,
we're gonna do transitions
we're gonna make a
ViewModifier be modified,
we're gonna make a Shape be
modified, all that stuff,
and it's a big, long demo.
So let's dive into it right now.
Let's start our animation demo here
with some implicit animation.
Again, this is an animation
where we're going to
have some very self-contained animation
that always gonna apply no matter what.
And it's not really coordinated
with a lot of other activity
going on in the animation system.
What are we gonna do for
this is to have our emojis
be really excited and celebrate
when they get a match
by doing a somersault.
So a somersault is
essentially rotating the
emoji around.
And rotation is really easy to do
in SwiftUI, there is a ViewModifier
for it called rotation effects.
So I'm gonna go down here
to my Text, here's my emoji,
and I'm just gonna add a rotation effect
on it.
and it takes an Angle so
I'm gonna do Angle degrees.
We learned about that when we did our Pie.
I'm gonna say it if the Card isMatched,
then let's start by just
having it go upside down,
which is a 180 degree rotation,
we'll eventually have
to do a full somersault
all the way around
and run.
Click on a card, we're
looking for a match.
Oh, there it is, and it went upside down
but it didn't do any
kind of animation there.
It's just as soon as it matched bloop,
they went upside down.
So how do we animate this?
Well, assuming that I always
wanted to do a somersault
when a card matches,
all I need to do is an
implicit animation here
and say .animation.
And I just specify the
parameters of the animation.
So I'm gonna have this
animation be a linear one
and we'll have it be a duration
of, let's say, one second.
This Animation object right
here we talked about in lecture,
it's the kind of thing you
definitely would want to
look over in the developer
documentation right here,
we can see all the
different kinds of animation
easeIn, easeInOut, linear
and all these other things.
I said we could do: delay animations,
obviously create them
with certain durations.
Here's all the springs
lots and lots to learn
there about animations.
And we'll be doing quite
a few of them today.
So let's try this and
see if this just works.
Will this now make that
happen over one second,
and here's one and oh, wow,
well one of them animated
here, not both of them.
Let's try another one.
But again, it is doing that
animation over a second
to do that, but it's not
doing this other one.
Now, the fact that it's only
doing one of these is really,
there's a good reason for that
and we're gonna cover
that a little bit later.
For now let's just make
sure the one that is working
is doing what we want.
Let's have it go to let's say, 360
so all the way around.
We're trying to find a
matching pair there it is,
whoo, it's somersault all the way around.
How about these guys?
Whoo and maybe once it's
matched, we are so excited
we just want to keep going or
somersaults and somersaults.
Let's take this animation
that we have here,
this linear animation, and
let's have it repeatForever.
So it's doing the same animation
it's just now it's gonna keep going.
Now the thing is, notice
that it does do it
but then it kinda reverses itself
and then it does it again
and reverses itself.
This is really not what we want
we want to go around and around.
Luckily, repeatForever has
a an argument to it here,
which is autoreverses and
we'll say autoreverses false,
we don't want it to reverse
we just want to keep repeating
that animation over and over.
And that's because this animation returns
to where it started so it makes sense
to keep doing it over and over.
The only thing about this animation
that want to be a little
careful here about is
eventually we're gonna add a new game
that you did in your homework
and when we do that
these Views get reused.
And we don't want this
animation to be starting off
in a new game.
So essentially, whenever
the Card is not matched,
we do not want to be
doing this repeat forever.
So anytime you do a
repeat forever animation,
you wanna be careful to turn it off
when it doesn't apply anymore.
You can do that right here
just by saying card is matched.
We'll do this nice repeating forever.
But otherwise, we're gonna go back doing
whatever the default animation is.
That's kind of like don't
do this animation anymore.
Make sure that didn't break anything.
We go we got a match.
Whoo, it's working
it's working.
Now we've done this implicit animation.
Let's move on to doing
an explicit animation.
Before I do the explicit animation,
I'm going to implement
some of what you did
in your homework specifically,
I'm going to implement shuffling Cards
and also I'm gonna implement new game.
So shuffling Cards,
that's an easy one going
over to our Model over here
and when we create our Cards,
I'm just gonna say cards.shuffle.
Obviously, if you haven't
done homework one,
hopefully you're not watching this video.
But next I'm gonna do the new game thing
where we have new games
and that's in homework two.
Hopefully you've done homework two,
it was due before this lecture.
But if you haven't finished homework two,
now's the time to pause this video,
go submit your assignment
two and then come back
and resume watching this.
So cards.shuffle, that should
fix that nice one liner.
Whoo, it's shuffled now these two things
are right next to each other.
All right, well, we can still match them.
Alright, how about new game,
new game requires us to have
an Intent in our ViewModel,
just like we have the
Intent to choose a Card,
we're gonna need an Intent
to create a new game.
So I'm gonna call this resetGame
and I'm gonna reset the game
just by creating a new Model
I'm gonna say Model =
EmojiMemoryGame.createMemoryGame
to create new memory game,
and that's all I need to do.
This is clearly gonna change the Model
that's going to cause
this ObservableObject
because this Model is @Published,
all this is gonna happen and
our whole View is gonna redraw
because of that change.
So now I need a button
somewhere in my UI, right?
In my UI, I don't really
have a new game button,
I can't cause a new game to appear.
So I'm gonna add a new button
at the bottom very simple way.
I'm just gonna have my grid of Cards here
in a VStack
with
a Button.
We didn't talk about Button
and I didn't expect you in
your homework number two,
to necessarily do a Button,
you could easily have just done a Text
with onTapGesture there,
that would have been fine.
But while we're here, let's go ahead
and learn a little bit about Button.
Button is very simple
it just has an action, which
is some closure to execute
when the Button gets pressed.
And then it has this label,
which is essentially any View
you want to be the label.
So I'm gonna have the label here be a Text
that says "New Game".
And in terms of what I'm
gonna do in this action,
let's double click there.
I'm gonna do that Intent
that I just talked about.
So self.viewModel.resetgame.
Usually, when something happens in the UI,
like we tap on a Card,
or a Button is clicked,
we're gonna be doing
either Intents
or we're gonna be do some
doing something that totally
only affects the UI, just
adjust the UI in some way
it doesn't really affect
what's in our Model.
Let's see if our new game Button works.
We click here run, well, there
is our new game down there
and we click.
Let's see if it's doing anything.
We've got this there and new game.
Whoo, yeah, it did reshuffle
them put new cards out there.
All right let's see if some cards match
then it puts them back.
So our new game Button is working.
By the way, what is the difference
between using Button here
and Text with onTapGesture?
Well Button is powerful, it
knows that it's a Button.
So as it appears on different platforms,
maybe Apple TV or Apple watch or whatever,
it's gonna draw this Button in a way
that makes sense on that platform.
Whereas we do Text with onTapGesture,
it's always gonna just
look like a piece of text
that we tap on.
So we would always want
to use a Button for reals
when we are doing a Button.
We don't want to do a Text
onTapGesture solution.
One other thing I want to
mention, while we're here
is this red "New Game" String.
These Strings are red
and I'm glad they're red.
Red usually means I look
I'll watch out and indeed
you do want to watch out
when you have red Strings,
if you have red Strings
that are gonna appear
in front of the user, you need
to do a little bit of work,
which we're not gonna cover now
to make these internationalizable.
So that you can have this say,
new game in French, or Chinese or Arabic
or whatever that has to be
something that can be fixed.
And so we're not gonna talk about that,
if you're interested in that stuff,
maybe starting the
documentation by looking
at something called localizedString key.
That's a way to at least get your Strings
starting to be localized.
There's other things that
need to be localized as well
like dates and things like
that, dates appear differently.
And again, we don't have
time to talk about that.
We're talking about animation today.
I just want to give you a
heads up that that is a thing,
where we eventually are
gonna have to be careful
about the Strings we put in here.
All right, new game worked
but as we saw over here,
it did not animate right,
do this and whoo, it
just immediately changed.
There's no animation.
So we would like this
whole thing to be animated,
that turns out to be really easy to do
using an explicit animation,
we're just gonna wrap this reset
game which had a big effect
on our Model and changed all our Cards.
Well, all those changes, we can animate
with one simple line of
code here withAnimation.
And just like when we
did implicit animation,
we're gonna specify the Animation we want.
I'm gonna use easeInOut.
notice I didn't type
the full Animation dot easeInOut,
Swift we can infer that
that's the obvious argument
to withAnimation here.
And then it takes a closure,
which takes no arguments,
return no arguments,
and you can put whatever
code you want in here.
And whatever this does to
our UI, whatever it is,
it's gonna get animated.
Let's see what it looks like to do that.
Whoo, oh my, it actually did a whole bunch
of animation there, nice.
Now if we want to see
exactly what's going on here
because there's some other
stuff going on there too
some fading going on,
we can change the duration
by having an easeInOut
Animation of a duration, let's say,
two or three seconds.
I'll slow that animation way down,
which is something I
always recommend doing
when you're doing animation
is to slow things down
and see what's going on.
So here we go, let's try a new game.
Okay, see the cards fade
out, back to being face-down,
and they move to their new position.
That's what's happening
here that is the animation.
So why is that happening?
That these things are fading out,
that ghost, see he fades
out, back to his card back?
Well, that's because I told you
that transition
is by default
opacity.
And what's happening
there when we switch that
it's transitioning to a new View
and so we're just fading the new one in
and fading the old one out.
We don't really want
that, we want our cards
actually to flip over when they go
from back to front and front to back,
we'll fix that in a few minutes.
But first, let's use this same
feature of explicit animation
to make it so clicking
on the cards is animated,
cause right now it's very abrupt,
if you click on a card and
things instantly appear
so that's not good.
And exact same thing here's
where we're choosing the Cards,
from imperative code that we
can just say withAnimation,
let's go ahead and make
this be a linear Animation
and we'll make it be long as
well just so we can really
see it in action, see what's going on.
Inside we just do whatever we're gonna do,
that's gonna cause a
bunch of changes to happen
and all those changes will be animated.
Okay, so there we go to clicking
and we can see that we're
getting this fade in fade out
when we choose a card.
And notice I clicked on
this card but it animated
every change that happened including
these other cards flipping face-down.
So that when you put
this explicit animation,
it's gonna animate
everything that happened
as a result of doing what you did there.
And that includes something
like let's say there's a match
let's try and find ourselves a match here.
We're not very good at this game are we.
Let's see, oh, there's a match.
By the way, we get our
implicit animation there
and if we click on another card here,
these two cards are gonna disappear.
Let's see how they disappear
they fade out, right?
Cause that's the default transition
we haven't specified any transitions
we're getting fade-in
and fade-out transitions
all over the place here.
What if we wanted those
things disappearing
to be a little cooler animation
like how about we'll have them
shrink down and disappear?
Really easy to do again,
we're gonna go over here
to find that View, where is that View?
It's right here.
This is the View, this cardified ZStack,
and it's only there when it's face-up
or if the Card is not matched.
So once the Card is
matched and is not face-up,
this View goes away.
It transitions out, okay
disappears from the View,
and so we can just say transition here
and to pick, for example,
AnyTransition.scale.
So scale is the one that
uses frame to make things
zoom in, down to nothingness
or out from nothing.
So let's see if we can
find another match here.
Now, it's hard to do because
we have such a slow animation here,
oh, there's a match.
Okay, when we click on something else,
let's see what happens to
animate these cars disappearance.
Whoo, now notice that only
they disappear by scaling down,
but all other animations
including that implicit animation,
they kept going.
And one thing you're gonna
learn about SwiftUI is that
all animations can be
happening all at the same time
and they all just work together.
It's really, that's one of the
nicest things about SwiftUI,
is how it handles all the interactions
of animations happening at the same time.
Now in your homework, you're
gonna have cards coming
and going as well.
But you're not gonna have them scale down,
you're gonna want your card
to fly across the screen.
So they're gonna fly away
instead of the screen,
you know, shrinking down.
And when they're dealt,
when they appear on screen,
they're going to fly in, not gonna
have this nice shrinking effect.
And adding that transition
for these things coming and going,
also makes it so that when
they come back you click
on new game and they come
back, they should zoom up.
This AnyTransition and remember is
that type-erased version of transition.
So let's go look at that in
the developer documentation
really quickly here it is AnyTransition.
So here's that identity,
opacity we talked about,
which is the default, scale,
slide, slide down to the side,
down here is offset,
which is the one that
makes the View fly around,
coming in and out.
So that's probably what
you want for your homework.
Here's modifier where you
get to specify the two
ViewModifiers. This asymmetric,
by the way, allows you so
that you can have cards
come in with one animation
and then disappear with another
animation if you wanted to.
See that AnyTransition says here,
it's a type-erased transition.
That type-erasing is what makes
all this very simple right here.
It's making the return
types of these things
not be transition angle
bracket ViewModifier of
offset... it's just AnyTransition.
And again, we'll talk
more about type-erasure
later in the quarter.
For now you can kind of ignore it
and just think of the
transition as just oh transition
and not worry about all the don't cares
and other stuff that might be involved
with a non type-erased transition.
So the elephant in the
room animation here,
that we don't have, is
cards flipping over.
Really, when we have a card game,
these cards when we click on them,
you don't want them fading in like this
we want them to flip over.
That's what cards do, they flip.
So how are we gonna do
that flip animation?
Well, Swift is gonna help us a lot
because it has an Animatable ViewModifier
called a rotation 3D effect,
which rotates it just like
we rotated on a match.
If you remember, on the
match, we rotated in 2D,
essentially round and round.
We can also rotate in
3D have this View rotate
in 3D around a different axis,
this y-vertical axis instead
of kind of rotating around
the axis of that point straight out at us.
So let's do a nice 3D
rotation of this card
and just see if it works.
It's called rotation3DEffect.
And you specify how much
you want to rotate the Card.
And here, again, we're
gonna do Angle.degrees.
And if the card isFaceUp,
then let's not have it rotated just normal
but if it's face-down,
let's rotate it 180 degrees.
Let's flip it all the way over.
Now this axis, because
this is a 3D rotation,
this axis is saying around which
axis do you want to rotate,
and this is three numbers,
so for example, (0, 0, 1)
would be a 2D rotation,
cause 1, the last one here is the z-axis.
That's the one that points up
out at you from the screen,
and what we want is the y-axis instead,
the y-axis is the vertical axis,
the one that goes from
the top of your screens
straight down to the
bottom of your screen,
we want this rotation to
happen around that axis.
See what that does, it's
not gonna quite be right.
Let's see what we get here.
Whoo, wow.
So that's kind of interesting.
It's rotating,
but the Views appearing
and disappearing there
that's still happening with the fade
and that's really really not what we want.
When we first click the card,
if both the back and the front are visible
one's fading out one's fading
in, and then by the end,
the back is totally faded out
and the front has faded in.
This is close, it's close,
we're on the road to making this work,
but it's not quite right.
So there's two ways I can
think of to make this work.
One, we could have our
own custom transition,
that transition that is transitioning
between the back and the front,
where the back kind of
like we're flipping it up,
the back is showing for
a while until it gets up on its edge
and then it kind of disappears
and then when the front comes on,
it starts out on its edge and
then kind of rotates down.
We could definitely write a
ViewModifier that does that
and then make a transition out of it
or we're using this sort of
half-flip up onto its edge
to have the card come in and come out.
It's slightly more complicated really,
than I think we need to do,
because if we remember how animation works
we know that ViewModifiers
are the main things
that are doing animation.
So why don't we just take
our ViewModifier which draws
this card and make it so that
it's smart about it rotating
itself so that it only shows
the front during the first half
of animation and only shows the
back during the second half.
In other words, we're gonna
have our Cardify over here.
Here's our Cardify
when it's rotating,
we're gonna make it so
you can rotate itself.
And as it's doing it,
it's gonna coordinate what's
face-up with the rotation.
First half of the rotation,
face-up will be their second
half rotation, face-down.
Well, the first thing
we're gonna do is take this
rotation3DEffect over here
and move it into
our
modifier.
So if we put this over here,
and have this ZStack be rotated over here.
Instead of having the card rotate
in a binary sense between 0 and 180,
we want to be able to control
the entire rotation of it
because in the first half, we
only want to show the face-up
and the second half, we
only want to show face-down.
So we're gonna kind of
change our ViewModifier here,
where the main var that is
involved is the rotation.
So I'm just gonna have rotation,
I'm gonna make a be a
Double, which is gonna be
my amount of rotation in
degrees just to be simple.
And if I'm gonna track the
rotation and animate it,
then isFaceUp really just becomes
a function of the rotation
if rotation is less than 90 degrees
of my 180 degree rotation,
then the card is face-up
otherwise it's face-down
so now I've linked rotation
and the face-up face-down.
And then when I have this
rotation3DEffect instead
of having the card isFaceUp
control the rotation,
let's just do the actual rotation.
Whatever the rotation
we set this modifier to,
that's the rotation is
gonna be and it's gonna pick
the right face, front or not
face front, rotation of it,
I still want to be able to have an init
that says isFaceUp.
But now when I do that,
that's just setting my
rotation equal to zero
if it's face-up, and
180 if it's face-down,
so let's say isFaceUp question
mark zero, otherwise 180.
So I've converted my ViewModifier here
to be based on rotation,
rather than face-up
and the face-up is always
tracking the rotation
cause this isFaceUp it's
just looking at the rotation
to see if we rotated enough.
Now how do we make it so that it animates
cause this is not enough
to make it animate
if we didn't run here
and click on these things.
It's doing the flip, but it's
still doing the wrong thing
about the face-up and face-down.
When the face-up and face-down
Views come and go here,
they're still just having opacity.
And that's because this ViewModifier
is not marked as Animatable.
So SwiftUI thinks, well, this ViewModifier
does not know how to animate.
So I'm just gonna do
normal animations in here.
I'll just animate this
normally it's being switched
by the init here to one or the other.
And I'm just taking these
Views that are coming
and going because of isFaceUp
and I'm just transitioning them
with the standard
transition, which is opacity.
So we can turn a ViewModifier
though into an Animatable
modifier by changing the protocol
implements to AnimatableModifier.
So Animatable modifier
really is just ViewModifier
and Animatable, and Animatable this is,
if we looked it up,
this animatableData var,
this communication between
the animation system
and our ViewModifier or our Shape.
So we just need to implement
this animatableData
animatableData inside
here, so let's do that.
Let's put it right down
here, var animatableData.
What does our ViewModifier animate?
It animates the rotation
of our View that's what you'd animate.
So this Double is our rotation.
Now, I could use this word animatableData
here instead of rotation
but that's not very nice code
to have animatableData here.
So let's do that trick I was talking about
on the slides where I'm just gonna have
this be a computed property
and I'm gonna return my rotation
and I'm gonna set my rotation equal
to the new value of this property.
Remember, get and set, that's how we do
computed properties that are
read-write and that's it.
So I essentially just renamed
rotation to be animatableData
because this is the name
that the animation systems
is going to look for.
By the way you can't even
though this really is all
that's going on here,
you can't actually just say this
you need to say that this
is an AnimatableModifier
because this AnimatableModifier protocol
while it is just this and
ViewModifier together,
it also signals to the system,
I wanna participate I'm
gonna be ViewModifier
that wants to participate
in the animation system
so make sure that you say
colon AnimatableModifier.
Alright, let's try this.
Tap that card well, oh my gosh,
that really was amazing the easy.
See in the first half of the flip there,
when the cards face-down,
it's only showing the back
and when it's face-up,
it's only showing the front and let's try
and make a card disappear into it
so you can see the cards face back down.
Here's a match right
here and make it go away.
And notice when it went away it still work
that animation work just
fine the scaling animation.
Notice also that when we flip cards over,
there's no fade-in anymore.
These cards are not fading, the
back or front is not fading.
That's because this ViewModifier
has taken control of the animation.
And so the animation
system is no longer trying
to reach in here and do
this animation itself.
It is assumes that this
ViewModifier knows what it's doing.
Let's go back to the problem
we had from the very beginning,
which is when things match
let's find some cards
that match here.
Now I'm not very good at this game again.
Okay, I think this one. Yeah,
okay, ready, here we go match.
This one spins.
This one does not spin.
Let's investigate why does this not spin?
This doesn't spin because this
card when we touched on it,
it matched and it was
faced down at the time
we switched it to face-up.
So when it came on screen
this all this fuse right here,
it was face-up and already matched.
So no change happened,
this card isMatched was already true
so there was no need to apply any change.
Animations only animate changes.
And so there was no change
cause that View came on screen matched,
it never changed to be matched
it just was matched when it came on.
So if we want a match happening
to be animated with the somersault,
we need that card, this Text,
basically the front of the
card, needs to be on screen
when the match happens.
But that's a problem for a card,
the second card in a match
because it's face-down.
So that emoji the front of the card
it's not on screen, but we
can still have it on screen,
but hidden and that's
an another way to deal
with having Views that are
appearing and disappearing
instead of having them
actually be if-then-ed out of
existence. Instead, we can just hide them.
And the way we hide
things is with something
we already know from last time, opacity,
fully transparent is hidden
and fully opaque is
fully visible on screen.
So let's use opacity to
have the back and front
of our cards be showing or not,
instead of using if then
that makes them completely
disappear or not.
So this is a different
way of thinking about
what's going on in this ZStack.
Instead of thinking of it
as these front Views come
when it's a face-up
and then they go when it's face-down
and this one comes when it's
face-down, this goes face-up.
Instead, I'm gonna have
all four of these always
on the card.
And when it's face-down,
I'm not gonna be able to see these three.
So let's take these three,
I'm gonna even group them
to make it easier on myself.
And let's set their opacity so
that if the card is face-up,
they're fully opaque
otherwise they're fully transparent.
And this one similarly
will say its opacity
if it's face-up, it's fully transparent.
Otherwise, based off of this,
move these things around, so
they're a little easier to read
So here now, there's no ifs in here,
there are no ifs inside this ViewBuilder
Views are not coming and going anymore.
It also means that this content,
this text is always on screen
even when we're face-down,
it's just hidden.
So that means that when it
gets set to matched later,
this implicit animation will be a change
until it'll get to run.
Let's give that a try.
Here's our card, whoop,
still works perfectly there,
the front of the card was not showing
see the front of the
card is there right now
it's on the other side of this card
if you want to think of it that way,
it's hidden though, its opacity is zero.
And when I click it its
opacity is still zero
and now its opacity is one
and the back's opacity gets set to zero.
So hopefully if we find
a match here somewhere,
this guy
and this guy, they'll both be spinning
and you can see as that card
turned around it was spinning.
Let's match another one here, this one
watch the card as it's turning around
you are gonna can see its already spinning
because it was there
hidden, and it was spinning,
hidden until it now became visible.
This trick of using opacity
to make things come and go
it's really a way that you can
control whether you're doing
an animation by Views coming
and going with transitions,
or whether you're controlling
it directly on screen
using opacity.
And both of them perfectly
valid ways to go.
You can just decide whether
it makes sense or not
later in this demo, you're gonna see
that we actually are gonna take advantage
of making our little Pie come and go.
When our Pie is animating,
we want it there as an animating thing,
but we're gonna put a
different Pie out there
when it's not, there's
gonna be a huge advantage
to knowing when it's coming and going.
So it's not always the case
that you want to use opacity
sometimes you want the
Views coming and going,
it just depends whether you're
triggering things off of that
and whether you want to use transitions,
or just normal animations.
Now the last thing I'm gonna do here is,
now that we have our animation,
working pretty well for flipping cards,
and for having them disappear,
I'm going to speed them back up again
and then we're gonna go
work on the animation
of this little Pie.
So let's speed it back
up, that's easy to do.
Go back here and just make our durations,
maybe we have the default durations,
let's try the default
durations of both of these,
usually under a second
most the default durations.
See what this looks like,
mhh that might be a little quick actually,
I'm not super happy with that
I'm okay with the disappearing that fast
but when flipping the
cards I wanna slow it down
a little bit.
So I'm gonna say duration,
let's say 0.75,
see how fast that works.
Okay, it looks better to me yeah,
certainly wouldn't won't
be any slower than that
but I think that looks pretty good.
This blue would obviously be something
we wanna put down in
our drawing constants.
I'm not gonna do it right
now for time savings
but anytime you see a
blue number in there,
these blue numbers here also
we didn't do from last time
these things should all be put down here
in drawing constants, which
you have a control panel
down here to tweak and turn the dials
to make your UI look how you want.
We have this Pie
and right now the Pie is stuck
in this position right here,
but we really whenever a card flips up,
we want it to start counting down
and when it flips back down,
we stopped counting and then push back up.
It keeps counting and then flipped down
it stopped counting and then of course
when it matches, then we stopped counting
because now you've matched it.
And if it totally disappears,
you don't get as many
points as if you do it
before it disappears.
To do this, we got a couple
of things that we have to do.
One is we're gonna have to
kind of enhance our Model
to know how much bonus
time is left, and all that.
So I have actually put some code
in here that I made available to you guys
on the forum before, so
you students all have it.
If you don't have it, you
can pause this video actually
and copy it from here if you can't
get a hold of it some other way.
But what this code is
basically doing is tracking
every time the card comes up and down
or gets mapped it tracking the time used
and then it answers questions like
how much time is remaining
or what percentage of
the time is remaining
and we can learn whether we earn the bonus
and start using the bonus time
and stop using the bonus time.
By the way these functions,
I'm gonna make sure
I call these when the card
goes face-up and face-down
in my Model and also when it's matched.
So let's use those property
observers we talked about
to call those functions.
Here is my
Cards' vars.
And I'm gonna make it so that every time
isFaceUp changed here,
and I'm gonna use didSet I
showed willSet in the slides,
but I'm just being different here
and showing you didSet.
When this happens, I'm gonna say
if the isFaceUp changed to true.
So the Card went face-up,
then I'm gonna start that
bonus time running again,
start using the bonus time.
Otherwise, if the Card went face-down,
I'm gonna stop using the bonus time.
So I'm just watching this
face-up and face-down in my Card
and whenever it changes that happens.
And this is more reliable than trying to
look at all the times I
say isFaceUp true or false
and try to also call
startUsingBonusTime then
I might make a mistake and
forget it somewhere whatever.
This way, it's reliable
every time I change this
boom we start and stop the time.
And similarly for isMatched here,
when didSet, we can stop using bonus time.
If isMatched is set to false, probably
maybe we're resetting the
game or resetting the Cards,
not sure what we're doing there.
But it seems like the bonus time
shouldn't start going again.
This property observer, a
really powerful way to sync up
what's going on inside your code.
So now our Model knows
how much time is remaining
on the bonus and all that stuff.
That's what all this code I did down here.
So let's use that stuff
in our UI to show that animation.
Now for our card Pie to animate,
we have to enhance our Shape
over here to do animation.
Now shapes really already
have this Animatable,
the same protocol we
had with ViewModifier,
it's pretty much on all Shapes,
all Shapes are assumed to
be able to do animation
it's just kind of part of being a Shape.
It's so common that we
don't even have to say
comma Animatable here,
shape just assumes that
you're gonna do it.
Now if you don't put animatableData,
you won't get any animation it'll build
but usually we want it.
So what do we want to animate here
in our Cardify ViewModifier,
we animated our rotation
as we went around,
well, in our animation of the Pie,
we kind of want to animate this angle,
see this angle that goes here,
as it goes around this
angle is gonna change
the end angle here.
And if we're gonna be a good pie,
let's make it so both
angles can be animated
the starting one and the ending one.
That way we can animate either side,
depending on what we thought look nice.
So how do I animate two
things at once, essentially,
I'm gonna use that AnimatablePair,
right.
And the AnimatablePair is
gonna be a pair of Doubles,
those Doubles are gonna be my Angles here
they're in radians.
Angle itself is not a
VectorArithmetic thing
but obviously the angle in
radians is a Double so that is.
So this animatableData again,
I'm gonna use the same trick of having
the get and set. It's a little trickier
because we have to use
AnimatablePair here,
but AnimatablePair just has
two vars, first and second,
which return the first Animatable thing
on the second Animatable thing.
So for us, we're just gonna
return an AnimatablePair
and the first thing is gonna
be the startAngle's radians.
And the second one is the endAngle's,
angle radians.
That's getting our animatableData
and then setting it is
just setting our startAngle
to be an Angle whose radians are
this AnimatablePair's, which
is the newValue, dot first
and the endAngle
is an Angle, radians,
which is the AnimatablePair's second.
So here we have connected
up two of our vars
to this animation, piecewise animation
and that is it that is all we need to do.
Because it just means that
this Shape is gonna be
redrawn over and over during animation
with these two things being animated
because they're the
things are being sliced up
into little pieces by
the animation system.
That's what animatableData is all about.
Now, I told you this
animation system is elegant,
and it is, having just this one var
as being the only entry
point in both directions in
something to animate.
Pretty nice design, I think.
And I don't work for Apple
so I'm an independent
third party, in my opinion,
I think they did a good job of that.
All right, so now this Pie is animatable
and now our Model knows how to keep track
of the time remaining.
So let's take our View and
put those two things together.
So here's our Pie, right here
and we wanted, I'm gonna
leave the startAngle
always straight up, zero
degrees, up at the top.
I'm gonna vary my endAngle depending on
how much time I have remaining.
Let's start with a simple one, which is,
let's take the Card's bonus remaining,
which is the percentage bonus
remaining it has times 360.
And I'm going backwards because this Pie
is negatively going down to zero
I eventually want this to be down to zero.
See what that does.
All right, here we go
oh, it's not animating.
Huh, that's weird.
Oh, but I thought I saw something there.
Oh, look so it is actually
showing us the time remaining
because I had that card
up for about, that much,
and this is six seconds, I
set it to be to go around.
So I think that card was up
face-up for about four seconds
so it did but it didn't
actually animate it,
and then this one look, it
looks like all the time is there
but if I click away and then click back,
oh, actually I used up all the time
cause that card was
sitting face-up so long.
Now this one, I think was
only up for a little bit.
Yeah see, that was only a tiny bit.
This one just showed up,
so it's actually working
it's adjusting this, but
it's not animating it.
So how are we gonna animate it?
There's a bit of a
challenge to animate this
because what this is really
animating is this angle
going from where it is now,
like right here, around to zero.
And I told you that animation
only shows you things
that have already happened.
But when the card appears,
this clock starts going,
it hasn't gotten to zero yet.
So how do I start an
animation that's gonna have
this thing go to zero when
zero hasn't happened yet?
That's a little bit of a
conundrum a catch 22 there.
So this catch 22 is gonna prevent us
from using this bonusRemaining
directly from the Model.
Now if I ask the Model,
what's the bonus remaining percentage?
It'll tell me the right
answer it always does
that's the Model's job.
So the Model's doing its job
however, the Model is
not constantly changing.
Oh, there's 4.1 seconds left,
oh, I changed now there's
only four seconds,
oh, I changed now
there's 3.9 seconds left,
the Model can't be
doing that's ridiculous.
It's doing its job but it does it in a way
that it's not causing our UI to change
and animations only animate change.
We just can't use this
directly from the Model,
we still have to be in
sync with the Model,
but we can't use this directly.
So I'm gonna animate this angle
using my own little temporary var here
its gonna have to be writable.
I'm gonna have to sync
it up with the Model.
So it is gonna be an @State
as we talked about in the slides,
and it's private, it's just for me to use
so, var, I'm gonna call it
my animatedBonusRemaining.
We'll make it be a Double
the number of degrees
and of course it has to be initialized
because all vars have to be initialized
even the ones that are @State.
So I'm gonna use that
instead of the bonusRemaining
directly from the Model,
I'm going to use my
animatedBonusRemaining.
Now somehow I have to make
this be the right values
to cause the animation to happen.
The first thing that I have to do
is get it to be synced up with the Model.
This has to be synced up with the Model.
How am I gonna do that?
Well, really, when do I
want it to be synced up?
Every single time this
View comes on screen,
I want it to sync with the Model.
Now, when does this come on screen?
Right now it comes on
screen with its container
we don't get any transitions
if we don't know when that happens, etc.
So I'm gonna make it only be on screen
if my card isConsumingBonusTime.
So this is just a var I have
in my Model that tells me
whether a card is currently at the moment
consuming bonus time.
That means it's face-up,
it's not matched yet
there's some bonus time
remaining might be other things.
I don't care it's up
to my Model to tell me
whether bones time is
currently being consumed.
And if it is, then this
View is gonna be on screen.
Now, why do I only want
this to be on screen
when it's actually animating?
Well, because I'm using
this animated value
as the bonus remaining, okay?
That really, I only want to be doing that
when I'm actually animating
and also, it lets me use onAppear.
So you remember onAppear we
talked about in the slide.
This is a function that calls this closure
anytime this View appears on screen.
And that's exactly what I want
because every time this
thing appears on screen,
I'm gonna reset this my
bonus time remaining up here
to be what's in the Model.
So this View is always gonna start out
synced up with the Model,
then I can proceed to animate it,
but we got to get it synced up.
So let's do that in a
little function up here
another little private, call it this func.
startBonusTimeAnimation, let's say.
And the very first thing I
wanna do in here is to set
my animatedBonusRemaining
equal to what's in the Model.
So that I'm always in sync.
and always gonna call this thing,
startBonusTimeAnimation.
Every single time this
Pie comes on screen,
it's gonna sync up with
the Model, really important
if you're gonna be having
your own version of something
that's in the Model that
you want to animate at least
have it sync up when it
first comes on screen.
So now what do I need to do?
My Pie comes on screen, it's showing
the right amount of bonus time remaining,
but now it wants to animate
ticking down to zero.
So I'm gonna do exactly what I just said
animate ticking down to zero
withAnimation
let's use a linear animation.
Please tick down to zero.
I'm just animating my bonus remaining
ticking down to zero.
But wait a second,
how long is it gonna take
to do this animation?
Well, it better be however
much time is left, right,
I don't want it to tick
down to zero slower
or faster than the time remaining.
So I'm just gonna have
my animation's duration,
equal the Card's bonusTimeRemaining.
That's how many seconds are left,
this is the percentage left
and this is the number of seconds left.
That's it, so this is
really all I need to do.
I just have my own little
bonus time remaining here,
which I sync up to the Model
and then immediately start
animating towards zero.
If Pie stays around that long,
we're gonna see the animation
go all the way to zero.
If it stops consuming bonus time,
this View is gonna go away.
It won't have finished its animation maybe
but that's okay it's just
gonna disappear here.
Now, what if we're not
consuming bonus time,
like let's say the Cards are matched.
They've already been matched,
not consuming bonus time.
I actually still want pie up there,
I just don't want it to be animating.
That's easy, in the else case right here,
I'm gonna put a Pie there as well.
Now, this Pie,
we don't care when it appears
because we're not animating.
And it certainly can't use
this animatedBonusRemaining
that's what this Pie up here is about.
So let's just have it match up with
whatever it says in the Model.
Now, I don't like that
duplicated code here,
see this padding and
opacity on both of them
so let's take this out of here.
And instead, we'll use a
Group around this whole thing
to apply padding and opacity to it.
This hopefully should
cover all the bases here.
This is the Pie when it's
animating when it appears,
we sync up with the Model
and then started going towards zero with
however much time is left.
And if we're not consuming
bonus time we're not animating
then we'll just do a normal Pie
with the bonus remaining there.
See if that works.
All right, let's watch.
Whoo, look at that
animating over to this one
oh, and even matched.
This is a great example here because
this one matched as soon as I clicked it,
so I didn't use any of my
bonus time so that looks right
And then this one, as
soon as I had a match,
it switched over to
using this Pie down here
it's just showing me
how much I had remaining
when the thing matched.
Right, let's look at another one here,
ghost and this guy, let's
click away from these
and this will start and go back
and see if the bonus
time that was remaining
when we went face-down continues.
Whoo, it did.
Whoo okay,
see what's going on there, all right
simple as that.
Now, this was a little trickier to do than
some animations
but it is not uncommon to
have a situation like this
where your Model or
whatever your data source
can tell you what's going on,
but it's not constantly changing.
And that's why sometimes
you'll have to create
your own var here that
you can animate towards,
but make sure you sync
it up with the Model
before you do it.
Okay, that is it for animation today
we covered a lot of ground.
We talked about implicit animations,
where we had the somersaulting
emojis being so excited
that they had a match.
We had a couple of explicit animations
we did one right here,
when we chose a card
we did another one right
here when we did a new game.
We also showed animating ViewModifiers
we had our card here be animating,
we even showed how to not
have Views coming and going
if we didn't want them coming and going,
we animated our Shape
and all we had to do there was say
what data inside of our
Shape is animatable,
which in our case was the
starting end angle over here.
And we also did some transitions, right,
we had this scale transition
so that when cards match,
they would disappear.
By the way we could do the
similar kind of transitions
with these Pies up here
if we put, for example,
a transition on the Pie of .scale,
then when our Pie appears,
it kind of zooms in to kind of a good look
kind of zoomed in.
If we don't have scale, I don't
know if you noticed before,
but it was actually kind of fading in
and maybe we might want to
say we don't want any of that
and we're just gonna use
an identity transition
and then this Pie, when it comes and goes,
it's just gonna appear. Which
is fine because it's usually
happens when the card is face-down
or it's switching between two Pies
that have the exact same
value if we are matching.
That is it for animation,
hopefully, that's all
you'll ever need to know
about animation.
I mean, of course animation
is a powerful subsystem,
we didn't have to add very
many lines of code to our app
to make it do all these crazy things.
But I haven't shown you every possible way
you could use in animation by any means.
So there's still a lot to learn
but to let you know the basics now.
Next week, I'm not sure
exactly what we're gonna do
next week, either we're
gonna do some gestures,
possibly, you know, pinch
gestures and things like that
or we might actually do
TextFields and Pickers
and I saw that question
on the class forums about that,
and maybe we'll do that.
We're not exactly quite sure
where we're gonna go next week
but stay tuned and you'll find out then,
- [Narrator] For more, please
visit us at stanford.edu
