MVVM Design Pattern in iOS

MVVM ( Model-ViewModel- Model )

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 b…


This content originally appeared on DEV Community and was authored by MKilmer

MVVM ( Model-ViewModel- Model )

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
}

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.

Github Project


This content originally appeared on DEV Community and was authored by MKilmer


Print Share Comment Cite Upload Translate Updates
APA

MKilmer | Sciencx (2021-11-12T17:10:42+00:00) MVVM Design Pattern in iOS. Retrieved from https://www.scien.cx/2021/11/12/mvvm-design-pattern-in-ios/

MLA
" » MVVM Design Pattern in iOS." MKilmer | Sciencx - Friday November 12, 2021, https://www.scien.cx/2021/11/12/mvvm-design-pattern-in-ios/
HARVARD
MKilmer | Sciencx Friday November 12, 2021 » MVVM Design Pattern in iOS., viewed ,<https://www.scien.cx/2021/11/12/mvvm-design-pattern-in-ios/>
VANCOUVER
MKilmer | Sciencx - » MVVM Design Pattern in iOS. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/11/12/mvvm-design-pattern-in-ios/
CHICAGO
" » MVVM Design Pattern in iOS." MKilmer | Sciencx - Accessed . https://www.scien.cx/2021/11/12/mvvm-design-pattern-in-ios/
IEEE
" » MVVM Design Pattern in iOS." MKilmer | Sciencx [Online]. Available: https://www.scien.cx/2021/11/12/mvvm-design-pattern-in-ios/. [Accessed: ]
rf:citation
» MVVM Design Pattern in iOS | MKilmer | Sciencx | https://www.scien.cx/2021/11/12/mvvm-design-pattern-in-ios/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.