I want to create a MVVM-C project, but also adequately test it. Naturally I want to create such a project that can access a Network Service.
Any comments or thoughts on this approach?
DependencyFactory:
protocol Factory {
var networkManager: HTTPManagerProtocol { get }
func makeInitialViewModel(coordinator: Coordinator) -> InitialViewModel
func makeInitialView(viewModel: InitialViewModel) -> InitialView
func makeDetailView(viewModel: DetailViewModel) -> DetailView
func makeDetailViewModel(coordinator: Coordinator) -> DetailViewModel
}
// replace the DependencyContainer for tests
class DependencyFactory: Factory {
var networkManager: HTTPManagerProtocol = HTTPManager()
// should not return an optional at the end of this project
func makeInitialCoordinator() -> ProjectCoordinator {
let coordinator = ProjectCoordinator(factory: self)
return coordinator
}
func makeInitialView(viewModel: InitialViewModel) -> InitialView {
let view = InitialView()
return view
}
func makeInitialViewModel(coordinator: Coordinator) -> InitialViewModel {
let viewModel = InitialViewModel(coordinator: coordinator, networkManager: networkManager)
return viewModel
}
func makeDetailView(viewModel: DetailViewModel) -> DetailView {
let view = DetailView()
return view
}
func makeDetailViewModel(coordinator: Coordinator) -> DetailViewModel {
let viewModel = DetailViewModel(coordinator: coordinator, networkManager: networkManager)
return viewModel
}
}
A basic HTTPManager (that doesn't really touch the network, but you get the point!)
protocol HTTPManagerProtocol {
func get(url: URL, completionBlock: @escaping (Result<Data, Error>) -> Void)
}
class HTTPManager: HTTPManagerProtocol {
public func get(url: URL, completionBlock: @escaping (Result<Data, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let data = Data("The Data from HTTPManager".utf8)
completionBlock(.success(data))
}
}
}
Project Coordinator
protocol Coordinator: class {
func start(_ navigationController: UINavigationController)
func moveToDetail()
}
class ProjectCoordinator: Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController?
private var factory: Factory
init(factory: Factory) {
self.factory = factory
}
func start(_ navigationController: UINavigationController) {
let vc = InitialViewController(factory: factory, coordinator: self)
self.navigationController = navigationController
navigationController.pushViewController(vc, animated: true)
}
func moveToDetail() {
let vc = DetailViewController(factory: factory, coordinator: self)
navigationController?.pushViewController(vc, animated: true)
}
}
InitialModel
struct InitialModel : Codable {
let dataString : String
}
InitialView
final class InitialView: UIView {
let traverseButton = UIButton(type: .custom)
let networkButton = UIButton(type: .custom)
let networkLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
self.backgroundColor = .red
traverseButton.frame = CGRect(x: 0, y: 0, width: 200, height: 100)
traverseButton.setTitle("Go to Detail", for: .normal)
traverseButton.setTitleColor(.black, for: .normal)
traverseButton.isUserInteractionEnabled = true
self.addSubview(traverseButton)
traverseButton.translatesAutoresizingMaskIntoConstraints = false
traverseButton.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
traverseButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
networkButton.frame = CGRect(x: 0, y: 0, width: 200, height: 100)
networkButton.setTitle("Make Network Call", for: .normal)
networkButton.setTitleColor(.black, for: .normal)
networkButton.isUserInteractionEnabled = true
self.addSubview(networkButton)
networkButton.translatesAutoresizingMaskIntoConstraints = false
networkButton.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
networkButton.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 100).isActive = true
networkLabel.text = "No network calls made"
networkLabel.backgroundColor = .purple
self.addSubview(networkLabel)
networkLabel.translatesAutoresizingMaskIntoConstraints = false
networkLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
networkLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 200).isActive = true
networkLabel.widthAnchor.constraint(equalToConstant: 300).isActive = true
networkLabel.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
func setNetworkLabel(text: String){
networkLabel.text = text
}
}
InitialViewController
class InitialViewController: UIViewController {
private var coordinator: Coordinator?
private var factory: Factory?
var intialView: InitialView?
lazy var viewModel: InitialViewModel? = {
return factory?.makeInitialViewModel(coordinator: coordinator!)
}()
init(factory: Factory, coordinator: Coordinator) {
self.factory = factory
self.coordinator = coordinator
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
if let initialView = factory?.makeInitialView(viewModel: viewModel!) {
initialView.traverseButton.addTarget(self, action: #selector(traverseButton(_:)), for: .touchDown)
initialView.networkButton.addTarget(self, action: #selector(networkButton), for: .touchDown)
self.intialView = initialView
self.view = initialView
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func traverseButton(_ sender: UIButton) {
coordinator?.moveToDetail()
}
@IBAction func networkButton(_ sender: UIButton) {
viewModel?.fetchData(completion: { data in
switch data {
case .failure: fatalError()
case .success(let data):
if let data = data.first {
self.intialView?.setNetworkLabel(text: data.dataString)
}
}
})
}
}
InitialViewModel
class InitialViewModel {
private var networkManager: HTTPManagerProtocol?
init(coordinator: Coordinator?, networkManager: HTTPManagerProtocol) {
self.networkManager = networkManager
}
func fetchData(completion: @escaping (Result<[InitialModel], Error>) -> Void) {
networkManager?.get(url: URL(string: "NOURL")!, completionBlock: { result in
DispatchQueue.main.async {
switch result {
case .failure:
completion(.failure(NSError()))
case .success(let data):
if let str = String(data: data, encoding: .utf8) {
let model = InitialModel(dataString: str)
completion(.success([model]))
}
}
}
})
}
}
DetailViewController
class DetailViewController: UIViewController {
weak var coordinator: Coordinator?
private var factory: Factory?
var detailView: DetailView?
lazy var viewModel: DetailViewModel? = {
return factory?.makeDetailViewModel(coordinator: coordinator!)
}()
init(factory: Factory, coordinator: Coordinator) {
self.factory = factory
self.coordinator = coordinator
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
if let detailView = factory?.makeDetailView(viewModel: viewModel!) {
self.detailView = detailView
self.view = detailView
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
DetailViewModel
class DetailViewModel {
private var networkManager: HTTPManagerProtocol?
init(coordinator: Coordinator?, networkManager: HTTPManagerProtocol) {
self.networkManager = networkManager
}
}
DetailView
final class DetailView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
self.backgroundColor = .blue
}
}