1
1

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 3 years have passed since last update.

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

Posted at

1. はじめに

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

ViewModelの中で使っているCoreDataManagerViewControllerから入れられるようにするという単純なものですが、少し前置きが長くなってしまったので、
結果はこちらDI後です。

2. DI前

ViewControllerViewModelに依存している

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

ViewModelCoreDataManagerに依存している

LessonImageViewModel
final class LessonImageViewModel {
    
    init() {
        mutate()
    }

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

CoreDataManagerはシングルトン

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を継承したものであれば、入れ替えができるようになります。

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

final class CoreDataManager: CoreDataProtocol {

    .....

2. ViewModelの依存を解消する

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

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

3. ViewControllerの依存を解消する

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

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!

    .....

4. 完了

このようにViewContorllermakeInstanceして使っていきます。

let lessonImageVC = LessonImageViewController.makeInstance(dependency: .init(viewModel: LessonImageViewModel(dependency: .init(coreDataProtocol: CoreDataManager.shared))))

(最初の.initViewControllerDependencyのもので、省略せずに書くと、LessonImageViewController.Dependency.init(viewModel:)、その後の.initは、LessonImageViewModel.Dependency.init(coreDataProtocol:)となります)

4. DI後

DI後がこちらです。
DI前では関係ない部分を全省略してしまったので、全体像がイメージしやすいように少しコード追記しました。
【UIKit x Combine x MVVM】の作りとなっています。(こちらで少しまとめました

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
    }
}
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)
    }
}
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していくのはキツイので、基本的にはライブラリを使用していきたいと思います。

参考

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?