WWDC Quick Look 💓 By SwiftGGTeam
SwiftUI on the Mac: The finishing touches

SwiftUI on the Mac: The finishing touches

元の動画を見る

ハイライト

SwiftUI を使用すると、Mac アプリが設定、AppStorage、テーブル ドラッグ アンド ドロップ、fileExporter、Continuity Camera を通じてシステム レベルのワークフローに接続できるため、開発者は宣言型コードを使用してユーザー設定、データ移動、ファイル エクスポート、デバイス インポートをサポートできます。

主な内容

Mac ユーザーはアプリを自分の方法で使用します。ダーク モードをオンにする人、サイドバーのアイコンを大きくする人、キーボード ショートカットに慣れている人、コンテンツを別のウィンドウにドラッグするだけの人もいます。

アプリがこれらの習慣を把握しない場合、機能自体は利用可能なままになり、エクスペリエンスが切り離されてしまいます。ユーザーは、メニュー、ウィンドウ、ファイル システムの間を行ったり来たりする必要があります。開発者はまた、環境設定ウィンドウ、ドラッグ アンド ドロップ、パネルの保存、デバイスのインポートなど、多くのプラットフォームの詳細を維持する必要があります。

このセッションは、ガーデニング アプリケーションに関する最初の部分に続きます。前の部分では、サイドバー、テーブル、マルチウィンドウ、メニューバー、検索がすでに設定されています。 2 番目の部分では、Mac アプリの高周波の詳細の最後の部分を扱います。

SwiftUI は最初にシステム設定にアクセスします。ダーク モードとサイドバー アイコンのサイズは、インターフェイスに自動的に影響します。開発者はリソース ディレクトリに設定することもできますAccentColorボタン、選択された状態、サイドバーのアイコンにアプリ独自のアクセント カラーを使用します。

次に、アプリは独自の設定ウィンドウを追加します。Settingsシーンはメニュー項目とCommand-,ショートカットキー、TabViewグループ分けを担当し、@AppStorageユーザー選択を入力してくださいUserDefaults

データ操作もMacユーザーに馴染みのある方法に変更されました。テーブル行が渡されましたitemProviderドラッグソースとなりフォームが通過しますonInsertドラッグアンドドロップを受け取ります。ユーザーは、あるウィンドウから別のウィンドウの庭に植物をドラッグできます。

最後に、アプリケーションはファイル システムとデバイスにアクセスします。fileExporter保存パネルを開き、データベースを CSV にエクスポートします。ImportFromDevicesCommandsそしてimportsItemProvidersContinuity Camera に接続した後、ユーザーは iPhone で写真を撮り、その写真は Mac アプリのプラント記録に直接入力されます。

##詳細

設定ウィンドウ: 設定、TabView、AppStorage

03:28

SwiftUI Mac アプリは次の場所から入手できます。App追加するSettingsシーン。システムは環境設定メニュー項目をメイン メニューに配置し、標準のショートカット キーを設定します。Command-,

デモの設定ウィンドウには、「一般」と「表示」の 2 つのペインがあります。これは次のようにして実現されます。TabView2 つのサブビューを配置し、追加しますtabItem

import SwiftUI

struct Garden: Identifiable {
    let id: UUID
    var name: String
    var displayYear: String
}

final class GardenStore: ObservableObject {
    @Published var gardens: [Garden] = [
        Garden(id: UUID(), name: "Indoor Plants", displayYear: "2021"),
        Garden(id: UUID(), name: "Backyard Flower Bed", displayYear: "2021")
    ]
}

@main
struct GardenApp: App {
    @StateObject private var store = GardenStore()

    var body: some Scene {
        WindowGroup {
            ContentView(store: store)
        }

        Settings {
            SettingsView(store: store)
        }
    }
}

struct SettingsView: View {
    @ObservedObject var store: GardenStore

    var body: some View {
        TabView {
            GeneralSettings(gardens: store.gardens)
                .tabItem {
                    Label("General", systemImage: "gear")
                }

            ViewingSettings()
                .tabItem {
                    Label("Viewing", systemImage: "eyeglasses")
                }
        }
        .padding()
    }
}

struct GeneralSettings: View {
    let gardens: [Garden]
    @AppStorage("defaultGarden") private var selection: String?

    var body: some View {
        Form {
            Picker("Default Garden", selection: $selection) {
                Text("None").tag(String?.none)

                ForEach(gardens) { garden in
                    Text("\(garden.name) \(garden.displayYear)")
                        .tag(Optional(garden.id.uuidString))
                }
            }
            .fixedSize()
        }
    }
}

struct ViewingSettings: View {
    var body: some View {
        Text("Viewing")
    }
}

struct ContentView: View {
    @ObservedObject var store: GardenStore

    var body: some View {
        Text("Gardens: \(store.gardens.count)")
    }
}

キーポイント:

  • SettingsそしてWindowGroup同じレベル。これはアプリケーション レベルの設定ウィンドウであることを示します。 -SettingsView(store: store)同じモデルを設定インターフェイスに渡すと、設定は現在のガーデン リストを読み取ることができます。 -TabViewツールバー アイコンを含む Mac 設定ウィンドウ スタイルを生成します。 -Label("General", systemImage: "gear")テキストとSFシンボルのアイコンを提供します。 -@AppStorage("defaultGarden")同じキーを使用してデフォルトのガーデンを永続化します。UserDefaults
  • Text("None").tag(String?.none)ピッカーに空の選択を与えます (デモでは None に相当します)。 -ForEach(gardens)ガーデンごとに 1 つのエントリを生成し、ガーデン ID 文字列を保持するタグを作成します。

07:47

設定が保存された後、メイン インターフェイスは同じキーを読み取る必要があります。デモンストレーションのロジックは次のとおりです。現在のウィンドウで庭園が選択されている場合は、ウィンドウ独自の選択を使用します。そうでない場合は、デフォルトのガーデンに戻ります。

import SwiftUI

struct GardenSelectionView: View {
    @State private var selectedGardenID: String?
    @AppStorage("defaultGarden") private var defaultGardenID: String?

    var selection: Binding<String?> {
        Binding(
            get: {
                selectedGardenID ?? defaultGardenID
            },
            set: {
                selectedGardenID = $0
            }
        )
    }

    var body: some View {
        Picker("Garden", selection: selection) {
            Text("Indoor Plants").tag(Optional("indoor-plants"))
            Text("Backyard Flower Bed").tag(Optional("backyard-flower-bed"))
        }
    }
}

キーポイント:

  • @State現在のウィンドウに一時的な選択内容を保存します。 -@AppStorage("defaultGarden")設定ウィンドウによって書き込まれたデフォルト値を読み取ります。 -Binding(get:set:)カスタム選択ロジック。 -get最初に戻るselectedGardenID、現在選択されていない場合に返されますdefaultGardenID
  • setアップデートのみselectedGardenID、ウィンドウでガーデンを切り替えるときにユーザーがグローバルなデフォルト値を変更できないようにします。

テーブルのドラッグ アンド ドロップ: TableRow、itemProvider、および onInsert

09:30

最初の部分では、アプリケーションに「Add Plants」メニュー項目が追加されました。もう 1 つの新しいワークフローは、直接ドラッグ アンド ドロップです。

SwiftUITable各行をドラッグソースに変えることができます。デモでのメソッドは次のとおりです。Tableイニシャライザからデータ配列を削除し、代わりに行ビルダーを使用し、それぞれにTableRow追加itemProvider

import SwiftUI
import UniformTypeIdentifiers

struct Plant: Identifiable, Hashable {
    let id = UUID()
    var name: String

    static let draggableType = UTType.plainText

    var itemProvider: NSItemProvider {
        NSItemProvider(object: name as NSString)
    }

    static func fromItemProviders(_ providers: [NSItemProvider], completion: @escaping ([Plant]) -> Void) {
        var plants: [Plant] = []
        let group = DispatchGroup()

        for provider in providers {
            group.enter()
            provider.loadObject(ofClass: NSString.self) { object, _ in
                if let name = object as? String {
                    plants.append(Plant(name: name))
                }
                group.leave()
            }
        }

        group.notify(queue: .main) {
            completion(plants)
        }
    }
}

struct GardenDetail: View {
    @State private var plants = [
        Plant(name: "Daisy"),
        Plant(name: "Fern")
    ]

    var body: some View {
        Table {
            TableColumn("Name", value: \.name)
        } rows: {
            ForEach(plants) { plant in
                TableRow(plant)
                    .itemProvider {
                        plant.itemProvider
                    }
            }
        }
        .onInsert(of: [Plant.draggableType]) { index, itemProviders in
            Plant.fromItemProviders(itemProviders) { newPlants in
                plants.insert(contentsOf: newPlants, at: index)
            }
        }
    }
}

キーポイント:

  • TableColumn("Name", value: \.name)テーブルの名前列を宣言します。 -rowsクロージャにより開発者は一行ずつ構築できるTableRowこれにより、各行に修飾子を簡単に追加できます。 -TableRow(plant)現在の植物オブジェクトをこの行にバインドします。 -itemProvider戻るplant.itemProvider、この行はドラッグして取り出すことができます。 -onInsert(of:)デモで使用される、フォームで受け入れられるコンテンツ タイプを宣言します。Plant.draggableType
  • 閉鎖中indexユーザーが置く位置です。 -itemProvidersこれは、ドラッグ アンド ドロップ システムによって渡されるデータ キャリアです。 -insert(contentsOf:at:)解析されたプラントをターゲットの場所に挿入します。

ファイルのエクスポート: CommandGroup と fileExporter

12:08

ユーザーはアプリケーションデータを取り出す必要があることがよくあります。バックアップ、共有、他のアプリケーションへのインポートはすべてファイル システムを使用します。

このデモンストレーションのためにCommandsファイルメニューにエクスポート項目を追加しました。メニュー ボタンは 1 つの状態の切り替えのみを担当し、実際の保存パネルはfileExporter開ける。

import SwiftUI
import UniformTypeIdentifiers

struct Store: FileDocument {
    static var readableContentTypes: [UTType] { [.commaSeparatedText] }

    var plants: [String] = ["Daisy", "Fern"]

    init() {}

    init(configuration: ReadConfiguration) throws {
        plants = []
    }

    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        let csv = plants.joined(separator: "\n")
        let data = Data(csv.utf8)
        return FileWrapper(regularFileWithContents: data)
    }
}

@main
struct ExportingGardenApp: App {
    @State private var store = Store()

    var body: some Scene {
        WindowGroup {
            Text("Garden")
        }
        .commands {
            ImportExportCommands(store: store)
        }
    }
}

struct ImportExportCommands: Commands {
    let store: Store
    @State private var isShowingExport = false

    var body: some Commands {
        CommandGroup(replacing: .importExport) {
            Section {
                Button("Export…") {
                    isShowingExport = true
                }
                .fileExporter(
                    isPresented: $isShowingExport,
                    document: store,
                    contentType: Store.readableContentTypes.first ?? .commaSeparatedText
                ) { result in
                    switch result {
                    case .success(let url):
                        print("Exported to \(url)")
                    case .failure(let error):
                        print("Export failed: \(error.localizedDescription)")
                    }
                }
            }
        }
    }
}

キーポイント:

  • Store従うFileDocumentとして使用できますfileExporterdocument
  • readableContentTypesデモに対応する CSV タイプを公開するStore.readableContentTypes.first
  • fileWrapper(configuration:)メモリデータをファイルコンテンツに変換します。 -.commandsアプリケーション シナリオにメニュー コマンドを追加します。 -CommandGroup(replacing: .importExport)システムが予期するインポートおよびエクスポートの場所にメニュー項目を配置します。 -Button("Export…")省略記号を使用して、次に保存パネルが表示されることをユーザーに通知します。 -isShowingExport = trueエクスポートプロセスをトリガーします。 -fileExporter表示ステータス、ドキュメント オブジェクト、コンテンツ タイプ、および完了コールバックを受け取ります。

Continuity Camera: iPhone から写真を直接インポート

15:08

ガーデニング アプリケーションの植物レコードには、すでにテキスト データが含まれています。写真は植物の成長状況を記録するのに適しています。

Continuity Camera を使用すると、Mac メニューで「iPhone からインポート > 写真を撮る」を選択できるようになります。 iPhone が写真を撮った後、その写真はアイテムプロバイダーを通じて SwiftUI ビューに渡されます。

import SwiftUI
import UniformTypeIdentifiers

struct PlantImage: Identifiable {
    let id: UUID
    var name: String
    var imageURL: URL?

    static let importImageTypes: [UTType] = [.image]

    static func importImageFromProviders(_ providers: [NSItemProvider], completion: @escaping (URL?) -> Void) {
        guard let provider = providers.first else {
            completion(nil)
            return
        }

        provider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { url, _ in
            completion(url)
        }
    }
}

struct GalleryView: View {
    @State private var plants = [
        PlantImage(id: UUID(), name: "Daisy"),
        PlantImage(id: UUID(), name: "Fern")
    ]
    @State private var selection = Set<PlantImage.ID>()

    var body: some View {
        List(plants, selection: $selection) { plant in
            Text(plant.name)
        }
        .importsItemProviders(selection.isEmpty ? [] : PlantImage.importImageTypes) { providers in
            PlantImage.importImageFromProviders(providers) { imageURL in
                guard let imageURL else { return }

                for id in selection {
                    if let index = plants.firstIndex(where: { $0.id == id }) {
                        plants[index].imageURL = imageURL
                    }
                }
            }
        }
    }
}

キーポイント:

  • ImportFromDevicesCommandsメイン メニューでデバイスからインポートするための入り口を提供します。デモンストレーションはそれを追加しますImportExportCommands後で。 -importsItemProviders現在のビューがデバイスのインポートによって生成された項目プロバイダーを受信できるようにします。 -selection.isEmpty ? [] : PlantImage.importImageTypesメニューの利用可能性を制御します。植物が選択されていない場合、画像のインポートは受け付けられません。 -PlantImage.importImageTypesここで使用される、デモ内のすべてのシステム ピク​​チャ タイプに対応します。.image高速イメージインポート機能。 -providersContinuity Camera から返される画像キャリアです。 -importImageFromProvidersディスクに保存された画像の URL を返します。 -for id in selection同じインポートされたイメージを、現在選択されているプラ​​ント レコードに適用します。 -plants[index].imageURL = imageURLモデルを更新すると、インターフェイスがステータスで更新されます。

重要なポイント

  1. 何をすべきか: データ管理 Mac アプリケーションに環境設定ウィンドウを追加します。 実行する価値がある理由:Settingsメインメニューに自動的にアクセスし、Command-,@AppStorage起動間でユーザーの選択内容を保存します。 開始方法:App追加するSettings { SettingsView(...) }、使用TabView一般、表示、エクスポートなどのペインを分割し、細かい設定を書き込む@AppStorage

  2. 対処方法: ウィンドウ間のドラッグ アンド ドロップ コピーをリストまたはテーブルに追加します。 実行する価値がある理由: Mac ユーザーは、データを整理するために複数のウィンドウを並べて開くことがよくあります。TableRow.itemProviderそしてonInsert同じ操作をドラッグ アンド ドロップ ワークフローに変えることができます。 開始方法: モデルに次のものを提供します。NSItemProvider、与えるTableRowに追加itemProvider、ターゲット内Table使用する場合onInsert(of:)プロバイダーを解析してデータを挿入します。

  3. 何をすべきか: CSV エクスポートをローカル データベースに追加します。 実行する価値がある理由:fileExporterシステム保存パネルを直接使用することで、ユーザーは使い慣れたファイル保存プロセスを実行できるため、アプリケーションはパネルの詳細を独自に管理する必要がありません。 開始方法: データ コンテナーを準拠させるFileDocument、存在するCommands「エクスポート」メニュー項目を追加して使用しますfileExporter(isPresented:document:contentType:)ファイルを書き出します。

  4. やるべきこと: 資産記録アプリケーションに「iPhone の写真からインポート」を追加します。 実行する価値がある理由: Continuity Camera は撮影デバイスと Mac アプリケーションを接続し、在庫管理、ガーデニング、メンテナンス記録、チケットの提出に適しています。 開始方法: アプリケーションコマンドに追加しますImportFromDevicesCommands、 追加importsItemProviders、返された画像 URL を選択したモデルに書き込みます。

  5. やるべきこと: アプリ専用のアクセントカラーを設定します。 実行する価値がある理由: ユーザーがシステムのマルチカラーのアクセント カラーを選択すると、アプリのボタン、選択項目のハイライト、サイドバーのアイコンで独自のテーマ カラーを使用できます。 開始方法: アセット カタログを開いて選択しますAccentColor、コンテンツをアプリケーションのテーマに一致するシステムカラーに設定します。デモンストレーションでは、ガーデニングのテーマに合わせてシステム グリーンを使用しています。

関連セッション

  • Mac 上の SwiftUI: 基礎を構築する - このセッションの最初の部分では、まず Mac アプリケーションのサイドバー、テーブル、ツールバー、検索、マルチウィンドウ、およびメニュー バーを構築します。
  • SwiftUI の新機能 — macOS Table、searchable、Canvas、FocusState、その他の API を含む、SwiftUI 2021 の全体的な新機能を確認してください。
  • SwiftUI でフォーカスを直接反映する - Mac およびマルチプラットフォーム アプリでのフォーカスの移動、キーボードの送信、および入力エクスペリエンスを補足します。
  • Demystify SwiftUI — SwiftUI のアイデンティティ、ライフサイクル、依存関係を理解し​​、状態をどこに配置するかを決定するのに役立ちます。

コメント

GitHub Issues · utterances