MVVM ( Model-View-ViewModel )
You can ask me : "Marcos, what's MVVM? Why use it there? Why not use another?"
## Well, adopting a design pattern will depend on many things. Whether it's a business strategy, be size of what will be developed, testing requirements and so on. In this article, i will show some concepts about MVVM and a code demonstration.
Let's go!!!
MVVM Flow
- ViewController / View will have a reference to the ViewModel
 - ViewController / View get some user action and will call ViewModel
 - ViewModel request some API Service and API Service will sends a response to ViewModel
 - ViewModel will notifies the ViewController / View with binding
 - The ViewController / View will update the UI with data
 
Project Informations
- Use jsonplaceholder REST API
 - URLSession to fetch data
 - The table view will show data from service
 
MVVM ( Model-ViewModel- Model )
M ( Model ) : Represents data. JUST it. Holds the data and has nothing and don't have business logic.
struct Post: Decodable {
  let userId:Int
  let id: Int
  let title: String
  let body: String
}
V ( View ) : Represents the UI. In the view, we have User Actions that will call view model to call api service. After it, the data will through to view ( view model is responsible to do it ) and shows informations in the screen.
class PostViewController: UIViewController {
    private var postViewModel: PostViewModel?
    private var postDataSource: PostTableViewDataSource<PostTableViewCell, Post>?
    private var postDelegate: PostTableViewDelegate?
    private lazy var postTableView: UITableView = {
       let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        return tableView
    }()
    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
        setupConstraints()
        updateUI()
    }
    private func setupViews() {
        postTableView.register(PostTableViewCell.self, forCellReuseIdentifier: PostTableViewCell.cellIdentifier)
        self.view.addSubview(postTableView)
    }
    private func setupConstraints() {
        NSLayoutConstraint.activate([
            postTableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            postTableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
            postTableView.widthAnchor.constraint(equalTo: self.view.widthAnchor),
            postTableView.heightAnchor.constraint(equalTo: self.view.heightAnchor),
            postTableView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
            postTableView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
        ])
    }
    private func updateUI() {
        postViewModel = PostViewModel()
        postDelegate = PostTableViewDelegate()
        postViewModel?.bindPostViewModelToController = { [weak self] in
            self?.updateDataSource()
        }
    }
    private func updateDataSource() {
        guard let posts = postViewModel?.getPosts() else { return }
        postDataSource = PostTableViewDataSource(cellIdentifier: PostTableViewCell.cellIdentifier, items: posts, configureCell: { (cell, post) in
            cell.configureCell(post: post)
        })
        DispatchQueue.main.async {
            self.postTableView.delegate = self.postDelegate
            self.postTableView.dataSource = self.postDataSource
            self.postTableView.reloadData()
        }
    }
}
PostTableViewCell
class PostTableViewCell: UITableViewCell {
    static public var cellIdentifier: String = "PostTableViewCellIdentifier"
    private lazy var titleLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.lineBreakMode = .byWordWrapping
        label.textColor = .black
        label.font = UIFont.boldSystemFont(ofSize: 13)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    private lazy var bodyLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.lineBreakMode = .byWordWrapping
        label.textColor = .blue
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    private var postStackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 3
        stackView.distribution = .fill
        stackView.translatesAutoresizingMaskIntoConstraints = false
        return stackView
    }()
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupViews()
        setupConstraints()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    override func prepareForReuse() {
        super.prepareForReuse()
        titleLabel.text = ""
        bodyLabel.text = ""
    }
    public func configureCell(post: Post) {
        titleLabel.text = post.title
        bodyLabel.text = post.body
    }
    private func setupViews() {
        contentView.addSubview(postStackView)
        postStackView.addArrangedSubview(titleLabel)
        postStackView.addArrangedSubview(bodyLabel)
    }
    private func setupConstraints() {
        NSLayoutConstraint.activate([
            postStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            postStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            postStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
            postStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
        ])
    }
}
PostTableViewDataSource
class PostTableViewDataSource<CELL: UITableViewCell, T>: NSObject, UITableViewDataSource {
    public var cellIdentifier: String
    public var items: Array<T>
    public var configureCell: (CELL, T) -> () = {_,_ in }
    init(cellIdentifier: String, items: Array<T>, configureCell: @escaping  (CELL, T) -> () ) {
        self.cellIdentifier = cellIdentifier
        self.configureCell = configureCell
        self.items = items
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? CELL {
            let item = items[indexPath.row]
            self.configureCell(cell, item)
            return cell
        }
        return UITableViewCell()
    }
}
PostTableViewDelegate
class PostTableViewDelegate: NSObject, UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 300.0
    }
}
VM ( ViewModel ) : Responsible to call service class to fetch data from the server.The ViewModel don't know what the views and what thew view does.
class PostViewModel: NSObject {
    private var postService: PostService?
    private var posts: Array<Post>? {
      didSet {
        self.bindPostToViewController()
      }
    }
    override init() {
        super.init()
        self.postService = PostService()
        self.callGetPosts()
    }
    public var bindPostToViewController: (() -> ()) = {}
    private func callGetPosts() {
        postService?.apiToGetPosts { (posts, error) in
            if error != nil {
                self.posts = posts
            }
        }
    }
}
posts is a property observer, so when we have API response, we will populate posts variable and call bindPostToViewController. Once we have data from view model, we can update the UI. bindPostToViewController will tell us when the response is already.
PostService
class PostService {
    private let postsPath = "https://jsonplaceholder.typicode.com/posts"
    public func apiToGetPosts(completion: @escaping([Post]?, Error?) -> ()) {
        guard let url = URL(string: postsPath) else { return }
        URLSession.shared.dataTask(with: url) { (data, response ,error) in
            if error != nil { completion(nil, error); return }
            guard let dataReceive = data else { return }
            do {
                let posts = try JSONDecoder().decode([Post].self, from: dataReceive)
                completion(posts, nil)
            } catch {
                completion(nil, error)
            }
        }.resume()
    }
}
Thanks for all! 🤘
I hope that i help you. Any question, please, tell me in comments.


    
Top comments (0)