[SC]()

iOS. Apple. Indies. Plus Things.

iOS 26: Notable UIKit Additions

// Written by Jordan Morgan // Jun 10th, 2025 // Read it in about 4 minutes // RE: UIKit

This post is brought to you by Emerge Tools, the best way to build on mobile.

2019 was the year Apple popped the lid off of SwiftUI. At the time, the responses came quick and fast and they were, well, a bit sensational. “UIKit is dead”, “It’s deprecated!”, “Useless now!”…the list goes on. But, yet again, here we are at dub dub 25’ and more UIKit improvements have landed.

Had initial reactions to SwiftUI come to fruition, you’d think UIKit would have no part in the Liquid Glassification of Apple’s ecosystem. But, in reality — of course it does. UIKit is still the framework propping up iOS, and if you peek under the hood far enough…it’s doing the same for SwiftUI, too.

As it tradition, let’s see what Cupertino & Friends™️ have for us this year in our iron-clad UI framework.

If you want to catch up on this series first, you can view the iOS 11, iOS 12, iOS 13, iOS 14, iOS 15, iOS 16, iOS 17, and iOS 18 versions of this article.

Observable Objects

Let’s start our notable UIKit additions post by…talking about SwiftUI. As one does. Observable made SwiftUI simply better, and now its UIKit’s turn to reap the same benefits. UIKit and Observable classes now work hand-in-hand. This is interesting, as the common line of thinking was that as SwiftUI evolved, the gap between it and UIKit would widen.

Instead, it’s shrinking.

For example, look at the animation interop changes from last year’s W.W.D.C., which allow UIKit and SwiftUI to coordinate animations.

With Observable, UIKit now tracks any changes in update methods like layoutSubviews. The implication? You don’t need to manually invalidate views and do the bookkeeping to update them:

import UIKit
import Observation

@Observable
class HitCounter {
    var hits: Int = 0
}

class TestVC: UIViewController {
    private var counter: HitCounter = .init()
    private let hitLabel: UILabel = .init(frame: .zero)

    override func viewDidLoad() {
        super.viewDidLoad()
        hitLabel.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(hitLabel)
        NSLayoutConstraint.activate([
            hitLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
            hitLabel.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
        ])
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            self.counter.hits = 10
        }
    }
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        
        // Dependency wired up
        self.hitLabel.text = "Hits: \(self.counter.hits)"
    }
}

#Preview {
    TestVC(nibName: nil, bundle: nil)
}

let concatenatedThoughts = """

Speaking of gaps shrinking, just look at all of the SwiftUI-ness in that tiny body of work. Observable tracking. Automatic view updates. Previewing canvas macros. You love to see it.

"""

Here, UIKit automatically sets up observation tracking for the hits property. When it changes, the view gets invalidated, viewWillLayoutSubviews runs again — and just like that, the label is updated. Again, the handshakes between SwiftUI and UIKit seem to grow firmer each year, and that’s only a good thing for the ecosystem.

Also, while we’re here — what’s always been the knock on SwiftUI? It’s that large lists just don’t…list well. While changes have been made under the hood to make that situation better, the conventional wisdom from those of us who’ve been around was something like “Just wrap UICollectionView!”

Now, maybe that doesn’t ring as true with the new long list optimizations? Either way, it is easier to do now. The same technique above extends to UIListContentConfiguration, which means cells in collection views can automatically be updated from Observable models, too:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "customcell", for: indexPath)
    let myModel = store.model(at: indexPath)
    cell.configurationUpdateHandler = { cell, state in
        var content = UIListContentConfiguration.cell()
        content.text = myModel.name
        cell.contentConfiguration = content
    }

    return cell
}

Even better? You can back-deploy this to iOS 18 by adding the UIObservationTrackingEnabled key to your info.plist file.

Update properties

A new lifecycle method on UIView and UIViewController!? What year is this?

I love it. Meet updateProperties(), which gets called right before layoutSubviews(). Like many life cycle functions, you can manually invoke it — setNeedsUpdateProperties(). So, what does it do and when do you use it?

In short, you can apply styling, or hydrate view-related details from models and other related tasks without forcing a full layout pass. You have one tiny thing to update? You don’t necessarily need to update the whole view hierarchy, but that’s just kinda how things have worked up until now. The key takeaway is that this runs independently from other layout methods (i.e. layoutSubviews()).

At a top level, the top-down layout pass goes like this now:

  1. Update traits
  2. Update properties.
  3. Layout subviews.

Apple wants to compartmentalize how you think about view layout now, meaning the job of updating view properties from your app’s data can happen in its own place (updateProperties()) and the job of your layout logic can happen in another (layoutSubviews()):

// Before, these two jobs were typically handled in the same place.
// Now, they can occur independently
class MyVC: UIViewController {
    override func updateProperties() {
        super.updateProperties()

        myLabel.text = myModel.text
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        myLabel.setFrameDimensions()
    }
}

Flushing animation updates

This change rocks. Keeping with the layout bookkeeping, traditionally UIKitters had to change things in an animation block, and then tell UIKit to invalidate the view to lay it out again:

// Before
UIView.animate {
    myModel.confirmationColor = validation.isValid ? .green : .red
    confirmationView.layoutIfNeeded()
}

You’ve got two jobs here — updating state, and telling your view to reflect it. An indecerous workflow in 2025. So, if you’re rolling with observable models, now you can skip the whole “Hey I updated a property, please redraw stuff in this animation block” dance:

UIView.animate(options: .flushUpdates) {
    myModel.confirmationColor = validation.isValid ? .green : .red
}

Due to the automatic change tracking UIKit has, UIKit will just take care of it for you now. We didn’t even have to reference confirmationView, UIKit knows that it depends on the confirmationColor of myModel.

This even works outside of Observable objects. You can even tweak constraint animations using the same .flushUpdates option. If you’ve been HasHTaG BleSSeD in your career to manually hold references to NSLayoutConstraint in the name of simply nudging a label down by 20 points, well…I guess you’re even more blessed now.

Bonus Points

  • Lots of new API around supporting the UIMenu changes in iPadOS.
  • Inspector API tweaks, specifically in regards to using them in a split view controller.
  • Key command now lets you support whether or not the command runs repeatedly or not, keyCommand.repeatBehavior = .nonRepeatable.
  • You can host SwiftUI scenes in UIKit apps via the UIHostingSceneDelegate protocol. Perfect for gradually adopting SwiftUI, or using the immersive spaces only found in the framework.
  • HDR extends to colors instead of only media, meaning you can crank things up when creating UIColor instances, and preferring their HDR selection in the color picker.
  • Symbols have .drawOff and .drawOn animations. It’s a nice touch, and it reminds of the magic replace stuff UIKit added last year.

Also, time marches forward. As such, some good ol’ friends of yore got slapped with a deprecation. UIApplication APIs are deprecated in favor of their UIScene counterparts, for example. To be honest, I thought that had already been done. So, if you haven’t embraced the UIScene world, well…you probably should.

What can I say? Another year, another UIKit. Like previous years, it’s much better. It continues to set the standard for what an imperative UI framework should be like, but at the same time — it’s slowly started to borrow ideas from its declarative counterpart. Interesting times! In iOS 26, UIKit is better than ever.

Until next time ✌️

···

Spot an issue, anything to add?

Reach Out.