WWDC Quick Look 💓 By SwiftGGTeam
Bring multiple windows to your SwiftUI app

Bring multiple windows to your SwiftUI app

元の動画を見る

ハイライト

SwiftUI には、当初からウィンドウ管理において明らかな欠点がありました。新しいウィンドウを開いたり、複数のウィンドウ インスタンスを管理したり、さまざまなシナリオに応じてさまざまな種類のウィンドウを定義したりすることは簡単ではありません。 iOS 16 と macOS Ventura は次のように強化されています。WindowGroupそして新しいものを紹介しますWindowシーン タイプはこの状況を大幅に改善します。


主要内容

SwiftUI アプリAppSceneそしてView構成。Viewインターフェイスを担当し、Sceneシステム ウィンドウ、ドキュメント ウィンドウ、設定ウィンドウなどのシェルにインターフェイスを配置する役割を果たします。

以前は、多くの SwiftUI アプリには 1 つしかありませんでした。WindowGroup。メイン ウィンドウにはこれで十分ですが、デスクトップ レベルのエクスペリエンスを作成しようとすると、特定の問題が発生します。読書リストには別のアクティビティ統計ウィンドウが必要です。本の詳細は、コンテキスト メニューから新しいウィンドウを開くのに適しています。ドキュメント アプリでは、新しいドキュメントを作成するか、ボタンから既存のファイルを開く必要があります。また、メニュー バー ツールはシステム メニュー バーに永続的に存在する必要があります。

このセッションでは、これらの質問を 3 つのカテゴリに分類します。

最初のタイプは新しいシーン タイプです。Windowグローバルステータスに適した単一のウィンドウを表します。MenuBarExtraこれは macOS 固有のシーンであり、システム メニュー バーに永続的なエントリが配置されます。

2 番目のカテゴリは、インターフェイス コードからウィンドウをアクティブに開くことです。 SwiftUIは環境を通じて提供されますopenWindownewDocumentopenDocument。ボタンは AppKit または UIKit ウィンドウ オブジェクトを認識する必要はなく、対応するアクションを呼び出すことだけが必要です。

3 番目のカテゴリは、ウィンドウの動作のカスタマイズです。シーンでは、デフォルトのコマンドを削除し、デフォルトの位置とサイズを指定し、ウィンドウを開くためのキーボード ショートカットをバインドできるようになりました。このようにして、ウィンドウは単なるシステムのデフォルトの動作ではなく、アプリ構造の一部になります。


詳細

シーンは結合可能: 1 つのアプリで複数の入り口を定義

(02:01) SwiftUI のアプリ本体は複数のシーンを返すことができます。メインインターフェースの場合WindowGroup、ドキュメントインターフェイス用DocumentGroup、macOS 設定の場合Settings。各シーンはシステム レベルのエントリ ポイントを説明します。

import SwiftUI
import UniformTypeIdentifiers

@main
struct MultiSceneApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }

        #if os(iOS) || os(macOS)
        DocumentGroup(viewing: CustomImageDocument.self) { file in
            ImageViewer(file.document)
        }
        #endif

        #if os(macOS)
        Settings {
            SettingsView()
        }
        #endif
    }
}

struct ContentView: View {
    var body: some View {
        Text("Content")
    }
}

struct ImageViewer: View {
    var document: CustomImageDocument

    init(_ document: CustomImageDocument) {
        self.document = document
    }

    var body: some View {
        Text("Image")
    }
}

struct SettingsView: View {
    var body: some View {
        Text("Settings")
    }
}

struct CustomImageDocument: FileDocument {
    var data: Data

    static var readableContentTypes: [UTType] { [UTType.image] }

    init(configuration: ReadConfiguration) throws {
        guard let data = configuration.file.regularFileContents
        else {
            throw CocoaError(.fileReadCorruptFile)
        }
        self.data = data
    }

    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        FileWrapper(regularFileWithContents: data)
    }
}

キーポイント:

  • WindowGroup通常のアプリ ウィンドウを定義します。これは、マルチ ウィンドウをサポートするプラットフォーム上で複数のインスタンスを持つことができます。 -DocumentGroupファイル タイプをドキュメント インターフェイスにバインドすると、システムがドキュメント ウィンドウのライフ サイクルを担当します。 -SettingsmacOS 上でのみコンパイルされ、アプリの設定インターフェイスを表します。 -CustomImageDocument従うFileDocument、合格readableContentTypes読み取り可能なファイルの種類を宣言します。

ウィンドウ: グローバル状態に固有のウィンドウを与える

04:38Window2022 年に追加された新しいシーン タイプです。WindowGroup違いは明らかです: 同じですWindowシーンにはウィンドウ インスタンスが 1 つだけあります。 BookClub のアクティビティ ウィンドウには、グローバルな読書アクティビティが表示されます。何度も開いても意味がないので、Window

import SwiftUI

@main
struct BookClub: App {
    @StateObject private var store = ReadingListStore()

    var body: some Scene {
        WindowGroup {
            ReadingListViewer(store: store)
        }
        Window("Activity", id: "activity") {
            ReadingActivity(store: store)
        }
    }
}

struct ReadingListViewer: View {
    @ObservedObject var store: ReadingListStore

    var body: some View {
        Text("Reading List")
    }
}

struct ReadingActivity: View {
    @ObservedObject var store: ReadingListStore

    var body: some View {
        Text("Reading Activity")
    }
}

class ReadingListStore: ObservableObject {
}

キーポイント:

  • @StateObject private var storeつけるAppここで、メイン ウィンドウとアクティビティ ウィンドウは同じアプリの状態を共有します。 -WindowGroup引き続きリーディング リストのメイン インターフェイスを担当します。 -Window("Activity", id: "activity")識別子を使用してシングルトン ウィンドウを定義します。 -id: "activity"後ほどopenWindow(id:)使用する場合、文字列はシーン定義と一致する必要があります。

(05:34) このウィンドウを開くときは、環境から削除してくださいopenWindow、ボタンアクションで呼び出すだけです。ウィンドウがすでに存在する場合、システムは既存のウィンドウを最前面に表示します。

struct OpenWindowButton: View {
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        Button("Open Activity Window") {
            openWindow(id: "activity")
        }
    }
}

キーポイント:

  • @Environment(\.openWindow)SwiftUI が提供するウィンドウを開くアクションを読みます。 -Buttonアクションは直接呼び出されますopenWindow、プラットフォーム ウィンドウ オブジェクトに触れずに。 -openWindow(id: "activity")上記に対応Windowシーン識別子。

WindowGroup は値によって詳細ウィンドウを開くことができます

(05:57) 詳細ページは使用に適していませんWindow。各ブックには独自の詳細ウィンドウがある場合があるので、for:新しいパラメータWindowGroupイニシャライザ。ボタンはブック ID を渡し、シーンはその ID に基づいてウィンドウを作成または再利用します。

import SwiftUI

@main
struct BookClub: App {
    @StateObject private var store = ReadingListStore()

    var body: some Scene {
        WindowGroup {
            ReadingListViewer(store: store)
        }
        Window("Activity", id: "activity") {
            ReadingActivity(store: store)
        }
        WindowGroup("Book Details", for: Book.ID.self) { $bookId in
            BookDetail(id: $bookId, store: store)
        }
    }
}

struct OpenWindowButton: View {
    var book: Book
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        Button("Open In New Window") {
            openWindow(value: book.id)
        }
    }
}

struct BookDetail: View {
    @Binding var id: Book.ID?
    @ObservedObject var store: ReadingListStore

    var body: some View {
        Text("Book Details")
    }
}

struct Book: Identifiable {
    var id: UUID
}

class ReadingListStore: ObservableObject {
}

キーポイント:

  • WindowGroup("Book Details", for: Book.ID.self)このシーンを受信すると宣言しますBook.IDtype の値を表示します。 -openWindow(value: book.id)同じ型を渡すことによってのみ、SwiftUI は対応するシーンを見つけることができます。
  • ビュービルダーが受け取るものはBinding<Book.ID?>、詳細ビューを開いた後にこの値を更新できます。
  • この講演では、値型モデル全体の代わりにモデル識別子を渡すことを推奨しています。このようにして複数のウィンドウが通過してもstore同じデータを指します。
  • 受信値は以下に準拠する必要がありますHashableそしてCodable: 前者は開いているウィンドウを照合するために使用され、後者はステータスを復元するために使用されます。

(09:36) この一致ルールにより、ウィンドウの重複の問題が解決されます。同じブックがすでに開いている場合、コンテキスト メニューから再度開くと、システムは既存のウィンドウを再利用して前面に移動します。

ドキュメント ウィンドウには環境アクションもあります

(06:16) ドキュメントアプリを使用できますnewDocument新しいドキュメントウィンドウを作成します。前提条件は、アプリが編集者ロールを持つ編集者を定義していることです。DocumentGroup、受信ドキュメントの種類が一致します。

import SwiftUI
import UniformTypeIdentifiers

@main
struct TextFileApp: App {
    var body: some Scene {
        DocumentGroup(viewing: TextFile.self) { file in
            TextEditor(text: file.$document.text)
        }
    }
}

struct NewDocumentButton: View {
    @Environment(\.newDocument) private var newDocument

    var body: some View {
        Button("Open New Document") {
            newDocument(TextFile())
        }
    }
}

struct TextFile: FileDocument {
    var text: String

    static var readableContentTypes: [UTType] { [UTType.plainText] }

    init() {
        text = ""
    }

    init(configuration: ReadConfiguration) throws {
        guard let data = configuration.file.regularFileContents,
              let string = String(data: data, encoding: .utf8)
        else {
            throw CocoaError(.fileReadCorruptFile)
        }
        text = string
    }

    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        let data = text.data(using: .utf8)!
        return FileWrapper(regularFileWithContents: data)
    }
}

キーポイント:

  • DocumentGroup(viewing: TextFile.self)バンドルTextFileドキュメントとTextEditorインターフェースが接続されました。 -@Environment(\.newDocument)新しいドキュメントを作成するアクションを取得します。 -newDocument(TextFile())呼び出しごとに新しい呼び出しが使用されますTextFile値を指定するとドキュメント ウィンドウが開きます。 -TextFile合格FileDocumentファイルの読み取りと書き込みを処理します。

(06:41) 利用可能なファイルはすでにありますopenDocument。これはファイル URL を受け取り、非同期のエラースロー呼び出しです。

struct OpenDocumentButton: View {
    var documentURL: URL
    @Environment(\.openDocument) private var openDocument

    var body: some View {
        Button("Open Document") {
            Task {
                do {
                    try await openDocument(at: documentURL)
                } catch {
                    // Handle error
                }
            }
        }
    }
}

キーポイント:

  • @Environment(\.openDocument)既存のドキュメントを開くアクションを取得します。 -Task非同期の呼び出しに使用されますopenDocument(at:)
  • documentURLディスク上のファイルを指します。 -catchこのブランチは、ファイルを開けない、タイプの不一致などのエラーを処理します。

03:01MenuBarExtramacOS特有のシーンです。最初にメイン ウィンドウを表示せず、システム メニュー バーにラベルを表示します。クリックするとメニューまたはフチなしウィンドウが表示されます。

import SwiftUI

@main
struct UtilityApp: App {
    var body: some Scene {
        MenuBarExtra("Utility App", systemImage: "hammer") {
            AppMenu()
        }
    }
}

struct AppMenu: View {
    var body: some View {
        Text("App Menu Item")
    }
}

キーポイント:

  • MenuBarExtraに直接現れるAppシーンリストにあります。
  • 最初のパラメータはメニューバー項目のタイトルです。 -systemImageSFシンボルをメニューバーアイコンとして使用します。
  • コンテンツ クロージャは、メニュー バー項目をクリックしたときに表示される SwiftUI ビューを返します。

(03:49) デフォルトのスタイルでは、メニューがプルダウンされます。コンテンツがダッシュボードに近い場合は、ウィンドウ スタイルに変更できます。

import SwiftUI

@main
struct UtilityApp: App {
    var body: some Scene {
        MenuBarExtra("Time Tracker", systemImage: "rectangle.stack.fill") {
            TimeTrackerChart()
        }
        .menuBarExtraStyle(.window)
    }
}

struct TimeTrackerChart: View {
    var body: some View {
        Text("Time Tracker Chart")
    }
}

キーポイント:

  • .menuBarExtraStyle(.window)プレゼンテーションを、メニュー バー項目に固定されたボーダーレス ウィンドウに変更します。
  • このスタイルは、より多くのスペースを必要とするグラフやコントロール パネルなどのコンテンツに適しています。 -MenuBarExtra別個のツール アプリとして使用することも、メイン ウィンドウ シーンと組み合わせて使用​​することもできます。

シーン モディファイアは、ウィンドウ コマンド、位置、サイズ、ショートカット キーを制御します。

(11:16) 複数WindowGroupデフォルトのメニューコマンドが表示されます。コンテキスト メニューからのみブックの詳細を開くことができる場合は、このシーンのデフォルトのコマンドを削除できます。

WindowGroup("Book Details", for: Book.ID.self) { $bookId in
    BookDetail(id: $bookId, store: store)
}
.commandsRemoved()

キーポイント:

  • commandsRemoved()現場で活躍します。
  • このシーンによって自動的に提供されるデフォルトのコマンドを削除します。
  • 講義例では、ファイルメニューのみを開いたままにします。WindowGroupプロジェクト。

(11:46) シーンに複数のモディファイアが必要な場合は、シーンをカスタマイズできますScene。こうすることで、アプリ定義が短くなり、再利用しやすくなります。

struct ReadingActivityScene: Scene {
    @ObservedObject var store: ReadingListStore

    var body: some Scene {
        Window("Activity", id: "activity") {
            ReadingActivity(store: store)
        }
        #if os(macOS)
        .defaultPosition(.topTrailing)
        .defaultSize(width: 400, height: 800)
        #endif
        #if os(macOS) || os(iOS)
        .keyboardShortcut("0", modifiers: [.option, .command])
        #endif
    }
}

キーポイント:

  • ReadingActivityScene従うScene、そしてViewコンポーネントを抽出するという考え方は同じです。 -.defaultPosition(.topTrailing)履歴状態がない場合のウィンドウのデフォルトの位置を指定します。 -.defaultSize(width: 400, height: 800)ウィンドウに初期サイズを指定しても、ウィンドウのサイズを変更できます。 -.keyboardShortcut("0", modifiers: [.option, .command])このシーンを開くコマンドを Option-Command-0 にバインドします。 -#if os(macOS)そして#if os(macOS) || os(iOS)プラットフォームの境界は、サポートされていないプラットフォームで関連する修飾子がコンパイルされるのを避けるために保持されます。

重要ポイント

  • 読書または学習アプリの詳細ウィンドウを作成: リストはメイン ウィンドウに残り、項目の詳細はWindowGroup("Details", for: Item.ID.self)開ける。まずはモデルに従うように頼むことから始めますIdentifiable、ボタンから呼び出されますopenWindow(value: item.id)

  • グローバル ステータス パネルを作成: 統計、アクティビティ記録、同期ステータス、その他のコンテンツが適していますWindow("Activity", id: "activity")。共有ステータスを入力しますApp@StateObject、メイン ウィンドウと補助ウィンドウの両方が同じストアを読み取ります。

  • メニュー バー ツールを作成: タイマー、クリップボード、ビルド ステータスをすべて使用できますMenuBarExtra常駐の macOS メニュー バー。コンテンツが単純な場合は、デフォルトのメニュー スタイルを使用します。チャートや複雑なコントロールが必要な場合に追加します。.menuBarExtraStyle(.window)

  • デスクトップレベルのエントリをウィンドウに追加: よく使用されるウィンドウを追加できます.keyboardShortcut。窓をビジネス入口からのみ開ける必要がある場合は、次を使用します。.commandsRemoved()デフォルトのメニュー コマンドを削除して、ユーザーが [ファイル] メニューからコンテキストなしで空のウィンドウを開けないようにします。

  • ドキュメント アプリにクイック オープン ボタンを追加: 新しいファイルを作成するためnewDocument(DocumentType())、既存のファイルを開くために使用されますtry await openDocument(at: url)。最初に確認してくださいDocumentGroup対応するドキュメント タイプをサポートし、ボタン内の環境アクションにアクセスします。


関連セッション

コメント

GitHub Issues · utterances