SwiftUIにおけるアプリの重要事項 – WWDC2020

Session概要

新しいAppプロトコルのおかげで、SwiftUIでアプリ全体を構築できるようになりました。App、SceneおよびViewがいかに相互につながり合うかをご確認ください。短時間で複雑な作業を経ることなく、最高クラスの製品に期待される機能を容易に実装する方法を学びましょう。新しいコマンドモディファイアを用いて、インタフェースに機能を簡単に追加する方法や、新しいWindowGroup APIの詳細をご紹介します。 https://developer.apple.com/videos/play/wwdc2020/10037/

Views, Scenes, Apps プロトコルについて

view, scene, appの構成

Views

  • 基本的な構成要素スクリーンに見えるもの全てをレンダリングする
  • SwiftUIで実装されたUIコンポーネントは全てViewであるが、全てのViewが同じアプリのものではない
    • アプリはスクリーン全体を完全にコントロールしているわけではないため
  • そのためプラットフォーム側で異なる領域でアプリの一部を表示する
    • SwiftUIでは、これらの異なる領域をScenesと呼ぶ

Scenes

  • Sceneの内容をスクリーンに表示する一般的な方法はWindowである
    • iPadOSでは、複数のWindowを並べて表示可能
    • iOS, watchOS, tvOSは各アプリを1つのフルスクリーンWindowで表示することを好む
    • macOSはSceneのコンテンツが様々な方法で表示される
      • 複数Window
      • アプリ内でのタブ表示(タブがSceneの代わりとなる)
  • これらのSceneの塊がAppのコンテンツを作る

複数のSceneでわけてアプリを管理する
複数のタブ(Scene)表示の例
複数のWindow表示の例

Apps

SwiftUIコードでのView, Scene, Apps

コードとview, scene, appとの関係性
// @mainはSwift5.3の新属性で、あるタイプをエントリーポイントとしてプログラムを実行する
// 通常Swiftプログラムの実行にはmain.swiftファイルが必要ですが、
// @mainによりその責任を抽象的に委任することが可能
@main
struct BookClubApp: App { // AppプロトコルはSceneを持つ
    @StateObject private var store = ReadingListStore()

    var body: some Scene {
        // SceneプロトコルはWindowGroupを持つ
        WindowGroup {
            ReadingListViewer(store: store)
        }
    }
}

struct ReadingListViewer: View {
    @ObservedObject var store: ReadingListStore

    var body: some View {
        NavigationView {
            List(store.books) { book in
                Text(book.title)
            }
            .navigationTitle("Currently Reading")
        }
    }
}

class ReadingListStore: ObservableObject {
    init() {}

    var books = [
        Book(title: "Book #1", author: "Author #1"),
        Book(title: "Book #2", author: "Author #2"),
        Book(title: "Book #3", author: "Author #3")
    ]
}

struct Book: Identifiable {
    let id = UUID()
    let title: String
    let author: String
}

SwiftUIのSceneアーキテクチャについて

  • WindowGroupはiPadアプリの複数新規作成を自動で提供する
    • Windowのそれぞれがインタフェースのそれぞれの状態に反映している
  • App Switcherでのタイトルは各SceneのNavigation titleが反映される
    • これはビューモディファイアによって行われる
    • macOSの場合、Menuに新規作成機能が追加される
  • macOSは複数のWindowサポートだけでなく、Windowのグループ化をサポートする
    • 複数のWindowをグループ化することで、1つのアプリにタブで統合される
    • この機能は特別な実装は不要で、SwiftUIが自動的に機能提供する
  • WindowGroupは新しい子Sceneをインスタンス化し、デフォルトでWindow内にコンテンツを表示する
    • macOSやiPadOSなどの複数Windowを表示するプラットフォームでは、WindowGroupは複数の子Sceneをインスタンス化する

アプリの複数新規作成の例、各SceneのViewの状態は独立している
複数のアプリを1つの アプリに統合した例
WindowGroupが複数のSceneを作成し、独立した状態を持つ

サンプルコードとAPIについて

  • SceneStorageプロパティラッパーを使うとView状態を維持できる
    • 保存状態識別のための一意のキーが必要
@main
struct BookClubApp: App {
    @StateObject private var store = ReadingListStore()

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

struct ReadingListViewer: View {
    @ObservedObject var store: ReadingListStore
    @SceneStorage("selectedItem") private var selectedItem: String?
    
    var selectedID: Binding<UUID?> {
        Binding<UUID?>(
            get: { selectedItem.flatMap { UUID(uuidString: $0) } },
            set: { selectedItem = $0?.uuidString }
        )
    }

    var body: some View {
        NavigationView {
            List(store.books) { book in
                NavigationLink(
                    destination: Text(book.title),
                    tag: book.id,
                    selection: selectedID
                ) {
                    Text(book.title)
                }
            }
            .navigationTitle("Currently Reading")
        }
    }
}

class ReadingListStore: ObservableObject {
    init() {}

    var books = [
        Book(title: "Book #1", author: "Author #1"),
        Book(title: "Book #2", author: "Author #2"),
        Book(title: "Book #3", author: "Author #3")
    ]
}

struct Book: Identifiable {
    let id = UUID()
    let title: String
    let author: String
}

アプリをカスタマイズ可能なその他API

  • DocumentGroup Sceneタイプ
    • 自動的に管理してオープン、編集、そしてドキュメントベースシーンを保存する
  • Settings Sceneタイプ
    • macOSで使用でき、スタンダードプリファレンスWindowに自動的に設定する
    • プリファレンスコマンドをアプリのMenuに自動的に設定する
    • また、Windowに正しいスタイル処理を施す
  • commandモディファイア
    • 状態駆動プログラムモデルでAppKit, UIKitのレスポンダーチェーンに似ている
// DocumentGroup Sceneのサンプル

import SwiftUI
import UniformTypeIdentifiers

@main
struct ShapeEditApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: ShapeDocument()) { file in
            DocumentView(document: file.$document)
        }
    }
}

struct DocumentView: View {
    @Binding var document: ShapeDocument
    
    var body: some View {
        Text(document.title)
            .frame(width: 300, height: 200)
    }
}

struct ShapeDocument: Codable {
    var title: String = "Untitled"
}

extension UTType {
    static let shapeEditDocument =
        UTType(exportedAs: "com.example.ShapeEdit.shapes")
}

extension ShapeDocument: FileDocument {
    static var readableContentTypes: [UTType] { [.shapeEditDocument] }
    
    init(fileWrapper: FileWrapper, contentType: UTType) throws {
        let data = fileWrapper.regularFileContents!
        self = try JSONDecoder().decode(Self.self, from: data)
    }

    func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws {
        let data = try JSONEncoder().encode(self)
        fileWrapper = FileWrapper(regularFileWithContents: data)
    }
}
// 設定 Sceneのサンプル
@main
struct BookClubApp: App {
    @StateObject private var store = ReadingListStore()

    @SceneBuilder var body: some Scene {
        WindowGroup {
            ReadingListViewer(store: store)
        }
        
    #if os(macOS)
        Settings {
            BookClubSettingsView()
        }
    #endif
    }
}

struct BookClubSettingsView: View {
    var body: some View {
        Text("Add your settings UI here.")
            .padding()
    }
}

struct ReadingListViewer: View {
    @ObservedObject var store: ReadingListStore

    var body: some View {
        NavigationView {
            List(store.books) { book in
                Text(book.title)
            }
            .navigationTitle("Currently Reading")
        }
    }
}

class ReadingListStore: ObservableObject {
    init() {}

    var books = [
        Book2(title: "Book #1", author: "Author #1"),
        Book2(title: "Book #2", author: "Author #2"),
        Book2(title: "Book #3", author: "Author #3")
    ]
}

struct Book: Identifiable {
    let id = UUID()
    let title: String
    let author: String
}
// commandモディファイアのサンプル
struct BookCommands: Commands {
    @FocusedBinding(\.selectedBook) private var selectedBook: Book?
    
    var body: some Commands {
        CommandMenu("Book") {
            Section {
                Button("Update Progress...", action: updateProgress)
                    .keyboardShortcut("u")
                Button("Mark Completed", action: markCompleted)
                    .keyboardShortcut("C")
            }
            .disabled(selectedBook == nil)
        }
    }
    
    private func updateProgress() {
        selectedBook?.updateProgress()
    }
    private func markCompleted() {
        selectedBook?.markCompleted()
    }
}
最新情報をチェックしよう!