Skip to main content
Notice removed Draw attention by WishIHadThreeGuns
Bounty Ended with anon's answer chosen by WishIHadThreeGuns
deleted 226 characters in body
Source Link

I've written the whole thing (and have tests in this GitHub Repo https://github.com/stevencurtis/MVVM-CDependency)

The whole thing is on GitHub - all suggestions welcome! https://github.com/stevencurtis/MVVM-CDependency

I've written the whole thing (and have tests in this GitHub Repo https://github.com/stevencurtis/MVVM-CDependency)

The whole thing is on GitHub - all suggestions welcome! https://github.com/stevencurtis/MVVM-CDependency

Notice added Draw attention by WishIHadThreeGuns
Bounty Started worth 50 reputation by WishIHadThreeGuns
added 222 characters in body
Source Link

I've written the whole thing (and have tests in this GitHub Repo https://github.com/stevencurtis/MVVM-CDependency)

The whole thing is on GitHub - all suggestions welcome! https://github.com/stevencurtis/MVVM-CDependency

I've written the whole thing (and have tests in this GitHub Repo https://github.com/stevencurtis/MVVM-CDependency)

The whole thing is on GitHub - all suggestions welcome! https://github.com/stevencurtis/MVVM-CDependency

Source Link

MVVM-C Swift with Dependency injection

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