【SwiftUI】「BookReaderApp」から学ぶObservation

# この記事は何？

SwiftUIのObservationを理解するために、Appleの開発者向けサイトからダウンロードできるサンプルアプリ「[BookReaderApp](https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app)」を独自に解説する。

> Swiftを基礎から学ぶには
自著、工学社より発売中の「[まるごと分かるSwiftプログラミング](https://amzn.asia/d/7YPWJwl)」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
> また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「[今日からはじめるプログラミング](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwii1qTuj_2BAxV4hlYBHbj5BqEQFnoECBYQAQ&url=https%3A%2F%2Fwww.udemy.com%2Fcourse%2Fstart-programming-today%2F&usg=AOvVaw2vs4Sccq-bpfA4ZD2rCyNm&opi=89978449)」をお勧めします。

> Observationマクロ自体については、[こちらの記事](https://qiita.com/imchino/items/324b80c0ca2490093d90)でより具体的に解説。
##  Read Me

サンプルアプリを実行するには:

1. ターゲット設定の「Signing & Capabilities」タブで開発チームを選択。
2. MacがmacOS 14以降を実行している場合はdestinationに「My Mac」を選択。それ以外の場合は、iOS 17以降を実行しているiOSデバイスまたはシミュレータを選択。
3. ビルドして実行。

# プロジェクト

図書を管理するためのシンプルなiOSアプリケーション。

## BookReaderApp

アプリにおけるビュー階層のトップレベルになるシーン。
作成するシーンには、`LibraryView`ビューを含むウィンドウグループがある。

```swift: BookReaderApp.swift
import SwiftUI

@main
struct BookReaderApp: App {
    @State private var library = Library()
    
    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environment(library)
        }
    }
}
```

データフローの観点では、状態オブジェクトを`LibraryView`ビューの環境に設定している点に注目する。

## Models

このアプリには3つのデータモデルがある。

### Library.swift


「図書のリストを管理するライブラリ」を示す、観測可能なデータモデル。

```swift:Library.swift
@Observable final class Library {
    var books: [Book] = [Book(), Book(), Book()]
    
    var availableBooksCount: Int {
        books.filter(\.isAvailable).count
    }
}

extension EnvironmentValues {
    var library: Library {
        get { self[LibraryKey.self] }
        set { self[LibraryKey.self] = newValue }
    }
}

private struct LibraryKey: EnvironmentKey {
    static var defaultValue: Library = Library()
}
```

現在のライブラリに独自の環境プロパティを定義して使用するには、以下の手順を行う。

1. `BookReaderApp.swift`を開く
2. `.environment(library)` を `.environment(\.library, library)`に置き換える
3. `LibraryView.swift`を開く
4. `@Environment(Library.self) private var library` を `@Environment(\.library) private var library`に置き換える

### Book.swift

図書を示す、監視可能なデータモデル。

```swift:Book.swift
@Observable final class Book: Identifiable {
    var title = "Sample Book Title"
    var author = Author()
    var isAvailable = true
}
```

### Author.swift

著者を示す、監視可能なデータモデル。

```swift:Author.swift
@Observable final class Author: Identifiable {
    var name = "Sample Author"
}
```

## Views

このアプリには4つのビューがあり、`LibraryView`ビューが頂点となる階層構造になっている。

### LibraryView.swift

このビューは、ライブラリで利用可能な図書をリスト形式で表示する。

![スクリーンショット 2023-10-17 21.44.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/06bd8f7c-0be5-46ec-ccb2-8bcb7725815d.png)

```swift:LibraryView.swift
struct LibraryView: View {
    @Environment(Library.self) private var library
    
    var body: some View {
        NavigationStack {
            List(library.books) { book in
                NavigationLink {
                    BookView(book: book)
                } label: {
                    LibraryItemView(book: book)
                }
            }
            .navigationTitle("Books available: \(library.availableBooksCount)")
        }
    }
}

#Preview {
    LibraryView()
        .environment(Library())
}
```

### LibraryItemView.swift

このビューは、`LibraryView`ビューのリストに並ぶ個々の図書を表示する。

![スクリーンショット 2023-10-17 21.43.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/d33c93c6-7ac5-00a8-3663-58428a49fe3a.png)

```swift:LibraryItemView.swift
struct LibraryItemView: View {
    var book: Book
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(book.title)
            Text("Written by: \(book.author.name)")
                .font(.caption)
        }
    }
}

#Preview {
    LibraryItemView(book: Book())
}
```

### BookView.swift

このビューは「図書のタイトル」と、それを編集するためのボタンを表示する。

![スクリーンショット 2023-10-17 21.47.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/5d979798-a436-4e9b-3402-2868be64e9fd.png)

```swift:BookView.swift
struct BookView: View {
    var book: Book
    @State private var isEditorPresented = false
    
    var body: some View {
        VStack {
            List {
                Text(book.title)
                Text("Written by: \(book.author.name)")
                HStack {
                    Text(book.isAvailable ? "Available for checkout" : "Waiting for return")
                    Spacer()
                    Button(book.isAvailable ? "Check out" : "Return") {
                        book.isAvailable.toggle()
                    }
                }
            }
            Button("Edit book") {
                isEditorPresented = true
            }
        }
        .sheet(isPresented: $isEditorPresented) {
            BookEditView(book: book)
        }
    }
}

#Preview {
    BookView(book: Book())
}
```

### BookEditView.swift

アプリのユーザーは、このビューを介して図書のタイトルを編集できる。

![スクリーンショット 2023-10-17 21.44.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/b6c95907-778d-6070-6fc0-aea5f23a862b.png)

```swift:BookEditView.swift
struct BookEditView: View {
    @Bindable var book: Book
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        VStack() {
            HStack {
                Text("Title")
                TextField("Title", text: $book.title)
                    .textFieldStyle(.roundedBorder)
                    .onSubmit {
                        dismiss()
                    }
            }
            
            Toggle(isOn: $book.isAvailable) {
                Text("Book is available")
            }
            
            Button("Close") {
                dismiss()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

#Preview {
    BookEditView(book: Book())
}
```