Pangu
Pangu

Reputation: 3819

How to re-structure code to follow MVVM design pattern in Swift?

In my code, I've been using the MVC pattern as Apple has shown in their demonstrations. However I'm attempting to shy away from it to write cleaner and robust code by following the MVVM pattern to reduce the bloated UIViewController:

My understanding of it is that the Controller and View does not know anything about the Model, but communicates with the middle man ViewModel, who stores information about the Model and passes it to the Controller, who in turn updates the View.

I have re-structured my code to have a Model and ViewModel like so:

struct PersonModel
{
    public var name: String
    public var position: String
    public var imageLink: String
    public var listOrder: Int

    init()
    {
        self.name = "Unknown"
        self.position = "Unknown"
        self.imageLink = "url link"
        self.listOrder = 0
    }

    init(name: String, position: String, imageLink: String, listOrder: Int)
    {
        self.name = name
        self.position = position
        self.imageLink = imageLink
        self.listOrder = listOrder
    }
}

class PersonViewModel
{
    fileprivate var personModel: PersonModel

    public var name: String {
        return personModel.name
    }

    public var position: String {
        return personModel.position
    }

    public var imageLink: String {
        return personModel.imageLink
    }

    public var listOrder: Int {
        return personModel.listOrder
    }

    init(personModel: PersonModel)
    {
        self.personModel = personModel
    }
}

This seems simple enough.

What I'm currently having trouble with is cleaning the other codes that currently reside in my UIViewController, which does some code to make retrieve a JSON object representing a PersonModel, and verifying its data contents.

var personInfo = [PersonModel]()

func retrieveFromDatabase()
{
    let json = JSON()

    json.getJSONData(link: "database link") { (json) in
        if json.isEmpty == false
        {
            self.storeJSONData(json: json)
        }
    }
}

func storeJSONData(json: [String : Any])
{
    for key in json.keys.sorted()
    {
        // Store each person information
        guard let info = json[key] as? [String: Any] else {
            return
        }

        let name = retrieveProperty(json: info,
                                    property: "Name Field")
        let position = retrieveProperty(json: info,
                                        property: "Position Field")
        let listOrder = retrieveProperty(json: info,
                                         property: "List Order Field")
        let imageLink = retrieveProperty(json: info,
                                         property: "Image Link Field")

        let person = checkData(name: name,
                               listOrder: Int(listOrder)!,
                               position: position,
                               imageLink: imageLink)

        personInfo.append(person)
    }
}

func retrieveProperty(json: [String : Any], property: String) -> String
{
    guard let attribute = json[property] as? String else {
        return ""
    }

    return attribute
}

func checkData(name: String, listOrder: Int, position: String, imageLink: String) -> PersonModel
{
    var person = PersonModel()
    person.listOrder = listOrder

    if name.isEmpty == false
    {
        person.name = name
    }

    if position.isEmpty == false
    {
        person.position = position
    }

    if imageLink.isEmpty == false
    {
        person.imageLink = imageLink
    }

    return person
}

func getPersonInfo(sectionRow: Int) -> PersonModel
{
    let person = PersonModel(name: personInfo[sectionRow].name,
                           position: personInfo[sectionRow].position,
                           imageLink: personInfo[sectionRow].imageLink,
                           listOrder: personInfo[sectionRow].listOrder)

    return person
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell",
                                                        for: indexPath) as! CustomCell

    let personModel = getPersonInfo(sectionRow: indexPath.row)

    guard let url = URL(string: personModel.imageLink) else {
        return UICollectionViewCell()
    }
    let imageResouce = ImageResource(downloadURL: url,
                                     cacheKey: personModel.imageLink)

    cell.staffImageView.kf.setImage(with: imageResouce,
                                    placeholder: #imageLiteral(resourceName: "default_profile"),
                                    options: [.transition( .fade(0.3) ),
                                              .fromMemoryCacheOrRefresh]
    )
    cell.nameLabel.text = personModel.name
    cell.positionLabel.text = personModel.position

    return cell
}
  1. To follow the MVVM pattern, would the above JSON code be migrated to the PersonViewModel instead of currently residing in the Controller?
  2. In my Controller class, I need to access an array of PersonModelView that contains my PersonModel data after all the JSON data has been stored from the function call storeJSONData. How can I properly structure my code so that my Controller can access it while still adhering to the MVVM pattern?

Please forgive me as I am new to learning this pattern, and would like to some guidance.

Thanks!

Upvotes: 0

Views: 2152

Answers (2)

kd02
kd02

Reputation: 430

To follow the MVVM pattern, would the above JSON code be migrated to the PersonViewModel instead of currently residing in the Controller?

I've found when doing MVVM it's much easier to move the logic that performs data queries/operations to a DAO (Data Access Object) and the relevant ViewModel then calls that object to get the data. This abstracts that logic so that it can be re-used by other ViewModels.

NB: don't make the DAO a singleton, not good practice.

In my Controller class, I need to access an array of PersonModelView that contains my PersonModel data after all the JSON data has been stored from the function call storeJSONData. How can I properly structure my code so that my Controller can access it while still adhering to the MVVM pattern?

In my opinion it's best to do this with delegates, make your ViewController a delegate of your ViewModel and pass the data, in this case array, from the ViewModel to the ViewController that way e.g.

struct PersonModel {
    // Model properties here
}

class PersonViewModel {

    weak var delegate: PersonViewModelDelegate?

    public func getPersons() -> [PersonViewModel] {

        // perform logic to get person array then return it to the delegate
        let persons = [PersonViewModel]
        delegate?.didGetPersons(persons: persons)
    }
}

protocol PersonViewModelDelegate {
    func didGetPersons(persons:[PersonViewModel])
}

class YourViewController:UIViewController {

    weak var personViewModel: PersonViewModel = PersonViewModel() {

        personViewModel.delegate = self
    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // your viewcontroller code goes here
}

extension YourViewController: PersonViewModelDelegate {


    func didGetPersons(persons: [PersonViewModel]) {
        // use persons array here
    }
}

Upvotes: 0

pacification
pacification

Reputation: 6018

To follow the MVVM pattern, would the above JSON code be migrated to the PersonViewModel instead of currently residing in the Controller?

I think, that MVVM pattern it's all about "frontend" part of mobile app. As viewModel doesn't really know about any UIKit things, it also shouldn't know about any kind of "how to convert JSON to model", "how to deal with cache" or "create alamofire request". This part is "backend", that's why different specific classes should be introduced right here (you may call them services, for example).

In my Controller class, I need to access an array of PersonModelView that contains my PersonModel data after all the JSON data has been stored from the function call storeJSONData. How can I properly structure my code so that my Controller can access it while still adhering to the MVVM pattern?

Note: maybe you shouldn't create "array of PersonModelView that contains my PersonModel", but create only one viewModel that contains array of models?

According to the top comment, i deal with that this way:

  • in Controller viewDidLaod method create viewModel;
  • in viewModel create any sort of configure of initialize func that can retrieve the models from you specific service;
  • and finally, notify Controller about all data stored. For this you can use closures or more specific tools like PromiseKit (google promises), RxSwift, etc.

This is couple things i personally try to follow.


Little bit of in depth explanation.

enter image description here

Legend: red lines goes down to fetch the data; the green ones bring data to the top (UI).

So, as i mentioned there is configure or getPersons method that call to PersonService getPersons method to retrieve PersonModels (this is important! not JSON, arrays of strings or any other simple types - we need to back final model or error (nil) ). How this happened? ViewModel has property

lazy var personService: PersonServiceProtocol = {
    let personParser: PersonParserProtocol = PersonParser()
    let personValidator: PersonValidationProtocol = PersonValidator()
    return PersonService(parser: parser, validator: validator)
}()

that manage all "backend" things for us. Also, there is one tricky thing: we are setting parser and validator in init, but PersonDB already stored in PersonService. Why? This happens, because in future you may have different kind of Persons that should be retrieved by PersonService. For this type of work you need manually set parser and validator for different sort of Person, but you really don't need to create DB for each type of Person.

When PersonDB successfully retrieve the data (JSON) and back it to the PersonService (step 4), you need call under JSON parser and validation funcs. It's very common to return closures right here (or Promise, Observable, if you use PromiseKit or RxSwift, because in this case you can write your code using chain pattern), i.e.

personDb.getPersons { result in
    if case let .success(json) = result, self.validator.validate(json) {
        let models = self.parser.parse(json)
        completion(.success(models))
    } else {
        // error`enter code here`
    }
}

Now you have final models and you can pass it back right to the viewModel or apply any modifications (for example, split them by sections) and then back to viewModel (step 5).

Sometime it looks like little bit overcoding here, because you basically can skip personService.getPerson() func and work manually with PersonDB, parser and validator in viewModel (i.e. you use personService.getPerson() only for call another function), but i think sooner or later you end up with massive view model and also this breaks single responsibility of you viewModel.

This is pretty straightforward architecture. It works, but you should carefully think about all parts of it: maybe something you can delegate to the Pod or so, maybe some parts (because of very small amount of work) can be merged together. Good luck.

Upvotes: 2

Related Questions