Reputation: 3819
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
}
PersonViewModel
instead of currently residing in the Controller
?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
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
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 service
s, for example).
In my
Controller
class, I need to access an array ofPersonModelView
that contains myPersonModel
data after all the JSON data has been stored from the function callstoreJSONData
. How can I properly structure my code so that myController
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:
Controller
viewDidLaod
method create viewModel
;viewModel
create any sort of configure
of initialize
func that can retrieve the models from you specific service
;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.
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 PersonModel
s (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 Person
s 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