Improving Existing Apps with Modern Best Practices
The best way to avoid technical debt is to incrementally build-up technical credit. This session builds on last year's Modernizing Existing Apps with Swift presentation to show you how you can continue modernizing your codebase while adopting best practices and adding new feature work.
Oh, please. My name's Woody. I work in software engineering at Apple. I'd like to welcome you to the conference, both those of you here in the room and those of you who are watching from home on the live screen.
There's a couple things I'd like to go through with you in the next 40 minutes. We're going to take a look at some ways that you can today start reducing your technical debt.
Then, we'll take a look at Asset Catalogs.
We'll take a look at the new design pattern. Well, it's not really new but a design pattern we'd like you to start using called Dependency Injection.
And in the latter half of the presentation will be an update from my presentation last year about Live playgrounds.
So, let's get going.
Last year in the session at one of the labs, I met one of you that was describing a scenario. You were describing something that I think is going to be quite familiar to many of you.
You've got your boss that's piling on these requests for features and the features generate revenue and you want to get paid, so you do that.
Then you've got your customers and they want to fix the bugs that cause them pain.
And you take pride in your work so you want to do that too. Some technical debt that you want to resolve. Sometimes I think about building software as like building up a pile of Jenga blocks. In the first version, it's pretty stable and you start piling on stuff on top of it and then it becomes unstable and begins to fall down after that. So, you want to take a step back and maybe resolve some of the lower-level issues.
With all these things, you then come to WWDC. And we pile on new API.
And then we give you a new version of Swift and it breaks source code compatibility but introduces some new features. And you were warned, so don't complain about it.
And then we introduced new platforms and new extension points and some of our existing apps like Siri and Messages.
And if you want to think of your role as a developer as a bit of a run loop with all these events being pushed into it, well, you know what happens then. There's too much stuff.
So, what you do? What are some things that you can start doing to get ready for the fall? First of all, if you were to support a deployment target of iOS 8 and 9, you would cover 95% of the devices. So, there's really no need any more to have any of those deployment targets set to 7.
Let's get rid of that. In fact, the general idea is that you take the current shipping version, so 9.3, and then set your deployment target to one version back, maybe 8.4.
But don't go to 8.3 or 8.2 anymore because then you and your customers don't benefit from the improvements that we've implemented under the hood in that 8.4 release.
When we come out with iOS X in the fall for the public, set your deployment target to 9.3. Next, Issue Navigator.
Resolve those issues. Take a look at them and fix them.
When we tell you that there's, when we tell you that there is a deprecated API, we deprecate API for a variety of reasons, including to achieve better error handling or to better, have better reporting or to permit more performance or just add flexibility in the arguments return values. And there's really no excuse for not moving on to the new API because we tell you right there. It tells you what to use. So, just use that. Switch over those.
Next, you might decide that you want to treat warnings as errors and we've had that for a while with Objective-C. But now we also have it in Xcode 8 for Swift. I know.
I think I'm the first person to tell you that.
I really like this idea. I think it's fantastic, treating warnings as errors, because it forces you and your team to address the issues. It's far too easy to ignore those yellow warnings and think you'll come back to them later and then you don't and that's the whole technical debt thing. Also, do you think this would work if you pitched maybe a 1.0 of a new project, a new app, and you said your team, we're going to ship 1.0, but we're not going to have any art work in it, it's not ready yet. We're not going to have any icons in it. We'll ship it and we'll get those caught up in the .1 release.
That would never fly. No one's going to do that, because this is how users interact with your app.
Well, actually it's how many users but not all of them interact with your app. Many of them interact with your app using the accessibility features that we have built into the operating systems.
So, why would ship your app with artwork for people that need it or want it or use it but then not have the accessibility features in there? That's not fair.
So, resolve some of the technical debt that comes to accessibility. Add support for that. We think that the accessibility support is as much a part of your user interface as the artwork is.
It's built into Xcode, so you can use Interface Builder for that. It's also easy to do programmatically.
We have a lot of locale-aware API.
You should be using those and that's less code that you have to write.
That generally isn't new, though.
But these are.
Dimensions and measurement formatting. If you've got recipe apps or fitness apps and you've been converting between metric and imperial on your own code, get rid of that code and use our code that's now available in the fall release.
We'd like you to support Peek, Pop and Quick actions. Right? You know, if you have iPhone 6S right now, you're probably accustomed to let's push hard on this and see if it does anything. No, nothing. So, let's push hard on that and see if it does anything. Well, that one does something. It's hard to discover. How awesome would it be if every app that we had already had support for Peek, Pop and Push and the 3-D touch? Next. We want you to run the Swift migrator using the developer preview of Xcode 8. If it doesn't migrate your code, file a bug report and tell us. Then we have an opportunity to fix that so that when you run migrator for real in the fall on the final release of Xcode 8, it'll likely work. If it doesn't work, we resolve some of our own technical debt with our API. The public interface is the same but the implementation might change between releases. And it could be that the way you use whatever API is a way that we didn't expect.
So, you have an edge case.
So, tell us about that too. This is why we do these preview releases.
We want you to file bug reports.
The report isn't just banter in the hallway. And it's not just writing on a dev forum and is not just sending an email to Apple.
The proper way to file a bug report is this. BugReport.Apple.com. Fill in the blanks.
And then once you do that, you get a number and then that's the number that you go and post back into the dev forums. There's a lot of Apple engineers that read the dev forums. We like to hear what you are saying but when you file, or when you report an issue in those groups and you don't include the bug report number, it's difficult for us internally to kind of follow through and get traction on it.
So, do that.
Next, you're probably not a great fan of filing bug reports with us.
Because you've filed them and then you wait and nothing seems to happen.
You get nothing back.
In the words of Whitney Houston, where lonely bug reports go? Well, they seem to go in this great big black hole because you never get anything back.
Or you think maybe you get it in front of an engineer and the engineer kind of ignores what you're saying.
I want to just assure you, this is not the case. One of the first things I did when I joined Apple is I did a search in the bug tracking system, the issue tracking system, to find all the issues that I filed as an external developer. And a lot of them had traction. I just didn't have any feedback from them. So, I wanted to show you that if you do file a bug, we do look at them.
The other thing that I find is you might spend a lot of time filing a bug.
A lot of time maybe you're making some sample code. You're trying a couple different devices. You're checking for regression. Like, that's great stuff to do. It takes you maybe half an hour or maybe an hour, and then you file it and it comes back and it says it's a dupe.
And you're like, oh, I just wasted half an hour, hour my life for something that somebody else already did.
So, I just want to briefly talk about dupes.
Because dupes first of all are not a voting system. It's not like if you copy somebody else's bug verbatim and you file a new bug with exactly the same thing and it, we're going to say oh, there's another vote for that, we should fix that first. It's not quite like that.
What we do do with them though, even though they're a dupe, it just means these two are related.
We need dupes too, not for voting purposes but because we can have five people file the same root bug, same root issue.
The first four don't give us enough information to be able to find it.
But maybe it's your report, maybe it's your fifth one which, albeit, is a dupe, but has the information in it that we need to find the issue.
In fact, an Apple engineer that's been with the company for gosh, a couple decades, made a quip a couple weeks ago.
Paul had said that each bug report as its host link.
All right. So, in summary, things to get going today. Fix your warnings. Replace your deprecated API.
Localize your App if it hasn't already.
Or support accessibility rather. Get Peek and Pop in there. And then take a look at the next release of Xcode and give us some feedback on that.
With that, let's move onto Asset Catalogs.
It's time to move onto Asset Catalogs.
For you. So, if you still have files in your File Navigator like this, go ahead and get them into an Asset Catalog like that.
The approach is you add a catalog from the File menu.
By the way, you don't have to add just one catalog. You can have as many catalogs as you like in the project.
Maybe you're working on an app and that app is a card game.
So, you have a catalog for the front images of the cards and a second catalog for the back images of the cards. We can do that.
Another example, you might have your graphic designer download Xcode for free from the App Store and create the Asset Catalog for you.
And then send it to you or check it into a code repository. We could do that too.
When we copy files into an Asset Catalog, they are exactly that. They are copies. We don't reference the original location ever. So, they do participate in version control.
To migrate your project, you tap the big plus button.
Choose Import From Project.
We're going to give you a list of all the Assets that are eligible to be migrated from the File Navigator into the Asset Catalog.
There you go. You migrated your image Assets.
If you previously used to use the bundle API, pathForResource etc., that doesn't work anymore because once we compile the app, the images aren't free-floating around anymore. So you won't find anything.
But if you're using image named, then we'll find them if they're in an Asset Catalog and we'll find them if they're just free-floating in your File Navigator still. This API has been around for a while. This API Image Name has a lot of advantages above and beyond just being able to find content from an image catalog.
For example, it internally caches the first time that you asked for an image by name. We load it up, we give you back a reference. The second time you ask for the same image by name, we just return another reference to the same thing.
That's not how the contents of file API works. Contents of file loads new images every time.
So, it's much more perform an, especially when you're scrolling through a table, to use image names. We support multiple representations of an asset. So, with image names, you'd give it a name, the API name.
You'd let the framework consider the device that you're running on.
Consider the resolution of the display, retina, non-retina, retina on a Plus device.
And possibly other differentiating factors for the media assets such as the amount of memory in the device or the version of Metal that is supported in the device.
And you get an image back.
There's two broad kinds of asset types for images that you can consider. We have scaled images. Those are like PNG's, JPEG's. And then we have single vector images or just vector-based images, like PDFs or SPG's. We treat them a little bit differently.
On an asset by asset basis, you can say this asset's going to be vector-based and that asset's going to be scale.
You do that by specifying the scale factor.
So, I'm going to talk first about some of the scaled images and then move onto the vector-based images.
For scaled images, in this example we're asking you to provide three different renderings of the artwork from 1X devices, non-retina, 2X and 3X.
If we don't find, because you didn't include it, the 2X and 3X, we'll take the 1X image and we'll scale it up so that it becomes, well, an image that's used on the higher end devices or the higher density devices. Likewise, if you just provided us with 3X artwork, we'd scale it down at runtime.
These two scenarios aren't that great. In this scenario, if I take the 1X image and I scale it up for an iPhone 6S Plus, it's going to look really jaggy. It's going to have a visual artifact called aliasing. It's not very desirable and the, well, your customers aren't going to enjoy it.
This one, at first glance you might think oh, I'll just provide the 3X artwork and let you scale it down.
But there's a huge problem with that because to scale down a 3X image, we have to open a 3X image. And it's really big.
And then we take an extraction of the pixels and create a scaled-down version for it.
It's an order, two orders of magnitude, possibly larger.
So, consider this.
You're on a device like a 5S, 5C.
There's no need for 3X images on it but you only provide 3X images.
So, we start off with the baseline of memory in use.
That's fine, but then we have to load up the 3X image, down convert it, get rid of the 3X image and keep this scaled-down version of it, which just temporarily creates a memory spike. And if you're good, it's probably going to be fine. But if you good, I mean if you're lucky it's going to be fine. But let's say you're scrolling at a table view and as you're scrolling, we have to do this a lot. All of a sudden the memory utilization for your app balloons and then what happens when you use too much memory? Anybody know, just say it out loud. Yeah, right. We terminate it. It's terminated because you didn't provide the artwork. So, provide the artwork. In fact, you can do this really easily with the task or, not a task but with an Automator workflow.
You give it a 3X image, let it scale it down, give it a name. Scale it down, give it a name. Great, throw those all into your Asset Catalog. And if you wanted to, you could, and this is another best practice. Use this naming convention, the non-retina ones is just a line justify.PNG. And then add 2X and add 3X the other two.
And then when you drag and drop the three of them into an Asset Catalog, we detect a naming convention. We create one asset with the three different representations as opposed to three separate assets.
For Vector Assets, Vector Assets are amazing because the file contains a set of instructions on how to draw the image as opposed to having it pre-rasturized.
This is the same image, not three different versions of it. Same image scaled to different sizes. When you specify that you're going to use single vector scaling, you're providing one vector-based image. And at build time, we rasterize it into the different sizes that we need.
It's much simpler for you. This is the kind of thing you'd likely do for toolbar images and navigation bar images.
There you go, scale factor single vector.
It's also possible to combine both.
You can set the scale factor to be vector and scales.
And what you do is you provide us a vector-based image, like the limits in the well called All.
Then, if you want to do any over-rides for the other scale factors, you just provide those as either other vector images or PNG's, JPEG scaled images.
Then when we build it, if we see that you're missing some assets, we'll just rasterize those based on the vector image that's in the All truck well or target.
Otherwise, we'll use the scaled ones that are provided.
New in Xcode 8, you can adjust the compression.
Come on, you.
You can adjust the compression. So, we can say for a JPEG image you want to use glossy compression but maybe for a PNG we don't want any compression all because it's going to be an item in the toolbar. We don't want to see any artifacts with that.
Another issue you can resolve by using Asset Catalogs, just in case the pitch isn't strong enough yet, is to fix the issue or resolve some issues. We're referring to rounded corners.
The issue with rounded corners is we have adaptive UI.
So, your button might contain a text label that fits perfectly well until the app is run in a different language and then it changes the size.
And when we change the size of the button, we still want to preserve the beautiful rounded corners on it.
The way you would even get a rounded button in the first place is in Interface Builder you'd select a button and specify a background image.
And then we apply that as the background.
In this example, I have an asset called rounded rectangle and I apply that as my background rectangle but you can see in the two sizes of the button, when it's stretched out it's really horrible looking.
So, one way you can fix this is the way that you've been fixing it all along which is using Stretchable Image.
And you say well, I want to preserve X number of pixels on both sides.
And that's fine but one of the themes of this presentation is having you write less code and rely more on the frameworks and the tools that we provide.
So, instead this is built into the Asset Editor. It's called Asset Slicer.
And you can use it to specify that, the portions in red, don't stretch these.
Don't distort them.
The portion that's illuminated, repeat these pixels.
And by doing that, you get perfect rounded corners and you don't have to do any code for it.
This is also something you could have your graphic designer do because it's part of an Asset Catalog. All right, next up, let's talk about a design pattern called Dependency Injection.
Let's first of all talk about what we're kind of fixing or trying to change by introducing this. So, we have UITextField and it works with the delegate and the delegate is called UITextField delegate. Fine. And in it, it contains methods that relate to text fields, like textFieldShouldBeginEditing.
Okay? We have WCSession. And WCSession delegate. What's inside the delegate? Methods that relate to WCSession.
You can see the pattern here.
Which we break with this one.
We have the app delegate and of course it has methods that relate to the application object.
But then we stick other stuff in there too like databases and essentially eventually you have everything in there including the kitchen sink just piling up. And we do it because it's really easy to reach back through the Application object, get the shared object, cast it and retrieve it. But it's such a strong coupling between your view controllers and your app delegate and your app delegate doesn't really need to be the place where that goes.
Instead of a pattern like this where each view controller reaches back to some common object, maybe that you're storing in your app delegate, you could switch it.
Using Dependency Injection, you take the model object that one view controller has and you pass it forward to the next view controller at the time that view controller is presented.
The idea is you give the view control everything that it needs to do its job.
So, for mail, you'd have a list of mailboxes.
A view controller that shows a list of mailboxes would have a model object representing an array of mailboxes. You tap on one of the mailboxes. Another view controller's going to present to show the messages in that mailbox. The middle one. And you pass forward the one mailbox and it shows those and you keep passing it forward.
The way you would do that in a segue is to override prepare for segue and pass it for repair.
The way you would do it if you were programmatically presenting is in the action for the button viewed in instantiate. The view controller, pass model object there.
The benefit of this technique is your view controllers are standalone now. They don't have these strong ties and dependencies. You can reuse them more.
Coming back, you've got some choices. You can do what we do, which is often to write a protocol and have you implement it so that there's a call back to say the view controller's dismissed and maybe update your model there. You can pass in a closure.
You can pass object models by reference. Or, you can, if you're using online segues, it's exactly the same as overwriting prepare for segue because it happens in both ways, forward directions and back directions.
Another reason why we like this is because view controllers are pretty much like Lego blocks.
They should be able to be rearranged independently of each other to create new structures. In this case, an iPhone SE's overall user experience is going to be different for your app than it would be on an iPad 12 inch, iPadPro 12 inch.
If your view controllers are independent, it's easy to do that. All right.
Now, last year at WWDC, I gave a presentation on topics including playgrounds and modernizing the UI.
And interoperability with Objective-C and Swift.
And since then, we've added things to the playgrounds, things that we couldn't do last year. So, I want to show you some of those things.
With that, we're going to move to a demo. So, this is the periodic table of elements app that I showed you last year. Since then, it's been updated a little bit. For example, the colors have changed a bit. Instead of using states of matter, gas, liquid, now it's the kind of object like a transition metal, metal, halogen, that kind of thing. We've also added support to render it out so it actually looks like a periodic table of elements and that's a collection view doing that.
I want to show you some of the code. One of the things that changed with this code base since the last time that you saw it is that the data model has moved over to Swift.
So now I have a class the represents an atomic element like nitrogen or oxygen.
And I have a class that represents a collection of those called, excuse me, periodic elements.
This class instantiates the elements, the individual atomic elements, by reading in a property list.
Which I've also stored in my app here.
But now that I've moved the data model to Swift, I can show you the data model in a playground.
Last year when I did this, I'm just going to make a new playground first.
New file, playground.
Last year when I did this, I was copying, pasting between my code and my playground.
And when you do this a lot, your playground can become really big with a lot of code in it. So, instead our playgrounds now have subfolders for source code and resources.
I can take my model objects and put them into the sources folder.
And then we compile them behind the scenes and we implicitly import all of their public symbols into the playground to make them available for you.
Which means if I take this resource file, my property list, and include that in the resources folder which makes it accessible to the playground, I can come over to the playground here and instantiate an instance of the atomic elements.
So, the ability to take these source files and have them automatically imported into your project when they're in the sources folder or the resources folder is a pretty, I think powerful way to keep your playgrounds themselves small and concise while also having more content inside of them. Now, above and beyond that, something else happened since last year that affects this app in particular. The International Union of Pure and Applied Chemistry confirmed the discovery of 4 additional elements.
So, now all of a sudden this Property List file that I've got embedded inside my app doesn't seem like such good idea anymore.
So, I thought well, we can fix that. We'll just put it online.
So I set up a little Web server and put it a JSON file at this time with all the elements.
And then the idea is that when my app launches, initially it'll use the embedded built-in data file.
That way if there's no network connectivity, there's still something for the customer to see. And this is also a pattern we recommend that you do.
Then we'll do a background check, and in the background we'll check to see if there's an update to the file. And if so, we'll put it in the caches folder and we'll reference that inside the app.
So, I want to do the network request in the playground.
And for that, we have the class formerly known as NSURL Session and now known as URL Session.
Which is shown here.
We grabbed the Session and the thing I like about the Session when I call it out on line 24, is I give it a completion handler.
Because this runs asynchronously in the background.
Go to the Network Request, let my code continue to run.
When the request either times out or the data comes in, it gives us call back. And in the call back on line 28, you can see I'm printing out one element here.
But you actually don't see it over there.
That's a problem.
So, just think about what's happening here.
We have a playground that's running.
We asked it to do a background operation.
At some indeterminate time in the future, we get a call back saying this is what we want to do.
But playgrounds typically execute like a script. The first line all the way through to the end and they stop.
Key word being typically. But they don't have to.
We can get a runloop and have asynchronous operations in our playgrounds.
To do that we import a new module called playground support.
And then you ask the playground for indefinite execution. And then it never stops, it just keeps processing.
So, now if I scroll down to this section, you can see I'm getting my callback.
There's cadmium. All right.
Next I want to expand on last year. Last I showed a drawing, a rounded corner core graphics image.
This time I don't just want to draw an image and preview it. I want to do a whole table view controller in the playground. And we have some new features in the playgrounds that let us do this.
Let me just open up some space in my app.
Call it View.
So, I'm going to make a subclass of UI Table View Controller.
This is all that it is, a selected area. I'm implementing the 2 methods at every Table View or Table View Controller are required to have. Number of objects, sorry, rather number of items, self-wrote index that and returnings themselves.
And I'd like to see that live.
So, this time what we do is we instantiate the view controller and then using that playground support module that I've already imported, the one that's right here on line 4, I grab the playground's Live View.
And then we take that view and we render it into the assistant editor who's no longer an Assistant Editor but rather our Live View.
See? It's a Live Table View. It scrolls, it's active.
Thank you. Is not just a static image. It's a real interactive table view. If you wanted to try some of our new delegate methods or even the existent delegate methods, you could throw them into the playground and now interact with them.
No need to put this into the simulator anymore.
So, now I want to take the two things and put them together.
Specifically, I have this background network request that's happening. And after I retrieve the data, I want to reload my table view. So, let me show you that. First of all, comment this.
And just to prove to you that it's totally loading the data from that site, I'm going to change this so I have an empty array of elements. All right.
So, I have an empty array of elements. I do the network request.
When the network request comes back and it's call back, it's a sign.
The new array of elements that I just pulled in.
And then we reload the Table View.
Is that what you would expect? It's not what I expected the first time that I saw that.
Oh, that must be a bug in Xcode. I'll just go file a bug report at Apple.com.
But it's actually not.
It's a bug in my code, believe it or not. It's a bug in my code and here's what's happening.
We have this background operation coming in.
The background operation has a background operation. It's not on the main thread.
UI is in the main thread.
So, in my closure, the completion handler, I can't be updating the main thread from there, which is what I'm trying to do when I reload my Table View.
When I tap on the cells, we are invalidating them and that's why they do refresh and you can see them at that time. This is also something we can fix. We have something new in Swift 3, which is a Swift API for Grand Central Dispatch.
So, instead of that C API we had before, I can now say a dispatch cue, which cues the main cue. What kind of operation? An asynchronous one. We don't need that anymore.
And here's what I wanted to do on the main thread. Now it bounces back to the main thread and it's there.
Dispatch cue, thank you, dispatch cue in Swift, Live Views in Swift and indefinite execution.
Let's switch back to the slides.
Thank you. So, just as a point of summary, there's the key points from the previous presentation, or the preset demonstration.
Some other tips. When I dragged those files into the sources folder or the resources folder, they are copied. They are not referenced. Also, only methods that are marked as public, methods, properties, and datatypes, etc that are marked as public are going to be available to you in the playground. So, you might have to go add some public specifiers because the default visibility specifier in Swift is internal, not public.
I mentioned using a caches folder.
Now, we still have some devices out there that are constrained storage wise.
And those customers try to updates to new versions of the operating system and they find out there's not enough space.
So, what we do when that happens, when the device is under storage pressure, is we go through the caches folders and we delete everything we can find in all the applications caches folders. This way we free up space.
We need your help with this. If you are temporarily downloading data and you're not putting into caches folder, put it in the caches folder. Next. I thought it'd be really neat to take, it's doing that again.
To take the view controllers that use dependency injection and rearrange them in a way that might suit a different platform.
So, I'm going switch back to the demo.
Turn on my Bluetooth.
And take the view controllers as is over the tvOS.
Well, there we go. I have paired my Siri remote to my Mac so I can use it with the simulator.
It's exactly the same code from before, same view, same table view, all that stuff is there. I can go over and I can see the periodic table like this. And I get that neat TV effect when I slide over the cells and its collection view.
It's the same layout code. It's the same view controller. It's all the same as what I had before.
I added this line of code so that when I selected a collection view cell, you would get that focused item.
And I used the OS Compiler directive to say this is only for tvOS.
Other than that, it's the same code.
Which kind of brings me to another point that if you ever think you are taking an app from one platform and just dropping it on another platform with no real other changes, you're possibly doing something wrong and might want to check out the human interface guidelines just to see how you can make that platform and that app on that platform more native and feel more like it belongs there.
Let me give you an example. Let's say that you made an app that does some sort of like cloud-based accounting.
And maybe you have an iPad version and on the iPad version, people fill out invoices.
It makes no sense, in my opinion, to fill out invoices on a 60-inch TV. You just wouldn't do it. Why would you do that? This is not an app you would just take as is and launch it on tvOS.
But, the iPad version of your app probably has some great visualization about how the company is performing. Maybe you've got some graphs or charts.
Those would be awesome on a TV. You could have a version of your app that has a Live Status board of how the company's doing. And maybe in the company's boardroom or something it's just on and it just animates through and shows an update. It's the same data model. It's the same data access.
But you pivoted that data in a way that makes it more applicable for the platform in which it's running. And that's an idea I want you to think about. How can we take the same data that we have and just pivot it in a way that works for that platform? In summary, modernizing your app is an ongoing process. We want you to rely on the frameworks as much as you can.
If you can get rid of code that's in your app and instead use code that we've got in our frameworks, that's code let's code for you to maintain.
We'd like you to do that.
We want you to get started today with finding issues with Xcode, with the Swift converter. We want you to architect your app with fewer inter-object dependencies so it's easier for you to rearrange your app.
Finally, we want you to consider bringing your app to other platforms of ours by pivoting your data model.
There are some related sessions you can check out.
There you go.
And other than that, you can check out our link for this particular session. And that is it for me. Thanks, everyone.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.