WWDC Quick Look 💓 By SwiftGGTeam
What's new in AppKit

What's new in AppKit

元の動画を見る

ハイライト

macOS Sequoia は AppKit の SwiftUI 統合をさらに進化:NSHostingMenu でメニュー定義を共有、NSView が SwiftUI Animation 型で中断可能なアニメーションをサポート。Text Entry Suggestions、システムカーソル、Save Panel のファイル形式ピッカーなど標準コンポーネントも追加。


主要内容

Mac 上の app は長年、矛盾に直面してきました:AppKit はシステムレベルの完全な制御を、SwiftUI は宣言型開発の効率を提供する一方、両者の境界はスムーズではありませんでした。メニューは NSMenu を手書き、动画はレガシー NSAnimationContext API、入力候補は app ごとに独自実装——これらの隙間が開発速度を落とし、保守コストを増やしていました。

Sequoia はこれらを一つずつ埋めます。NSHostingMenu で SwiftUI View からメニュー内容を定義し、AppKit と SwiftUI で同じメニュー定義を共有。NSAnimationContext.animate(with:) が SwiftUI Animation 型(カスタム含む)を受け付け、アニメーションは自然に中断・リダイレクト可能。NSTextFieldsuggestionsDelegate が追加され、標準プロトコルでカスタム入力候補を提供。システムカーソル(frame resize、column/row resize、zoom in/out)がついに SDK で公開——自前描画は不要です。

システム機能面では、Writing Tools が標準 NSTextView で自動動作、Genmoji がインライン画像としてテキストフローに入り、Image Playground は ImagePlaygroundViewController で数行で統合。Window Tiling は既存 app すべてに自動適用されますが、ウィンドウの最小/最大サイズ制約が妥当か確認が必要です。


詳細

Image Playground 統合(02:09

extension DocumentCanvasViewController {

    @IBAction
    func importFromImagePlayground(_ sender: Any?) {
        let playground = ImagePlaygroundViewController()
        playground.delegate = self

        playground.concepts = [.text("birthday card")]
        playground.sourceImage = NSImage(named: "balloons")

        presentAsSheet(playground)
    }

}

extension DocumentCanvasViewController: ImagePlaygroundViewController.Delegate {

    func imagePlaygroundViewController(
        _ imagePlaygroundViewController: ImagePlaygroundViewController,
        didCreateImageAt resultingImageURL: URL
    ) {
        if let image = NSImage(contentsOf: resultingImageURL) {
            imageView.image = image
        }
        dismiss(imagePlaygroundViewController)
    }

}

キーポイント:

  • ImagePlaygroundViewController が体験全体の入口。delegate を設定してライフサイクルイベントを監視
  • concepts はテキスト概念、sourceImage はグラフィック参照——どちらも任意で、ユーザーの出発点になる
  • 生成完了時、コールバックはサンドボックス一時ディレクトリのファイル URL を返す——自分で読み込み永続化
  • presentAsSheet(_:) で表示し、delegate コールバックで dismiss

Window Tiling とウィンドウサイズ(05:50

Window Tiling はウィンドウを画面端にドラッグすると半分または四分の一を自動充填。Option + ドラッグで最近接エッジへのスナップをプレビュー。並列 2 ウィンドウは同期リサイズ。新規ウィンドウは cascadingReferenceFrame で既存ウィンドウの非タイル frame からカスケードオフセットを計算。ターミナル系 app では resizeIncrements で文字幅・高さステップに制限:

window.resizeIncrements = NSSize(width: characterWidth, height: characterHeight)

NSHostingMenu:AppKit 向け SwiftUI メニュー(07:05

struct ActionMenu: View {

    var body: some View {
        Toggle("Use Groups", isOn: $useGroups)
        Picker("Sort By", selection: $sortOrder) {
            ForEach(SortOrder.allCases) { Text($0.title) }
        }.pickerStyle(.inline)
        Button("Customize View…") { <#Action#> }
    }

}

let menu = NSHostingMenu(rootView: ActionMenu())

let pullDown = NSPopUpButton(image: image, pullDownMenu: menu)

キーポイント:

  • メニュー内容は標準 SwiftUI 宣言:Toggle でスイッチ、Picker で選択、Button でアクション
  • NSHostingMenu は SwiftUI View を NSMenu にラップする新 NSMenu サブクラス
  • ラップ後の menu は NSMenu を受け付ける AppKit コンテキスト(NSPopUpButton のプルダウンなど)で使用可能

NSView の SwiftUI アニメーション(07:43

NSAnimationContext.animate(with: .spring(duration: 0.3)) {
    drawer.isExpanded.toggle()
}

@Invalidating プロパティラッパーと組み合わせ、プロパティ変更で自動再レイアウト:

class PaletteView: NSView {

    @Invalidating(.layout)
    var isExpanded: Bool = false

    private func onHover(_ isHovered: Bool) {
        NSAnimationContext.animate(with: .spring) {
            isExpanded = isHovered
            layoutSubtreeIfNeeded()
        }
    }

}

キーポイント:

  • NSAnimationContext.animate(with:) は CustomAnimation を含む任意の SwiftUI Animation 型を受け付け
  • アニメーションは自然に中断・リダイレクト可能——途中で方向変更してもカクつかない
  • @Invalidating(.layout) でプロパティ変更時に view の再レイアウトをマーク

テキストハイライト(10:31

let attributes: [NSAttributedString.Key: Any] = [
    .textHighlight: NSAttributedString.TextHighlightStyle.systemDefault,
    .textHighlightColorScheme: NSAttributedString.TextHighlightColorScheme.pink,
]

リッチテキスト NSTextView で自動利用可能。Font メニューの Highlight サブメニューでカラースキームを選択。

Text Entry Suggestions(17:49

class MuseumTextSuggestionsController: NSTextSuggestionsDelegate {

    typealias SuggestionItemType = Museum

    func textField(
        _ textField: NSTextField,
        provideUpdatedSuggestions responseHandler: @escaping ((ItemResponse) -> Void)
    ) {
        let searchString = textField.stringValue

        func museumItem(_ museum: Museum) -> Item {
            var item = NSSuggestionItem(representedValue: museum, title: museum.name)
            item.secondaryTitle = museum.address
            return item
        }

        let favoriteMuseums = Museum.favorites.filter({
            $0.matches(searchString)
        })
        let favorites = NSSuggestionItemSection(
            title: NSLocalizedString("Favorites", comment: ""),
            items: favoriteMuseums.map(museumItem(_:))
        )
        var response = NSSuggestionItemResponse(itemSections: [favorites])
        response.phase = .intermediate
        responseHandler(response)

        Task {
            let otherMuseums = await Museum.allMatching(searchString)
            let nonFavorites = NSSuggestionItemSection(items: otherMuseums.map(museumItem(_:)))
            var response = NSSuggestionItemResponse(itemSections: [favorites, nonFavorites])
            response.phase = .final
            responseHandler(response)
        }
    }

}

キーポイント:

  • textField.suggestionsDelegate を設定するだけで有効。NSTextField と NSSearchField などサブクラスもサポート
  • 2 段階返却可能:まず .intermediate 同期結果、次に非同期 .final 結果
  • NSSuggestionItemsecondaryTitle で追加情報、NSSuggestionItemSection でグループ化

重要ポイント

  • 何をするか:NSTextField に Text Entry Suggestions を導入し、検索候補やオートコンプリートを提供。なぜ価値があるか:ユーザーは入力時の即時フィードバックを期待。同期+非同期の 2 段階返却をサポートするシステム標準 API で、自作ポップオーバーより一貫。始め方NSTextSuggestionsDelegate を実装し textField.suggestionsDelegate を設定。まず .intermediate ローカル結果、次に非同期 .final リモート結果。

  • 何をするか:NSHostingMenu でメニュー定義を SwiftUI に移行し、AppKit/SwiftUI で共有。なぜ価値があるか:メニューロジックを 1 箇所で保守。Toggle、Picker、Button の宣言型記述は NSMenuItem 手動構築より明確。始め方:SwiftUI View をメニュー body として作成し NSHostingMenu(rootView:) でラップ、既存 NSMenu 構築コードを置換。

  • 何をするか:レガシー NSAnimationContext アニメーションを SwiftUI Animation に置換。なぜ価値があるか:Spring やカスタムアニメーションを 1 宣言で使用、中断・リダイレクトも自然でインタラクション向上。始め方NSAnimationContext.runAnimationGroupNSAnimationContext.animate(with: .spring(duration:)) に置換し、アニメーション対象プロパティに @Invalidating(.layout) を付与。

  • 何をするか:Image Playground を画像入力ソースとして統合。なぜ価値があるか:写真ライブラリや Finder からの画像挿入を既にサポートしている app なら、Image Playground は摩擦ゼロの第 3 の経路。始め方ImagePlaygroundViewController を作成、delegate と任意の concepts/sourceImage を設定、sheet で表示、delegate でファイル URL を読み込み。

  • 何をするか:自前描画カーソルをシステムカーソル API に置換。なぜ価値があるか:システムカーソルはアクセシビリティの大サイズとポインタ色カスタマイズを自動サポート。始め方NSCursor.frameResize(position:directions:)NSCursor.columnResize(directions:)NSCursor.zoomIn/.zoomOut でカスタムカーソル画像を置換。


関連セッション

コメント

GitHub Issues · utterances