Architecting Your App for Multiple Windows

Session 258 WWDC 2019

Dive into the details about what it means to support multitasking in iOS 13. Understand how previous best practices fit together with new ideas. Learn the nuances of structuring your application to support multiple windows, and how to instantiate your UI, handle windows coming and going, and manage your app’s underlying window resources.

[ Music ]

[ Applause ]

My name is Janum Trivedi, and I'm an engineer on the UIKit frameworks team.

First, let's talk about architecting your app for multiple windows.

On iOS 13, supporting multiple windows is a fantastic way to make your existing apps more useful and allow your users to be substantially more productive.

We'll be going over three primary topics today.

We'll get started by doing a quick overview of the changes to the application life cycle that will allow multiple windows on iOS 13 to be possible.

Then, we'll dive deeper into the new UIScene delegate and we'll talk about what kind of work you should be doing there.

Finally, we'll go over some best practices in ArchitectureKit so you and your team can take advantage of to make sure you provide your users with a consistent, seamless multitasking experience.

Now, to get started, let's talk about some of the roles and responsibilities of the App Delegate in iOS 12 and before.

The App Delegate had two primary roles.

The first was to notify your application of process level events.

So, the system would notify your App Delegate when your process was launching or was about to terminate.

But it also had the second role, which was letting your application know the state of its UI.

So, via some methods like did enter foreground and will resign active, the system will let you know your UI state.

And this is totally fine in iOS 12 and before, because applications had one process and also just one user interface instance to match it.

So, today your App Delegate probably looks a little bit something like this, except maybe not quite this short.

So, in your did finish launching with options, you do a couple things.

First you do some one-time non-UI global setup, like connecting to a database or initializing your data structures.

And immediately after that, you set up your user interface.

And again, this is totally valid in 12 and before, but iOS 13's pattern is invalid.

Why? Because applications now still just share one process but may have multiple user interface instances or scene sessions.

That means the responsibility of the App Delegate needs to change a bit.

It's still responsible for process events and life cycle, but it's no longer responsible for anything related to your UI lifecycle.

Instead, that'll all be handled by your UIScene Delegate.

Well, what does that mean for you?

Any UI setup or teardown work that you used to do in your App Delegate now needs to migrate to the corresponding methods in your Scene Delegate.

In fact, on iOS 13, if your application adopts the new scene lifecycle, UIKit will stop calling the old App Delegate methods that relate to UI state.

Instead, we'll call the new Scene Delegate methods, and it's pretty simple since there's a 1-to-1 mapping for most of these.

But don't worry - if you want to adopt multiple windows support on iOS 13, that doesn't mean you need to drop support for 12 and before.

If you're back deploying, you can simply keep both sets of these methods and UIKit will call the correct set at runtime.

Now, before we dive into the exact delegate methods, there's one more additional responsibility that the App Delegate gets.

And that is the system will now notify your App Delegate when a new Scene Session is being created, or an existing Scene Session is being discarded.

Let's make this lifecycle a little bit more concrete.

Say I've been working on this little blue app right here and I want to launch it for the first time.

Let's go through the call stack.

First, your App Delegate's going to get the same did finish launching with options call.

Again, it's still fine to do your one-time non-UI setup here.

Then immediately after that, the system's going to create a scene session.

But before it creates the actual UI Scene, it's going to ask our application for UIScene configuration.

This configuration specifies what scene delegate, what story board, and if you specified, what scene subclass you want to create the scene with.

It's worth noting that you can define these scene configurations either dynamically in code or preferably statically in your info.plist.

This also gives you an opportunity to select the right configuration.

You may have a main scene configuration, and you may have an accessory scene.

So, you should look at the options parameter that's provided here, to use that as the context to pick the right scene configuration.

Once you've defined these, for example in your info.plist, it's really simple.

You just refer to it by name, making sure you pass in the role of the incoming sessions role.

Great.

Now our app is launched.

We have a scene session.

But we don't see any UI, and that's where our scene delegates did connect to scene session comes in.

Let's look at what kind of work we should do here.

This is where you set up your UI window with the new designated UI window initializer.

We'll notice that we pass in the window scene provided.

But importantly, we need to also check for any relevant user activities or state restoration activities to configure our window with.

We'll talk about that more in a second.

Yay, now we see our app.

So, what happens when one of our users actually swipes up to go back home?

Well, the old familiar will resign active and did enter background methods get called on your scene delegate.

But, now there's something interesting.

At some point in time after, your scene may be disconnected.

Well, what does that mean?

Well, in order to reclaim resources, the system may at some point after your scene enters the background, release that scene from memory.

That also means your scene delegate will be released from memory and any window hierarchies or view hierarchies held by your scene delegate will be released as well.

This gives you an opportunity to deallocate and release any large resources in memory that were retained by somewhere else in your application that related to this scene.

But it's important to not use this to actually delete any user data or state permanently, as the scene may reconnect and return later.

But then let's talk about what actually happens when our user actually swipes up on a scene session on switch and explicitly wants to destroy it?

Well, the system will call our App Delegates didDiscardSceneSessions.

This finally gives us an opportunity to actually permanently delegate any user state or data associated with the scene such as an unsaved draft in a text editing app.

Now, it's also possible that one of your users removed one or more UIScenes from the switcher by swiping up while your actual app process was not running.

If your process was not running, the system will keep track of the discarded sessions and call this shortly after your application's next launch.

Now, let's talk about some architecture patters that you can consider integrating into your apps.

We'll start by talking about state restoration.

In iOS 13, state restoration is no longer a nicety.

It is crucial for your application to implement scene-based state restoration.

Let's go over why that is.

Here's our app switcher.

I have a document app and I'm planning a road trip and I have four different sessions of different documents open right now.

But I'm really focusing on packing list and agenda.

At some point, these other two backgrounded road trip and attendee scenes, have been disconnected and released by the system.

If I don't implement state restoration here, when I go back to the road trip, I'm not going to return to the state that I was previously in.

I'm not going to scene the document that I was editing.

Instead, I'll just start over, like it's a brand-new window, and that's not a great user experience.

Well, how can we resolve this?

iOS 13 has a brand-new scene-based state restoration API.

And it's super simple.

It works by not any more encoding view hierarchies, but instead just encoding the state which will allow you to recreate your window.

This is all based on NSUser activity as well.

So, if your application leverages powerful technologies like spotlight search or handoff, you can use these same activities to encode the state of your app.

It's also worth noting that in iOS 13, the state restoration archive that you give back to the system will match the same data protection class of the rest of your application.

What does this look like in code?

Well, in our scene delegate, we implement state restoration activity for scene and then I call a method that finds the most active relevant user activity form in the current window.

Then we return that.

After some time, when that scene re-enters the foreground and becomes connected, we check if the session contains a state restoration activity.

If it does, we use that activity.

If not, we can create a brand-new window without any state.

This means that no matter what, our users will never notice when scenes get disconnected in the background because they shouldn't.

Finally, let's talk about one more important issue that you may run into when adopting support for multiple windows.

And that is how to best keep application scenes in sync.

Let me make this more concrete.

I've been working on a new chat application right here, and as we see, I just recently added support for multiple windows on iOS 13.

And I have a chat with my friend, Giovanni, who will join me on stage in a couple minutes, and notice that we're viewing the same conversation in two different view controllers and two different scenes at the same time.

So, let's say I want to send Giovanni a message and let him know I'm ready for lunch.

Oh, only one of our scenes updated.

So, why is that?

Well, it turns out that on iOS, a lot of apps are structured in this kind of way in which view controllers receive an event, maybe via button tap, me pressing the send button.

And then the view controller itself updates its own UI.

After that, our view controller notifies our model or model controller.

And this is mostly okay when we just are talking about one user interface instance.

But now if we introduce a second view controller in a different scene that's showing the same data, at no point is this new view controller being notified to update itself with this new data.

That's an issue.

Well, we can resolve this.

Architecturally, now if our view controllers, upon receiving an event, immediately and only notify our model controller, then we can have our model controller actually notify any relevant subscribers or view controllers, telling them that they should update with this new data.

There are a number of ways that we can do this.

We can use delegates, notifications.

We can even use the fantastic new Swift Combine framework released this year.

But let's go over a lightweight Swift example that you can consider integrating into your apps.

Here's the current method that gets called when I press the return button on sending a message.

We create a message model object.

My view controller updates its own views.

And then we notify our model controller to persist this.

And the first thing we need to do is this view controller should not be mutating its own view state.

Instead, we're just going to get rid of that code.

We'll add that back later in a second.

Now, let's look at what that model controller add method is actually doing.

It's pretty simple.

All we're doing is persisting that new message.

But we actually want the model controller to now notify if any other view controllers or connected scenes that there has been an update.

How are we going to send this update down?

We want a structured way to package this event such that it's strongly typed and it's easily debuggable and testable.

So, let's go ahead and actually create a new type and we'll call it update event.

It's a Swift enum with associated values.

So, we'll add a new message type.

This is the object that our model controller is going to create on receiving a new message and will then send down to any relevant view controllers or scenes.

Because we want to post this, we'll use NSNotification center as the backing store for this.

So, we'll add this handy post method that lets us in one line create a new update event and then post it to any subscribers.

The implementation is fairly straightforward.

We just post a notification to the new message notification name channel.

But the trick here is that we're including the update event object itself in the notifications object.

This will come in handy, as we'll see in a second.

Now, when our model controller is notified that a new message was added, after we persist it, we can just create this new event and call post.

Then, if we look at how we have to change our view controllers, we observe this new event.

In this case, the new message notification name.

And then we create a handler method that we get the notification from in the arguments.

And remember, when we pass the update event as the notifications object, we can now pull that event right back out from the notification.

Then, we can easily switch on the kind of case of the event and because we created an associated enum, we can pull the message out.

Now, we can update our user interface here.

So, let's see what happens when I send Giovanni that same message after implementing this new architecture.

Hey! All of our scenes update.

[ Applause ]

So, we've gone over a lot today.

We've gone over some of the differences in the app versus scene delegate and the differences in responsibilities.

We've also gone over some of the major scene delegate methods and what kind of work you should do there.

We also talked about why state restoration is so crucial for you to use on iOS 13 and how you can leverage the new scene-based API to do so.

Finally, we talked about some high-level patterns for creating a one-way data flow such that we can keep all of our scenes in sync while they share the same data.

Thank you.

[ Applause ]

Apple, Inc. AAPL
1 Infinite Loop Cupertino CA 95014 US