2021 felt like a bit of a relaxing year for us SwiftUI developers. We got some small tweaks here and there and some fun new views like TimelineView, but nothing to shake the boat too much.
This year, that all changed, with the introduction of a huge swathe of updates that I think finally answer the question "is it ready for production?".
We've listed our favourites below (and how to use them with a sample coffee browsing app).
Navigation has always been a bit of a challenge in SwiftUI. NavigationLink is a useful tool, but getting control over your navigation stack in code means having lots of empty links with stored bindings to activate them. Deeplinks have been a huge pain as a result if you don't want to fall back to UIKit.
Apple has solved all these challenges with NavigationStack. A powerful, programatic interface that allows us to keep the simplicity of SwiftUI but regain the control we had in UIKit.
Switching to the new navigation setup requires just a few quick changes. We replace NavigationView with NavigationStack, and provide it with a path argument. This can be simply an array of anything hashable, so we'll just pass it an array of Coffee as our path.
We'll then swap our NavigationLink for the new initialiser that takes an object as its value argument, and leave our labels the same as before.
Finally, we'll use the navigationDestination modifier to tell SwiftUI what the destination should be for any given hashable. You can have multiple of these, so your stack could support multiple objects.
Let's see what that code could look like.
Here, we've gone a little further than the default. We've passed a function to our child view that lets it pop to the root. With NavigationStack this is simple as you can just empty the array of navigation items and the system will pop back to the root element.
We've also used onOpenUrl to enable deep link support. Its quick and easy to empty the stack and add a new coffee when someone opens a deeplink.
They didn't just stop at NavigationStack though. For those of us who need to make sure our apps run nicely on both iPad and iPhone, there's NavigationSplitView.
This lets us define a sidebar to make the most of that extra space, with a detail view that can be anything we want.
Here I've combined it with a NavigationStack so that the selected view can perform navigation of its own.
When the selected coffee changes, I reset the stack so our root is the correct coffee, whilst allowing the detail view to do whatever it wants. This enables the view to function perfectly even when its collapsed.
When we run this on the iPhone, the view automagically collapses into a standard stack style navigation. This same collapsing applies when we shrink the size of the window on an iPad, like in the brand new stage manager.
SwiftUI has always struggled with views that need to be sized based on the width available to them, which then define their own size. This was previously trivial in UIKit with relative sizing, and tools like UICollectionViewFlowLayout, but we just didn't have a way to do it - until now.
Layout allows you to have granular control over sizing, allowing you to place individual views wherever you want - without GeometryReader.
The best example of UICollectionViewFlowLayout was the ever popular tag layout. This list of tags should fill as many as it can on each row, before flowing to the next one when it runs out of space. Lets re-build that with Layout to get to know it.
To get started, you'll need a new struct that conforms to Layout. This requires two new methods, sizeThatFits and placeSubviews.
sizeThatFits needs to tell SwiftUI how big the view will be when its laid out, and then placeSubviews actually needs to place the views. Both of these methods give us excellent control over the placement and size of everything.
The best way to make use of these functions is to build a custom layout object that you can share between both of these functions. This will be highly specific to each view that you build, but here's what ours looks like. You can see the complete code in our sample at the end.
Here's the resulting layout in-use within a list. It's really simple to use your custom layouts, and all that complexity is hidden away.
Completely out of the blue, we got a powerful, beautiful charts library that allows us to create lovely visualisations of data with minimal effort.
To get started, all you'll need is a simple data set and a place to put your chart.
For our example, we're going to show the distance run each day over a week. Our data source is a struct that has the day of the week as text, and the distance as a double.
Chart is the core building block, which wraps around our data. Inside Chart we'll use variations of Mark to show our data. We're going to start with BarMark, which we'll provide axis values for.
These marks are highly styleable, so in our case we'll use a lovely teal gradient. Thanks to the new .gradient variable available on every SwiftUI color, this is a one-liner.
There's plenty of options for your charts, including various types of marks, styles and labels. Here's a small set that we came up with as we got to know it better.
Grid is another tool in our layout chest that allows us to create simpler layouts, faster. It's a stripped down version of LazyVGrid and LazyHGrid that puts more of the responsibility on the developer. You get explicit control over what items are in each row, and how many columns they take up. If you're building something simple like a grid of squares, or something more complex with a highly specific placement, Grid could be the tool for you.
You'll use Grid as the wrapper, and then GridRow to define the contents of a row. If you need to, use the gridCellColumns modifier to tell the grid to make an item fill more columns than it would by default.
Grid has some really useful convenience features too, like the fact that the column will make sure its wide enough for the widest item. This means that layout consistency is much easier than before - check out how this text is given the space of the longest item.
Our final view is a small improvement, but it goes a long way to ensuring that we get the most out of our content whatever size we render it on.
ViewThatFits allows you to provide multiple different views which SwiftUI will then iterate over and find the one that best fits the container. A great example of this would be instead of having text that truncates when the view is too small ( or the text is too big ) you could use ViewThatFits to slightly alter your copy.
You can see here that as the view gets smaller, I change the font size a bit to try and avoid truncation, before finally shrinking my copy a little and making the font even smaller.
So they're our favourites from WWDC for SwiftUI. We think these additions will make a huge difference, and thats before we even look at the other new tech in iOS 16.
You can find our sample code in our iOS engineers sample repository, here. Give it a download and have a play around - you'll need Xcode 14 beta one to do so.