#この記事は何か
SwiftUIフレームワークは、アプリのデータモデルとビューを接続します。
この投稿では、SwiftUIアプリの「モデルのデータを管理する方法」について、Apple Developerサイトのリファレンスを独自に解釈・翻訳します。
###環境
- macOS 11.0.1
- Xcode 12.2
- Swift 5.3
#概要
通常、アプリ内でデータを保存したり処理するには、アプリのUIやその他のロジックとは分離されたデータモデルを使用します。データモデルを分離することでモジュール化が促進され、テストしやすくなり、アプリがどのように動作するかがわかりやすくなります。
従来はビューコントローラを使用して、モデルとUIの間でデータをやり取りしていました。SwiftUIは、このやり取りのほとんどを自動的に処理してくれます。データの変更に合わせてビューを更新するには、データモデルのクラスを観測可能なオブジェクトにして、そのプロパティをパブリッシュ(公開)します。そして、そのインスタンスを宣言する際に「特殊な属性」を使用します。また、「ユーザーによって行われたデータの変更」を確実にモデルに反映するには、UIコントロールをモデルのプロパティにバインドします。これらの機能を併用することで、データを**Source of Truth(信頼できる情報源)**にできます。
##観測可能なモデルデータを作成する
モデルデータにおける変更をSwiftUIに感知させるには、モデルとなるクラスにObservableObject
プロトコルを採用します。
次のコードは、観測可能なオブジェクトとしてBook
クラスを作成します。
class Book: ObservableObject {
}
システムは、クラスの付属型ObjectWillChangePublisher
型を自動的に推論します。そして、自動的に合成されるプロトコル要件のobjectWillChange
メソッドによって、@Published
属性プロパティの変更を発信します。プロパティをパブリッシュするには、宣言に@Published
属性をマークします。
class Book: ObservableObject {
@Published var title = "Great Expectations"
}
不要な公開プロパティによるオーバーヘッドは回避してください。値に変更が発生し、UIにとって重要なプロパティだけを公開します。例えば、Book
クラスには、初期化しても変更されない識別子を示すidentifier
プロパティがあるかもしれません。
class Book: ObservableObject {
@Published var title = "Great Expectations"
let identifier = UUID() // 変更されない識別子
}
識別子をUIに表示することもできますが、このプロパティは@Publised
属性ではないので、SwiftUIがidentifierプロパティの変更を監視することはありません。
##観測可能なオブジェクトにおける変更を感知する
SwiftUIに観測可能なオブジェクトを監視させるには、プロパティの宣言に@ObservedObject
属性を追加します。
struct BookView: View {
@ObservedObject var book: Book // モデルのインスタンスを監視させる
var body: some View {
Text(book.title)
}
}
上記のような観測オブジェクトを示すプロパティを個別に子ビューに渡すことができます。データが変更されると、ディスクから新しいデータをロードしたかのように、SwiftUIは影響を受けるビューのみ表示を更新します。また、観測可能なオブジェクト自体を子ビューに渡して、ビュー階層のレベル間でモデルオブジェクトを共有することもできます。
struct BookView: View {
@ObservedObject var book: Book
var body: some View {
BookEditView(book: book) // 子ビューを初期化する際にモデルを渡す
}
}
// Bookビューの子ビュー
struct BookEditView: View {
@ObservedObject var book: Book // モデルを共有
// ...
}
##ビュー内のモデルオブジェクトを初期化する
SwiftUIは何度もビューを作成・再作成するので、与えられた入力値のセットで常に同じビューを初期化できることが重要です。そのため、観測オブジェクトをビュー内で作成することは危険です。その代わりに、SwiftUIは@StateObject
属性を提供します。この属性を使用すると、ビュー内でも安全にBook
型インスタンスを作成することができます。
struct LibraryView: View {
@StateObject var book = Book() // このライブラリビューで作成、管理される
var body: some View {
BookView(book: book)
}
}
SwiftUIが「ビュー側でオブジェクトインスタンスを作成・管理される」と知っていることを除けば、@StateObject
属性プロパティは「観測オブジェクト」と同じです。何度でも、ビューを再作成できます。上のコードのようにローカルで使用したり、あるいは「別のビュー」の@ObservedObject
属性プロパティに渡したりできます。
SwiftUIはビュー内で@StateObject
属性のインスタンスを再作成しませんが、それぞれのビューに対して個別のインスタンスを作成します。以下のコードにおいて、それぞれのLibraryView
は一意のBook
インスタンスを取得します。
VStack {
LibraryView()
LibraryView()
}
また、トップレベルのApp
インスタンス、およびScene
インスタンスの中で@StateObject
属性のインスタンスを作成することもできます。例えば、「ブックリーダーアプリ内で本のコレクションを保持する」ためにLibrary
という観測可能なオブジェクトを定義する場合、アプリのトップレベルに単一のLibrary
インスタンスを作成します。
@main
struct BookReader: App {
@StateObject var library = Library()
// ...
}
##アプリケーション全体でオブジェクトを共有する
「アプリ全体にわたって使用したいデータモデル」のオブジェクトを何層も貫通させたくない場合は、environmentObject(_:)
モディファイアを使用します。ビュー間で受け渡しながら共有する代わりに、モデルオブジェクトを環境に配置できます。
@main
struct BookReader: App {
@StateObject var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environmentObject(library)
}
}
}
プロパティを@EnvironmentObject
属性で宣言することで、モディファイアを適用したビューはその子孫にわたって、データモデルのインスタンスにアクセスできます。
struct LibraryView: View {
@EnvironmentObject var library: Library
// ...
}
上記のように、@EnvironmentObject
属性のインスタンスは「アプリにおけるビュー階層の最上位」に作成して使用します。あるいは、ビュー階層におけるサブツリーのルートビューに作成しても良いでしょう。いずれにしても、オブジェクトを使用するビューや子孫ビューでは、「プレビュープロバイダにも追加すること」を忘れないでください。
struct LibraryView_Previews: PreviewProvider {
static var previews: some View {
LibraryView()
.environmentObject(Library())
}
}
##バインディングを使って、双方向の接続を作成する
UIコントロールからデータモデルを変更できるようにするには、対応するプロパティへのバインディングを使用します。バインディングを経由することにより、更新が自動的にデータモデルに反映されます。@OvservedObject
属性、@StateObject
属性、および@EnvironmentObject
属性プロパティにバインディングするには、オブジェクト名にドル記号 $
を付けます。例えば、ユーザーに「本のタイトル」を編集させるためにBookEditView
ビューにTextField
コントロールを追加する場合、TextField
と$book.title
プロパティのバインディングを設定します。
struct BookEditView: View {
@ObservedObject var book: Book
var body: some View {
TextField("Title", text: $book.title)
}
}
バインディングは「ビューの要素」を「基礎となるモデル」に接続して、ユーザーがモデルデータの値を直接変更できるようにします。