SwiftのObservableObjectを完全に理解するための15のステップ

SwiftのObservableObjectの徹底解説Swift
この記事は約22分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

SwiftUIを使用する際、状態管理の仕組みは非常に重要です。

その中でもObservableObjectは、データの変更を監視し、変更があった際にViewを更新する役割を果たすクラスです。

この記事では、SwiftのObservableObjectに関して、基本的な使い方から応用、注意点、カスタマイズ方法まで徹底的に解説していきます。

ObservableObjectはSwiftUIのデータフローの中心的存在であり、多くのSwiftUIアプリケーションで利用されています。

そのため、ObservableObjectをしっかりと理解することは、SwiftUIのアプリケーションを効率よく開発するための鍵となります。

この記事を通して、ObservableObjectの仕組みと使用法を学び、SwiftUIでの状態管理に自信を持つことができるようになりましょう。

●ObservableObjectとは

ObservableObjectは、SwiftUIでのデータフローを管理するためのプロトコルです。

このプロトコルを採用することで、データの変更を監視し、変更があった際にViewを自動的に更新することができます。

具体的には、ObservableObjectを適用したクラス内のデータが変更されたとき、その変更を購読しているViewに通知を行い、Viewの再描画をトリガーします。

これにより、データの変更とUIの更新がシームレスに連携することが実現されます。

ObservableObjectの利点は、データとUIの紐付けを簡単に実現できること、また、データの変更を自動的に検知し、関連するViewのみを効率的に更新することができる点にあります。

○ObservableObjectの基本的な役割

ObservableObjectの主な役割は、データの変更を監視し、その変更を購読しているViewに通知を送ることです。

この通知メカニズムにより、データの変更とUIの更新が連動して動作することが保証されます。

例えば、ユーザーによる入力や外部からのデータフィードの更新など、アプリケーション内のデータが変更されるシチュエーションは数多く存在します。

ObservableObjectは、これらのデータ変更を検知し、関連するViewの更新を行うための仕組みを提供しています。

また、ObservableObjectは、単なるデータの変更だけでなく、複数のデータ間の関連性や依存関係も考慮した更新を行うことができます。

これにより、アプリケーションの複雑なデータフローも、効率的かつ簡単に管理することが可能となります。

まとめると、ObservableObjectの役割は次の3つです。

  1. データの変更を監視する。
  2. 変更があった場合、関連するViewに通知を送る。
  3. 複雑なデータフローを効率的に管理する。

●ObservableObjectの使い方

Swiftのデータのフロー管理には、多くのフレームワークや機能が存在しています。

その中でもObservableObjectはSwiftUIと連携するための重要な役割を果たしています。

ここでは、ObservableObjectの基本的な使い方を3つのサンプルコードを通して詳しく解説します。

○サンプルコード1:ObservableObjectの基本的な実装

SwiftUIとの連携を意識したデータ管理のための第一歩として、ObservableObjectの基本的な実装を見てみましょう。

import SwiftUI
import Combine

class SampleData: ObservableObject {
    var name: String = "初期名"
}

このコードでは、SampleDataというクラスをObservableObjectとして定義しています。

この例では、nameという文字列を持っており、初期値として”初期名”が設定されています。

○サンプルコード2:@Publishedプロパティラッパーを使った状態の変更

次に、ObservableObject内のプロパティの変更を検知するための重要な要素である@Publishedプロパティラッパーの使い方を見てみましょう。

class SampleData: ObservableObject {
    @Published var name: String = "初期名"
}

このコードでは、nameプロパティの前に@Publishedプロパティラッパーを追加しました。

この例では、nameプロパティの変更をSwiftUIのViewなどで検知することができます。

○サンプルコード3:オブザーバーとしてのViewの役割

実際に、ObservableObjectの変更をViewで受け取る方法を解説します。

struct SampleView: View {
    @ObservedObject var data: SampleData

    var body: some View {
        Text(data.name)
    }
}

このコードでは、SampleViewというViewがSampleDataというObservableObjectをオブザーバーとして持っています。

ObservableObjectのnameプロパティの変更があると、Viewが自動的に更新されます。

この例では、nameプロパティの値がTextで表示されるようになっています。

この実装により、SampleDataクラスのnameプロパティの値が変わると、SampleViewのTextもそれに応じて自動的に更新されるのが確認できます。

●ObservableObjectの応用例

SwiftのObservableObjectをより実践的に使いこなすためには、いくつかの応用的な方法を理解する必要があります。

ここでは、ObservableObjectの応用例として、複数の状態変数を持つ場合や、外部からの状態変更をどのように受け取るか、さらにはObservableObjectを使って複雑なUIの更新をどのように行うかについて詳しく解説します。

○サンプルコード4:複数の状態変数を持つObservableObject

通常のObservableObjectの実装では一つのプロパティを監視することが多いですが、複数のプロパティを監視する場合もあります。

import SwiftUI
import Combine

class MultipleStateObject: ObservableObject {
    @Published var name: String = ""
    @Published var age: Int = 0
}

このコードでは、MultipleStateObjectというクラスを定義しており、nameageという2つのプロパティを持っています。

@Publishedプロパティラッパーを用いることで、これらのプロパティの変更を監視することができます。

この例では、名前と年齢を監視しているのがわかります。

○サンプルコード5:外部からの状態変更を受け取る方法

ObservableObjectを外部から状態を変更する場合には、外部の情報をどのようにObservableObjectに通知するかが問題となります。

class ExternalStateObject: ObservableObject {
    @Published var value: Int = 0

    func updateValue(with newValue: Int) {
        self.value = newValue
    }
}

上記のコードでは、ExternalStateObjectというクラス内に、外部から受け取るvalueという状態変数と、その値を更新するためのupdateValue(with:)メソッドが定義されています。

この例では、外部からupdateValue(with:)メソッドを呼び出すことで、valueの値を変更することができます。

○サンプルコード6:ObservableObjectを使った複雑なUIの更新

ObservableObjectはUIの更新にも大いに役立ちます。

特に複雑なUIの場合、適切なデータの更新が必要となるため、ObservableObjectの役割は非常に大きくなります。

struct ComplexUI: View {
    @ObservedObject var data: ExternalStateObject

    var body: some View {
        VStack {
            Text("Value: \(data.value)")
            Button("Increment") {
                data.updateValue(with: data.value + 1)
            }
        }
    }
}

上記のコードでは、ComplexUIというView内でExternalStateObjectのインスタンスを監視しています。

ボタンをクリックするとupdateValue(with:)メソッドが呼び出され、その結果としてUIが更新されます。

この例では、ボタンをクリックすることでテキストに表示される値が増加するようになっています。

●注意点と対処法

SwiftのObservableObjectを使用する際の注意点とそれに伴う対処法について、具体的なサンプルコードとともに詳しく解説していきます。

SwiftのObservableObjectは非常に強力なツールであり、適切に使用することで多くの恩恵を受けることができますが、一方でその特性を正しく理解しないと予期しない動作やバグの原因となる場合もあります。

○サンプルコード7:ObservableObjectの更新が反映されない時の対処法

このコードでは、ObservableObjectの更新がUIに反映されない場面を表現し、その原因と対処法を示しています。

この例では、ObservableObject内の変数を更新したにも関わらず、それがUIに正しく反映されないという状況を取り上げています。

import SwiftUI
import Combine

class SampleData: ObservableObject {
    var value: Int = 0
}

struct ContentView: View {
    @ObservedObject var data = SampleData()

    var body: some View {
        VStack {
            Text("\(data.value)")
            Button("Increment") {
                data.value += 1
            }
        }
    }
}

上記のコードを見ると、value変数はObservableObjectに属していますが、@Publishedプロパティラッパーがついていないため、この変数の値が変更されてもViewが更新されません。

この問題を解決するためには、次のように@Publishedプロパティラッパーをvalue変数に追加すればよいです。

@Published var value: Int = 0

この修正を加えることで、value変数の値が変更されると、関連するViewも自動的に更新されるようになります。

○サンプルコード8:ObservableObjectのメモリリーク対策

このコードでは、ObservableObjectを使用した際のメモリリークの可能性と、それを回避する方法を表しています。

この例では、ObservableObjectとそれを参照するオブジェクト間の強参照サイクルが発生し、メモリが解放されないという問題を取り上げています。

import SwiftUI
import Combine

class SampleData: ObservableObject {
    var otherObject = OtherClass()
    init() {
        otherObject.data = self
    }
}

class OtherClass {
    var data: SampleData?
}

上記のコードを見ると、SampleDataクラスとOtherClassクラスが相互に参照を持っており、これが強参照サイクルを引き起こしています。

この問題を解決するためには、一方の参照を弱参照(weak)や非所有参照(unowned)にする必要があります。

下記のようにOtherClass内のdata変数を弱参照にすることで、この問題を解決できます。

class OtherClass {
    weak var data: SampleData?
}

この修正により、SampleDataインスタンスとOtherClassインスタンス間の循環参照が解消され、メモリリークが回避されます。

ObservableObjectを使用する際は、特に複数のオブジェクトが相互に参照を持つ場合、このようなメモリリークのリスクを常に意識し、適切な対処を行うよう心掛けてください。

●カスタマイズ方法

ObservableObjectの機能性は、基本的な使い方だけでなく、カスタム通知の作成や複数のObservableObjectを組み合わせることによって、さらにその利便性を向上させることができます。

ここでは、そうしたカスタマイズ方法に焦点を当て、Swiftでの実装方法を具体的に紹介していきます。

○サンプルコード9:ObservableObjectのカスタム通知の作成

SwiftのObservableObjectでは、デフォルトでの状態変更の通知だけでなく、カスタムの通知を作成することも可能です。

これによって、特定の条件下でのみ通知を行いたい、といった状況にも柔軟に対応することができます。

import SwiftUI
import Combine

class CustomNotifier: ObservableObject {
    // 通常の状態変数
    @Published var count: Int = 0
    // カスタム通知のためのPublisher
    let objectWillChange = PassthroughSubject<Void, Never>()

    func increment() {
        count += 1
        // countが5の倍数の時だけカスタム通知を行う
        if count % 5 == 0 {
            objectWillChange.send()
        }
    }
}

このコードでは、PassthroughSubjectを使ってカスタムの通知を作成しています。

incrementメソッドでは、countが5の倍数のときだけobjectWillChange.send()を呼び出し、カスタム通知を行っています。

このコードを実行すると、カスタム通知はcountが5の倍数の時だけ行われることが確認できます。

○サンプルコード10:複数のObservableObjectを組み合わせる方法

複数のObservableObjectを組み合わせることで、より柔軟なデータフローを構築することが可能となります。

下記のサンプルでは、2つのObservableObjectを連携させてみます。

import SwiftUI
import Combine

class FirstObject: ObservableObject {
    @Published var data: Int = 0
}

class SecondObject: ObservableObject {
    @Published var firstObjectData: Int = 0
    var cancellables = Set<AnyCancellable>()

    init(firstObject: FirstObject) {
        firstObject.$data
            .sink(receiveValue: { [weak self] newData in
                self?.firstObjectData = newData
            })
            .store(in: &cancellables)
    }
}

このコードでは、FirstObjectdataが変更されたとき、その変更がSecondObjectfirstObjectDataにも反映されるようにしています。

sinkを使用し、FirstObjectのデータ変更を購読しています。

このコードを利用すると、FirstObjectのデータが変更されると、それが自動的にSecondObjectにも反映されることがわかります。

●応用的な使い方

Swiftでの開発において、ObservableObjectを更に深く活用することで、より複雑なデータフローや外部のデータとの同期、Combineフレームワークとの組み合わせなど、多岐にわたる機能を実現することができます。

ここでは、これらの応用的な使い方をサンプルコードを交えながら、詳細に解説していきます。

○サンプルコード11:ObservableObjectを用いた複雑なデータフローの構築

このコードではObservableObjectを使って、複数のデータソースから情報を取得し、それを組み合わせてUIに反映する複雑なデータフローを構築する方法を表しています。

この例では、二つのデータソースから取得した情報を結合して、一つの情報として表示します。

import SwiftUI
import Combine

class ComplexDataFlow: ObservableObject {
    @Published var dataSource1: String = ""
    @Published var dataSource2: String = ""

    var combinedData: String {
        return "\(dataSource1) \(dataSource2)"
    }
}

struct ComplexDataFlowView: View {
    @ObservedObject var dataFlow = ComplexDataFlow()

    var body: some View {
        Text(dataFlow.combinedData)
    }
}

このコードの中心となるComplexDataFlowクラスでは、dataSource1dataSource2の二つの@Publishedプロパティを持っています。

これらのデータが更新されると、combinedDataプロパティを介してそれらを結合し、結果をUIに反映します。

○サンプルコード12:ObservableObjectとCombineフレームワークの組み合わせ

このコードでは、ObservableObjectとCombineフレームワークを組み合わせることで、データのストリーム処理や変更の通知を効率的に実現しています。

この例では、入力されたテキストの変更を検知し、それに応じて他の値も更新します。

import SwiftUI
import Combine

class CombineObservable: ObservableObject {
    @Published var inputText: String = ""
    @Published var processedText: String = ""

    private var cancellables = Set<AnyCancellable>()

    init() {
        $inputText
            .map { text in
                return text.uppercased()
            }
            .assign(to: &$processedText)
            .store(in: &cancellables)
    }
}

struct CombineObservableView: View {
    @ObservedObject var combineObservable = CombineObservable()

    var body: some View {
        VStack {
            TextField("Enter text", text: $combineObservable.inputText)
            Text(combineObservable.processedText)
        }
    }
}

このコードでは、CombineObservableクラスが入力されたテキストを大文字に変換してprocessedTextとして保存する機能を持っています。

Combineフレームワークのmap関数を使用してこの変換を行い、assign関数を用いて結果を@Publishedプロパティに保存しています。

○サンプルコード13:ObservableObjectを用いた外部データの同期

このコードではObservableObjectを使って、外部からのデータを取得し、それをアプリ内で同期して表示しています。

この例では、外部APIから取得したデータをアプリに同期しています。

import SwiftUI
import Combine

class ExternalDataSync: ObservableObject {
    @Published var externalData: String = ""

    func fetchDataFromAPI() {
        // ここでは外部APIからのデータ取得をシミュレートしています
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            DispatchQueue.main.async {
                self.externalData = "Fetched Data from API"
            }
        }
    }
}

struct ExternalDataSyncView: View {
    @ObservedObject var dataSync = ExternalDataSync()

    var body: some View {
        VStack {
            Text(dataSync.externalData)
            Button("Fetch Data", action: dataSync.fetchDataFromAPI)
        }
    }
}

このコードのExternalDataSyncクラスでは、fetchDataFromAPI関数を呼び出すことで外部APIからデータを非同期に取得し、それを@Publishedプロパティに保存することでUIに反映しています。

ボタンを押すと、シミュレートされたAPIからのデータ取得が行われ、その結果がテキストとして表示されます。

●高度なカスタマイズと最適化

SwiftのObservableObjectは、データの変更を監視し、変更を検知した際に通知を送る仕組みを提供しています。

しかし、プロジェクトが大きくなると、ObservableObjectのカスタマイズや最適化のニーズが高まります。

ここでは、ObservableObjectをより高度にカスタマイズし、最適化する方法について詳しく解説します。

○サンプルコード14:ObservableObjectの最適化技巧

このコードでは、ObservableObjectを使用した際のパフォーマンス最適化のテクニックを表しています。

この例では、不要な通知を防ぐための条件を追加し、オブザーバブルオブジェクトの更新を効率的に行っています。

import SwiftUI
import Combine

class EfficientObservable: ObservableObject {
    @Published var value: Int = 0 {
        willSet {
            // 新しい値と現在の値が同じであれば更新しない
            if newValue == value {
                objectWillChange.send()
            }
        }
    }
}

struct EfficientView: View {
    @ObservedObject var observable = EfficientObservable()

    var body: some View {
        Text("\(observable.value)")
            .onTapGesture {
                // 値を更新
                observable.value += 1
            }
    }
}

このコードを実行すると、EfficientObservableオブジェクトのvalueプロパティが更新されるたびに、その変更がEfficientViewに反映されます。

ただし、新しい値と現在の値が同じ場合は、不要な通知を送らないようにしています。

○サンプルコード15:ObservableObjectとSwiftUIの高度な連携技法

このコードでは、ObservableObjectとSwiftUIの連携を高度にカスタマイズして、特定の条件下でのみViewを更新する技法を表しています。

この例では、特定の範囲の値の変更のみを検知して、それに応じてViewを更新しています。

import SwiftUI
import Combine

class CustomObservable: ObservableObject {
    private var internalValue: Int = 0
    var value: Int {
        get { internalValue }
        set {
            // 値が10の倍数であれば更新
            if newValue % 10 == 0 {
                internalValue = newValue
                objectWillChange.send()
            }
        }
    }
}

struct CustomView: View {
    @ObservedObject var observable = CustomObservable()

    var body: some View {
        Text("\(observable.value)")
            .onTapGesture {
                // 値を10増加
                observable.value += 10
            }
    }
}

このコードを実行すると、CustomObservablevalueプロパティが10の倍数になったときのみ、その変更がCustomViewに反映されます。

それ以外の値の変更は無視され、Viewの更新は行われません。

まとめ

SwiftのObservableObjectは、データの変更を監視し、変更を検知した際に通知を送る強力な仕組みです。

この記事を通じて、ObservableObjectの基本的な役割から高度なカスタマイズ・最適化方法までを学ぶことができたかと思います。

特に、不要な通知を削減するための条件追加や、特定の条件下でのみViewを更新するような高度な連携技法は、実践的なSwiftUIアプリケーション開発において非常に役立つ情報となります。

ObservableObjectを効果的に使用することで、データの変更とUIの更新をシームレスに連携させることができ、効率的なアプリケーションの構築が可能となります。

今後もSwiftとSwiftUIの技術が進化する中で、この知識をベースにさらなる深化を追求していくことをおすすめします。