9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

iOSAdvent Calendar 2021

Day 24

自前でDIコンテナを作ってみる試みとRxSwiftを利用した構成への適用を試してみる

Last updated at Posted at 2021-12-24

1. はじめに

皆様お疲れ様です。「iOS Advent Calendar」の24日目を担当させて頂きます、fumiyasac(Fumiya Sakai)と申します。何卒よろしくお願い致します。

Swift/Kotlin愛好会Advent Calendarでは、「Androidアプリでバックグラウンド再生機能を実現するためのヒントとiOSアプリとの見比べた際の特徴を簡単にまとめてみた」というタイトルにて、iOS/Androidアプリでのバックグラウンド再生機能を実現する上で、ポイントになり得る点を双方の比較をしながら解説した記事を書きましたので、こちらもご覧頂けますと嬉しく思います。

今回はUIまわりのトピックではなく、実務や個人開発を通じて、OSSライブラリのDI(Dependency Injection)を実現するためのライブラリを参考にしながら、自前で実装する&リプレイスするが機会がありましたので、自分なりの事例に関して簡単ではありますがご紹介ができればと思います。

以前登壇した際の発表資料:

また、今回の内容につきましては、potatotips #76 (iOS/Android開発Tips共有会)にて登壇の際に利用した資料を下記のリンクにて共有しておりますので、こちらも是非ご活用頂けますと幸いです。紹介しているスライドの方では、実際の業務内で導入するまでのアイデアや検証の過程、そして導入をしていくところまでの流れを簡単にまとめています。

2. 自作したDIコンテナにおいて参考にした資料やドキュメント等の紹介

以前にも「ライブラリを利用しない実現方法のアイデア」の一例として【実装MEMO】PropertyWrappersの機能を利用したDependency Injectionのコードに触れた際の備忘録をMedium内で軽くまとめた事もあり、これを応用できないかを最初は考えました。ですが、実際のプロジェクトへ導入するための検証を重ねていく中で少しそぐわなかったので、よく利用されているOSSライブラリでの実装方法等も参考にしながら下記の様な形をとってみました。

⭐️ 2-1. 今回自作を試みたDIコンテナ例で主に参考にした資料

今回参考にした手法は、「How to : create your SUPER simple dependency injector container in Swift」で紹介されていた方法になります。この記事内で紹介されている方法でのポイントは、大きなシングルトンの中に型と名前で管理された必要な責務のインスタンスを格納するという点になるかと思います。

今回、適用を考えていたアプリにおいて一番大事な部分は、

  1. コンストラクタインジェクションができること
  2. Local / Remoteで同じProtocolを使う際も名前をつけ方で管理できること
    という2点だったので、この方針で大きな問題はなさそうと判断しました。

まずは、DIコンテナ用のクラスを準備は下記の様な形で準備をすることとなります。

【1. 責務毎のインスタンスを登録するためのDIコンテナ用のクラス】

DependeciesContainer.swift
import Foundation

final class DependeciesContainer {

    // MARK: - DIコンテナ自体はSingletonとして保持する

    static let shared = DependeciesContainer()

    private init() {}

    // MEMO: DependencyKeyクラスを利用して「型」と「名前」を利用して分類する
    private var dependecies: [DependencyKey: Any] = [:]

    // MARK: - Function

    func register<T>(
        _ type: T.Type,
        impl: Any,
        name: String? = nil
    ) {
        let dependencyKey = DependencyKey(type: type, name: name)
        dependecies[dependencyKey] = impl
    }

    func resolve<T>(
        _ type: T.Type,
        name: String? = nil
    ) -> T {
        let dependencyKey = DependencyKey(type: type, name: name)
        if let dep = dependecies[dependencyKey] as? T {
            return dep
        } else {
            // MEMO: 設定し忘れがあった場合にはfatalErrorで検知できるようにする
            let protocolTypeName = NSString(string: "\(type)").components(separatedBy: ".").last!
            fatalError("\(protocolTypeName)の依存性を解決できませんでした。当該実装クラス:\(protocolTypeName).")
        }
    }
}

final class DependencyKey: Hashable, Equatable {

    // MARK: - Properties

    private let type: Any.Type
    private let name: String?

    // MARK: - Initializer

    init(type: Any.Type, name: String? = nil) {
        self.type = type
        self.name = name
    }

    // MARK: - Hashable, Equatable

    func hash(into hasher: inout Hasher) {
        hasher.combine(ObjectIdentifier(type))
        hasher.combine(name)
    }

    static func == (lhs: DependencyKey, rhs: DependencyKey) -> Bool {
        return lhs.type == rhs.type && lhs.name == rhs.name
    }
}

そして次に、それぞれの責務におけるクラス同士の依存関係の解決を下記の様な形で図っていくためのクラスを定義し、このクラスのインスタンスをAppDelegate.swiftへ適用するような形とします。

【2. 依存関係の解決を図る部分の処理】

DependenciesDefinition.swift
final class DependenciesDefinition {

    // MARK: - Function

    func inject() {

        // MEMO: インスタンスを保持するための場所
        let dependecies = DependeciesContainer.shared

        // ※途中省略

        // MARK: - Infrastructure
        // → アプリ内DataStore処理やRestAPI/GraphQLのクライアントに関する処理

        dependecies.register(
            MovieQualityLocalStore.self,
            impl: MovieQualityLocalStoreImpl()
        )
        dependecies.register(
            ApiClient.self,
            impl: ApiClientManager.shared
        )
        // ※以降はInfrastructure層の依存関係解決処理が続きます...

        // MARK: - Repository
        // → アプリ内DataStore処理からのデータ取得&保存処理やRestAPI/GraphQLからのデータ取得処理

        dependecies.register(
            FeaturedMovieRepository.self,
            impl: FeaturedMovieRepositoryImpl(
                apiClient: dependecies.resolve(ApiClient.self),
                backgroundScheduler: dependecies.resolve(ImmediateSchedulerType.self, name: background) // API通信処理はバックグラウンドスレッドで実施したい
            )
        )
        // ※以降はRepository層の依存関係解決処理が続きます...

        // MARK: - UseCase
        // → Repositoryクラス/Serviceクラスで定義した処理を組み合わせて実現するビジネスロジックに関する処理

        dependecies.register(
            GetMainUseCase.self,
            impl: GetMainUseCaseImpl(
                mainBannerRepository: dependecies.resolve(MainBannerRepository.self),
                mainNewsRepository: dependecies.resolve(MainNewsRepository.self),
                featuredMovieRepository: dependecies.resolve(FeaturedMovieRepository.self),
                mainMovieRepository: dependecies.resolve(MainMovieRepository.self)
            )
        )
        // ※以降はUseCase層の依存関係解決処理が続きます...

        // MEMO: 上記の処理例では、Infrastructure → Repository → UseCaseという順番で依存関係の解決を図っていく形となります。
    }
}

【3. アプリ起動時に必要なインスタンスを初期化する】

AppDelegate.swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        // MEMO: 自作でのDependeciesContainerをインスタンス化する
        DependenciesDefinition().inject()

        // ※途中省略
    }
}

形としてはかなり愚直な形の記述方法にはなってしまいますが、それぞれの責務毎にある程度分離された形となっているならば整理はしやすいのではないかと思います。ただ裏を返せば、責務の分離を曖昧な感じにしてしまうと必要のない順番を気にしなければいけなくなるので、その点には少し気をつけると良いかもしれません。

※ この記事ではタイトルにRxSwiftと入ってはいますが、このDIコンテナを利用した実装についてはRxSwift等のライブラリを利用していない場合でも利用できます。

⭐️ 2-2. iOS/AndroidでのDIライブラリやその他Dependency Injectionを理解する上での参考資料

ライブラリに依存しない形ではあるけれども、できるだけ似た感覚で書ける形を模索する際においても、OSSライブラリを活用したDIの導入事例や他のプロジェクトの実装事例に関する記事等にも幅広く触れることで、現在開発中のアプリに導入する際の検討材料や知見になるかと思いますので、まずは簡単なサンプル実装を試してみたりすることで事前のイメージを掴んでおくと更に良さそうに思います。

【iOSでのライブラリ例】

【Androidでのライブラリ例】

【今回改めて復習する際に参考にした資料】

3. RxSwiftを利用した構成に対しての適用していく部分の解説

ここからは、前述したDIを適用した際の記載例についてもいくつかの例をピックアップしてみます。以降で紹介するコード例については、下記の図で示す様な形のアーキテクチャを想定しています。構成の基本的な概要としては、画面の表示処理部分についてはMVPパターンを基本とし、Modelに相当する部分については、UseCase / Domain / Infrastructureの3層に処理を分離することでできるだけ疎結合な形をとっている点がポイントになります。また、ロジックに近い部分についてはRxSwiftの Single / Maybe / Completable を利用することで各種処理をつなげていく様なイメージになります。

di_container_image.png

⭐️ 3-1. 処理レイヤーによって利用するスレッドを適切に指定するために第2引数を活用する

前述したDIコンテナについては第2引数に名前を定義することを利用して、メインスレッド/バックグラウンドスレッドでの操作を必要な責務の処理に応じて適用できるようにする例になります。

// ----------
// (1) DependenciesDefinitionクラスのinject()内での記述
// ----------
// 処理を実行する際にバックグラウンドスレッドにしたい際につける名前
let background = "background"
// メインスレッド動作
dependecies.register(
    ImmediateSchedulerType.self,
    impl: MainScheduler.instance
)
// バックグラウンドスレッド動作
dependecies.register(
    ImmediateSchedulerType.self,
    impl: SerialDispatchQueueScheduler(qos: .default),
    name: background // 名前を付与する
)

// ----------
// (2) Presenter側での処理はUIへの取得データ反映処理なのでメインスレッドで実行
// ----------
static func createMainPresneter() -> MainPresenter {
    return MainPresenterImpl(
        getMainUseCase: dependecies.resolve(GetMainUseCase.self),
        getFavoriteMainMoviesUseCase: dependecies.resolve(GetFavoriteMainMoviesUseCase.self),
        saveFavoriteMainMovieUseCase: dependecies.resolve(SaveFavoriteMainMovieUseCase.self),
        // MEMO: 名前がない場合はメインスレッドでの処理となる
        mainScheduler: dependecies.resolve(ImmediateSchedulerType.self)
    )
}

// ----------
// (3) Repository側での処理はAPI / GraphQLでの処理なのでバックグラウンドスレッドで実行
// ----------
dependecies.register(
    CategoryRepository.self,
    impl: CategoryRepositoryImpl(
        graphQLClient: dependecies.resolve(GraphQLClient.self),
        readApiCacheClient: dependecies.resolve(ReadApiCacheClient.self),
        // MEMO: 第2引数で名前を付与した場合はバックグラウンドスレッドでの処理となる
        backgroundScheduler: dependecies.resolve(ImmediateSchedulerType.self, name: background)
    )
)

⭐️ 3-2. Cacheを利用するか?サーバーとの通信を利用するか適切に指定するために第2引数を活用する

先程のメインスレッド/バックグラウンドスレッドでの操作と同じ様な要領で、Localでのキャッシュ機構からのデータ取得処理/GraphQLから非同期通信でのデータ取得処理についても、第2引数での名前を利用した管理を使う例になります。記載しているコードについてはRxSwiftを利用した処理での事例になりますが、責務に応じたスレッドの適用と同時にLocalでのキャッシュ機構からのデータ取得処理/GraphQLから非同期通信でのデータ取得処理についても適切な名前をつけながら整理していくイメージを持って頂くと分かりやすいかもしれません。

// ----------
// (1)-1: DependenciesDefinitionクラスのinject()内での記述
// ----------
// Cacheからのデータ取得をする際につける名前
let local = "local"
// 処理を実行する際にバックグラウンドスレッドにしたい際につける名前
let background = "background"

// ----------
// (1)-2: GraphQLから非同期通信でデータを取得する
// ----------
dependecies.register(
    CourseRepository.self,
    impl: CourseRepositoryImpl(
        graphQLClient: dependecies.resolve(GraphQLClient.self),
        readApiCacheClient: dependecies.resolve(ReadApiCacheClient.self),
        backgroundScheduler: dependecies.resolve(ImmediateSchedulerType.self, name: background)
    )
)

// ----------
// (1)-3: Localでのキャッシュ機構からデータを取得する
// ----------
dependecies.register(
    CourseRepository.self,
    impl: LocalCourseRepositoryImpl(
        readApiCacheClient: dependecies.resolve(ReadApiCacheClient.self),
        backgroundScheduler: dependecies.resolve(ImmediateSchedulerType.self, name: background)
    ),
    // MEMO: 第2引数で名前を付与した場合は同じ型でもLocalから取得したデータを利用する
    name: local
)

// ----------
// (2)-1 Localのキャッシュ機構またはGraphQLでの処理を利用したRepository層の例(RxSwiftを利用した処理記載例)
// ----------
import RxSwift

protocol CourseRepository {
    func findAll() -> Single<[Course]>
}

// ----------
// (2)-2: Localのキャッシュ機構からデータを取得する処理例
// ----------
final class LocalCourseRepositoryImpl: CourseRepository {
    private let readApiCacheClient: ReadApiCacheClient
    private let backgroundScheduler: ImmediateSchedulerType

    init(
        readApiCacheClient: ReadApiCacheClient,
        backgroundScheduler: ImmediateSchedulerType
    ) {
        self.readApiCacheClient = readApiCacheClient
        self.backgroundScheduler = backgroundScheduler
    }

    func findAll() -> Single<[Course]> {
        // キャッシュ機構に格納されているEntityデータを取得してDomainModelへ変換して返す
        return readApiCacheClient.getCourseEntities()
            .subscribe(
                on: backgroundScheduler  // ← Repository処理なのでバックグラウンドスレッドで実行したい
            ).map { courseEntities in
                courseEntities.map { courseEntity in
                    Course(courseEntity)
                }
            }
    }
}

// ----------
// (2)-3: GraphQLでの処理からデータを取得する処理例
// ----------
final class CourseRepositoryImpl: CourseRepository {
    private let graphQLClient: GraphQLClient
    private let readApiCacheClient: ReadApiCacheClient
    private let backgroundScheduler: ImmediateSchedulerType

    init(
        graphQLClient: GraphQLClient,
        readApiCacheClient: ReadApiCacheClient,
        backgroundScheduler: ImmediateSchedulerType
    ) {
        self.graphQLClient = graphQLClient
        self.readApiCacheClient = readApiCacheClient
        self.backgroundScheduler = backgroundScheduler
    }

    func findAll() -> Single<[Course]> {
         // GraphQLからEntityデータを取得してDomainModelへ変換して返す
        return graphQLClient.getCourses()
            .subscribe(
                on: backgroundScheduler  // ← Repository処理なのでバックグラウンドスレッドで実行したい
            ).do(
                afterSuccess: { [weak self] entities in
                    guard let weakSelf = self else { return }
                    // GraphQLからEntityデータを取得が成功した後にキャッシュ機構に保存する
                    weakSelf.readApiCacheClient.saveCourseEntities(entities)
                }
            ).map { entities in
                entities.map { entity in
                    Course(entity)
                }
            }
    }
}

// ----------
// (3) Local・Remote対応のUseCaseをそれぞれ作成する
// ----------
dependecies.register(
    GetCoursesUseCase.self,
    impl: GetCoursesUseCaseImpl(
        courseRepository: dependecies.resolve(CourseRepository.self)
    )
)
dependecies.register(
    GetCoursesUseCase.self,
    impl: GetCoursesUseCaseImpl(
        courseRepository: dependecies.resolve(CourseRepository.self, name: local)
    ),
    // MEMO: 第2引数で名前を付与した場合は同じ型でもLocalから取得したデータを利用する
    name: local
)

// ----------
// (4) Presenter側での処理を記載する(RxSwiftを利用した処理記載例)
// ※ こちらの処理については実際のPresenter処理内部における一部分を抜粋したものになります
// ----------
private let getCoursesUseCase: GetCoursesUseCase
private let localGetCoursesUseCase: GetCoursesUseCase
private let mainScheduler: ImmediateSchedulerType
private let disposeBag = DisposeBag()

func viewWillAppear() {
    // まずはキャッシュ取得処理を実行する
    prefetchAndSetup { [weak self] in
        guard let weakSelf = self else {
            return
        }
        // まずはキャッシュ取得処理が終了次第、続けてAPI取得処理を実行する
        weakSelf.fetchAndSetup()
    }
}

private func prefetchAndSetup(completion: @escaping () -> Void) {
    localGetCoursesUseCase.execute()
        .do(
            onSubscribe: { [weak self] in
                guard let weakSelf = self else {
                    return
                }
                // 処理開始時に実行したい処理
            },
            onDispose: {
                // キャッシュ取得処理の購読が終わったら実行したい処理をクロージャーで引き渡す
                // 例: キャッシュデータ反映処理後にAPI通信処理を試みる流れ
                completion()
            }
        )
        .observe(
            on: mainScheduler // ← Presenter処理なのでメインスレッドで実行したい
        )
        .subscribe(
            onSuccess: { [weak self] courseDtos in
                guard let weakSelf = self else {
                    return
                }
                // キャッシュ取得処理が成功した場合
                // 例: キャッシュデータをまずは画面に一時的に表示する
            },
            onFailure: { _ in
                // キャッシュ取得処理が失敗した場合
            }
        )
        .disposed(by: disposeBag)
}

private func fetchAndSetup() {
    getCoursesUseCase.execute()
        .observe(
            on: mainScheduler // ← Presenter処理なのでメインスレッドで実行したい
        )
        .subscribe(
            onSuccess: { [weak self] courseDtos in
                guard let weakSelf = self else {
                    return
                }
                // API取得処理が成功した場合
                // 例: API取得データを正式に画面に表示する
            },
            onFailure: { [weak self] _ in
                guard let weakSelf = self else {
                    return
                }
                // API取得処理が失敗した場合
                // 例: 通信エラーを通知するダイアログを表示する
            }
        )
        .disposed(by: disposeBag)
}

⭐️ 3-3. PresenterとViewController部分に関する処理例

最後に画面を生成するタイミングでPresenterクラスを渡す部分に関する例になります。画面生成時にPresenterクラスを初期化したいので、この部分についてはDIコンテナに直接登録しないでFactoryメソッドを別途定義する形をとっています。ViewControllerを初期化するタイミングで一緒にPresenterを初期化するFactoryメソッドを適用する点がこの部分のポイントになります。

// ----------
// (1)-1: Infrastructure/Repository/UseCaseまでの依存関係をDIコンテナに登録する
// ----------
final class DependenciesDefinition {

    // MARK: - Function

    func inject() {

        // MEMO: Cacheからのデータ取得をする際につける名前
        let local = "local"

        // MEMO: 処理を実行する際にバックグラウンドスレッドにしたい際につける名前
        let background = "background"

        // MEMO: インスタンスを保持するための場所
        let dependecies = DependeciesContainer.shared

        // ※以降はInfrastructure/Repository/UseCaseの依存関係の解決処理が続く...
    }
}

// ----------
// (1)-2: Presenterに適用するためのFactoryメソッドを定義する
// ----------
final class PresenterFactory {

    // MEMO: Cacheからのデータ取得をする際につける名前
    private static let local = "local"

    // MEMO: 処理を実行する際にバックグラウンドスレッドにしたい際につける名前
    private static let background = "background"

    // MEMO: インスタンスを保持するための場所
    private static let dependecies = DependeciesContainer.shared

    // MARK: - Static Function

    static func createMainPresenter() -> MainPresenter {
        return MainPresenterImpl(
            getCoursesUseCase: dependecies.resolve(GetCoursesUseCase.self),
            localGetCoursesUseCase: dependecies.resolve(GetCoursesUseCase.self, name: local),
            mainScheduler: dependecies.resolve(ImmediateSchedulerType.self)
        )
    }

    // ※以降は各種Presenterに適用するFactoryメソッド定義が続く...
}

// ----------
// (2) PresenterについてはFactoryメソッドを利用して適用する
// ----------
final class MainViewController: UIViewController {

    // MARK: - Property
    
    private let presenter: MainPresenter

    // ※途中省略

    // MARK: - Override

    override func viewDidLoad() {
        super.viewDidLoad()

        presenter = PresenterFactory.createMainPresenter()
    }

    // ※途中省略
}

余談になりますが、Presenterクラスの初期化処理をviewDidLoad()のタイミングで実施していますが、iOS13以降ではStoryboardでもDependency Injectionができる様になったので、そのタイミングで適用する形でも実現する事ができます。

// MEMO: iOS13以上でStoryboardを利用している場合は、下記の様な形でStoryboardからViewControllerを生成するタイミングでPresenterを適用することも可能です。

// (1) 対象となるViewController側ではInitializerで画面に必要なPresenterを受け取れる様にしておく
final class NewsViewController: UIViewController {

    // MARK: - Property

    private let presenter: NewsPresenter

    // ※途中省略

    // MARK: - Initializer

    init?(coder: NSCoder, presenter: NewsPresenter) {
        self.presenter = presenter
        super.init(coder: coder)
    }

    required init?(coder: NSCoder) {
        fatalError()
    }

    // ※途中省略
}

// (2) 任意の画面において画面遷移等をするタイミング等で下記の様な形でPresenterを適用する
let newsViewController = UIStoryboard(name: "News", bundle: nil).instantiateInitialViewController { coder in
    NewsViewController(
        coder: coder,
        presenter: PresenterFactory.createNewsPresenter()
    )
}

4. その他RxSwiftを利用した処理例についての補足

ここからは補足として、DIコンテナ側と直接関係する処理ではありませんが、RxSwiftを利用した適切な形へ変換していく際の処理事例を掲載しておきます。

ここで紹介するのは、下図の様な形で複数のRepositoryの処理(実際はGraphQLやRestAPIからの非同期通信処理)を利用して、画面表示に必要なデータへ変換する際の事例となります。RxSwiftの.flatMap.zip等のオペレータを活用することで、適切な形に変換をかける処理や取得処理のタイミングを合わせていく処理を組み合わせていく点に注目しながら見ていくと良さそうに思います。

rxswift_example.png

import Foundation
import RxSwift

final class GetMainUseCaseImpl: GetMainUseCase {
    private let categoryRepository: CategoryRepository
    private let rankingContentsRepository: RankingContentsRepository
    private let courseRepository: CourseRepository
    private let userCourseRepository: UserCourseRepository
    private let stockedCourseRepository: StockedCourseRepository

    init(
        categoryRepository: CategoryRepository,
        rankingContentsRepository: RankingContentsRepository,
        courseRepository: CourseRepository,
        userCourseRepository: UserCourseRepository,
        stockedCourseRepository: StockedCourseRepository
    ) {
        self.categoryRepository = categoryRepository
        self.rankingContentsRepository = rankingContentsRepository
        self.courseRepository = courseRepository
        self.userCourseRepository = userCourseRepository
        self.stockedCourseRepository = stockedCourseRepository
    }

    func execute(
        recentCoursesLimit: Int,
        recommendedCoursesLimit: Int
    ) -> Single<MainDto> {

        // MEMO: まずはお気に入り登録中のコース情報を取得する処理を実行し、取得できた情報を利用して後続の処理を実行する。
        return stockedCourseRepository.findAll().flatMap { [weak self] stockedCourses in
            guard let weakSelf = self else {
                return Single.error(CommonError.notExistSelf)
            }
            // MEMO: MainDtoを作成するために必要となるデータ取得をそれぞれ試みる。
            // - 1. カテゴリー一覧
            // - 2. 最近見たコース一覧
            // - 3. おすすめコース一覧
            // - 4. トピック別ランキング
            let categoryDtosSingle = weakSelf.getCategoryDtosSingle()
            let recentCourseDtosSingle = weakSelf.getRecentCourseDtosSingle(
                limit: recentCoursesLimit,
                stockedCourses: stockedCourses
            )
            let recommendedCourseDtosSingle = weakSelf.getRecommendedCourseDtosSingle(
                limit: recommendedCoursesLimit,
                stockedCourses: stockedCourses
            )
            let getRankingCoursesSingle = weakSelf.getRankingContentsDtosSingle(
                stockedCourses: stockedCourses
            )

            // MEMO: Single.zipを利用し、画面表示に必要な値が全て取得できた場合にだけ、画面表示に必要なMainDtoを返す形とする。
            return Single.zip(
                categoryDtosSingle,
                recentCourseDtosSingle,
                recommendedCourseDtosSingle,
                getRankingCoursesSingle
            ).map { tuple -> MainDto in // ← 取得結果をMainDtoに変換する必要がある
                let (
                    categoryDtos,
                    recentCourseDtos,
                    recommendedCourses,
                    rankingContentsDtos
                ) = tuple
                return MainDto(
                    categoryDtos: categoryDtos,
                    recentCourseDtos: recentCourseDtos,
                    recommendedCourseDtos: recommendedCourseDtos,
                    rankingContentsDtos: rankingContentsDtos
                )
            }
        }
    }

    private func getCategoryDtosSingle() -> Single<[CategoryDto]> {

        // MEMO: カテゴリー一覧については、データを取得してCategoryDtoへ変換するだけとなる。
        return categoryRepository.findAll()
            .map { categories in
                categories.map { category in
                    CategoryDto(
                        category: category
                    )
                }
            }
    }

    private func getRecentCourseDtosSingle(
        limit: Int,
        stockedCourses: [StockedCourse]
    ) -> Single<[CourseDto]> {

        // MEMO: まずは、最近見たコース一覧のマスタデータ一覧を取得する。
        return courseRepository.findRecentWithLimit(
            limit
        ).flatMap { courses in // ← .flatMapでの変換
            // MEMO: 次に、最近見たコース一覧のidに紐づく自分が取り組んでいるコース情報一覧を取得する。
            self.userCourseRepository.findByIds(
                courses.map { course in
                    course.id
                }
            ).map { userCourses in
                // MEMO: CourseDtoの作成
                // - 1. 取得できた最近見たコース一覧のマスタデータ一覧
                // - 2. 引数で受け取ったお気に入り登録中のコース情報
                // - 3. 取得できた自分が取り組んでいるコース情報一覧
                courses.map { course in
                    CourseDto(
                        course: course,
                        stockedCourses: stockedCourses,
                        userCourses: userCourses
                    )
                }
            }
        }
    }

    private func getRecommendedCourseDtosSingle(
        limit: Int,
        stockedCourses: [StockedCourse]
    ) -> Single<[CourseDto]> {

        // MEMO: まずは、おすすめコース一覧のマスタデータ一覧を取得する。
        return courseRepository.findRecommendedWithLimit(
            limit
        ).flatMap { [weak self] courses in // ← .flatMapでの変換
            guard let weakSelf = self else {
                return Single.error(CommonError.notExistSelf)
            }
            // MEMO: 次に、最近見たコース一覧のidに紐づく自分が取り組んでいるコース情報一覧を取得する。
            return weakSelf.userCourseRepository.findByIds(
                courses.map { course in
                    course.id
                }
            ).map { userCourses in
                // MEMO: CourseDtoの作成
                // - 1. 取得できた最近見たコース一覧のマスタデータ一覧
                // - 2. 引数で受け取ったお気に入り登録中のコース情報
                // - 3. 取得できた自分が取り組んでいるコース情報一覧
                courses.map { course in
                    CourseDto(
                        course: course,
                        stockedCourses: stockedCourses,
                        userCourses: userCourses
                    )
                }
            }
        }
    }

    private func getRankingContentsDtosSingle(
        stockedCourses: [StockedCourse]
    ) -> Single<[RankingContentsDto]> {

        // MEMO: まずは、ランキングコンテンツを全て取得する。
        return getAllRankingContents()
            .flatMap { [weak self] allRankingContents in // ← .flatMapでの変換
                guard let weakSelf = self else {
                    return Single.error(CommonError.notExistSelf)
                }
                // MEMO: このままの状態では[Single<FeaturedContentsDto>]となってしまうので、Single<[FeaturedContentsDto]>に変換するためにSingle.zipを適用する。
                return Single.zip(
                    allRankingContents.map { rankingContents in
                        // MEMO: 取得したランキングコンテンツを元にしてRankingContentsDtoを取得する。
                        weakSelf.getRankingContentsDto(rankingContents: rankingContents, stockedCourses: stockedCourses)
                    }
                )
            }
    }

    // 特集コンテンツの表示に必要なDtoを取得する(特集セクション1つ分に相当するランキングデータをDtoへ変換する)
    private func getRankingContentsDto(
        rankingContents: RankingContents,
        stockedCourses: [StockedCourse]
    ) -> Single<RankingContentsDto> {

        // MEMO: まずは、ランキングコンテンツに格納されているCourseのID一覧から、ランキングで表示するコースのマスタデータ一覧を取得する。
        return courseRepository.findByIds(
            rankingContents.courseIds
        ).flatMap { [weak self] courses in // ← .flatMapでの変換
            guard let weakSelf = self else {
                return Single.error(CommonError.notExistSelf)
            }
            // MEMO: 次に、ランキングで表示するコース一覧のidに紐づく自分が取り組んでいるコース情報一覧を取得する。
            return weakSelf.userCourseRepository
                .findByIds(
                    courses.map { course in
                        course.id
                    }
                )
                .map { userCourses in
                    // MEMO: CourseDtoの作成
                    // - 1. 取得できた最近見たコース一覧のマスタデータ一覧
                    // - 2. 引数で受け取ったお気に入り登録中のコース情報
                    // - 3. 取得できた自分が取り組んでいるコース情報一覧
                    courses.map { course in
                        CourseDto(
                            course: course,
                            stockedCourses: stockedCourses,
                            userCourses: userCourses
                        )
                    }
                }.map { CourseDtos in
                    // MEMO: RankingContentsDtoの作成
                    // - 1. 作成したCourseDtoの一覧
                    // - 2. ランキングコンテンツからの必要な情報
                    RankingContentsDto(
                        label: rankingContents.label,
                        labelId: rankingContents.labelId,
                        courseDtos: CourseDtos
                    )
                }
        }
    }

    // ランキングコンテンツを全て取得する(セクションが複数存在することを想定)
    private func getAllRankingContents() -> Single<[RankingContents]> {
        return rankingContentsRepository.getAllRankingContentsByTopics()
    }
}

5. あとがき

iOSではSwinjectやDIKit・AndroidではDagger2やDaggerHiltについては多少自分の手元でも軽く試した経験はあったものの、DIコンテナ部分を自前で作成して実際のアプリ内に適用したり、既存のライブラリからこれまでの実装方針をできるだけ崩すことない形でのリプレイスを経験を通して、iOS/Android共にDI用の有名ライブラリがあり、それぞれ特徴的な部分があるので、平素の開発においては慣習的となっている部分に改めて深く触れることでその特徴やメリット・デメリットを再認識する良い機会になったと共に、既存である程度の規模感があるプロジェクトでは、チームの中で「しっくり来る形」を見出す準備の大切さを感じると同時に、自前でこの部分を実装する際にはチームの状況等を鑑みて良い特徴となり得る部分を積極的に取り入れていくと良さそうに思いました。

今年を改めて軽く振り返ってみると、AndroidやFlutterに関するインプットは不定期ではありますが、実践できてはいたものの特にiOS側のアウトプットは若干少な目になってしまった点は反省の余地があるかと思います。そして、まだまだ新しい技術を利用したUI実装については実践し切れていない部分もあったので、来年はバランスを考えながらアウトプットできればと考えておりますので、引き続きよろしくお願い致します。

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?