0

I am at the stage of learning new swift and I designed my application as mvc design pattern. I went on an adventure to learn mvvm :D.

There are parts that I still don't understand. I learned that I need to transfer without using UIKit in the ViewModel part, but I couldn't figure out how to transfer it. I have to find the way to it. I have 10 Viewcontroller pages and I want to make them all according to mvvm.

I'm trying to convert my design from MVC to MVVM but i am getting this error how can i solve it?

BreedsViewController

import UIKit
import ProgressHUD

protocol BreedsViewControllerInterface: AnyObject {
    func prepareCollectionView()
}

final class BreedsViewController: UIViewController {
    
    @IBOutlet weak var categoryCollectionView: UICollectionView!
    // main storyboard collection View adding (dataSource, delegate)
    @IBOutlet weak var popularCollectionView: UICollectionView!
    // main storyboard collection View adding (dataSource, delegate)
    @IBOutlet weak var specialsCollectionView: UICollectionView!
    // main storyboard collection View adding (dataSource, delegate)
    
    private lazy var viewModel = BreedsVM()
    
    // data, move mvvm
    var categories: [DogCategory] = []
    var populars: [Breed] = []
    var downCategories:[Breed] = []
        
    override func viewDidLoad() {
        super.viewDidLoad()
        
        viewModel.view = self
        viewModel.viewDidLoad()
    }
    
    private func registerCell() {
        categoryCollectionView.register(UINib(nibName: CategoryCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: CategoryCollectionViewCell.identifier)
        
        popularCollectionView.register(UINib(nibName: DogPortraitCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: DogPortraitCollectionViewCell.identifier)
        
        specialsCollectionView.register(UINib(nibName: DogLandscapeCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: DogLandscapeCollectionViewCell.identifier)
    }

}

extension BreedsViewController: UICollectionViewDelegate, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        
        switch collectionView {
        case categoryCollectionView:
            return categories.count
        case popularCollectionView:
            return populars.count
        case specialsCollectionView:
            return downCategories.count
        default: return 0
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        switch collectionView {
        case categoryCollectionView:
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CategoryCollectionViewCell.identifier, for: indexPath) as! CategoryCollectionViewCell
            
            cell.setup(category: categories[indexPath.row])
            return cell
        case popularCollectionView:
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DogPortraitCollectionViewCell.identifier, for: indexPath) as! DogPortraitCollectionViewCell
            cell.setup(breed: populars[indexPath.row])
            return cell
            
        case specialsCollectionView:
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DogLandscapeCollectionViewCell.identifier, for: indexPath) as! DogLandscapeCollectionViewCell
            
            cell.setup(breed: downCategories[indexPath.row])
            return cell
        default: return UICollectionViewCell()
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
                
        if collectionView == categoryCollectionView {
            let controller = ListDogsViewController.instantiate()
            controller.category = categories[indexPath.row]
            navigationController?.pushViewController(controller, animated: true)
        } else {
            let controller = FavoriteDetailViewController.instantiate()
            controller.breed = collectionView == popularCollectionView ? populars[indexPath.row] : downCategories[indexPath.row]
            navigationController?.pushViewController(controller, animated: true)
        }
    }
}


extension BreedsViewController: BreedsViewControllerInterface {
    
    func prepareCollectionView() {
        registerCell()
        ProgressHUD.show()
        
        NetworkService.shared.fetchAllCategories { [weak self] (result) in
            switch result {
            case.success(let allBreed):
                ProgressHUD.dismiss()
                self?.categories = allBreed.categories ?? []
                self?.populars = allBreed.populars ?? []
                self?.downCategories = allBreed.downCategories ?? []
                
                self?.categoryCollectionView.reloadData()
                self?.popularCollectionView.reloadData()
                self?.specialsCollectionView.reloadData()
            case.failure(let error):
                ProgressHUD.showError(error.localizedDescription)
            }
        }
    }
    
}

BreedsVM

import Foundation

protocol BreedsVMInterface {
    var view: BreedsViewControllerInterface? { get set }
    
    func viewDidLoad()
    func didSelectItemAt(indexPath: IndexPath)
}

final class BreedsVM {
    weak var view: BreedsViewControllerInterface?
    
}

extension BreedsVM: BreedsVMInterface {
    func didSelectItemAt(indexPath: IndexPath) {
        
    }
    
    func viewDidLoad() {
        view?.prepareCollectionView()
    }
    
}

For example, I want to apply didselectItemAt according to Mvvm. When I want to do this, I get the following error. How can I solve it?

Changed BreedsViewController

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        
        viewModel.didSelectItemAt(indexPath: indexPath)
        
    }

Changed BreedsVM

import Foundation

protocol BreedsVMInterface {
    var view: BreedsViewControllerInterface? { get set }
    
    func viewDidLoad()
    func didSelectItemAt(indexPath: IndexPath)
}

final class BreedsVM {
    weak var view: BreedsViewControllerInterface?
    
    var categories: [DogCategory] = []
    var populars: [Breed] = []
    var downCategories:[Breed] = []
    
}

extension BreedsVM: BreedsVMInterface {
    func didSelectItemAt(indexPath: IndexPath) {
        if collectionView == categoryCollectionView {
            let controller = ListDogsViewController.instantiate()
            controller.category = categories[indexPath.row]
            navigationController?.pushViewController(controller, animated: true)
        } else {
            let controller = FavoriteDetailViewController.instantiate()
            controller.breed = collectionView == popularCollectionView ? populars[indexPath.row] : downCategories[indexPath.row]
            navigationController?.pushViewController(controller, animated: true)
        }
    }
    
    func viewDidLoad() {
        view?.prepareCollectionView()
    }
    
}

BreedsVM's warnings and errors

Cannot find 'categoryCollectionView' in scope Cannot find 'collectionView' in scope Cannot find 'popularCollectionView' in scope

LordGrim
  • 39
  • 6
  • Have a look at one of my answers in this subject. https://stackoverflow.com/questions/67538721/how-to-use-mvvm-correctly-in-swiftui/67539836#67539836 – xTwisteDx Dec 13 '22 at 15:28
  • I partially understood, I will have a question, how can I connect IBOutlet parts to breedVM or fix the error in the above code block, I still haven't been able to figure it out. – LordGrim Dec 13 '22 at 15:47

1 Answers1

1

When we move from MVC to any other architecture, we do so to achieve the separation of business logic and UI Logic so for example in MVVM, the ViewModel shouldn't know anything about the UI and also the ViewController should be dumb just makes UI stuff ( changing color, show and hide UI elements, .. ) and also in MVVM, the connection should be from one side the ViewController, the ViewController should have an instance from the ViewModel but the ViewModel should have any reference from the ViewController, but how we achieve the changing of the UI after processing some logic? by binding, and this can be done through number of ways, for example: Combine or RxSwift or even closures, but for simplicity we can start by making the binding using closures so let's take an example:

// ViewModel

class BreedsViewModel {

    // MARK: - Closures
    var fetchCategoriesSucceeded: ( (_ categories: [DogCategory], _ populars: [Breed], _ downCategories: [Breed]) -> Void )?
    var fetchCategoriesFailed: ( (_ errorMessage: String) -> Void )?

    // MARK: - Fetch Categories API
    func fetchCategories(){
        // Also this should be injected to the ViewModel instead of using it as a singleton, read more about dependency injection
        NetworkService.shared.fetchAllCategories { [weak self] (result) in
            switch result {
            case.success(let allBreed):
                self?.fetchCategoriesSucceeded?(allBreed.categories, allBreed.populars, allBreed.downCategories)
            case.failure(let error):
                self?.fetchCategoriesFailed?(error.localizedDescription)
            }
        }
    }
}

// ViewController

class BreedsViewController: UIViewController {

    var viewModel = BreedsViewModel() // This should be injected to the view controller

    private var categories: [DogCategory] = []
    private var populars: [Breed] = []
    private var downCategories:[Breed] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        bindViewModel()
        fetchCategories()
    }

    private func fetchCategories(){
//        ProgressHUD.show()
        viewModel.fetchCategories()
    }

    private func bindViewModel() {
        viewModel.fetchCategoriesSucceeded = { [weak self] categories, populars, downCategories in
//            ProgressHUD.dismiss()
            self?.categories = categories
            self?.populars = populars
            self?.downCategories = downCategories
//            collectionView.reloadData()
    }

        viewModel.fetchCategoriesFailed = { [weak self] errorMessage in
//            ProgressHUD.showError(errorMessage)
        }
    }
}

As you can see now, the ViewModel doesn't know anything about the UI, just getting the data from the API then notify the ViewController through the closure and when the ViewController notified, it should update the UI.

I can see also what you are trying to achive is more related to MVP, there are a Presenter and a ViewController, the Presenter will have a weak reference from the ViewController and update the view controller through a delegate

// Presenter

protocol BreedsPresenterDelegate: AnyObject {
    func fetchCategoriesSucceeded(_ categories: [DogCategory], _ populars: [Breed], _ downCategories: [Breed])
    func fetchCategoriesFailed(_ errorMessage: String)
}

class BreedsPresenter {

    weak var delegate: BreedsPresenterDelegate?

    func fetchCategories(){
        NetworkService.shared.fetchAllCategories { [weak self] (result) in
            switch result {
            case.success(let allBreed):
                self?.delegate?.fetchCategoriesSucceeded(allBreed.categories, allBreed.populars, allBreed.downCategories)
            case.failure(let error):
                self?.delegate?.fetchCategoriesFailed(error.localizedDescription)
            }
        }
    }
}

// ViewController

class BreedsViewController: UIViewController {

    var presenter = BreedsPresenter() // This should be injected to the view controller

    private var categories: [DogCategory] = []
    private var populars: [Breed] = []
    private var downCategories:[Breed] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        presenter.delegate = self
        fetchCategories()
    }

    private func fetchCategories(){
  //        ProgressHUD.show()
        presenter.fetchCategories()
    }
}

extension BreedsViewController: BreedsPresenterDelegate {
    func fetchCategoriesSucceeded(_ categories: [DogCategory], _ populars: [Breed], _ downCategories: [Breed]) {
//    ProgressHUD.dismiss()
        self.categories = categories
        self.populars = populars
        self.downCategories = downCategories
//        collectionView.reloadData()
    }

    func fetchCategoriesFailed(_ errorMessage: String) {
//        ProgressHUD.showError(errorMessage)
    }
}

I hope this helps.

  • I understand solutions and I defined variables like viewModel.Example thank you very much but collectionView.reloadData() in error ViewController (Reference to member 'reloadData' cannot be resolved without a contextual type) – LordGrim Dec 18 '22 at 14:06
  • I think you have a private reference to the collectionView in BreedsViewController maybe an IBOutlet or something and then you can access it in the ViewController and handle data source in the CollectionViewDataSource as usual, try it and lets discuss if the issue is in something else – Abdallah Eid Dec 19 '22 at 15:55