ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
AppKitでSwiftUIを使用する
Shortcuts AppではSwiftUIとAppKitの両方を使用することで、macOSで最高レベルのエクスペリエンスを実現することができるようになりました。Shortcutsチームより、AppKitコードでSwiftUIビューをホストする方法、レイアウトやサイズ設定の処理方法、レスポンダチェーンへの参加方法、ナビゲーションフォーカスの有効化方法などを紹介しますので、是非ご覧ください。また、AppKitビューをホストする方法も紹介します。これで、既存のコードをAppのSwiftUIレイアウトに移行できるようになります。
リソース
関連ビデオ
WWDC22
WWDC21
-
ダウンロード
♪陽気な器楽用のヒップホップ楽曲♪ ♪ 「AppKitでSwiftUIを使用する」へようこそ ShortcutsチームのエンジニアのIanです macOS Montereyでは ShortcutsがmacOSにきました Shortcutsは MacのSwiftUIを多用しています SwiftUIは iOSやwatchOS上の Appと共通の表示を共有しつつ プラットフォームに合わせて 経験をカスタマイズすることを支援します このセッションではShortcutsでの いくつかの例を紹介し あなたのMac appにSwiftUIを導入する 方法を説明します まず App内でSwiftUIの表示を ホストする方法の例を紹介し 次にAppKitとSwiftUI間で データを渡す方法について話します また コレクションやテーブル表示のセルで SwiftUI表示をホストすること AppKitに埋め込まれたときに SwiftUI表示のレイアウトと サイジングを処理する方法 SwiftUI表示をレスポンダチェーンに参加させて フォーカス可能にする方法 最後にSwiftUIでAppKit表示を ホストする方法について説明します まずはAppKitでSwiftUIを ホストする方法からです Shortcutsではメインウィンドウに AppKitのSplitViewコントローラがあり 左側の補足はSwiftUIを使って書かれています 補足表示はSwiftUI Listとして実装され リストはAppでナビゲートできる すべての場所の行を持つ部分を表示します 表示は 選択された項目の結合によって どの項目が選択されたかを記録します 選択可能な項目は SidebarItemタイプの実例として表現されます この場合 すでにSplitView コントローラがあるので 補足表示をホストするために NSHostingControllerと 呼ばれるSwiftUIのクラスを使用します SwiftUIの補足表示は その ホスティングコントローラの ルート表示として渡されます ホスティングコントローラは 他の表示コントローラと 同様に使用できるので ここではsplitViewItemとして構成し それをsplitViewControllerに追加しています これで補足はSplitViewでホストされますが 選択範囲が変わったときに 動作するには SplitViewの 右側に別のページを表示させる必要があります 現在 選択された項目の状態はSwiftUI内にのみ 存在します それをSplitViewと補足で共有できる場所に 移動する必要があるのです これを行う良い方法はSwiftUIの外部に保存でき 共有する必要がある状態を含むモデル物体を 作成することです この物体をSelectionModelと呼ぶことにします さて 補足はまだSelectionModelで状態を 読み書きできますが SelectionModelはSplitViewへの参照も保存でき 選択が変更されると 詳細表示を更新します コード上ではSelectionModelは ObservableObjectに準拠したクラスである 観測可能な物体であることで モデルに保存された状態が 変更されたとき SwiftUIは 表示をリロードできます このモデルには2つの特性があります まず splitViewControllerへの参照を保持し 詳細表示を更新できるようにします そして2つ目は現在どの補足項目が 選択されているかを記憶することです この特性は 選択項目が変更されたときに SwiftUI補足表示を更新 できるように公開されます 最後に モデルはdidSetを実装しているので 誰かが補足の選択を変更するたびに モデルは詳細表示に新しい ページを表示できます AppKitでSwiftUIをホスト する方法を説明したので コレクションとテーブルの セルに話を移しましょう 他のプラットフォームからmacOSにShortcutsを 導入する際コレクション表示のセルや ホーム画面のウィジェットに ショートカットを表示する SwiftUIの象徴的な表示が 既に構築されていました macOSでは これらと同じ表示が NSCollectionViewのセルに表示されます 項目がたくさんあるコレクション表示や テーブル表示ではスクロール すると各セル表示が リサイクルされ 時間と共に 異なる内容が表示されます セルの再利用を確実に実行するには ユーザーが スクロールする際にセルからサブ表示を 追加したり削除したりを避ける必要があります 各セルにSwiftUI表示を表示するとき 単一のホスティング表示を使い セルの内容を 変更する必要があるときに異なるルート表示で それを更新します SwiftUIをホストするコレクション表示セルを 構築するのに必要なものはこれだけです この例では ショートカット表示を 表示するセルを作っています 各セルはホストするNSHostingViewを含みます セルはコンテンツが設定される前に 作成されるため この値は最初はゼロで ショートカットを表示する準備ができたときに 初めて設定されます displayShortcut方法はセルにショートカットを 表示するように設定する際に データソースから呼ばれます この方法はSwiftUI ShortcutViewを作成します そしてもし既にhostingView があれば そのhostingViewの rootViewが新しい表示に設定されます そうでない場合もしそれが初回であれば newHostingViewが作成され セルのサブ表示として追加されます SwiftUIをホストするセルの ライフサイクルを紹介します まずセルが初期化され まだ表示するショートカット がないため サブ表示が ない状態でスタートします 最初にdisplayShortcutが呼ばれたとき 表示するshortcutViewで hostingViewが作成されます これはVStack 画像スペーサー 2つの テキスト表示を含むSwiftUI の表示階層を作成します このセルが画面外にスクロールされた場合 システムによってデキュー される可能性があるため 別のショートカットを表示する必要があります このとき新しいShortcutViewが作成され HostingViewに付与されます HostingViewはすでに別のショートカット表示を 表示していたので VStackやスペーサーなど 表示全体の構造を再利用し 変更された画像 テキスト バックグラウンドのみを更新します さて 次はレイアウトとサイジングについてです ホスティングコントローラと ホスティング表示は SwiftUI表示の理想的な幅と 高さに基づいた固有のサイズを持っています SwiftUIは自動的にAuto Layout制約を作成し 更新し これをAppKitの 配置システムが適切な表示の サイズ調整に使用します また 表示は最小から最大まで 様々なサイズに対応する フレキシブルなものです SwiftUIはこれらにも制約を作ります SwiftUIホスティング表示を階層に埋め込む場合 スーパー表示や他の隣接する表示に 独自の自動レイアウト制約を 適用する必要があります フレーム修飾子や他のSwiftUIレイアウトを 使用すると 幅を固定サイズに上書きするなど 作成される制約の更新が行われます ウィンドウはユーザーが サイズを変更できるため ウィンドウには最小サイズと 最大サイズがあります HostingViewsがウィンドウのトップレベルの contentViewとして設定されたとき SwiftUIは表示されるコンテンツに基づいて ウィンドウの最小と最大の サイズを自動的に更新します また コンテンツに応じて ウィンドウのサイズを縦 横またはその両方に 変更できます ホスティングコントローラに 置かれたSwiftUIの表示は モードに提示された時に内容 に基づいたサイズになります 例えば ここに示すようNSViewControllerで ポップオーバーの公開APIを使用して ホスティングコントローラを 提示することで SwiftUIの 表示をAppKitのポップオーバーに簡単に 配置できます presentAsSheet方法を使ってSwiftUIの表示を シートとして表示できます 最後にモードウィンドウの場合 presentAsModalWindow方法を使用すると 閉じるまで相互連関を遮る ウィンドウを提示できます ウィンドウはコンテンツに 合わせたサイズになります macOS VenturaではNSHostingViewと NSHostingControllerに新しいAPIが追加され 自動的に追加される制約を カスタマイズすることが できるようになりました デフォルトではホスティングコントローラと 表示は 最小サイズ標準サイズ および 最大サイズに関する制約を作成します 表示を常に柔軟なサイズに したい場合や AppKitで 周囲の表示にすでに制約が 加えられている場合など パフォーマンス上の理由から これらのいくつかを 無効にしても構いません ホスティングコントローラの 場合 表示の理想的な サイズを好ましい コンテンツサイズに決定させるために preferredContentSize オプションを有効にできます AppにSwiftUI表示を追加し始めるとき Appの他の表示と同じように レスポンダチェーンと フォーカスシステムに参加することが重要です ShortcutsではエディタはSwiftUI表示として 実装されています しかしエディタではAppKitで 実装されるメインメニューで 定義されたメニューバー コマンドの処理が必要です カット コピー ペーストなどのコマンドです また アクションを上下に 移動させるために 独自の 独自のカスタムメニューも いくつか実装しています AppKitでは 表示階層は ”レスポンダチェーン”と 呼ばれる表示のチェーンを構成しています 集中的な応答をファースト レスポンダと呼びます メニュー項目が選択されると その項目のセレクタが ファーストレスポンダに送られます しかし最初のレスポンダがそのセレクタに 応答しない場合次のレスポンダに セレクタが送られ何かがセレクタを処理するか Appに到達するまで セレクタが送られます SwiftUIのファーストレスポンダに 相当するのは 集中的な表示です フォーカス可能SwiftUI表示 はキーボード入力に応答し レスポンダチェーンに送る セレクタを処理できます テキスト領域のような一部の 表示はすでにフォーカス可能 ですが フォーカス可能な 修飾子を使用することで 他の表示もフォーカス可能にできます SwiftUIには コピー カット ペーストなどの一般的な コマンドを処理するための 修飾子がいくつかあります これらはペーストボードの 内外に値を渡すもので Appの中でデータを 転送させる簡単な方法です ショートカット・エディター では onMoveCommandと onExitコマンド修飾子を使って 矢印キーとエスケープキーを処理します onCommand修飾子はAppKitの一般的なセレクタや Appで定義した独自のカスタムセレクタを 処理するために使用できます ここでは AppKitからのselectAllコマンドと Shortcuts appで定義されたmoveActionUpと moveActionDownのコマンドを処理しています Appでフォーカスとキーボードの操作可能性を テストする時は 多くの 操作はそれが有効な時にのみ フォーカス可能であるため Keyboard System Settingを 開き Full Keyboard Navigationをオン またはオフにしてテストしてください キーボードとの相性を よくするためにできることは まだまだたくさんあります 例えば FocusStateやfocused modifierといった APIがありどの表示が集中的かを プログラムで変更できます フォーカスとキーボードに ついてさらに学ぶには 「SwiftUIでフォーカスを指示して反映する」 のビデオを見に行くべきです 最後に AppKitの表示の ホストについて説明します ShortcutsがSwiftUIレイアウトの中で AppKitの表示をホストしている例がありますが AppにSwiftUIを採用する際に AppKitの表示もホストする必要が 出てくる可能性があります 1つの例は SwiftUIのショートカットエディタの 内部で AppleScriptエディタ の表示が埋め込まれており これはmacOS上の他のいくつかのシステムAppと 共有されるAppKitコントロールです SwiftUIは AppKitの表示と 表示コントローラーを SwiftUIの表示階層内に 埋め込むことを可能にする 2つの表現可能なプロトコルを提供します SwiftUIの表示と同様に representableはAppKitの 表示を作成 更新する方法 についての記述です AppKitの多くのクラスは デリゲート オブザーバを 持ち あるいはKVOや通知に 依存して観測されるため プロトコルにはオプションで コーディネータオブジェクト も含まれており 表示や 表示コントローラに付随して 実装することが可能です ここでは ホストされたオブジェクトと そのコーディネータの ライフサイクルを紹介します まずはホストされた表示の初期化から始めます これは表示が初めて表示されようとする時に 起こります 初期化の間にSwiftUIが行う最初のことは コーディネータを作ることです これはオプションですが デリゲーションや状態管理に 必要であれば独自のタイプを定義し それをmakeCoordinatorから 返すことができます コーディネータのインスタンスは 表示の存続期間中1つだけ残ります 次に makeNSViewまたは makeNSViewController方法が 呼び出されます ここでは 表示の新しいインスタンスを 作成する方法をSwiftUIに説明します コンテキストには先ほど 作成されたコーディネータが 含まれているので ここで コーディネータを表示の デリゲートなどのオブザーバ として割り当てるといいです いったん表示が作成されると SwiftUIの状態や環境が変更されるたびに update view方法が呼び出されます ここで SwiftUIの周囲の状態と環境と 同期させるためにAppKitの 表示に保存されている プロパティや状態を更新するのは あなたの責任です update方法は頻繁に呼び出される可能性が あるため 表示に加える変更はできるだけ 少なくする必要があります 何が変更されたかを確認し 変更があった場合にのみ 表示の影響を受ける部分を再読み込みする 必要があります SwiftUIがホストされた 表示を表示し終わったとき それは取り除かれます 表示とコーディネータの両方が解放されます この状態が解放される前に 表現可能なプロトコルは 必要に応じて状態をクリーンアップするための オプション方法を実装することを提供します さてライフサイクルを理解し 表現可能なプロトコルを理解したところで ShortcutsがApp内のカスタム スクリプトエディタ表示を どのようにホストしているか紹介します スクリプトエディタはScri- ptEditorViewというNSViewで エディタに書かれたコードは sourceCodeプロパティを 通じてアクセスし変更することができ 表示を無効にして変更ができないように することができます スクリプトエディタにはデリゲートもあり 誰かがソースコードを変更すると その旨が通知されます AppKitの表示をホストする時 最初にSwiftUIのどこに 表示が配置されるのか どのデータを受け渡しする 必要があるのかを考えてください ショートカットではこの表示は コンパイルボタンの横にある コンテナ表示に配置されます コンパイルボタンのハンドラは 表示に 入力されたソースコードに アクセスする必要があります ソースコードはSwiftUIで Stateプロパティラッパーを 使用して格納されます 表現可能なものはこの状態に対して読み取りと 書き込みの両方が必要である 表現可能なものを構築するために NSViewをホストするので NSViewRepresentableに 準拠したタイプの作成から始めます SwiftUIから設定する必要があるものごとに プロパティを追加します ソースコードには SwiftUIに 格納された状態を読み書き するバインディングが使用されています 最初に実装する必要がある 方法はmakeNSViewです ここでは 表示の 新しいインスタンスを作成する方法を説明し 必要な一回限りの設定を行います ここでは デリゲートに コーディネータを設定します コーディネータについては もう少し詳しくお話します 次に updateNSViewを実装します sourceCodeが変更されたとき またはSwiftUIの環境が 変更されたときに呼び出されます sourceCodeプロパティが設定されると スクリプトエディタが大量の作業を行うので 表示に既にある値を比較し変更された場合のみ プロパティを設定することで 無駄な作業を省くようにしています updateNSViewに渡されるコンテキストは SwiftUI環境を含みます isEnabled環境キーはスクリプトエディタの isEditableプロパティに渡されるので SwiftUIの表示階層の残りの 部分がそうである場合 編集は無効にされます 誰かが表示のソースコードを 変更するたびにソースコード バインディングは新しい値の キャプチャが必要です そのためにScriptEditorViewDelegateに 準拠したCoordinatorを作ることにします コーディネータは更新する 必要があるソースコード バインディングを含むrepre- sentable値を格納します そして sourceCodeDidChange方法では バインディングに表示からの 新しい文字列の値が 設定されます 最後に コーディネータを作成し 更新する方法を SwiftUIで表現できるように 伝える必要があります まず新しいコーディネータを作成するために makeCoordinator方法を 実装する必要があります コーディネータの寿命はホスト表示と同じで ホスト表示と同様にコーディネータに追加した プロパティは表現可能な変更に伴って 最新の状態に保つ必要があります updateNSViewはrepresen- tableに格納された値が変化 した時に呼び出されるので ここではコーディネータ上の representableプロパティが更新されます SwiftUIにAppKitを追加し AppKitにSwiftUIを追加する 方法もわかったので AppにSwiftUIを統合し始めましょう まず サイドバーやテーブル コレクション表示のセルから 始めるとよいでしょう 表示のサイズが正しく設定され 一般的なコマンドと フォーカスが処理されていることを確認します お忙しい中ありがとうございました あなたが何を作るのか楽しみです
-
-
1:29 - SidebarView and SidebarItem
struct SidebarView: View { @State private var selectedItem: SidebarItem var body: some View { List(selection: $selectedItem) { ... Section("Shortcuts") { ... } Section("Folders") { ... } } } } enum SidebarItem: Hashable { case gallery case allShortcuts ... case folder(Folder) }
-
1:53 - Hosting SwiftUI sidebar
let splitViewController = NSSplitViewController() let sidebar = NSHostingController(rootView: SidebarView(...)) let splitViewItem = NSSplitViewItem(viewController: sidebar) splitViewController.addSplitViewItem(splitViewItem)
-
3:06 - Sidebar selection model
class SelectionModel: ObservableObject { @Published var selectedItem: SidebarItem = .allShortcuts } // AppKit Window Controller cancellable = selectionModel.$selectedItem.sink { newItem in // update the NSSplitViewController detail }
-
4:37 - Collection view item hosting SwiftUI
class ShortcutItemView: NSCollectionViewItem { private var hostingView: NSHostingView<ShortcutView>? func displayShortcut(_ shortcut: Shortcut) { let shortcutView = ShortcutView(shortcut: shortcut) if let hostingView = hostingView { hostingView.rootView = shortcutView } else { let newHostingView = NSHostingView(rootView: shortcutView) view.addSubview(newHostingView) setupConstraints(for: newHostingView) self.hostingView = newHostingView } } }
-
7:55 - Popover presentation
viewController.present(NSHostingController(rootView: ...), asPopoverRelativeTo: rect, of: view, preferredEdge: .maxY, behavior: .transient)
-
8:15 - Sheet presentation
viewController.presentAsSheet(NSHostingController(rootView: ...))
-
8:22 - Modal window presentation
let hostingController = NSHostingController(rootView: ModalView()) hostingController.title = "Window Title" viewController.presentAsModalWindow(hostingController)
-
8:45 - Sizing options
hostingController.sizingOptions = [.minSize, .intrinsicContentSize, .maxSize]
-
10:47 - Copy, Cut, and Paste commands
Image(...) .focusable() .copyable { ... } .cuttable { ... } .pasteDestination(payloadType: Image.self) { ... }
-
11:02 - Respond to standard commands
struct ShortcutsEditorView: View { var body: some View { ScrollView { ... } .onMoveCommand { moveSelection(direction: $0) } .onExitCommand { cancelOperations() } .onCommand(#selector(NSResponder.selectAll(_:)) { selectAllActions() } .onCommand(#selector(moveActionUp(_:)) { moveSelectedAction(.up) } .onCommand(#selector(moveActionDown(_:)) { moveSelectedAction(.down) } } }
-
15:18 - Script editor
class ScriptEditorView: NSView { var sourceCode: String var isEditable: Bool weak var delegate: ScriptEditorViewDelegate? } protocol ScriptEditorViewDelegate: AnyObject { func sourceCodeDidChange(in view: ScriptEditorView) -> Void }
-
15:40 - Script editor container
struct ScriptEditorContainerView: View { @State var sourceCode: String = "" var body: some View { VStack { CompileButton { compile(code: sourceCode) } Divider() ScriptEditorRepresentable(sourceCode: $sourceCode) } } }
-
16:13 - Script editor representable
struct ScriptEditorRepresentable: NSViewRepresentable { @Binding var sourceCode: String func makeNSView(context: Context) -> ScriptEditorView { let scriptEditor = ScriptEditorView(frame: .zero) scriptEditor.delegate = context.coordinator return scriptEditor } func updateNSView(_ nsView: ScriptEditorView, context: Context) { if sourceCode != scriptEditor.sourceCode { scriptEditor.sourceCode = sourceCode } scriptEditor.isEditable = context.environment.isEnabled context.coordinator.representable = self } func makeCoordinator() -> Coordinator { Coordinator(representable: self) } } class Coordinator: NSObject, ScriptEditorViewDelegate { var representable: ScriptEditorRepresentable init(representable: ScriptEditorRepresentable) { ... } func sourceCodeDidChange(in view: ScriptEditorView) { representable.sourceCode = view.sourceCode } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。