WWDC Quick Look 💓 By SwiftGGTeam
What’s new in UIKit

What’s new in UIKit

元の動画を見る

ハイライト

iOS 26 の UIKit では Swift Observation が layoutSubviewsupdateProperties などの更新メソッドに統合され、@Observable プロパティを参照するだけで依存関係が自動的に確立され、ビューも自動で無効化されます。手動で setNeedsLayout を呼ぶ必要はもうありません。


主要内容

UIKit を長く触っている開発者にはおなじみの流れがあります。モデルが変わったら setNeedsLayout を呼び、layoutSubviews の中でモデルを読んでビューを更新する。アニメーションを書くときは closure 内で手動で layoutIfNeeded() を呼ばないと、制約の変更がアニメーションに乗ってくれません。こうした依存関係はすべて人が頭で管理しており、書き漏らすと更新不足や過剰更新が発生します。とくに UICollectionView の cell 構成では踏み抜きがちなポイントです。

iOS 26 の UIKit は、Swift Observation をコアの更新フローに直接組み込みました。layoutSubviewsupdateProperties、cell の configurationUpdateHandler といったメソッドの中で @Observable オブジェクトのプロパティを読むだけで、UIKit が自動で依存関係を記録し、プロパティが変化したタイミングで該当ビューを正確に無効化してくれます。新たに追加された updateProperties メソッドは「コンテンツ・スタイルの更新」を「レイアウト計算」から切り離したもので、前者は layoutSubviews よりも前に走り、両者は互いに独立しています。これにより、関係のないイベントでレイアウト全体がトリガーされてしまう事態を避けられます。アニメーションには flushUpdates オプションも追加され、UIKit がアニメーション開始前に保留中の更新をすべて自動で flush してくれるので、layoutIfNeeded() を手書きする必要がなくなりました。この組み合わせにより、UIKit の書き味は SwiftUI の宣言的なメンタルモデルにより近づきつつ、従来のビュー階層とパフォーマンスモデルはそのまま維持されています。

これに加え、iPadOS 26 では macOS のメニューバーが iPad に持ち込まれ、画面上端からスワイプダウンするだけで呼び出せるようになりました。UISplitViewController は inspector 列をネイティブにサポートし、SwiftUI の scene は UIHostingSceneDelegate を経由して UIKit app に組み込めます。UIColor は HDR に対応し、通知システムは強型付きの NotificationCenter.Message へとアップグレードされました。SF Symbols 7 では draw アニメーションも導入されています。さらに重要な変更として、iOS 26 以降のバージョンでは最新 SDK でビルドした app は UIScene ライフサイクルへ移行しないと起動できなくなります。


詳細

自動観察トラッキング: @Observable を読むだけで追跡

UIKit は layoutSubviewsupdateProperties などのメソッド内で参照した @Observable プロパティを自動で追跡してくれるようになりました (10:54)。

// Using an Observable object and automatic observation tracking

@Observable class UnreadMessagesModel {
    var showStatus: Bool
    var statusText: String
}

class MessageListViewController: UIViewController {
    var unreadMessagesModel: UnreadMessagesModel

    var statusLabel: UILabel
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()

        statusLabel.alpha = unreadMessagesModel.showStatus ? 1.0 : 0.0
        statusLabel.text = unreadMessagesModel.statusText
    }
}

ポイント:

  • UnreadMessagesModel@Observable を付けるだけで、2 つのプロパティが観察可能になります。
  • viewWillLayoutSubviews 内で showStatusstatusText を読むと、UIKit が依存関係を自動的に記録します。
  • 以降、これらのプロパティのいずれかが書き換えられると、UIKit がそのビューを自動で invalidate し、viewWillLayoutSubviews を再実行してくれます。
  • KVO も Combine も setNeedsLayout の手書きも不要です。
  • iOS 18 では Info.plistUIObservationTrackingEnabled を追加することで、後方デプロイにも対応できます。

updateProperties: コンテンツ更新とレイアウトの分離

新しく追加された updateProperties メソッドは layoutSubviews より前に実行され、コンテンツ、スタイル、振る舞いの構成を専門に担当します (13:27)。

// Using automatic observation tracking and updateProperties()

@Observable class BadgeModel {
   var badgeCount: Int?
}

class MyViewController: UIViewController {
   var model: BadgeModel
   let folderButton: UIBarButtonItem

    override func updateProperties() {
        super.updateProperties()

        if let badgeCount = model.badgeCount {
            folderButton.badge = .count(badgeCount)
        } else {
            folderButton.badge = nil
        }
   }
}

ポイント:

  • updateProperties は trait 更新のあと、layoutSubviews の前に実行されます。
  • ここでは badge のコンテンツだけを構成しているため、badgeCount が変わってもこの部分のロジックだけが再実行され、レイアウト pass 全体は走りません。
  • 内部では trait collection を読めますし (この時点で更新済みです)、レイアウトを invalidate することもできます。UIKit はその直後にレイアウトを実行します。
  • 手動で発火させたいときは setNeedsUpdateProperties を呼びます。
  • これは layoutSubviews を置き換えるものではなく、補完するものです。コンテンツ・スタイルはこちらに、ジオメトリ計算は layoutSubviews に置きましょう。

flushUpdates: アニメーション内で更新を自動 flush

iOS 26 では UIView アニメーションに .flushUpdates オプションが追加され、アニメーション開始時と終了時に保留中の無効化が自動で適用されるようになりました (16:57)。

// Using the flushUpdates animation option to automatically animate updates

// Automatically animate changes with Observable objects
UIView.animate(options: .flushUpdates) {
    model.badgeColor = .red
}

ポイント:

  • closure 内では @Observable プロパティを書き換えるだけで、layoutIfNeeded() を呼ぶ必要はありません。
  • UIKit はアニメーション開始前に一度、終了時にもう一度更新を flush します。
  • 該当プロパティに依存しているビューは自動でアニメーションに取り込まれます。
  • Auto Layout の制約変更にも同じことが言えます。closure 内で constantisActive を書き換えると、依存しているビューは自動で新しい位置までスムーズにアニメーションします。

メインメニューシステムの構成

iPadOS 26 ではメニューバーが導入され、UIMenuBuilder に加えて UIMainMenuSystem.Configuration が新設されました。これによりデフォルトコマンドを宣言的に取捨選択できます (4:56)。

// Main menu system configuration

var config = UIMainMenuSystem.Configuration()

// Declare support for default commands, like printing
config.printingPreference = .included

// Opt out of default commands, like inspector
config.inspectorPreference = .removed

// Configure the Find commands to be a single "Search" element
config.findingConfiguration.style = .search

ポイント:

  • printingPreference = .included で、システムの印刷コマンド一式を app に取り込めます。
  • inspectorPreference = .removed を指定すると、新しく追加された inspector の切り替えコマンドを無効化できます。
  • findingConfiguration.style = .search は、Find コマンド群を 1 つの Search にまとめる設定です。画像や音楽系の app では Find Next/Previous より Search のほうが適しています。
  • 設定の反映はメニューの再構築を引き起こすため、application(_:didFinishLaunchingWithOptions:) で 1 回だけ呼び出すのが定石です。
  • 注意点として、iOS 26 以降は storyboard 内のメニューが読み込まれなくなるため、コードでメニューを構築する必要があります。

Focus-based deferred menu element

History のようなメニュー内容は現在のフォーカス (たとえばどのブラウジングプロファイルか) に依存するため、メインメニューの build に直接書き込むことはできません (8:06)。

// Focus-based deferred menu elements

class BrowserViewController: UIViewController {

    // ...
  
    override func provider(
        for deferredElement: UIDeferredMenuElement
    ) -> UIDeferredMenuElement.Provider? {
        if deferredElement.identifier == .browserHistory {
            return UIDeferredMenuElement.Provider { completion in
                let browserHistoryMenuElements = profile.browserHistoryElements()
                completion(browserHistoryMenuElements)
            }
        }
        return nil
    }
}

ポイント:

  • App Delegate 側では UIDeferredMenuElement.usingFocus(identifier:shouldCacheItems:) でプレースホルダ要素を作り、メインメニューに挿入しておきます。
  • View controller 側では provider(for:) をオーバーライドし、identifier に応じて UIDeferredMenuElement.Provider を返します。UIKit はレスポンダチェーンを上に辿り、コンテンツを提供できる最初の responder を見つけてくれます。
  • これにより、メニューの中身はオンデマンドで埋められるため、プロファイル切り替えのたびにメインメニュー全体を再構築する必要がなくなります。

SwiftUI scene を UIKit app に埋め込む

UIHostingSceneDelegate を使うと、UIKit app から SwiftUI の WindowGroup や visionOS の ImmersiveSpace をホストできます (18:07)。

// Setting up a UIHostingSceneDelegate

import UIKit
import SwiftUI

class ZenGardenSceneDelegate: UIResponder, UIHostingSceneDelegate {
    static var rootScene: some Scene {
        WindowGroup(id: "zengarden") {
            ZenGardenView()
        }

        #if os(visionOS)
        ImmersiveSpace(id: "zengardenspace") {
            ZenGardenSpace()
        }
        .immersionStyle(selection: .constant(.full),
                        in: .mixed, .progressive, .full)
        #endif 
    }
}

ポイント:

  • rootScene は SwiftUI の Scene で、複数の WindowGroup や ImmersiveSpace を含めることができます。
  • application(_:configurationForConnecting:options:)delegateClass をこのクラスに指定すれば接続できます。
  • UISceneSessionActivationRequest(hostingDelegateClass:id:) を使うと、特定の scene をプログラム的にリクエストできます。たとえば visionOS の没入空間を開くといった用途に向いています。
  • UIKit app に SwiftUI を段階的に取り込みたい場合や、visionOS の没入空間が必要なケースに最適です。

強型付きの NotificationCenter.Message

NotificationCenter.default.addObserver(of:for:) は強型付きの message を返すようになり、userInfo 辞書とお別れできます (20:54)。

// Adopting Swift notifications

override func viewDidLoad() {
    super.viewDidLoad()

    let keyboardObserver = NotificationCenter.default.addObserver(
        of: UIScreen.self
        for: .keyboardWillShow
    ) { message in
        UIView.animate(
            withDuration: message.animationDuration, delay: 0, options: .flushUpdates
        ) {
            // Use message.endFrame to animate the layout of views with the keyboard
            let keyboardOverlap = view.bounds.maxY - message.endFrame.minY
            bottomConstraint.constant = keyboardOverlap
        }
    }
}

ポイント:

  • for: .keyboardWillShow は強型付きの NotificationCenter.Message 名です。
  • message.animationDurationmessage.endFrame でキーボードのアニメーション時間や終了 frame を直接取得できます。userInfo[UIResponder.keyboardFrameEndUserInfoKey] を引いて強制キャストする辞書ベースの書き方は不要です。
  • .flushUpdates と組み合わせれば、制約の書き換えが自動的にキーボードのアニメーションに追従します。

重要ポイント

1. cell 構成を configurationUpdateHandler + @Observable に再設計する

なぜやる価値があるか: 既存の cell 構成コードはしばしば cellForItemAtprepareForReuse、KVO のコールバックに散らばっており、状態同期のミスが起きやすくなっています。configurationUpdateHandler はすでに自動観察トラッキングに対応しているため、1 か所で定義しておけば自動的に更新されます (11:48)。

始め方: cell のデータモデルを @Observable class に変更し、cellForItemAt で dequeue したあとに configurationUpdateHandler を割り当てます。handler の中ではモデルのプロパティを読み、UIListContentConfiguration を構成します。モデルのプロパティが変化すれば cell の再構成が自動で走り、setNeedsConfigurationUpdate を手動で呼ぶ必要はありません。

2. 「コンテンツ・スタイル」のロジックを layoutSubviews から updateProperties に移す

なぜやる価値があるか: layoutSubviews はリサイズ、スクロール、subview の変更などのたびに走るため、その中で badge やテキスト、色などのコンテンツ更新を行うと、無駄な計算を大量に発生させてしまいます。新しい updateProperties はコンテンツ関連の無効化が起きたときだけ実行されるので、パフォーマンスも責務分離も改善できます。

始め方: 既存の viewWillLayoutSubviews / layoutSubviews の実装を見直し、「モデルを読んでプロパティを設定する」コードはすべて override func updateProperties() に移します。レイアウト計算 (frame や制約の constant) は元のメソッドに残します。手動で発火させる必要があれば setNeedsUpdateProperties() を呼びます。

3. 今すぐ UIScene への移行作業を始める

なぜやる価値があるか: iOS 26 以降のバージョンで最新 SDK を使ってビルドした app は、UIScene ライフサイクルに移行していないと起動できません。UIApplicationDelegate の古いコールバックや UIWindow の初期化メソッドの多くもすでに deprecated になっています (21:19)。

始め方: Apple のテクニカルノート Migrating to the UIKit scene-based life cycle に目を通し、まず Info.plistUIApplicationSceneManifest を追加するところから始めましょう。シングルウィンドウの app でも移行は必要です。application(_:didFinishLaunchingWithOptions:) 内の UI セットアップは UISceneDelegate.scene(_:willConnectTo:options:) に移し、UIWindow は init(windowScene:) で初期化するように書き換えます。

4. キーボード・通知系のコードを強型付きの NotificationCenter.Message にアップグレードする

なぜやる価値があるか: 従来の keyboardWillShowNotification + userInfo 辞書を引くコードは可読性が低く、key 名の typo にも弱く、毎回強制キャストが必要でした。強型付きの message なら animationDurationendFrame のようなプロパティが直接取得でき、コンパイル時にチェックも入ります。

始め方: addObserver(forName:object:queue:using:)addObserver(of:for:) に置き換え、コールバックのシグネチャを (message) in に変更し、message.xxx でプロパティを直接読み取るようにします。アニメーション closure には .flushUpdates を付けると、制約の書き換えが自動でキーボードのアニメーションに追従します。


関連セッション

コメント

GitHub Issues · utterances