MVVM ํจํด์ ์ฌ์ฉํ์ฌ ์๋ฒ(API)์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ ํ ์ด๋ธ ๋ทฐ์ ํ์ํ๋ ์์
์๋ฒ์์ ์ง์ ๋ฐ์ดํฐ ๋ชฉ๋ก์ ๋ํ ๋ฆฌ์คํฐ์ค(response)๋ฅผ ์ ๊ณตํ์ฌ ์ด ๋ชฉ๋ก์ ํ ์ด๋ธ ๋ทฐ์ ํ์ํ๋ค.
MVVM ๊ตฌ์ฑ ์์์ ๊ฐ์์ ์ญํ
- ๋ทฐ ์ปจํธ๋กค๋ฌ (View Controller): UI ๊ด๋ จ ์์ ๋ง ์ํํ๋ค. ์๋ฅผ ๋ค์ด ์ ๋ณด ํ์ ๋ฐ ๊ฐ์ ธ์ค๊ธฐ ๋ฑ์ ์์ ์ด ์๋ค. ๋ทฐ ์ปจํธ๋กค๋ฌ๋ ๋ทฐ ๋ ์ด์ด์ ์ผ๋ถ์ด๋ค.
- ๋ทฐ๋ชจ๋ธ (View Model): ๋ทฐ ์ปจํธ๋กค๋ฌ๋ก๋ถํฐ ์ ๋ณด๋ฅผ ์์ ํ๊ณ ์ด ๋ชจ๋ ์ ๋ณด๋ฅผ ์ฒ๋ฆฌํ ๋ค ๋ทฐ ์ปจํธ๋กค๋ฌ๋ก ๋ค์ ๋ณด๋ธ๋ค.
- ๋ชจ๋ธ (Model): MVC์์์ ๊ฐ์ ๋ชจ๋ธ์ด๋ค. ๋ทฐ๋ชจ๋ธ์ด ์ฌ์ฉํ๊ณ ๋ทฐ๋ชจ๋ธ์ด ์ ์ ๋ฐ์ดํธ ์ ๋ณด๋ฅผ ๋ณด๋ผ ๋๋ง๋ค ๋ชจ๋ธ๋ ์ ๋ฐ์ดํธํ๋ค.
์์ ๋์ ๋ฐฉ์
- ๋ทฐ ์ปจํธ๋กค๋ฌ๊ฐ ํธ์ถ๋๋ฉด ๋ทฐ๋ ๋ทฐ ๋ชจ๋ธ์ ๋ํ ์ฐธ์กฐ๋ฅผ ๊ฐ๊ฒ ๋๋ค.
- ๋ทฐ๋ ์ฌ์ฉ์์ ์ก์ (user action)์ ์ํํ๊ณ ๋ทฐ๋ชจ๋ธ์ ํธ์ถํ๋ค.
- ๋ทฐ๋ชจ๋ธ์ APIService๋ฅผ ์์ฒญํ๊ณ APIService๋ ๋ทฐ๋ชจ๋ธ์ ์๋ต(response)์ ๋ณด๋ธ๋ค.
- ๋ฆฌ์คํฐ์ค(์๋ต)๋ฅผ ๋ฐ์ผ๋ฉด ๋ทฐ๋ชจ๋ธ์ ๋ฐ์ธ๋ฉ์ ํตํด ๋ทฐ์ ์๋ฆฐ๋ค.
- ๋ทฐ๋ ๋ฐ์ดํฐ๋ก UI๋ฅผ ์ ๋ฐ์ดํธํ๋ค.
Model : Employees, EmployeeData
๋ชจ๋ธ์ ๋จ์ ๋ฐ์ดํฐ๋ฅผ ๋ํ๋ธ๋ค. ๋จ์ํ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ ํ๊ณ ๋น์ฆ๋์ค ๋ก์ง๊ณผ ์๋ฌด ๊ด๋ จ์ด ์๋ค. ์ฐ๋ฆฌ๊ฐ API์์ ๊ธฐ๋ํ๋ ๋จ์ํ ๋ฐ์ดํฐ ๊ตฌ์กฐ์ด๋ค.
API URL์ ๋ํ ๋ฆฌ์คํฐ์ค๋ฅผ ํ์ธํ ๋ค ์ด ๋ฆฌ์คํฐ์ค์ ๋์ํ๋ ๋ชจ๋ธ ํด๋์ค๋ฅผ ์์ฑ
// MARK: - Employee
struct Employees: Decodable {
let status: String
let data: [EmployeeData]
}
// MARK: - EmployeeData
struct EmployeeData: Decodable {
let id, employeeName, employeeSalary, employeeAge: String
let profileImage: String
enum CodingKeys: String, CodingKey {
case id
case employeeName = "employee_name"
case employeeSalary = "employee_salary"
case employeeAge = "employee_age"
case profileImage = "profile_image"
}
}
ViewModel : EmployeesViewModel (์ถ๊ฐ์ )
๋ทฐ ๋ชจ๋ธ์ MVVM์ ์ฃผ์ ๊ตฌ์ฑ ์์์ด๋ค.
๋ทฐ๋ชจ๋ธ์ ๋ทฐ๊ฐ ๋ฌด์์ธ์ง ๋๋ ๋ทฐ๊ฐ ๋ฌด์์ ํ๋์ง ๊ฒฐ์ฝ ์์ง ๋ชปํ๋ค!
์ด๋ฅผ ํตํด ๋์ฑ ํ ์คํธ๊ฐ ๊ฐ๋ฅํ๋๋ก ๋ง๋ค๊ณ ๋ทฐ์์ ๋ณต์ก์ฑ์ ์ ๊ฑฐํ๋ค.
๋ทฐ๋ชจ๋ธ(EmployeesViewModel)์์ APIService ํด๋์ค๋ฅผ ํธ์ถํ์ฌ ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ๋ค.
import Foundation
class EmployeesViewModel : NSObject {
private var apiService : APIService!
/*
๋ฐ์ธ๋ฉ ์ฝ๋ ์ถ๊ฐ ๋ ๊ณณ
*/
override init() {
super.init()
self.apiService = APIService()
callFuncToGetEmpData()
}
func callFuncToGetEmpData() {
self.apiService.apiToGetEmployeeData { (empData) in
self.empData = empData
}
}
}
API Service
APIService ํด๋์ค๋ URLSession ํด๋์ค๋ฅผ ์ฌ์ฉํ์ฌ ์ง์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฐ๋จํ ํด๋์ค์ด๋ค. ์ฌ๊ธฐ์์ ๋ชจ๋ ๋คํธ์ํน ๋ชจ๋ธ์ ์ฌ์ฉํ์ฌ ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๋ค! ๋ทฐ๋ชจ๋ธ ํด๋์ค์์ ์ด APIService ํด๋์ค๋ฅผ ํธ์ถํ๋ค.
import Foundation
class APIService : NSObject {
private let sourcesURL = URL(string: "<http://dummy.restapiexample.com/api/v1/employees>")!
func apiToGetEmployeeData(completion : @escaping (Employees) -> ()){
URLSession.shared.dataTask(with: sourcesURL) { (data, urlResponse, error) in
if let data = data {
let jsonDecoder = JSONDecoder()
let empData = try! jsonDecoder.decode(Employees.self, from: data)
completion(empData)
}
}.resume()
}
}
์ด๋ ๊ฒ ๋ทฐ๋ชจ๋ธ ํด๋์ค์์ API ๋ฆฌ์คํฐ์ค๋ฅผ ๋ฐ์ ์ ์๊ฒ ๋์๋๋ฐ. ์ด์ ๋ทฐ ์ปจํธ๋กค๋ฌ์ ๋ทฐ๋ชจ๋ธ์ ๋ฐ์ธ๋ฉํ ์ฐจ๋ก์ด๋ค.
MVVM Bindings : Model Reference์ didset ์ถ๊ฐ ๋ฐ bindํจ์ ์ถ๊ฐ
MVVM ๋ฐ์ธ๋ฉ์ MVVM์์ ๊ฐ์ฅ ์ค์ํ ์ญํ ์ ํ๋ค.
๋ทฐ๋ชจ๋ธ๊ณผ ๋ทฐ ์ปจํธ๋กค๋ฌ ๊ฐ์ ํต์ ํ๋ ๋ฐฉ๋ฒ์ด ์ค์ํ๋ฐ ์ฌ๋ฌ๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก ๋ฐ์ธ๋ฉ์ ํ ์ ์๋ค.
import Foundation
class EmployeesViewModel : NSObject {
private var apiService : APIService!
//๋ฐ์ธ๋ฉ์ ์ํด ์ถ๊ฐ๋ ์ฝ๋ ์ฌ๊ธฐ์ ๋ถํฐ
private(set) var empData : Employees! {
didSet {
self.bindEmployeeViewModelToController()
}
}
var bindEmployeeViewModelToController : (() -> ()) = {}
// ์ฌ๊ธฐ๊น์ง
override init() {
super.init()
self.apiService = APIService()
callFuncToGetEmpData()
}
func callFuncToGetEmpData() {
self.apiService.apiToGetEmployeeData { (empData) in
self.empData = empData
}
}
}
bindEmployeeViewModelToController๋ผ๋ ์ด๋ฆ์ผ๋ก ๋ทฐ๋ชจ๋ธ ํด๋์ค์ ํ๋กํผํฐ๋ฅผ ์์ฑํ๋ค.
์ด ํ๋กํผํฐ๋ ๋ทฐ ์ปจํธ๋กค๋ฌ์์ ํด๋์ค์์ ํธ์ถํด์ผ ํ๋ค.
APIService์์ ํด๋น ๊ฒฐ๊ณผ๋ฅผ ๊ฒ์ํ๊ณ ๋ทฐ์ ๋ณ๊ฒฝ ์ฌํญ์ด ์์์ ๋ด์ ์ง์(๋ชจ๋ธ) ์ ํ์ empData๋ผ๋ ์ด๋ฆ์ผ๋ก ๋ทฐ๋ชจ๋ธ ํด๋์ค์ ์์ฑํ๋ค.
empData๋ API ์๋น์ค์์ ๋ฐ์ ์๋ต์ด ๋ด๊ธฐ๊ฒ ๋๋ค.
ํ๋กํผํฐ ์ต์ ๋ฒ๋ฅผ ์ด์ฉํ์ฌ API์ ์๋ต์ผ๋ก empData์ ๊ฐ์ ๋ฐ๋ ์ฆ์ empData์ didSet์ ํธ์ถํ๊ณ empData์ didSet ๋ด๋ถ์์ bindEmployeeViewModelToController()๋ฅผ ํธ์ถํ๋ค.
๋ทฐ๋ชจ๋ธ์์ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๋ฉด ์ด์ UI๋ฅผ ์ ๋ฐ์ดํธํ ์ฐจ๋ก์ด๋ค!
View: ViewController
๋ทฐ๋ชจ๋ธ ํด๋์ค์์ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๋ ค๋ฉด ๋ทฐ ์ปจํธ๋กค๋ฌ ํด๋์ค ๋ด๋ถ์ ๋ทฐ๋ชจ๋ธ์ ๊ฐ์ง๊ณ ์์ด์ผ ํ๋ค.
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var employeeTableView: UITableView!
private var employeeViewModel : EmployeesViewModel!
private var dataSource : EmployeeTableViewDataSource<EmployeeTableViewCell,EmployeeData>!
override func viewDidLoad() {
super.viewDidLoad()
callToViewModelForUIUpdate()
}
func callToViewModelForUIUpdate(){
self.employeeViewModel = EmployeesViewModel()
self.employeeViewModel.bindEmployeeViewModelToController = {
self.updateDataSource()
}
}
func updateDataSource(){
self.dataSource = EmployeeTableViewDataSource(cellIdentifier: "EmployeeTableViewCell", items: self.employeeViewModel.empData.data, configureCell: { (cell, evm) in
cell.employeeIdLabel.text = evm.id
cell.employeeNameLabel.text = evm.employeeName
})
DispatchQueue.main.async {
self.employeeTableView.dataSource = self.dataSource
self.employeeTableView.reloadData()
}
}
}
DataSource : EmployeeTableViewDataSource
UI๋ฅผ ์ ๋ฐ์ดํธํ๊ธฐ ์ํด ๋ทฐ ์ปจํธ๋กค๋ฌ์ ํ ์ด๋ธ ๋ทฐ ์ฝ๋๋ฅผ ์์ฑํ ์๋ ์์ง๋ง ๋ทฐ ์ปจํธ๋กค๋ฌ๊ฐ ๋ ๊น๋ํ๊ฒ ๋ง๋ค๊ธฐ ์ํด UITableViewDataSource ๋ฅผ ๊ตฌํํ EmployeeTableViewDataSource ๊ฐ ๋ง๋ค์ด์ ธ ์๋ค.
import Foundation
import UIKit
class EmployeeTableViewDataSource<CELL : UITableViewCell,T> : NSObject, UITableViewDataSource {
private var cellIdentifier : String!
private var items : [T]!
var configureCell : (CELL, T) -> () = {_,_ in }
init(cellIdentifier : String, items : [T], configureCell : @escaping (CELL, T) -> ()) {
self.cellIdentifier = cellIdentifier
self.items = items
self.configureCell = configureCell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! CELL
let item = self.items[indexPath.row]
self.configureCell(cell, item)
return cell
}
}
EmployeeTableViewCell
import UIKit
class EmployeeTableViewCell: UITableViewCell {
@IBOutlet weak var employeeIdLabel: UILabel!
@IBOutlet weak var employeeNameLabel: UILabel!
var employee : EmployeeData? {
didSet {
employeeIdLabel.text = employee?.id
employeeNameLabel.text = employee?.employeeName
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Reference
'iOS' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[iOS] Xcode์ Target ์ด๋? (0) | 2023.02.13 |
---|---|
[iOS] iOS Concurrency - 1. GCD/Operation, ๋๊ธฐ/๋น๋๊ธฐ, ์ง๋ ฌ/๋์ (0) | 2023.02.11 |
[iOS] UIButton.Configuration ์ฌ์ฉํด๋ณด๊ธฐ (0) | 2023.02.07 |
[iOS] ์ ํ๋ฆฐ์์ Xcode ์ปฌ๋ฌ์์ ๋ฐ๋ก ์ถ์ถ (0) | 2023.02.05 |
[iOS] ์คํฌ๋กค ์ ํ์์ ๋ํ๋๊ฒ ํ๊ธฐ (0) | 2023.02.02 |