【Swift】テストが書けないので自前でDIしてみた

#1. はじめに
テストを書くにあたり、DIが必要となりました。
本来ならライブラリを使うべきなのだと思いますが、DIについての理解を深めたく自前で一度やってみました。

`ViewModel`の中で使っている`CoreDataManager`を`ViewController`から入れられるようにするという単純なものですが、少し前置きが長くなってしまったので、
結果はこちら[DI後](#4-di注入後)です。

#2. DI前

`ViewController`が`ViewModel`に依存している

```swift:LessonImageViewController
final class LessonImageViewController: UIViewController {
    
    private let viewModel = LessonImageViewModel()
    
    .....
```

`ViewModel`が`CoreDataManager`に依存している

```swift:LessonImageViewModel
final class LessonImageViewModel {
    
    init() {
        mutate()
    }

    let coreDataMangaer = CoreDataManager.shared
    
    override func mutate() {
        
        .....
```

`CoreDataManager`はシングルトン

```swift:CoreDataManager
final class CoreDataManager {
    static let shared = CoreDataManager()
    private init() { }
    
    func loadAllFavoriteLessonDataWithImage() -> [Lesson] {
        let fetchRequest = createRequest(objecteType: .lesson)
        .....
    }
    
    func loadAllLessonDataWithImage() -> [Lesson] {
        let fetchRequest = createRequest(objecteType: .lesson)
        .....
    }
}

```

このままでは、`CoreDataManager`をテスト用に入れ替えることができないので、`ViewController`から`CoreDataManager`を渡せるようにDIしていきたい。

#3. 手順
`CoreDataManager`->`ViewModel`->`ViewController`という順に修正していきます。

#### 1. `CoreDataManager`のメソッドをプロトコルで切り出す
切り出すことで、`CoreDataProtocol`を継承したものであれば、入れ替えができるようになります。

```swift
protocol CoreDataProtocol {
    func loadAllLessonDataWithImage() -> [Lesson]
    func loadAllFavoriteLessonDataWithImage() -> [Lesson]
}

final class CoreDataManager: CoreDataProtocol {

    .....
```


#### 2. `ViewModel`の依存を解消する
`ViewModel`の中に`Dependency`という構造体を定義して、`.init`で外部から`CoreDataProtocol`に準拠するクラスを入れ込めるようにします。

```swift
final class LessonImageViewModel {
    struct Dependency {
        let coreDataProtocol: CoreDataProtocol
    }
    init(dependency: Dependency) {
        coreDataMangaer = dependency.coreDataProtocol
    }
    let coreDataMangaer: CoreDataProtocol
    
    .....
```

#### 3. `ViewController`の依存を解消する
最後に、`ViewController`の中に同じく`Dependency`という構造体を定義して、`.makeInstance`で外部から`ViewModel`を入れ込めるようにします。

```swift
final class LessonImageViewController: UIViewController {
    struct Dependency {
        let viewModel: LessonImageViewModel
    }
    static func makeInstance(dependency: Dependency) -> LessonImageViewController {
        let viewController = R.storyboard.lessonImage.lessonImage()!
        viewController.viewModel = dependency.viewModel
        return viewController
    }
    private var viewModel: LessonImageViewModel!

    .....
```

#### ４. 完了
このように`ViewContorller`を`makeInstance`して使っていきます。

```swift
let lessonImageVC = LessonImageViewController.makeInstance(dependency: .init(viewModel: LessonImageViewModel(dependency: .init(coreDataProtocol: CoreDataManager.shared))))
```
（最初の`.init`は`ViewController`の`Dependency`のもので、省略せずに書くと、`LessonImageViewController.Dependency.init(viewModel:)`、その後の`.init`は、`LessonImageViewModel.Dependency.init(coreDataProtocol:)`となります）

#4. DI後
DI後がこちらです。
DI前では関係ない部分を全省略してしまったので、全体像がイメージしやすいように少しコード追記しました。
【UIKit x Combine x MVVM】の作りとなっています。（[こちらで少しまとめました](https://qiita.com/yoshitaka/items/9a7300325173a1616a37 "【Swift】UIKit × CombineでViewとViewModelを分けてみた")）

```swift:LessonImageViewController
import UIKit
import Combine

final class LessonImageViewController: UIViewController {
    struct Dependency {
        let viewModel: LessonImageViewModel
    }
    static func makeInstance(dependency: Dependency) -> LessonImageViewController {
        let viewController = R.storyboard.lessonImage.lessonImage()!
        viewController.viewModel = dependency.viewModel
        return viewController
    }
    private var viewModel: LessonImageViewModel!

    @IBOutlet weak var allBarButton: UIBarButtonItem!
    @IBOutlet weak var favoriteBarButton: UIBarButtonItem!
    @IBOutlet weak var customCollectionView: UICollectionView!

    private var subscriptions = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()
        bind()

        customCollectionView.delegate = self
        customCollectionView.dataSource = self
        customCollectionView.register(UINib(nibName: "ImageCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "ImageCell")
    }
    override func bind() {
        viewModel.allBarButtonIsOn
            .map { $0 ? .colorButtonOn : .colorButtonOff }
            .assign(to: \.tintColor, on: allBarButton)
            .store(in: &subscriptions)
        
        viewModel.favoriteBarButtonIsOn.sink { [weak self] isOn in
            guard let self = self else { return }
            self.favoriteBarButton.tintColor = isOn ? .colorButtonOn : .colorButtonOff
        }.store(in: &subscriptions)
        
        viewModel.lessonsArray.sink { [weak self] _ in
            guard let self = self else { return }
            self.customCollectionView.reloadData()
        }.store(in: &subscriptions)
    }
    
    @IBAction func allButtonPressed(_ sender: UIBarButtonItem) {
        viewModel.allButtonPressed.send()
    }
    
    @IBAction func favoriteButtonPressed(_ sender: UIBarButtonItem) {
        viewModel.favoriteButtonPressed.send()
    }
}

extension LessonImageViewController: UICollectionViewDelegate, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return viewModel.lessonsArray.value.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let customCell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath)
        guard let imageCell = customCell as? ImageCollectionViewCell else { return customCell }
        imageCell.setLessonData(lesson: viewModel.lessonsArray.value[indexPath.row], row: indexPath.row)
        return imageCell
    }
}
```



```swift:LessonImageViewModel
import Combine
import UIKit

final class LessonImageViewModel {
    struct Dependency {
        let coreDataProtocol: CoreDataProtocol
    }
    init(dependency: Dependency) {
        coreDataMangaer = dependency.coreDataProtocol
        mutate()
    }
    let coreDataMangaer: CoreDataProtocol

    private var subscriptions = Set<AnyCancellable>()
    let allButtonPressed = PassthroughSubject<Void, Never>()
    let favoriteButtonPressed = PassthroughSubject<Void, Never>()
    let dataReload = PassthroughSubject<Void, Never>()
    
    private(set) var allBarButtonIsOn = CurrentValueSubject<Bool, Never>(true)
    private(set) var favoriteBarButtonIsOn = CurrentValueSubject<Bool, Never>(false)
    private(set) var lessonsArray = CurrentValueSubject<[Lesson], Never>([])
    var tableMode = CurrentValueSubject<TableMode, Never>(.allTableView)
    
    override func mutate() {
        allButtonPressed.sink { [weak self] _ in
            guard let self = self else { return }
            self.allBarButtonIsOn.send(true)
            self.favoriteBarButtonIsOn.send(false)
            self.tableMode.send(.allTableView)
            self.dataReload.send()
        }.store(in: &subscriptions)
        
        favoriteButtonPressed.sink { [weak self] _ in
            guard let self = self else { return }
            self.allBarButtonIsOn.send(false)
            self.favoriteBarButtonIsOn.send(true)
            self.tableMode.send(.favoriteTableView)
            self.dataReload.send()
        }.store(in: &subscriptions)

        dataReload.sink { [weak self] _ in
            guard let self = self else { return }
            if self.tableMode.value == .allTableView {
                self.lessonsArray.send(self.coreDataMangaer.loadAllLessonDataWithImage())
            } else {
                self.lessonsArray.send(self.coreDataMangaer.loadAllFavoriteLessonDataWithImage())
            }
        }.store(in: &subscriptions)
    }
}
```

```swift:CoreDataManager

protocol CoreDataProtocol {
    func loadAllLessonDataWithImage() -> [Lesson]
    func loadAllFavoriteLessonDataWithImage() -> [Lesson]
}

final class CoreDataManager: CoreDataProtocol {
    static let shared = CoreDataManager()
    private init() { }
    
    func loadAllFavoriteLessonDataWithImage() -> [Lesson] {
        let fetchRequest = createRequest(objecteType: .lesson)
        let predicate = NSPredicate(format: "%K == %@", "favorite", NSNumber(value: true))
        fetchRequest.predicate = predicate
        let orderSort = NSSortDescriptor(key: "orderNum", ascending: true)
        let timeSort = NSSortDescriptor(key: "timeStamp", ascending: false)
        fetchRequest.sortDescriptors = [orderSort, timeSort]
        do {
            var lessons = try managerObjectContext.fetch(fetchRequest) as! [Lesson]
            lessons = lessons.filter { $0.imageSaved }
            return lessons
        } catch {
            fatalError("loadData error")
        }
    }
    
    func loadAllLessonDataWithImage() -> [Lesson] {
        let fetchRequest = createRequest(objecteType: .lesson)
        let orderSort = NSSortDescriptor(key: "orderNum", ascending: true)
        let timeSort = NSSortDescriptor(key: "timeStamp", ascending: false)
        fetchRequest.sortDescriptors = [orderSort, timeSort]
        do {
            var lessons = try managerObjectContext.fetch(fetchRequest) as! [Lesson]
            lessons = lessons.filter { $0.imageSaved }
            return lessons
        } catch {
            fatalError("loadData error")
        }
    }
}
```

#5. 感想
DIについては理解しているつもりでいましたが、自前でやろうとすると混乱する部分があり、今回の実装でやっと本当に理解できた気がします。（そう思いたい）
しかしながら、毎回自前でDIしていくのはキツイので、基本的にはライブラリを使用していきたいと思います。

#参考

https://qiita.com/fumiyasac@github/items/8d6b77c3547b8b7839ad

https://scior.hatenablog.com/entry/2020/08/28/210000

https://qiita.com/eKushida/items/78a58559aedd851d105c