We got new API for colors and gradients, more scrollview improvements, tab improvements, and more.
This is another good year for SwiftUI, with another batch of scrollview improvements, some welcome macOS features, remarkable control over text rendering, and more – the team at Apple have a lot to be proud of, and many developers will breathe a sigh of relief as API such as fine-grained subview control is now public for all of us to use.
But there's also one major architectural change you need to be aware of, so let's start with that…
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
For a long time, the View
protocol looked a bit like this:
protocol View {
@MainActor var body: some View
}
That meant code in your view's body
ran on the main actor, but code elsewhere in your view did not.
This allowed our views to do work across tasks naturally, but caused problems when using @Observable
classes that ran on the main actor. For example, code like this simply wouldn't compile:
@Observable @MainActor
class ViewModel {
var name = "Anonymous"
}
struct ContentView: View {
@State private var viewModel = ViewModel()
var body: some View {
Text("Hello, \(viewModel.name)!")
}
}
That would throw up "Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context", which is a rather complex way of saying "your class says it must be on the main actor, but you're creating it away from the main actor."
When you rebuild your code with Xcode 16 that error goes away completely, and with no work from us – it's just gone. However, it's important to know why. You see, the View
protocol now looks more like this:
@MainActor protocol View {
var body: some View
}
The difference is small, but makes a huge difference: the @MainActor
attribute moved from body
up to the protocol itself, which means the body
property along with all other properties and methods we make are run on the main actor.
You can see the impact with this sample code:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.onAppear(perform: doSampleWork)
}
func doSampleWork() {
Task {
for i in 1...10_000 {
print("Work 1-\(i)")
}
}
Task {
for i in 1...10_000 {
print("Work 2-\(i)")
}
}
}
}
Previously that would run both tasks at the same time, so you'd see "Work 1" and "Work 2" outputs being printed intermingled. However, now that View
runs on the main actor, my whole view runs on the main actor, and therefore the doSampleWork()
work method also runs on the main actor.
This means the onus is on you to make sure you push work off the main actor as necessary, otherwise you'll see a pretty dramatic decrease in performance.
So, the overall change is a welcome one: fewer errors for the most common work. However, it will create a little churn in the short term as you spin work off to other actors manually.
We got another batch of major improvements this year, delivering more scrollview power, incredible new animation effects, and better control over how we place subviews:
Note: This list is currently incomplete; some of the new APIs didn't quite ship for seed 1, and some others aren't quite functioning well enough for me to talk about. Hopefully seed 2 or 3 will see improvements here.
Alongside those major features, we also received some smaller but still important adjustments:
@Entry
macro makes it much easier to create and use custom environment valuesTabView
now has dedicated Tab
children (This sounds small, but the new tab layout needs to be handled carefully to ensure your app works great on both iOS and iPadOS!).rotate
animation for SF SymbolsThat @Entry
change alone is a real delight – it makes things like environment and preference key significantly easier.
We're now five years into SwiftUI, so you might expect the platform has reached maturity. However, there are a handful of omissions that continue to cause problems, and we can only hope these get addressed soon.
First, we still don't have any kind of WebKit or Safari integration. While a full WebView
might perhaps be a lot of work, even some kind of SafariView
to match SFSafariViewController
would be something. I've filed feedback, I've talked to Apple's engineers in labs, and at this point I don't know what else to do. UIKit had UIWebView
in its very first release – what do we need to get something similar in SwiftUI?
Second, working with the keychain remains incredibly hard. This API has always been problematic, but by ignoring it SwiftUI makes the problem worse – it's trivial to use @AppStorage
, but doing so sacrifices essential user security. Sadly, we're in a state where the wrong choice is by far the easiest to reach for.
Third, we desperately need more control over remote images. SwiftUI for iOS 15 gave us the rather surprising AsyncImageView
– still the only API that silently swallows errors, from what I can tell – but years on we haven't acquired any ability to adjust caching, retries, and more. Some configuration API similar to defaultAppStorage()
would make a huge difference.
And finally, TextEditor
still has no rich text support. I can imagine this being an extremely complex task, not least because it's clear the SwiftUI team want their text to retain meaningful metadata rather than just being blobs of attributes. However, this missing support limits where SwiftUI can be used, and I know many apps would benefit from adding more functionality here.
Those are just four ideas, and I know other folks have their own priorities. Please do continue to file feedback with Apple – I know it can feel like a bit of a black hole sometimes, but your feedback reports are read and discussed internally, and every time someone duplicates a request it's effectively one more vote for that feature.
SwiftUI is by far the best way to create apps for Apple's platforms, and this release continues to stretch its lead. Once we reach feature parity with UIKit – yes, WKWebView
and SFSafariViewController
, but also DataScannerViewController
, list section index titles, and pretty much everything that still needs @UIApplicationDelegateAdaptor
– then really there's nothing holding it back.
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.