DEV Community

MKilmer
MKilmer

Posted on • Updated on

MVVM Design Pattern in iOS

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!!!

Alt Text


MVVM Flow

  1. ViewController / View will have a reference to the ViewModel
  2. ViewController / View get some user action and will call ViewModel
  3. ViewModel request some API Service and API Service will sends a response to ViewModel
  4. ViewModel will notifies the ViewController / View with binding
  5. The ViewController / View will update the UI with data

1_8MiNUZRqM1XDtjtifxTSqA


Project Informations

  1. Use jsonplaceholder REST API
  2. URLSession to fetch data
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

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()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
        ])
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

PostTableViewDelegate

class PostTableViewDelegate: NSObject, UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 300.0
    }
}
Enter fullscreen mode Exit fullscreen mode

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
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

Thanks for all! 🤘

I hope that i help you. Any question, please, tell me in comments.

Github Project

Discussion (0)