🌍

Xcode 15のPreview Macroにおける依存注入について

2023/12/03に公開

本記事はSwiftWednesday Advent Calendar 2023の2日目の記事です。
本記事では、Xcode 15から利用可能なPreview Macroにおける依存注入について紹介します。

はじめに

Xcode 15から、開発者は新たなPreview Macro機能を利用できるようになりました。この機能を利用することで、これまで利用されてきたPreviewProviderと比較して、プレビューの表示のための実装をより簡単に実装することができるようになりました。一方で、Viewの依存をDIする際にPreviewProviderの時と同様の書き方をすることは難しくなりました。この記事では、Preview Macro利用時の依存注入(DI)のアプローチを2つの方法で紹介します。

前提となるViewについて

初めに、基本となるViewの構造について説明します。例として、ユーザー情報を表示するViewを考えます。


struct ContentView: View {
 let user: User
 
 var body: some View {
   // ユーザー情報を表示する
 }
}

このViewでプレビューを行う際、ユーザー情報を依存注入(DI)する必要があります。また、異なる状態をプレビューするために、複数のプレビューインスタンスを用意します。しかし、下記のように毎回ユーザーのインスタンスを生成する方法では、同様のコードを繰り返すため、可読性の観点で少し読みづらいという問題があります。

#Preview {
  ContentView(
    user: User(
      name: "hoge",
      age: 23,
      someBoolValue: false
      anotherBoolValue: true
    )
  )
}

#Preview {
  ContentView(
    user: User(
      name: "hoge",
      age: 23,
      someBoolValue: true
      anotherBoolValue: true
    )
  )
}

#Preview {
  // ...
}

この問題を解決するために、makeUserというファクトリーメソッドを導入します。このメソッドを使用して、異なるプレビューを簡単に生成できます。

func makeUser(someBoolValue: Bool, anotherBoolValue) -> User {
  .init(
    name: "hoge",
    age: 23,
    someBoolValue: someBoolValue
    anotherBoolValue: anotherBoolValue
  )
}

方法1: Privateメソッドとしての切り出し

最初の方法は、makeUserメソッドをprivateメソッドとしてContentViewのファイル内に切り出すことです。このアプローチの利点は、コードの構造が明確になり、再利用性が向上することです。一方で、デメリットとして、メソッドがViewの外部に存在することでコードの一貫性が損なわれる可能性があります。

struct ContentView: View { ... }

private func makeUser(someBoolValue: Bool, anotherBoolValue) -> User {
  .init(
    name: "hoge",
    age: 23,
    someBoolValue: someBoolValue
    anotherBoolValue: anotherBoolValue
  )
}

#Preview {
  ContentView(
    user: makeUser(someBoolValue: true, anotherBoolValue: true)
  )
}

#Preview {
  ContentView(
    user: makeUser(someBoolValue: false, anotherBoolValue: false)
  )
}

#PreView {
  // ...
}

方法2: DeveloperToolsSupportextensionとしての切り出し

二つ目の方法は、makeUserメソッドをDeveloperToolsSupport.Previewextensionとして切り出すことです。この方法のメリットは、プレビュー固有のロジックがDeveloperToolsSupport.Previewの内部に独立して管理されることにあります。

private extension DeveloperToolsSupport.Preview {
  static func makeUser(/* ... */) -> User {
    // 略
  }
}

#Preview {
  ContentView(
    user: DeveloperToolsSupport.Preview.makeUser(/* ... */)
  )
}

#PreView {
  // ...
}

番外編: PreviewProviderを利用する

PreviewProviderを利用する方法も有効です。ContentView_Previews内にmakeUserメソッドを定義し、プレビューをグループ化して管理することで、コードの構造が整理され、再利用が容易になります。

struct ContentView_Previews: PreviewProvider {
    private static func makeUser(/* ... */) -> User {
        // ...
    }
    
    static var previews: some View {
        Group {
            ContentView(user: makeUser(/* ... */)
            ContentView(user: makeUser(/* ... */)
        }
    }
}

おわりに

Xcode 15のPreview Macro機能を利用することで、これまでよりも簡単にプレビューの実装を記述することが可能になります。本記事で紹介した方法を活用することで、Preview Macroを用いたDIの実装を簡略化することができます。各方法には特定の利点と使用シナリオがありますので、プロジェクトのニーズに合わせて最適な方法を選択することが大切です。
一方で、プロジェクトの要件によっては、従来のPreview Providerを使用したアプローチの方が適切な場合もあります。Preview Macroの新機能と従来の方法の両方を理解し、プロジェクトに最も適したアプローチを選択することで、高品質でメンテナンスしやすいiOSアプリの開発が実現できると考えています。

明日は @uhooi さんです!

開発環境

  • Swift compiler version info:
    • Apple Swift version 5.9 (swiftlang-5.9.0.128.108 clang-1500.0.40.1)
  • Xcode version info:
    • Xcode 15.0 Build version 15A240d

参考引用文献

DeNA Engineers

Discussion