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)