๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐ŸŽ iOS

[Swift] MVVM ๊ฐ„๋‹จํ•˜๊ฒŒ ์•Œ์•„๋ณด๊ธฐ

by ํ‹ด๋”” 2022. 6. 4.
728x90
๋ฐ˜์‘ํ˜•

https://www.pexels.com/ko-kr/photo/1786512/

 

์ •์˜

์˜ค๋ธŒ์ ํŠธ๋ฅผ Model, View, ViewModel๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๋””์ž์ธ ํŒจํ„ด

 

Model

  • ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์ •์˜
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Œ
  • structs ํ˜น์€ ์•„์ฃผ ๊ฐ„๋‹จํ•œ class๋กœ ๊ตฌํ˜„

View

  • ์‚ฌ์šฉ์ž์™€ ์ƒํ˜ธ ์ž‘์šฉํ•˜๊ณ  ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•  ๊ฒฝ์šฐ ViewModel์—๊ฒŒ ์•Œ๋ ค์คŒ
  • UI๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ๋ฅผ ํฌํ•จํ•˜๊ณ  ์ด ์™ธ์— ViewModel์— ์žˆ๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ํ˜ธ์ถœํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ๋“ค์–ด ์žˆ์Œ

ViewModel

  • view๋กœ ๋ถ€ํ„ฐ ์‚ฌ์šฉ์ž์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ๋ฐ›๊ณ  ์ด์— ๋งž๋Š” ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋จ
  • UIKit ๋ถˆํ•„์š”
  • ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๊ณ  ๋ทฐ ์š”์†Œ๋ฅผ ์—…๋ฐ์ดํŠธํ•จ
  • model ์ •๋ณด view์— fetch ํ•˜๊ธฐ ์œ„ํ•ด ๊ทธ ๊ฐ’์„ ๋ณ€๊ฒฝ (๋ทฐ์— ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ํ•œ๋‹ค๋Š” ๋œป)

 

์žฅ์  

  • ๊ธฐ์กด MVC์—์„œ ๋งŽ์€ ์—ญํ™œ์„ ๋‹ด๋‹นํ•˜๋˜ ViewController์˜ ์—ญํ™œ์„ ViewModel์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ

๋‹จ์ 

  • ์„ค๊ณ„ ์–ด๋ ค์›€ (์ „์ฒด๋‹ค MVVM์„ ๊ฐ€์งˆ ํ•„์š”๋Š” ์—†์Œ. ํ•„์š”ํ•œ ๊ณณ์—์„œ MVVM์œผ๋กœ ๊ตฌํ˜„)
  • ๋ทฐ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ ๋ณต์žกํ•  ์ˆ˜๋ก ViewModel์˜ ํฌ๊ธฐ๊ฐ€ ์ปค์ง

 

 

tutorial

์ „์ฒด ์ฝ”๋“œ ๋งํฌ ๐Ÿ‘‰๐Ÿป  ๊นƒํ—ˆ๋ธŒ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Model ์ƒ์„ฑ

์‚ฌ์šฉํ•œ api : https://jsonplaceholder.typicode.com/posts

  • userId, id, title, body ๊ฐ€ ํ•˜๋‚˜์˜ post, post๋ฅผ ์–ด๋ ˆ์ด๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” api
import Foundation

typealias Posts = [Post]
struct Post: Codable {
    let userID, id: Int
    let title, body: String

    enum CodingKeys: String, CodingKey {
        case userID = "userId"
        case id, title, body
    }
}
  • Model ํด๋”์— Post.swift ํŒŒ์ผ์„ ์ƒ์„ฑํ•จ.
  • json ๊ตฌ์กฐ๋ฅผ Codable๋กœ ์ •์˜
  • ํŒ: https://app.quicktype.io/ json ์„ ์—ฌ๊ธฐ์— ๋ณต๋ถ™ํ•˜๋ฉด ๋ฉ‹์ง„ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค! ๋‹ค๋งŒ ๋ณ€์ˆ˜๋ช…, ํด๋ž˜์Šค๋ช… ์ด๋Ÿฐ๊ฑฐ ๋ณ€๊ฒฝํ•ด์•ผ ํ•  ์ˆ˜๋„ ์žˆ๊ณ  ์ˆ˜์ •์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋„ ์žˆ์œผ๋‹ˆ ํ…Œ์ŠคํŠธ ํ•  ๋•Œ๋งŒ ์ฐธ๊ณ ...

Service ์ƒ์„ฑ

import Foundation

class PostNetworkManager {
    static let shared = PostNetworkManager()
    private init() { }
    
    func getPosts(completion: @escaping (Bool, Posts?) -> ()) {
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
            return
        }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            guard error == nil else {
                completion(false, nil)
                print("error ::: \(error!)")
                return
            }
            
            guard let response = response as? HTTPURLResponse, (200 ..< 300) ~= response.statusCode else {
                completion(false, nil)
                return
            }
            
            do {
                let model = try JSONDecoder().decode(Posts.self, from: data!)
                completion(true, model)
            } catch {
                completion(false, nil)
            }
        }.resume()
    }
}
  • Service ํด๋”์— PostNetworkManager.swift ํŒŒ์ผ ์ƒ์„ฑ
  • ๋„คํŠธ์›Œํ‚น ์‹ฑ๊ธ€ํ†ค ์ƒ์„ฑ
  • Post ๋ชจ๋ธ๋กœ decode ํ•ด์„œ [Post] ๋ฐฐ์—ด ๋ฐ˜ํ™˜ (์ฝ”๋“œ์—์„œ๋Š” Posts)

ViewModel ์ƒ์„ฑ

import Foundation

class PostViewModel {
    var tableReload: (() -> Void)?
    var posts: Posts? = [Post]() {
        // ํ…Œ์ด๋ธ” ๋ทฐ ๋ฆฌ๋กœ๋“œ ์ฒ˜๋ฆฌ
        didSet {
            tableReload?()
        }
    }
    
    func fetchData() {
        PostNetworkManager.shared.getPosts { success, data in
            if success {
                self.posts = data
            } else {
                // ์—๋Ÿฌ ์ฒ˜๋ฆฌ
            }
            
        }
    }
}
  • ViewModel์€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง, ๋ฐ์ดํ„ฐ request ๋ฐ ๊ฐ€๊ณต ๋“ฑ์„ ์ฒ˜๋ฆฌํ•จ. UIKit ์ž„ํฌํŠธ ๋ถˆํ•„์š”
  • ViewController์—๋Š” UI ๊ด€๋ จ๋œ ์ฝ”๋“œ๋ฅผ ํฌํ•จํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋‚˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ViewModel์— ํฌํ•จ์‹œํ‚ด
  • UITableViewCell์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๊ณตํ•˜๋Š” ViewModel์„ ์ƒ์„ฑํ•˜๊ธฐ๋„ ํ•จ
  • ๋ฐ์ดํ„ฐ์˜ ๊ฐ€๊ณต๋„ ViewModel์—์„œ ์ฒ˜๋ฆฌ

View ์ƒ์„ฑ

import UIKit

class PostViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    let viewModel = PostViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        tableView.backgroundColor = .gray
        tableView.delegate = self
        tableView.dataSource = self
        
        tableView.register(PostTableViewCell.nib, forCellReuseIdentifier: PostTableViewCell.identifier)
        
        viewModel.tableReload = {
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        }
        viewModel.fetchData()
    }
}

extension PostViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.posts?.count ?? 0
    }
}

extension PostViewController: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableView.automaticDimension
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: PostTableViewCell.identifier, for: indexPath) as? PostTableViewCell else {
            return UITableViewCell()
        }
        if let data = viewModel.posts?[indexPath.row] {
            cell.data = data
        }
        
        return cell
    }
}

 

 

import UIKit

class PostTableViewCell: UITableViewCell {
    
    @IBOutlet weak var userLabel: UILabel!
    @IBOutlet weak var idLabel: UILabel!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var bodyLabel: UILabel!
    
    var data: Post? {
        didSet {
            userLabel.text = "\(data?.userID ?? 0)"
            idLabel.text = "\(data?.id ?? 0)"
            titleLabel.text = data?.title
            bodyLabel.text = data?.body
        }
    }
    
    static var identifier: String {
        return String(describing: self)
    }
    
    static var nib: UINib {
        return UINib(nibName: identifier, bundle: nil)
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }
}

 

 

์ฐธ๊ณ  ๋„์„œ ๋ฐ ์›นํŽ˜์ด์ง€

์ฝ”๋“œ๋Š” ์•„๋ž˜ ์ถœ์ฒ˜์—์„œ ๊ฐ€์ ธ์˜ค์ง€ ์•Š์•„์„œ ์ฝ”๋“œ ์˜ฎ๊ธฐ์‹ค๋•Œ ์ถœ์ฒ˜ ํ‘œ๊ธฐ ๋ถ€ํƒ๋“œ๋ ค์š”!

 

How to implement MVVM pattern with Swift in iOS | John Codeos - Blog with Free iOS & Android Development Tutorials

I'm showing you how to implement the MVVM pattern and organize your iOS project with folders.

johncodeos.com

 

728x90
๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€