1. はじめに
テストを書くにあたり、DIが必要となりました。
本来ならライブラリを使うべきなのだと思いますが、DIについての理解を深めたく自前で一度やってみました。
ViewModel
の中で使っているCoreDataManager
をViewController
から入れられるようにするという単純なものですが、少し前置きが長くなってしまったので、
結果はこちらDI後です。
2. DI前
ViewController
がViewModel
に依存している
final class LessonImageViewController: UIViewController {
private let viewModel = LessonImageViewModel()
.....
ViewModel
がCoreDataManager
に依存している
final class LessonImageViewModel {
init() {
mutate()
}
let coreDataMangaer = CoreDataManager.shared
override func mutate() {
.....
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. 完了
このようにViewContorller
をmakeInstance
して使っていきます。
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】の作りとなっています。(こちらで少しまとめました)
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
}
}
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)
}
}
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していくのはキツイので、基本的にはライブラリを使用していきたいと思います。
参考