هر زمان که میخواهیم شروع به توسعه یک اپلیکیشن جدید نمائیم، این سوال به ذهنمان میرسد که بهتر است از کدام الگوی معماری (architecture pattern) برای پروژه جدیدمان استفاده کنیم. بیشترین الگوی معماری استفاده شده در iOS معماری MVC است. بیشتر توسعهدهندگان از الگوی MVC برای پروژهای خود استفاده میکنند. پروژههای کوچک به خوبی با MVC کار میکنند، اما زمانی که اندازه پروژه شما به تدریج بزرگ شود، کدهای شما نیز کثیف و شلوغ خواهد شد.
همیشه استفاده از الگوی معماری، مناسب به نظر میرسد، اما ما نباید همیشه اکیداً از یک الگوی معماری در پروژه خود استفاده کنیم. تمام الگوهای معماری به اندازه کافی برای این که به شما همه امکانات را بدهند، مناسب نیستند، هرکدام مزایا و معایبی دارند. اگر ما ماژولهای زیادی در پروژه خود داشته باشیم، میتوانیم بر اساس ماژولها نیز الگوی معماری خود را انتخاب کنیم. برخی ماژولها با MVVM سازگار هستند، اما ممکن است ماژول جدیدی که شما میخواهید به پروژه اضافه کنید، با MVVM سازگار نباشد، پس بهتر است که از الگوهای معماری دیگری مانند MVP و VIPER استفاده کنید. پس نتیجه میگیرم که ما الزاماً نباید به یک الگوی معماری تکیه کنیم و به جای آن میتوانیم بر اساس ماژولها تصمیم بگیریم.
مقالات زیادی در اینترنت وجود دارد که مزایا و معایب الگوی MVVM را بررسی کرده و شرح دادهاند. اما در این مقاله قصد داریم به اجرای عملیاتی این الگو بپردازیم و تنها به خواندن مفاهیمی از آن بسنده نکنیم.
بیایید شروع کنیم
در این پروژه ما یک اپلیکیشن ساده با الگوی طراحی MVVM خواهیم ساخت. در بیشتر اپلیکیشنها ما یک (ViewController UI) داریم که نیاز دارد دیتا را از server API واکشی کند و در UI نمایش دهد. ما این رفتار را با استفاده از الگوی MVVM پیاده سازی میکنیم.
تصویر خروجی نهایی مورد نظر در انتهای مقاله به این صورت است.
در اینجا ما از یک وب سرویس ساختگی (dummy) استفاده میکنیم که روی اینترنت در دسترس است.
http://dummy.restapiexample.com/api/v1/employees
این وب سرویس در پاسخ به ما یک لیست از دیتای کارمندان را برمیگرداند که ما این لیست را در table view نمایش میدهیم.
بیایید ساختار کد خود را پیاده سازی کنیم و فایلهای مورد نیاز را در گروههای مرتبط با هر کدام بسازیم. ما سه فایل جدید در هر گروه ایجاد کردهایم. (Models, ViewModels, API Service)
مدل
مدل نمایش دهنده دیتای ساده است. به سادگی دیتا را نگاه میدارد و کاری به هیچ یک از منطقهای تجاری (business logics) ندارد. میتوان گفت که مدل یک ساختار ساده از دیتایی است که ما از 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" } }
جریان اپلیکیشن به این صورت خواهد بود:
الف. View controller صدا زده میشود و view یک رفرنس به ViewModel خواهد داشت.
ب. View تعدادی از اکشنهای کاربرای را دریافت خواهد کرد و ViewModel را صدا خواهد زد.
ج. ViewModel درخواست APIService را فراخوانی میکند و APIService پاسخ را به ViewModel باز میگرداند.
د. زمانی که پاسخ دریافت شود، ViewModel از طریق binding با خبر میشود.
ه. View با دیتا UI را به روزرسانی خواهد کرد.
خب حالا نوبت کد نویسی به صورت مرحلهای است. در ابتدا ViewController باید صدا زده شود و از ViewController کلاس ViewModel را صدا میزنیم. در حال حاضر binding بین این دو را نمینویسیم و بعداً به سراغ آن خواهیم رفت.
کلاس ViewModel
کلاس ViewModel جز اصلی این الگو معماری است. ViewModel هرگز متوجه این که view چیست و چه کاری انجام میدهد، نخواهد شد. این امر موجب میشود تا این معماری قابلیت تست بیشتری داشته باشد و پیچیدگیها را از view حذف میکند.
در ViewModel ما APIService را برای واکشی دیتا از سرور صدا خواهیم زد.
زمانی که این کد را در کلاس ViewModel می نویسید، به شما خطا خواهد که کلاس APIService را نساخته اید. پس بیاید تا حالا کلاس APIService را پیاده سازی کنیم.
کلاس APIService
کلاس APIService یک کلاس ساده است که در آن دادهی کارمندان را با استفاده از کلاس URLSession واکشی مینماییم. شما میتوانید از مدل شبکهای برای واکشی داده از server خود استفاده کنید. از کلاس ViewModel که ساخته بودیم کلاس APIService را صدا میزنیم.
iimport 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 را در کلاس ViewModelدریافت کنیم، وقت آن رسیده که اتصالات بین ViewController و ViewModel را انجام دهیم.
MVVM اتصالات
اتصالات MVVM نقش اساسی در پروژه ما ایفا میکنند. این که چگونه ما بین ViewController و ViewModel ارتباط برقرار میکنیم از اهمیت زیادی برخوردار است. ما میتوانیم اتصالات را به روشهای مختلفی پیاده سازی کنیم.
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 } } }
ما میتوایم یک متغیر در کلاس ViewModel به این صورت ایجاد کنیم:
var bindEmployeeViewModelToController : (() -> ()) = { }
این متغییر باید از کلاس ViewController صدا زده شود.
ما یک متغییر دیگر با نام empData از نوع مدل کارمندان میسازیم که نتیجه دریافتی APIService را گرفته و View را باخبر میکند که تغییری رخ داده است.
private(set) var empData : Employees! {didSet{self.bindEmployeeViewModelToController()}}
متغیر empData به گونهای تنظیم شده تا دریافت از APIService را پاسخ دهد. با استفاده از observer میتوان به محض دریافت مقدار در empData به عنوان پاسخ API، didSet متغیر empData صدا زده میشود و پس از آن bindEmployeeViewModelToController در داخل didSet صدا زده میشود.
هر زمان که ما دیتا را از ViewModel به View رساندیم، نوبت به روزرسانی UI است.
بخش View
برای دریافت دیتا از ViewModel ما باید ViewModel را در کلاس ViewController لینک کنیم.
self.employeeViewModel.bindEmployeeViewModelToController = {self.updateDataSource()}
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() } } }
به منظور به روز رسانی UI شما میتوانید کد یک TableView را در ViewController خود نیز بنویسید، اما برای این که ViewController را کمتر کثیف یا ماژولار کنیم در این قسمت یک کلاس مجزا به نام EmployeeTableViewDataSource ایجاد کردیم که از UITableViewDataSource اکستند شده است.
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 } }
همانطور که قبلا نیز اشاره شده هر الگوی معماری مزایا و معایب خود را دارد، اگر با استفاده از الگوی MVVM فواید زیادی نصیب ما شود، معایبی نیز برای آن وجود دارند.
در انتهای این مقاله ما موفق به ساخت یک اپلیکیشن ساده با استفاده از الگوی MVVM شدیم. امیدوارم برایتان مفید واقع شود.
با سپاس
برای دسترسی به Source Code این اپلیکیشن به این لینک مراجعه شود.
وبسایت Medium