iOS. Apple. Indies. Plus Things.

iOS 17: Notable UIKit Additions

// Written by Jordan Morgan // Jun 5th, 2023 // Read it in about 5 minutes // RE: UIKit

I’m really starting to wonder why I write this annual post anymore 😅.

During the entirety of The Platforms State of the Union, I think I heard the term UIKit or UIViewController…twice? Maybe three, if you count the bit where the new #Preview{ } syntax works for UIKit controls now.

But alas - as long as the framework keeps chuggin’ with diffs each W.W.D.C., then I’ll keep writing this annual post. To that end, here are some notable UIKit additions for iOS 17. If you want to catch up on this series first, you can view the iOS 11, iOS 12, iOS 13, iOS 14, iOS 15 and iOS 16 versions of this article.

Unavailable Content
Ah yes, the empty data view. Here’s a snippet of code I currently use to implement it in UIKit, complete with Objective-C and swizzling:

#pragma mark - Swizzle

void swizzleInstanceUpdateMethods(id self) {
    if (!implementationLookupTable) implementationLookupTable = [[NSMutableDictionary alloc] initWithCapacity:2];
    if ([self isKindOfClass:[ASCollectionNode class]]) {
        // Swizzle batchUpdates for collection node which is all we'll need
        Method methodBatchUpdates = class_getInstanceMethod([self class], @selector(performBatchUpdates:completion:));
        IMP performBatchUpdates_orig = method_setImplementation(methodBatchUpdates, (IMP)swizzledBatchUpdates);
        [implementationLookupTable setValue:[NSValue valueWithPointer:performBatchUpdates_orig] forKey:[self instanceLookupKeyForSelector:@selector(performBatchUpdates:completion:)]];
    } else if ([self isKindOfClass:[ASTableNode class]]) {
        // Swizzle batchUpdates and reloadData for table node since we'll need both
        Method methodBatchUpdates = class_getInstanceMethod([self class], @selector(performBatchUpdates:completion:));
        IMP performBatchUpdates_orig = method_setImplementation(methodBatchUpdates, (IMP)swizzledBatchUpdates);
        [implementationLookupTable setValue:[NSValue valueWithPointer:performBatchUpdates_orig] forKey:[self instanceLookupKeyForSelector:@selector(performBatchUpdates:completion:)]];
        Method methodReloadDataWithCompletion = class_getInstanceMethod([self class], @selector(reloadDataWithCompletion:));
        IMP performReloadDataWithCompletion_orig = method_setImplementation(methodReloadDataWithCompletion, (IMP)swizzledReloadDataWithCompletion);
        [implementationLookupTable setValue:[NSValue valueWithPointer:performReloadDataWithCompletion_orig] forKey:[self instanceLookupKeyForSelector:@selector(reloadDataWithCompletion:)]];

// And a whole lot, LOT more...

…it’s…a little easier now. Enter UIContentUnavailableView:

var config = UIContentUnavailableConfiguration.empty()
config.text = "Nothing Here"
config.image = .init(systemName: "plus")

let unavailable = UIContentUnavailableView(configuration: config)

Which yields:

An empty data view

The API is built on top of the content configurations that were introduced back in iOS 14. They have built in configurations for a few things: empty states, loading states and and searching.

Preview Syntax
As I mentioned in the opening, Swift’s slick new macros allow for Xcode Preview support in UIKit. This didn’t used to be straightforward, but it has always been possible. Behold at how laughably simple this is now (please excuse the odd syntax highlighting, my CSS isn’t quite yet ready for language changes):

#Preview("My Controller") {

That’s it, now you’ll get a live Xcode preview just like you would for SwiftUI. Amazing:

Xcode Preview using UIKit

Automatic Label Vibrancy
Labels will now, at least on visionOS, automatically support vibrancy. From an API standpoint, it uses UILabelVibrancy:

let lbl = UILabel(frame. zero)
lbl.preferredVibrancy = .automatic

On visionOS, that’s always set to .automatic, but it’s .none everywhere else. Of note, you can only opt in to vibrancy, you can’t use this API to opt out of it. Plus, it’ll only effect situations where vibrancy would apply, which basically means when it’s over system materials.

Symbol Animations in Buttons
There is a whole lot of new API for animating SF Symbols. I haven’t dug into everything yet, but there are developer sessions later on in the week over it. I’ll be interested to learn more.

In the meantime, there is API I’m seeing for UIButton to get symbol animations by setting one property:

let actionHandler = UIAction { ac in
actionHandler.title = "Tap Me"
actionHandler.image = .init(systemName: "plus.circle.fill")

let btn = UIButton(primaryAction: actionHandler)
btn.frame = view.bounds

// This contextually animates symbols
btn.isSymbolAnimationEnabled = true

…which yields a nice little bobble, bounce animation:

A UIButton animating a plus SF Symbol on tap.

This is available for UIBarButtonItem as well.

Symbol Animations in Images
To stay on those symbol animations for a minute, these are too fun to mess around with. There are several built-in symbol animations, and they all appear to use those juicy spring animations. For example, if you want an up arrow that looks like it drank 14 cups of coffee, here you go:

let symbolView = UIImageView(image: .init(systemName: "arrowshape.up.circle.fill"))
symbolView.frame = view.bounds
symbolView.contentMode = .scaleAspectFit
symbolView.addSymbolEffect(.bounce, options: .repeating, animated: true) { context in
	if context.isFinished {
		print("Animation done - but this won't, because it repeats.")

Behold bouncy boi:

A UIButton pumping an up button over and over.

Even better - combine it with the variable fill APIs added last year, and you can come up with some truly interesting effects:

Timer.publish(every: 0.35, on: .main, in: .default)
    .sink { [weak self] _ in
        switch self?.count {
        case 0.0:
            self?.count = 0.2
        case 0.2:
            self?.count = 0.6
        case 0.6:
            self?.count = 1.0
            self?.count = 0.0
        guard let fillCount = self?.count else {
        self?.symbolView.image = .init(systemName: "ellipsis.rectangle.fill",
                                       variableValue: fillCount)
    }.store(in: &subs)

A UIButton pumping an up button over and over.

I can tell you now, I will be utilizing an insufferable amount of these:

Tons of SF Symbols animating

More Font Styles
We got huge and hugerer now:

let xl: UIFont = .preferredFont(forTextStyle: .extraLargeTitle)
let xl2: UIFont = .preferredFont(forTextStyle: .extraLargeTitle2)

Preview of both of those:

Large titles

New Text Content Types
There are additions to boost autofill, specifically for credit cards and birthdays:

let bdayTextField = UITextField(frame: .zero)
bdayTextField.textContentType = .birthdateDay /* or .birthDate/Month/Year */

// Or credit cards
let creditCardTextField = UITextField(frame: .zero)
creditCardTextField.textContentType = .creditCardExpiration /* or .month/year/code plus several others */

UIKit Nuggets
Of course, I always write this before the developer sessions drop. So, a lot more is coming. Here are things I saw but didn’t cover because I don’t quite understand them fully, or I couldn’t get them to work in beta one:

  • Massive trait collection capabilities: You can observe trait collection changes, make entirely custom ones, fire arbitrary logic when they change and that’s in addition to bringing them to SwiftUI’s environment.
  • Text Sizing Rules: There is a new sizingRule property for labels, text fields and text views which appear to control sizing based off of a font’s ascenders and descenders when using preferred font styles.
  • Scene Activation: I’m seeing a lot of changes around scenes, and how they activate. I assume this is to support the new spaces stuff in visionOS.
  • Menus: Related, but not specifically iOS - you’ll see that context menu interactions are available on tvOS now.
  • visionOS: Of course, we can’t forget this one. It’s not every day we get a new device idiom! Enter UIUserInterfaceIdiom.reality.
  • Efficient Images: There is a new UIImageReader that seems interesting, allowing for tweaks like pixel per inch, thumbnail size and more. It appears to be meant for downsampling or efficient decoding.
  • Smart Text Entry: Support for the inline text prediction enhancements.

Final Thoughts

What else is there to say? The messaging has officially shifted. It started with “UIKit and SwiftUI are both great ways to build an app”, and went to “We’ve got great ways to put the UIKit stuff into SwiftUI, so you can use the SwiftUI-only APIs”, and last year it was straight up “SwiftUI is the best way to build an app.”

This year, UIKit really had the nail in the coffin feel to me, even though that’s hyperbolic. But it’s not where the puck is going, and Apple has made that abundantly clear. Does that bother me? No, not at all. It’s not going away, much like Objective-C is still here - and I’ll always celebrate how efficient, robust and capable the framework has been for my career (and still is).

But that was then, and now? It’s all about visionOS, interactive widgets - the list goes on. And I’m here for it, but I’ll still never forget the framework that got me started in this biz. Long live UIKit 🍻.

Until next time ✌️


Spot an issue, anything to add?

Reach Out.