Swift refactor project to MVVM-C

398 views Asked by At

So i am trying to refactor an existing project from MMVM and to add coordinator. i have the following classes:

protocol Coordinator {

 func start()

}

class BaseCoordinator: Coordinator {

private var presenter: UINavigationController
private var genreViewController: ViewController?
private var viewModel = GenreViewModel()

init(presenter: UINavigationController) {
    self.presenter = presenter
}

func start() {
    let genreViewController = ViewController()
    genreViewController.viewModel = viewModel
    self.genreViewController = genreViewController
    presenter.pushViewController(genreViewController, animated: true)
}
}
 

class AppCoordinator: Coordinator {

private let window: UIWindow
private let rootViewController: UINavigationController
private var genereListCoordinator: BaseCoordinator?

init(window: UIWindow) {
    self.window = window
    rootViewController = UINavigationController()
    rootViewController.navigationBar.prefersLargeTitles = true
    
    genereListCoordinator = BaseCoordinator(presenter: rootViewController)
}

func start() {
    window.rootViewController = rootViewController
    genereListCoordinator?.start()
    window.makeKeyAndVisible()
}

}

In appDelegate i do as below:

      var window: UIWindow?
var applicationCoordinator: AppCoordinator?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    let window = UIWindow(frame: UIScreen.main.bounds)
    let appCordinator = AppCoordinator(window: window)
    self.window = window
    self.applicationCoordinator = appCordinator
    
    appCordinator.start()
    return true
}

VC is:

  class ViewController: UIViewController {

@IBOutlet weak var collectionView: UICollectionView!

var viewModel: GenreViewModel!

override func viewDidLoad() {
    super.viewDidLoad()
    
    
    if let flowLayout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
        flowLayout.itemSize = CGSize(width: self.collectionView.bounds.width, height: 50)
    }
    self.collectionView.delegate = self
    self.collectionView.dataSource = self
    collectionView.backgroundView = UIImageView(image: UIImage(named: "131249-dark-grey-low-poly-abstract-background-design-vector.jpg"))
    self.viewModel.delegate = self
    self.getData()
}

func getData() {
    MBProgressHUD.showAdded(to: self.view, animated: true)
    viewModel.getGenres()
}

}

extension ViewController: GenreViewModelDelegate { func didfinish(succsess: Bool) { MBProgressHUD.hide(for: self.view, animated: true) if succsess { self.collectionView.reloadData() } else { let action = UIAlertAction(title: "Try again", style: .default, handler: { (action) in self.getData() }) Alerts.showAlert(vc: self, action: action) } } }

extension ViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: 100, height: 100) } }

extension ViewController: UICollectionViewDataSource {

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return viewModel.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GenreCollectionViewCell.reuseIdentifier, for: indexPath) as? GenreCollectionViewCell else {
        return UICollectionViewCell()
    }
    let cellViewModel = viewModel.cellViewModel(index: indexPath.row)
    cell.viewModel = cellViewModel
    return cell
}

func numberOfSections(in collectionView: UICollectionView) -> Int {
    return 1
}

}

 extension ViewController: UICollectionViewDelegate {

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let cellViewModel = viewModel.cellViewModel(index: indexPath.row)
    viewModel.didSelectGenre(index: (cellViewModel?.id)!)
}
}

VM is :

protocol GenreViewModelDelegate: class {
func didfinish(succsess: Bool)
}

protocol GenreListCoordinatorDelegate: class {
func movieListDidGenre(id: String)
 }

class GenreViewModel {

weak var coordinatorDelegate: GenreListCoordinatorDelegate?

var networking =  Networking()

var genresModels = [Genres]()

weak var delegate: GenreViewModelDelegate?

func getGenres() {
    self.networking.preformNetwokTask(endPoint: TheMoviedbApi.genre, type: Genre.self, success: { [weak self] (response) in
        print(response)
        if let genres = response.genres {
            self?.genresModels = genres
            self?.delegate?.didfinish(succsess: true)
        } else {
            self?.delegate?.didfinish(succsess: false)
        }
    }) {
        self.delegate?.didfinish(succsess: false)
    }
}

var count: Int {
    return genresModels.count
}

public func cellViewModel(index: Int) -> GenreCollectionViewCellModel? {
    
    let genreCollectionViewCellModel = GenreCollectionViewCellModel(genre: genresModels[index])
    return genreCollectionViewCellModel
    
}

public func didSelectGenre(index: String) {
    coordinatorDelegate?.movieListDidGenre(id: index)
}
}

The problem is that when i am trying to inject the viewModel to the ViewController and the push it in the start function it wont work-when the viewDidLoad invoked the viewModel in the VC is nil.

1

There are 1 answers

0
Ben On

With the same code, I managed to make it work, the viewModel property is populated on my side.


// Coordinator
protocol Coordinator {
    func start()
}

class BaseCoordinator: Coordinator {

    private var presenter: UINavigationController
    private var genreViewController: ViewController?
    private var viewModel = ViewModel()

    init(presenter: UINavigationController) {
        self.presenter = presenter
    }

    func start() {
        let genreViewController = ViewController()
        genreViewController.viewModel = viewModel
        self.genreViewController = genreViewController
        presenter.pushViewController(genreViewController, animated: true)
    }
}

// ViewController + ViewModel
struct ViewModel { }

class ViewController: UIViewController {

    var viewModel: ViewModel!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        view.backgroundColor = .white
    }
}

class AppCoordinator: Coordinator {

    private let window: UIWindow
    private let rootViewController: UINavigationController
    private var genereListCoordinator: BaseCoordinator?

    init(window: UIWindow) {
        self.window = window
        rootViewController = UINavigationController()
        rootViewController.navigationBar.prefersLargeTitles = true

        genereListCoordinator = BaseCoordinator(presenter: rootViewController)
    }

    func start() {
        window.rootViewController = rootViewController
        window.makeKeyAndVisible()
        genereListCoordinator?.start()
    }
}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var applicationCoordinator: AppCoordinator?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        let window = UIWindow()
        let appCordinator = AppCoordinator(window: window)
        self.window = window
        self.applicationCoordinator = appCordinator

        appCordinator.start()

        return true
    }
}

Often the issues with Coordinator pattern is to retaining the navigation stack which would not fire viewDidLoad or don't display screen at all for instance. In your case, if only viewModel is missing, I believe it comes from a ViewController constructor issue or an override.

I can see an @IBOutlet in your ViewController which makes me think you are using storyboard / xib file. However, the BaseCoordinator only use a init(), could it be the issue? If using Storyboard, you should try with instantiateViewController(...).

On another note, you should look into retaining all stack of coordinators to handle a full navigation and avoid retaining viewModel or other properties within coordinator when needed. If UINavigationController retain child UIViewController, you wouldn't need to as well.