WWDC2014 Session 229

Transcript

X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
[ Silence ]
[ Applause ]
>> Thank you.
Thank you very much.
My name is Andy Matuschak.
I work on the UIKit Framework.
And later I'm going to be joined
by my colleague, Colin Barrett,
who also works on UIKit.
And today we're going to talk
about Advanced iOS Architectural
Patterns, which is going to sort
of be a continuation of Bill
Dudney's talk from this morning,
the Core iOS Application
Architecture and Patterns.
We're going to be taking some
of those ideas a little further
and just in case you didn't see
that talk this morning or even
if you did, I want to
tell a little story
that explains I think
how we all got here.
So maybe you start
off by yourself
and the project starts
small, your well intentioned,
you're keeping everything
orderly, you're adding features
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
you're keeping everything
orderly, you're adding features
and everything is still good.
You add more features
and they kind of pile up
and you got momentum
behind you now.
You have all these users
and they're demanding.
You had some more developers
because maybe that will help,
but it doesn't seem to
have quite the impact
on your progress that
you'd been hoping for
and you're making changes
faster and faster and bugs seem
to be appearing faster
and faster.
And it seems like all you're
doing is spending your time
fighting bugs.
And you think, OK, well
I'll knock a few down
and a few more come back.
You're not adding new features.
You had some unit tests
to try to get a handle
on what's going on here.
Somebody's told you that's going
to help things, but it seems
to help initially and then
the bugs start coming back.
And then you learn actually your
unit test can have bugs too.
[ Laughter ]
What do we do?
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
What do we do?
People say: Software
Architecture.
And you go, "All right, cool.
Software Architecture.
All right."
What's Software Architecture?
You search around the Internet,
you get a whole bunch of blogs.
They're listing like, "Oh man.
Never do this stuff it's going
to make your architecture bad."
And it seems like there's a
lot of contradicting opinions.
Some people say start
small and some people want
to design everything up front.
Seems like these discussions
just keep coming back
to taste, maybe dogma.
All you really know maybe is
that your taste is improving a
lot faster than your ability.
[ Laughter ]
You can tell that something
is starting to get smelly.
You can tell you've
got a problem,
but maybe you don't know
what to do about it.
So we're going to try to help.
We can only scratched
the surface today.
The idea is to take some
time honored concepts
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
The idea is to take some
time honored concepts
from computer science
and software engineering
and to present them, hopefully,
less abstractly and in a way
that might make sense to you as
an iOS developer with your apps.
Today, we're hoping to
provide insight, not dogma.
[ Applause ]
We're not--
[ Applause ]
I'm so glad.
I'm so glad to hear that.
Epistemology is important.
You need to know why you're
having architectural problems.
Not just a bag of tricks
that you can use when faced
with any particular issue.
That way, hopefully you'll
have a real objective measure
that you can use to
compare approaches.
That way, when your
colleagues disagree with you,
you can actually talk
about what might be better
and what might be
worse and why rather
than just citing a
bunch of catch phrases.
All right.
So today, we're going to
talk about 3 broad approaches
that you can use to consider the
complexity of your application
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that you can use to consider the
complexity of your application
and evaluate approaches
to dealing with it.
First, when we have tangled
meshes of information networks
like this, we're
going to clean them
up by actually designing
the information flow
in our application rather
than just letting it
grow ad hoc like a weed.
And then when our objects start
to become amorphous blobs,
we're going to separate them
actually define what their
responsibilities are
and hopefully get each
of those circles
a little smaller.
And finally when the very ground
seems to be shifting out from
under you, we're going to
take advantage of immutability
and in particular, some
new features of Swift
to really get a handle
on things.
So those are the big
concepts from today.
And we'll start by talking about
designing information flow.
Now, much of the complexity
in a modern application
is really just shuffling
information at events around.
You have delegate handlers,
and target action callbacks,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
You have delegate handlers,
and target action callbacks,
you have completion blocks,
and you have some signals.
And how should those
information and events go
through your application?
You start wiring stuff up.
You set these object
as a delegate
of this other object over here.
You say add target action, and
every time you're doing that,
you're adding a little
node to this network.
And that network is getting
gnarlier and gnarlier
and maybe you've drawn
networks like this for say,
object ownership in
your application.
Which object owns which other
object or maybe for a workflow.
But in this section,
we're going to talk
about thinking conceptually
about this diagram
as it pertains to information
flow in your application.
Where is information coming
from and where is it going?
And the first question
I'll ask is
where is truth in
my application?
And in order to do
that, and in order
to keep things hopefully
somewhat concrete,
we're going to turn
to a demo application
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
we're going to turn
to a demo application
which is having its own
problems with information flow.
All right.
So over here, we have an
application and it's going
to be written in Swift.
And that's just because
I like Swift quite a lot.
But the topics we're discussing
in this particular section
really don't require Swift.
All right.
So with the journaling
application,
write down what's going on in
your life, keep track of things.
Maybe you're Abraham Lincoln.
And like any good
journaling app,
there's a UI to add an entry.
You've gotten a bug report
from a user talking about a bug
with the photo edition
UI in your application.
So we'll go ahead and start
making a journal entry.
We will add a photo, taken
this one from apple.com
and there's this feature that
lets me show and hide the photos
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and there's this feature that
lets me show and hide the photos
that I've attached
to my journal entry.
Everything is looking
good so far.
But this bug report I got said
that the app seems to break
when I'm playing
with this button.
So, I'll play with the button.
Something really glitchy
is happening there
and we have a crash.
That's no good.
Now we could look
at this back trace
but probably the problem
has already happened.
And we can see the issue is--
now this exclamation
point here is going awry.
Rather than trying to
address this specific crasher,
I'd like to draw your attention
to the infrastructural problem
which is underlying
this crasher.
So let's take a look at
how exactly this expanding
and collapsing of the
PhotoDrawer feature is working.
Here's expandPhotoDrawer.
And first we say, OK do
we have a PhotoDrawerView?
If we don't, make
one, get it all laid
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
If we don't, make
one, get it all laid
out so it'll expand
from zero height.
Animate it to full height and,
you know, update that button,
a little bit of bookkeeping.
Of course then, in
collapsePhotoDrawer,
if we already have a
PhotoDrawer which we want
to collapse, we animate it shut.
And when the animation
is done, we remove it.
It seems pretty straightforward.
You know, you might
say, well this option,
this option seems
to be the problem.
Like you're letting
users interact
with your UI while the
animation is going on.
You shouldn't do that.
And, OK, so you could
remove this option.
But you know, we do actually
want to keep things fluid.
There's no reason that the
world should have to stop just
because this PhotoDrawer
is opening up.
And furthermore, we're
starting to get a little sense
of that taste versus ability
tradeoff we were talking
about earlier.
I mean, hopefully you're looking
at this and you're saying that
"Well, I don't exactly
see anything wrong.
So trying to work
around this bug
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So trying to work
around this bug
by just disabling the
button doesn't seem
like the best thing to do.
What's really going on?"
And even if we were to
disable this button,
things get somewhat worse.
If I rerun this application,
I'll show you there's
sort of a side feature.
So say that it was the case
that you could not interact
with the Show Photos button
while the animation is going on.
Well, down here is our body
and as we start typing into it,
we hide that PhotoDrawer
because we want
to have more space
for the keyboard.
So it's actually not good enough
just to disable this button
to make this problem go away.
The problem will still exist
because we collapsed
this PhotoDrawer
when we focused the body.
If I were to keep going on
like that, it would crash.
So there's something more
fundamental at play here.
There's some mold growing in the
corner and I'd like to return
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
There's some mold growing in the
corner and I'd like to return
to a diagrammatic approach
to examine that mold.
Now this is boxes and
arrows representation
of the information flow
on our application.
We've got this photos
drawer tap action
and we've also got a
text view focus action,
and they're both doing something
based on a piece of information.
And that piece of
information is:
does that photos drawer exist,
does that drawer
view exist already?
And we saw in the code, if it
doesn't exist, OK we'll make it.
And if does exist, we'll
start an animation.
When that animation is
done, we'll throw it out.
That's fundamentally
what's going on here.
This bug is occurring because
we're getting our information
from the wrong place.
There's another piece
of state, which is,
is the drawer semantically
expanded?
And that's separate from,
does the drawer view exist.
Because the draw view isn't torn
down until the animation
completes.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
down until the animation
completes.
If we start collapsing the
drawer, then we want to think
about the state of the system
as the drawer is collapsed now,
just like if you start animating
a view to the other side
of the screen, its model value
is already at its destination.
The drawer view's
existence is more
like the presentation
value of the view.
It's like a proxy for
that underlying truth.
Should the drawer
view be expanded,
you can module any
animations that are going on.
So we can deal with this problem
by reifying that piece of state.
We can make a property.
Say, you know, is the drawer
view expanded right now,
updated immediately
when we start collapsing
that drawer view and
when we start expanding
that drawer view.
And then once we do that,
we can look at this diagram
and say, "Wait, wait, wait.
Why are these actions getting
their truth from whether
or not the drawer view exists?"
What really matters is whether
or not the drawer is supposed
to be expanded or not.
And when you tap
in the text view,
you should only really be
collapsing the PhotoDrawer
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
you should only really be
collapsing the PhotoDrawer
if it is actually expanded.
You don't want to try to
collapse the PhotoDrawer if it's
in the process of
being collapsed already
but that drawer view
hasn't been removed yet.
Now in this talk, I'm
not actually going
to make the code changes
that we're talking about here
because we have a lot of
material to get through
and I want to cover it quickly.
But here we've been able
to solve this problem,
at a conceptual level at least,
by thinking about our system
in terms of how information
is moving through it:
where are we getting data,
how we're making decisions.
And considering first:
where is truth?
Who really knows that state
which one place should
be consulted for it?
And that will help
us get all the boxes
that we need in our diagram.
Now, once we've gotten all the
boxes we need in our diagram,
I want to draw your
attention to the difference
between that root level
truth, that one place
in your application
that actually knows
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
in your application
that actually knows
where things are supposed
to be and the values
which are merely
derived from it.
So in order to do that,
again, let's return
to our demo application.
And you'll see here that
as I continue typing,
there's a character counter.
And that character
counter is there
because you might be
an extremely outgoing
and extroverted person with
your journal and you might want
to take advantage of
this feature that we have
which automatically shares all
of your journal entries
to Twitter.
So we gave you a character count
in order to help you make sure
that it's actually going to
fit when you post to Twitter.
Simple enough.
It goes up as we delete,
and it goes down as
we add new characters.
And that's all fine.
But if we think about this
photo's feature that we have,
you know, when we post to
Twitter, we're going to have
to put a URL for that photo
in the body of the tweet.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to put a URL for that photo
in the body of the tweet.
So that needs to be included
in the character count.
And we got a bug from users
saying that they tried
to make a post and Twitter
rejected it even though the UI
said it was OK.
So, I added this photo,
you could see it's added
to this entry, and we
can see that the number
of remaining characters
is still 140.
And if I start typing,
watch I'll enter one key,
and we jump down to 118.
It's like it immediately
reconsidered the URL.
I'm sure you've all seen
this kind of bug quite a lot.
Something is stale.
Something that was supposed
to happen has not happened,
and unfortunately, that kind
of problem is really
difficult to debug.
There's nowhere to break.
So, we can look at
the code and say, "OK,
what's supposed to happen?"
There is an updateCharacterCount
method here
and it's very simple.
It just updates this character
count label with the result
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
It just updates this character
count label with the result
of our Twitter entry encoder.
So there's 2 possible
problems here.
Possible problem number 1 is:
the Twitter entry
encoder is at fault.
It's returning a wrong value.
Possible problem number 2 is:
this method is simply not being
called when we add a photo.
So, we can look to invalidate
one of these hypotheses.
We can look to sort of guide
to where we should be addressing
our debugging abilities.
We can do that by just
adding a break point here
and I will go ahead and create
a new journal entry here
to set this up again.
I'll add a photo.
And we see
that updateCharacterCount
is never called.
But when I enter a key
here, it is called.
So our problem appears to be
that this method is just
not even getting called
when it should be
called, and let's continue
to follow this rabbit
hole upwards, I guess,
and see who calls
updateCharacterCount.
Now it's only called in
one place and it's called
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Now it's only called in
one place and it's called
on "textViewDidChange".
Now kind of an esoteric
factoid about the UIKit
and at AppKit APIs is that
if you programatically set a
textViews value, the textView
does not emit delegate callbacks
saying that the textViews
value changed.
And then the same thing is true
for basically anything else
that has a delegate callback.
If you programatically set the
value that is corresponding
to that callback, UIKit/AppKit
will not call you back.
So, you could react to
this by saying, oh geez,
got to work around this
weird UIKit behavior.
I guess we'll add an extra
call to updateCharacterCount
when we add a photo
which happens down here,
somewhere, right here.
You know, we could-- we can call
updateCharacterCount there but,
you know, what if we added
an audio recording feature.
You know, take a voice note
about how your day is going.
What if we added a
video recording feature?
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
What if we added a
video recording feature?
All of these things are
going to have to remember
to update the character
count, and, I hope anyway
that you're starting to
get that sort of taste
versus ability smell again.
Something seems amiss here,
and it's not exactly
clear what it is.
So again, I propose that
the solution is thinking
about the information flow
of your application
diagrammatically.
Here again is a diagram
of the information flow
in the application.
There's a text view and
there's a character count.
Both of these things are getting
their values from the model.
So that seems OK.
We've got truth, the
model is holding the truth
and we're using the right
inputs in our information flow,
but we're still not
getting the right behavior.
The reason for that is
that derived values have
to be treated differently.
Let's think about what these
derived values are in comparison
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Let's think about what these
derived values are in comparison
to the truth which
provides their inputs.
Now, if you have
a derived value,
it does a couple of things.
It computes some other
value like a character count
from some set of inputs.
That's straightforward; we saw
that in updateCharacterCount.
Separately, it needs to
recompute that output when one
of the inputs changes.
These requirements are really
very much like a cache.
And I propose to you that
everywhere in your program
where you have something
like a character count,
which seems like a very
straightforward thing,
it's just a value derived
from another value.
All of those things have many of
the same properties of a cache,
which means that they're
subject to the same problems
that a cache has as well.
For instance, when your
original data changes,
you need to make sure
to update the cash data
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
you need to make sure
to update the cash data
or else you are going
to have staleness bugs.
And on the flip side, if the
original data hasn't changed,
then you shouldn't be going
about generating new
cache representations
of that information
either, or else you're going
to have efficiency problems.
Now our problem here is
staleness, and you can see
that the same issues apply for
truth versus derived values
as they do for caches and the
values which are being cached.
So let's focus on the
staleness issue here and try
to understand what's going
on with our application.
How do we get here?
Where did we go wrong?
And how do we model it with
these information diagrams?
So, returning to this diagram,
we see that the model's value
defines the text view value.
I guess I didn't show it to you,
but take me on my word on that,
you know, the model's
value changes
and we set the text view values.
It's very straight forward.
We haven't seen any bugs with
that, at least not yet anyway.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
We haven't seen any bugs with
that, at least not yet anyway.
So let's go ahead and assume
that this is how it works ,
nd when the model is changed,
the text view is invalidated.
So, all of that is fine.
The problem that we're having
is with the character count.
We've seen the definition
of updateCharacterCount.
We know that the character
count is deriving its value
from the model.
But when the model changes,
the character count
is not invalidated.
Rather it's when the
text view changes,
the character count
is being invalidated.
And the lopsidedness of this
diagram should hopefully
indicate something to you.
You know, each of these arrows
is basically doing half a job.
You're splitting up
these responsibilities
in 2 different places,
and that's really
the cause of our bug.
We can solve our bug by merging
those responsibilities back
to one place.
We can solve the
staleness problem by saying,
"OK whenever the model
changes, we need to make sure
to update all the values which
are derived from that model."
It's really easy
to do this in Swift
because we've got these
fancy new property observers.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
because we've got these
fancy new property observers.
All we have to do is add
that updateCharacterCount
call to our didSet block.
And in general, you're
going to be way better off
if you always keep those
arrows merged together;
unless there's a very
pressing reason not to.
So, that's just one more tool
in your tool bag for thinking
about the complexity
in your application.
Actually draw out these
information flows.
What are the boxes?
What are the arrows?
Do each of those arrows
actually mean both definition
and invalidation?
Because they need to.
Or else you might have
staleness problems.
Now, finally, I'd like to turn
your attention to the text view.
Now we said, OK, the text
view is getting its value
from the model, and that
part appears to work.
But we know that when the
user types into the text view,
that model must be
getting updated.
After all, I can make
a new journal entry
and it appears to get saved.
So there must be some reverse
relationship here, too.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So there must be some reverse
relationship here, too.
How do we think about that
schematically in the same way?
And in particular, this looks
like an infinite
loop, but it's not.
We don't have a stack overflow.
So these arrows must be
asymmetric in some way.
And I propose to
you that in thinking
about how new truth is created,
you need to carefully
consider the asymmetry
of those relationships
in your application.
All right.
So let's return to our app and
talk about creating new truth.
Now our application has
another feature I haven't told
you about.
That feature is that I can share
my journal with a loved one.
I can let my fiancée see
all these entries I write.
And that's a great feature,
but it's complicated somewhat
by the fact that there's
this Edit button here.
You know, if I were to
start editing this entry,
and you see our character
count issue on display again,
if I were to start
editing this entry--
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
if I were to start
editing this entry--
oh, you don't need this
break point anymore.
Then, we don't expect for
my fiancée if she's staring
at her phone to actually see
this edited journal entry,
right.
Because I could hit Cancel,
and I'd be throwing it out,
presumably the old entry
would still be there.
So, I said that we're
working with the model here,
but we must not be
working with the model.
We must be working
with some other model
that has a relationship
to the first model.
Because if there were
really one model,
then there will be no real way
to implement this cancel button
and she would be seeing those
changes as soon as I made them.
I'm going to suggest that
thinking about the relationship
between this editing UIs model
and the underlying model,
which it is editing, is actually
the same as the relationship
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
which it is editing, is actually
the same as the relationship
between the model underlying
the text view and the new values
that the text view is
emitting to the model.
But if we're going to think
about this effectively,
we really need to be
able to think about it
from a diagrammatic
perspective so that we can see
where everything is
and how it's going.
So, let's talk about
these arrows.
Let's talk about what they mean.
What's that asymmetry?
We know if we start
with just a text view,
that the text views value is
defined by the models value.
We also know that if
we edit the text view,
somehow that model value
is getting updated.
But, the text view is
deferent to the model value.
If the model value
were to change out from
out of the text view, somebody
were to just set the body
to something else, that text
view would probably just update
immediately to that new
value and if some other piece
of the program were wondering
what is the body value
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
of the program were wondering
what is the body value
of this journal entry, they
wouldn't ask the text view,
or at least we hope that they
wouldn't as we saw earlier.
Instead, we hope, that
they would ask the model.
The model is truth, and these
derived values are deferent
to truth.
These new values they
emit, as you edit,
are like suggestions
flowing back up the graph
to the model along
these dashed arrows.
Truth: new pieces
of information.
Fact: these things flow from
the model to the derived values
and suggested new pieces
of information flow back
from the text view to the model;
because of course there
could be validation.
The behavior of our
application with respect
to that 140-character
Twitter limit could be
that we just truncate.
So, it could be that that
suggested value is not
applied exactly.
In this way, this
relationship is asymmetrical,
and we can use the same
structure of thinking to think
about the "entry viewing
controller", which presented
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
about the "entry viewing
controller", which presented
that editing controller, and
the "entry editing controller".
There's some original model, and
that's what my fiancée is seeing
if she looks at the app.
And that's the one that's
going to still be there
if we hit Cancel and
then there's the model
that we're editing
which began from a copy
of the original model.
So in the same way as
these other relationships
in our graph, the model that
we're editing is derived
from the original model.
And when we hit that
Done button,
our editing controller
is suggesting a new value
for the model to the
viewing controller.
That is how new truth
is created.
I want you to imagine with me,
because it isn't on the slide,
that there is a dashed arrow
coming from the model back
up to the original model;
that is how you can think
about this relationship.
Just as the model may
validate the text views value
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Just as the model may
validate the text views value
as it's coming out
of the text view,
the original entry view
controller may be validating the
edited entry.
For instance, if she were able
to edit those journal
entries also,
there could be a conflict now.
And so when the new
model value is suggested,
some UI might be presented to
allow that merge to happen.
In the same way, the editing
controller, its value is derived
from the viewing controller
and the new value
is only a suggestion
to the viewing controller.
So, we've worked through how
these techniques can help us
solve several real world
application problems
and hopefully they
suggest to you ways
that you can solve the
kinds of problems you face
in your app every day.
You just have to actually
think about how information
and events are flowing
through your application.
First, where is truth?
Who really knows?
Who is really responsible
for those fundamental pieces
of state, information and
events in your application?
And then once you start
thinking about how they flow
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And then once you start
thinking about how they flow
through your application,
remember the difference
between truth and the
values derived from truth.
Remember that values derived
from truth are in many ways
like a cache and needed
to be treated as such.
And finally, remember that
when new truth is created,
that relationship is
asymmetrical by necessity
in order to avoid some
kind of infinite loop.
One direction -- facts flow.
And then the other direction
-- suggested new values flow,
which may need to be merged
or validated in some way.
And by thinking systematically
about the information flow
in your application, you
can really get a handle
on complexity.
All right.
So, now let's move on.
I'm going to invite
Colin Barrett up to talk
about defining clear
responsibilities
for your application.
[ Applause ]
>> Thank you, Andy.
Good afternoon, everybody.
In this next section, I'm going
to show you how to identify
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
In this next section, I'm going
to show you how to identify
and tease apart responsibilities
in your application.
Let's start with an
example: form validation.
We've all at least
used a form like this.
Many of us have,
maybe, implemented one.
You know, it's not as simple as
just putting some text fields
on the screen and a button.
There are rules about
what values are allowed
in the different text
fields, the four we have here,
and how the other
interface elements
in this view are enabled and
disabled, or shown and hidden,
depending on the values
of these text fields.
So, let's say we're
implementing this view here.
Where would we start?
Well, we'll need to know
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Well, we'll need to know
when the user finishes
editing a text field.
So, we'll implement
the text field "did
and editing" delegate message
on our view controller.
And in that delegate
message we're going to need
to consider each text field
and if they're all valid,
enable the sign-up button.
But, we can't exactly consider
all of these fields uniformly.
They all have different rules.
Let's look at the rules
for the username field.
We have this regular expression
that our server engineers gave
to us and we have to match
that against the contents
of the username text field.
If we don't have any--
If it doesn't match,
we're going to need to
show the users somehow
that they've messed up
and then you go back
and correct the mistake.
Now, there's one other
complication here and that's
if our text fields are empty.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
If our text field is empty
when we're checking each field,
we're going to want
to leave it alone.
But when we're considering the
state of the sign-up button,
we're going to want to leave
the sign-up button disabled.
So, we're going to want
to leave it disabled
because we don't want
the user to proceed
with a partially
filled out form.
So, let's go back to this
username part and see what
that would look like in code.
So, yes, this is a lot of code,
but it's all different
sorts of things.
It's intermingled.
We have local variables to track
the state of the text fields
and of empty text fields.
We have a bunch of regular
expression code right next
to that, and in between all of
that, we have these 2 lines,
which are really
the most important
for what the view controller
does, which is to manage
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
for what the view controller
does, which is to manage
and configure its
constituent views.
So, much like we can
diagram information flow
in our application, we can
also diagram responsibilities.
Who does what?
Here is the diagram that we've
been thinking about so far.
We have our view
controller, which manages
and configures its constituent
views, and we have our views,
which display the
data they're given
with core animation
and core graphics.
But we've identified a third
overlapping responsibility:
validation.
This may not seem so bad, but if
we add another view controller
that also has to do similar
validation, they don't have
that same logic in two places.
A bug in one has to
be fixed in the other.
And since these are
in different places,
it's likely that they'll
diverge over time.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
it's likely that they'll
diverge over time.
A type of rule get added in
one place but not the other
because it's not
necessary there,
making it even more
difficult to change
and fix these common
issues over time.
So, what we want to do is
separate that responsibility
out into one place that can be
shared by both view controllers.
So, to do that, we first need
to identify what
validation actually is.
When thinking about
these types of questions,
it's useful to think of
the inputs and outputs.
Or, to put in another way,
what information do I need
and what questions am
I trying to answer?
So, as I build through this
common sense explanation
of what validation is, I'm
going to fill in the inputs
and outputs in this table.
So, validation seems to be that
if you give me an input value,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So, validation seems to be that
if you give me an input value,
I can tell you if it's valid.
And if it's not valid,
I can tell you why.
These pretty directly translate
to fairly simple data types.
And if we look at what that
looks like here in Swift --
forgive the syntax
error there --
we have our output
types right here
but we're missing our input.
That's because we want to
leave this actually open
to interpretation in this case.
Because a good technique
for dealing
with complicated
responsibilities is to be able
to build larger units
out of smaller ones.
So, in this case, we'll
build larger validators
out of smaller ones.
So for those larger validators,
the input will be implicit
in the constituent
validator's input.
This technique is
called composition.
We're composing a larger
validator out of smaller ones
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
We're composing a larger
validator out of smaller ones
and it works totally
fine in Objective-C, too.
I also really like Swift.
So, let's look at what
a username object,
username validator object,
that implements this validation
protocol would look like.
Well, we have our input
here, it's a String,
and this validateWitherror
function is where we're going
to have our regular
expression code.
It's now isolated.
It's not intermingled with
all of these other code.
We can do likewise for
password validation.
But, maybe wondering that this
only represents the validity
of a single password
field, then that's correct.
We need to represent that two
password fields match in value,
in addition to not
being too short,
not being high MoM,
things like that.
So, we'll create a
SetPasswordValidator
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So, we'll create a
SetPasswordValidator
that has two password
validators.
This is composition in action.
It's going to represent,
in addition so that the password
fields individually are valid,
that they're also
matching in value.
We can also apply this
composition technique
to our overall form.
We have our usernameValidator,
setPasswordValidator
and emailAddressValidator.
This is also where we'll
handle the behavior of nil,
we talked about earlier.
The constituent validators
will allow nil,
because we don't want the
individual text fields
to show any sort of error
state when they're empty.
That would be just confusing.
But, this SignUpValidator
will check
that its constituent
validators have non-nil input.
And if any of those inputs are
nil, it'll say it's not valid,
allowing us to easily
disable sign-up button.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
allowing us to easily
disable sign-up button.
So, let's return to that
workflow we were looking
at earlier and see
what that looks like.
Now, we're able to consider
each text field uniformly
because all we do
is set the input
on the corresponding validator.
We're also able to decorate all
of the text fields
uniformly as well.
The logic and that
responsibility has been
moved elsewhere.
We're only talking to
an abstract interface.
So, let's define clear
responsibilities.
We've separated out the
responsibility of validation
from the rest of our program.
We've used composition to build
up larger pieces
from smaller ones.
Let's move on now to
our third section,
simplifying with immutability.
You may have heard on the
internet that mutability is
"bad", and you should
feel bad for using it.
But nobody's really
told you why.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
But nobody's really
told you why.
Why is it bad?
I mean, it's easy to use,
you just set it, right.
We're working in an imperative
oriented languages here.
We have statements.
So, to illustrate that, let's
look at another diagram.
In this diagram, the red box
represents immutable object,
which has the value of five,
and these circles here
represent other objects
that are passing this
mutable data around.
So, when A passes this
mutable object to B,
B probably doesn't want
that object changing
out from under it.
So, how do we ensure
that that happens?
Well, A still has a
reference to this object.
So, B depends on-oh, sorry, if--
yes, exactly, if A changes it,
then B sees that change.
So, B depends on A
behaving in a specific way.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So, B depends on A
behaving in a specific way.
They're linked in this sort
of responsibility
and dependency graph.
Now, likewise if this
object now gets passed to C,
these other objects could
still have references to it.
They could still change it.
So, C may depend on A and B.
But it actually gets a little
bit worse than that, because A
or B could be waiting for C
to make some sort of change.
Maybe A has registered
for KVO observation.
Maybe B is the delegate of
this object and is waiting
for some sort of
callback to happen.
The specific timing
of that could be part
of B's implementation.
If C changes when it calls some
setter, that could screw up B.
So, we're seeing here
how mutability ties all
of these things together.
It's one of the biggest
reasons why you feel that drag
that Andy was talking about as
your application grows bigger.
And it also explains why adding
abstraction often doesn't help.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And it also explains why adding
abstraction often doesn't help.
As long as you're passing
around the same mutable data,
it doesn't matter how
many layers you have.
You're in fact just
lashing more mutable layers
onto this big ball.
So, let's look at
what would happen
if this object was
immutable instead.
We couldn't change its value.
When A passes it to B, well,
A still has a reference to it,
but it can't change it.
No one can change it.
It can never change.
If B does need to make a
change, it has to make a copy
and pass that copy to C.
There are no arrows here;
there's no dependencies.
Everything has the data that
they have and that's it.
So, there is a tradeoff
here though, right?
Mutability is easier
to think about locally.
Think about building up
a string or an array.
It's also wasteful to
create intermediate copies
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
It's also wasteful to
create intermediate copies
when no else is mutating our--
when no one else can
see those mutations yet.
You may be wondering
is there a better way?
Happy to say this, Swift
Structs are that better way.
They have opt-in mutability
via the mutating keyword.
This lets you choose whether
or not a struct is mutable
or immutable, based on
using the let or our keyword
to introduce the binding to
the name when you declare it.
So, Swift Structs also have the
property that they are called
by value which just means
that a new copy is automatically
created from you when you pass
that struct from one
function to another.
You don't pass a reference
or a pointer like you do
with an Objective-C
or Swift object.
So, let's go back to
that diagram and see what
that would look like
one more time.
Here we have our Swift
Struct, happy green box,
and when we pass it from A to
B, an implicit copy is made
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and when we pass it from A to
B, an implicit copy is made
and that's what's passed to B.
B can happily mutate.
A won't see those changes
and again when we pass it,
another copy is made,
and C gets its own copy.
So, that's one sort
of immutability,
and that's how Swift
Structs can help us be--
get a lot of the
benefits of immutability
without sacrificing programming,
ease of programability.
But there's another kind
of immutability I
want to talk about.
We can see that in
UIMotionEffect.
You're not familiar
with UIMotionEffect?
It simply adjusts properties on
views based on gyroscope data.
We use this to achieve
the parallax effects
on the Home screen and
elsewhere throughout the OS.
MotionEffects are
reusable across many views.
If you have one type of motion,
you only have to
create one effect.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
MotionEffects also have very
low latency requirements.
We don't want to get
behind the gyro data
because that looks laggy and it
really ruins the magical effect
of parallax on the Home
screen and other places.
So, again, we're going to show
how this is also immutability.
Once again by considering
the inputs and outputs
of the MotionEffect process.
MotionEffects take
a device pose,
which is simply a description
of the device's orientation
in space.
In return, a relative
offset for each key path
that we want to change.
It may be somewhat surprising
to describe a motion
effect as immutable.
It doesn't really have a bunch
of properties not
unless we're dealing
UIInterpolatingMotionEffect.
It feels more like a function.
But immutability is actually a
deep idea, and has deep power.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
But immutability is actually a
deep idea, and has deep power.
Because motion effects take
an absolute device pose
and return relative offsets,
they can always return
the same dictionary
for the same input device pose.
In much the same way that
if you don't have a setter,
you'll always return the
same value for your getter.
So, to really show that
this design is immutable,
let's look at some alternative
designs for motion effects.
Let's say that we thought
that having delta inputs rather
then an actual absolute device
pose was a better design.
Well, we would still have
our device pose data,
but it will be deltas and
we'd still be giving back
relative offsets.
But to calculate those offsets,
we would actually need to figure
out what the current
orientation of the device was.
So, we would depend
upon all previous poses
as an implicit form of input.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
as an implicit form of input.
Now our inputs are not just
all-possible device poses
but all possible sequences
of device pose deltas.
That's a lot bigger input space.
It's harder to test, it's
harder to reason about
and it's also not immutable
because as our application
changes over time,
we're going to get
different answers.
Likewise, if we had
absolute offsets rather
than relative offsets, we would
have a value for each key path
that we're returning
in absolute offset,
we would be dependent
on a particular view.
And that particular view would
make this MotionEffect not
something that was reusable
across many different places,
and again would make
it so that its value
for a particular device
pose would change over time.
So, that's simplifying
with immutability.
We've seen two different
forms of immutability,
and how we can leverage
those to build simpler,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and how we can leverage
those to build simpler,
more easy to reason about,
portions of our application.
Being able to reason
abstractly about pieces
of your application is critical.
If you have to know every
nook and cranny of the system
and wonder if changing one thing
is going to cause something else
to break, your app just
becomes this big spider web,
that you can never
really escape from.
We've talked about a lot today.
So, let's just review everything
we've gone over right now.
First, we talked about how
to design information
flow in your application.
We learned the difference
between truth and derived value,
and how new truth is created.
We saw how to define
clear responsibilities,
how to tease apart
different portions of your app
and how to isolate them.
We also saw how to
use that isolation
to build things with
composition.
We also saw how to
simplify with immutability
to increase our ability
to abstractly reason
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to increase our ability
to abstractly reason
about our application.
So, what now?
You've heard this talk; maybe,
hopefully, you're all fired
up about these concepts.
Go back to your app; put
some of these in action.
When you're designing a feature,
think about the information
flow,
think about the
responsibilities.
Share this talk with
your coworkers.
Tell them about a time in your
app, specifically your app,
where there was a bug, where you
were maybe updating some sort
of cache, that you didn't
really even realize was a cache
until just now.
That's all we've got today.
For more information,
contact Jake Behrens.
Bill gave a talk this
morning about some other types
of design patterns
in our frameworks.
Thank you for listening.
Have a great afternoon.
[ Applause ]