4
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

【SwiftUI】アプリケーションのモデルデータを管理する

この記事は何か

SwiftUIフレームワークは、アプリのデータモデルとビューを接続します。
この投稿では、SwiftUIアプリの「モデルのデータを管理する方法」について、Apple Developerサイトのリファレンスを独自に解釈・翻訳します。

環境

  • macOS 11.0.1
  • Xcode 12.2
  • Swift 5.3

概要

通常、アプリ内でデータを保存したり処理するには、アプリのUIやその他のロジックとは分離されたデータモデルを使用します。データモデルを分離することでモジュール化が促進され、テストしやすくなり、アプリがどのように動作するかがわかりやすくなります。

従来はビューコントローラを使用して、モデルとUIの間でデータをやり取りしていました。SwiftUIは、このやり取りのほとんどを自動的に処理してくれます。データの変更に合わせてビューを更新するには、データモデルのクラスを観測可能なオブジェクトにして、そのプロパティをパブリッシュ(公開)します。そして、そのインスタンスを宣言する際に「特殊な属性」を使用します。また、「ユーザーによって行われたデータの変更」を確実にモデルに反映するには、UIコントロールをモデルのプロパティにバインドします。これらの機能を併用することで、データをSource of Truth(信頼できる情報源)にできます。

観測可能なモデルデータを作成する

モデルデータにおける変更をSwiftUIに感知させるには、モデルとなるクラスにObservableObjectプロトコルを採用します。
次のコードは、観測可能なオブジェクトとしてBookクラスを作成します。

観測可能なBookクラスを作成する
class Book: ObservableObject {
}

システムは、クラスの付属型ObjectWillChangePublisher型を自動的に推論します。そして、自動的に合成されるプロトコル要件のobjectWillChangeメソッドによって、@Published属性プロパティの変更を発信します。プロパティをパブリッシュするには、宣言に@Published属性をマークします。

titleプロパティを@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属性を追加します。

ビューのプロパティを監視オブジェクトにする@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インスタンスを取得します。

LibraryView毎に、ユニークなBookインスタンスが作成される
VStack {
    LibraryView()
    LibraryView()
}

また、トップレベルのAppインスタンス、およびSceneインスタンスの中で@StateObject属性のインスタンスを作成することもできます。例えば、「ブックリーダーアプリ内で本のコレクションを保持する」ためにLibraryという観測可能なオブジェクトを定義する場合、アプリのトップレベルに単一のLibraryインスタンスを作成します。

@main
struct BookReader: App {
    @StateObject var library = Library()

    // ...
}

アプリケーション全体でオブジェクトを共有する

「アプリ全体にわたって使用したいデータモデル」のオブジェクトを何層も貫通させたくない場合は、environmentObject(_:)モディファイアを使用します。ビュー間で受け渡しながら共有する代わりに、モデルオブジェクトを環境に配置できます。

アプリケーションを示すBookReader型
@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)
    }
}

バインディングは「ビューの要素」を「基礎となるモデル」に接続して、ユーザーがモデルデータの値を直接変更できるようにします。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
4
Help us understand the problem. What are the problem?