この記事は何?
SwiftUIのObservationを理解するために、Appleの開発者向けサイトからダウンロードできるサンプルアプリ「BookReaderApp」を独自に解説する。
Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「今日からはじめるプログラミング」をお勧めします。
Observationマクロ自体については、こちらの記事でより具体的に解説。
Read Me
サンプルアプリを実行するには:
- ターゲット設定の「Signing & Capabilities」タブで開発チームを選択。
- MacがmacOS 14以降を実行している場合はdestinationに「My Mac」を選択。それ以外の場合は、iOS 17以降を実行しているiOSデバイスまたはシミュレータを選択。
- ビルドして実行。
プロジェクト
図書を管理するためのシンプルなiOSアプリケーション。
BookReaderApp
アプリにおけるビュー階層のトップレベルになるシーン。
作成するシーンには、LibraryView
ビューを含むウィンドウグループがある。
import SwiftUI
@main
struct BookReaderApp: App {
@State private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environment(library)
}
}
}
データフローの観点では、状態オブジェクトをLibraryView
ビューの環境に設定している点に注目する。
Models
このアプリには3つのデータモデルがある。
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()
}
現在のライブラリに独自の環境プロパティを定義して使用するには、以下の手順を行う。
-
BookReaderApp.swift
を開く -
.environment(library)
を.environment(\.library, library)
に置き換える -
LibraryView.swift
を開く -
@Environment(Library.self) private var library
を@Environment(\.library) private var library
に置き換える
Book.swift
図書を示す、監視可能なデータモデル。
@Observable final class Book: Identifiable {
var title = "Sample Book Title"
var author = Author()
var isAvailable = true
}
Author.swift
著者を示す、監視可能なデータモデル。
@Observable final class Author: Identifiable {
var name = "Sample Author"
}
Views
このアプリには4つのビューがあり、LibraryView
ビューが頂点となる階層構造になっている。
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
ビューのリストに並ぶ個々の図書を表示する。
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
このビューは「図書のタイトル」と、それを編集するためのボタンを表示する。
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
アプリのユーザーは、このビューを介して図書のタイトルを編集できる。
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())
}