メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

SwiftUIにおけるEnvironmentの活用法

こんにちは!メドピアにてモバイルアプリエンジニアをしている王です。 今回の「ClinPeerアプリ開発の裏側連載記事」では、SwiftUIのEnvironmentについてお話しできればと思います。 tech.medpeer.co.jp

SwiftUIのEnvironmentは、ビュー間でデータを共有するための強力な依存性注入(DI)の仕組みです。多くのSwiftUIプロジェクトで活用されており、ビュー間のデータ伝達を簡素化し、アプリ設計の柔軟性を高めます。ClinPeerでは、ほぼすべての画面をSwiftUIで構築しており、Environmentの活用について考察しています。

SwiftUIにおけるUI専用の依存性注入

依存性注入(Dependency Injection、DI)は、モダンなソフトウェア開発において重要な技術であり、以下の利点があります。

  • 疎結合:コンポーネント間の依存関係を最小限に抑えることで、モジュールの再利用性と保守性が向上します。
  • 保守性:依存関係が明示的になることで、コードの理解と修正が容易になります。
  • テスト容易性:依存関係を差し替えることで、ユニットテストが容易になります。

SwiftUIのEnvironmentは、ビューがロードされた後にのみ値を取得できるという特徴があります。この設計は、SwiftUIが宣言型UIフレームワークであることを反映しており、ビューの構築と更新がデータや環境と一貫性を保つようになっています。

値型以外の活用

Environmentは、単なる値型の注入にとどまらず、関数、ファクトリーメソッド、プロトコル制約など、さまざまな形式の依存性を注入できます。実際、SwiftUIの標準Environment値には、editModedismissmanagedObjectContextなど、非値型の例も含まれています。

開発者は、Environmentの活用を値型に限定せず、SwiftUIの特性を活かして、より柔軟で疎結合なコンポーネントを構築することが重要です。

Observationフレームワークの活用

EnvironmentObjectを使用する際、依存性の注入を忘れるとアプリがクラッシュするリスクがあります。一方、Environmentはデフォルト値を要求するため、より安全で信頼性の高い設計が可能です。iOS 17以降、Observationフレームワークの導入により、可観測オブジェクトの注入がさらに容易になりました。

extension EnvironmentValues {
    @Entry var article: Article = .init()
}

@Observable
class Article {
    // プロパティやメソッドを定義
}

struct ContentView: View {
    @Environment(\.article) var article
    var body: some View {
        // ビューの構築
    }
}

この方法では、クラッシュのリスクを回避し、同じ型の可観測インスタンスを複数使い分ける柔軟性も得られます。

extension EnvironmentValues {
    @Entry var article: Article = .init()
    @Entry var article1: Article = .init()
    @Entry var article2: Article = .init()
}

Environmentの最適化

Environmentを使用してアプリの状態を管理する際、ビューの更新効率がユーザー体験に影響を与えることがあります。以下の最適化戦略を採用することで、不要なビューの再描画を抑えることができます。

精密な注入

複数のサブ状態を含む複合値型に対して、特定のプロパティのみを注入することで、不要な更新を回避できます。ビューが実際に必要とする部分の状態だけを購読することで、より効率的なリアクティブUIを構築できます。

struct UserState {
    var height = 175 // 単位: cm
    var weight = 75  // 単位: kg
}

extension EnvironmentValues {
    @Entry var userState = UserState()
}

struct HeightView: View {
    // heightのみが更新対象
    @Environment(\.userState.height) var height
    var body: some View {
        Text("身長: \(height) cm")
    }
}

struct WeightView: View {
    // weightのみが更新対象
    @Environment(\.userState.weight) var weight
    var body: some View {
        Text("体重: \(weight) kg")
    }
}

struct BMIView: View {
    @Environment(\.userState) var userState
    var body: some View {
        let heightInMeters = Double(userState.height) / 100.0
        let bmi = Double(userState.weight) / (heightInMeters * heightInMeters)
        return Text(String(format: "BMI: %.2f", bmi))
    }
}

struct RootView: View {
    @State var userState = UserState()
    var body: some View {
        List {
            Button("身長を変更") {
                userState.height = Int.random(in: 130...220)
            }
            Button("体重を変更") {
                userState.weight = Int.random(in: 35...120)
            }
            HeightView()
            WeightView()
            BMIView()
        }
        .environment(\.userState, userState)
    }
}

条件付きの更新

transformEnvironmentを使用すると、特定の条件を満たす場合にのみ環境値を更新できます。これにより、更新頻度を減らし、アプリの応答性と滑らかさを向上させることができます。

struct RootView: View {
    @State var userState = UserState()
    @State var height = 175
    var body: some View {
        List {
            Button("身長を変更") {
                height = Int.random(in: 130...220)
            }
            HeightView()
        }
        .transformEnvironment(\.userState) { state in
            guard height > 150 else {
                print("無視: \(height)")
                return
            }
            state.height = height // height > 150 の場合のみ更新
        }
    }
}

Environmentとサードパーティ製DIフレームワークの併用

SwiftUIのEnvironmentは優れた機能を提供しますが、ビューのライフサイクルに厳密に制限されます。ビジネスロジックをViewModel層に分離したり、ユニットテストを行う場合、サードパーティ製のDIフレームワークの使用が有効です。ClinPeerでは、FactoryというSDKを使用しています。

@Observable
class UserState {
    var height: Int = 175
    var weight: Int = 75
}

extension Container {
    var userState: Factory<UserState> {
        Factory(self) { UserState() }
            .scope(.shared)
    }
}

// 使用例
@Injected(\.userState) private var userState

ハイブリッドアーキテクチャの利点

Environmentとサードパーティ製DIツールを併用することで、以下の利点が得られます。

  • 柔軟なアーキテクチャ:ビュー層ではSwiftUIのEnvironmentを使用し、ビジネスロジック層ではFactoryなどのDIツールを活用できます。
  • テストの容易性:ビジネスロジックをUIから完全に切り離すことで、ユニットテストやモック化が簡単になります。
  • 保守性の向上:特定のUIフレームワークへの依存を最小限に抑え、コードベースの長期的な安定性と再利用性を高めます。

まとめ

SwiftUIのEnvironmentは、依存性注入とビューのライフサイクルを一体化した設計であり、視覚的なUIコンポーネントの境界を明確にしながら、ビュー階層におけるデータの効率的かつ制御可能な伝達を実現します。

また、精密な注入や選択的な変更によって、不要なビュー更新を避けることでアプリ全体の効率を高めることができます。 さらに、SwiftUIのエコシステムにおいては、柔軟にサードパーティ製のDIフレームワークを取り入れることで、より複雑なユースケースにも対応可能です。

Environmentの設計に対する深い理解は、拡張性が高く品質の高いSwiftUIアプリを構築するための確かな基盤となります。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp