Tham gia ngay khoá học lập trình iOS, hình thức học tập rất linh hoạt cho bạn lựa chọn và sẽ có mức học phí khác nhau tuỳ theo bạn chọn học Online, Offline hoặc FlipLearning(Kết hợp giữa Online và Offline). Ngoài ra bạn có thể tham gia thực tập toàn thời gian tại Techmaster để rút ngắn thời gian học và tăng cơ hội việc làm.
Có thể bạn cũng đã biết về mô hình MVC(Model-View-Controller) nhưng trong iOS có thể nó sẽ thành Massive-View-Controller vì nếu bạn đã làm một dự án lớn thì có thể các view controllers có thể sẽ hơn 1000 dòng code, đấy chính là vấn đề tại sao nó biến các view controllers ở lên hỗ độn và khi đọc rất khó chịu.
Dưới đấy là cấu trúc theo lý thuyết MVC:
Nhưng trong thực tế đôi khi nó sẽ khác:
Như các bạn thấy thành phần Controllers sẽ phình to ra khi dự án phát triển.
Trong bài này hôm nay chúng ta sẽ tìm hiểu về MVVM(Model-View-ViewModel). Cấu trúc nó sẽ như sau:
Bây giờ thì thành phần Controller có nhiện vụ hiển thị những gì mà View Model muốn hiển thị và nó không quan tâm và cũng không biết về Model. Các thành phần như UILabel, UITextField,… nó chỉ hiển thị dữ liệu mà ViewModel cung cấp.
Vậy lợi ích ở đây là gì?
Ví dụ: Controller sẽ không phình to nữa. Nếu muốn chỉnh sửa lại Model, chúng ta đơn giản chỉ cần làm ở View Model. Ví dụ khi thay đổi và hiển thị các định dạng khác nhau với NSDate. Thường thì chúng ta sẽ chỉnh sửa nó ngay trong ViewController, có nghĩa là ViewController sẽ phình to ra. Thay vì thế chúng ta sẽ xử lý định dạng để hiển thị ngày tháng ở trong ViewModel nó sẽ chỉ rõ ra class/file nào chịu trách nhiệm cho việc chuyển đổi từ NSData sang String.
Lợi ích nữa khi sử dụng MVVM là ứng dụng sẽ dễ kiểm thử hơn, bạn có thể dễ dàng viết các tests cho các ViewModels vì các thành phần liên kết dưới loose coupling.
Model, ViewModel, Unit Test
Tôi sẽ tạo class Car
class Car {
var model: String
var make: String
var kilowatts: Int
var photoURL: String
init(model: String, make: String, kilowatts: Int, photoURL: String) {
self.model = model
self.make = make
self.kilowatts = kilowatts
self.photoURL = photoURL
}
}
Và tiếp theo là ViewModel tôi đặt tên là CarViewModel:
class CarViewModel {
private var car: Car
static let horsepowerPerKilowatt = 1.34102209
var modelText: String {
return car.model
}
var makeText: String {
return car.make
}
var horsepowerText: String {
let horsepower = Int(round(Double(car.kilowatts) * CarViewModel.horsepowerPerKilowatt))
return "\(horsepower) HP"
}
var titleText: String {
return "\(car.make) \(car.model)"
}
var photoURL: NSURL? {
return NSURL(string: car.photoURL)
}
init(car: Car) {
self.car = car
}
}
Tiếp theo tôi sẽ viết TDD test(test driven development)
Chúng ta sẽ khởi tạo đối tượng Ferrari F12 như sau:
func testCarViewModelWithFerrariF12() {
let ferrariF12 = Car(model: "F12", make: "Ferrari", horsepower: 730, photoURL: "http://auto.ferrari.com/en_EN/media/wp-content/uploads/sites/5/2013/07/Ferrari-F12berlinetta.jpg")
let ferrariViewModel = CarViewModel(car: ferrariF12)
XCTAssertEqual(ferrariViewModel.modelText, "F12")
XCTAssertEqual(ferrariViewModel.makeText, "Ferrari")
XCTAssertEqual(ferrariViewModel.horsepowerText, "730 HP")
XCTAssertEqual(ferrariViewModel.photoURL, NSURL(string: ferrariF12.photoURL!))
XCTAssertEqual(ferrariViewModel.titleText, "Ferrari F12")
}
Bạn có thể ấn command+U để chạy. Bây giờ chúng ta có thể thấy lợi ích của việc sử dụng MVVM. Class Car khá đơn giản, nhưng CarViewModel lại thêm một số các phần getter của các thuộc tính. Ví dụ với biến titleText, nó liên kết với thuộc tính của Car tương tự với horsepowerText,… Bây giờ bạn có thể ấn command+U để chạy test này.
User interface(UITableView)
Để giải thích chi tiết hơn về MVVM, chúng ta sẽ tạo một mảng cars và hiển thị nó lên UITableView. Đầu tiên tôi sẽ tạo một class mới có tên TableViewController mà nó là subclass của UITableViewController
Tôi xoá ViewController hiện tại ở trong Main.storyboard vào kéo một TableViewController vào
Sau đó tôi set định danh cho cell là CarCell và chỉnh style thành Right Detail(ở mỗi cell chúng ta sẽ hiển thị photo, title, .. của Car)
Bây giờ chúng ta sẽ viết các đoạn mã cho class TableViewController. Nếu chúng ta đang làm theo mô hình MVVM thì ở thành phần hiển thị là các Views sẽ không cần biết về model Car. Trong ứng dụng thực thế thì ứng dụng sẽ lấy dữ liệu từ bên ngoài app nhưng trong ví dụ này chúng ta sẽ tạo dữ liệu cứng và để ở AppDelegate.
let cars: [CarViewModel] = {
let ferrariF12 = Car(model: "F12", make: "Ferrari", horsepower: 730, photoURL: "http://auto.ferrari.com/en_EN/media/wp-content/uploads/sites/5/2013/07/Ferrari-F12berlinetta.jpg")
let zondaF = Car(model: "Zonda F", make: "Pagani", horsepower: 602, photoURL: "http://storage.pagani.com/view/1024/BIG_zg-4-def.jpg")
let lamboAventador = Car(model: "Aventador", make: "Lamborghini", horsepower: 700, photoURL: "http://cdn.lamborghini.com/content/models/aventador_lp700-4_roadster/gallery_2013/roadster_21.jpg")
return [CarViewModel(car: ferrariF12), CarViewModel(car: zondaF), CarViewModel(car: lamboAventador)]
}()
Và ở TableViewController tôi sẽ lấy dữ liệu từ Appdelegate:
let cars: [CarViewModel] = (UIApplication.sharedApplication().delegate as! AppDelegate).cars
Chúng ta tiếp tục khai báo 2 hàm sau:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cars.count
}
Tiếp theo đến phần hiển thị dữ liệu:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CarCell", forIndexPath: indexPath)
let carViewModel = cars[indexPath.row]
cell.textLabel?.text = carViewModel.titleText
cell.detailTextLabel?.text = carViewModel.horsepowerText
loadImage(cell, photoURL: carViewModel.photoURL)
return cell
}
Các bạn có thể thấy có function loadImage, dĩ nhiên chúng ta sẽ không muốn block main thread nên lúc tải ảnh tôi sẽ chon nó chạy ở background và khi tải xong thì hiển thị ở main thread.
func loadImage(cell: UITableViewCell, photoURL: NSURL?) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
guard let imageURL = photoURL, imageData = NSData(contentsOfURL: imageURL) else {
return
}
dispatch_async(dispatch_get_main_queue()) {
cell.imageView?.image = UIImage(data: imageData)
cell.setNeedsLayout()
}
}
}
Từ iOS 9 trở đi khi truy vấn đến một URL thì cần khai báo như sau ở plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Bây giờ khi chạy ứng dụng thì nó sẽ hiển thị dữ liệu như sau:
Dưới đây tôi sẽ viết một đoạn mã đơn giản về UI test:
class MVVMUITests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
XCUIApplication().launch()
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testFerrariF12DataDisplayed() {
let app = XCUIApplication()
let table = app.tables.elementBoundByIndex(0)
let ferrariCell = table.cells.elementBoundByIndex(0)
XCTAssert(ferrariCell.staticTexts["Ferrari F12"].exists)
XCTAssert(ferrariCell.staticTexts["730 HP"].exists)
let zondaCell = table.cells.elementBoundByIndex(1)
XCTAssert(zondaCell.staticTexts["Pagani Zonda F"].exists)
XCTAssert(zondaCell.staticTexts["602 HP"].exists)
let lamboCell = table.cells.elementBoundByIndex(2)
XCTAssert(lamboCell.staticTexts["Lamborghini Aventador"].exists)
XCTAssert(lamboCell.staticTexts["700 HP"].exists)
}
}
Khi dữ liệu thay đổi
Giả sử Car model cần thay đổi, dữ liệu hay đổi khi kilowatts của Car thay đổi, vậy chúng ta sẽ chỉ cần kiểm tra xem kilowatts có giá trị hay không trước khi tính toán
class Car {
var model: String?
var make: String?
var kilowatts: Int?
var photoURL: String?
init(model: String, make: String, kilowatts: Int, photoURL: String) {
self.model = model
self.make = make
self.kilowatts = kilowatts
self.photoURL = photoURL
}
}
Và thay đổi CarViewModel như sau:
// class CarViewModel {
// ...
static let horsepowerPerKilowatt = 1.34102209
var horsepowerText: String? {
guard let kilowatts = car?.kilowatts else {
return nil
}
let horsepower = Int(round(Double(kilowatts) * CarViewModel.horsepowerPerKilowatt))
return "\(horsepower) HP"
}
// ...
// }
Dĩ nhiên ở AppDelegate chúng ta cũng cần thay đổi phần khởi tạo dữ liệu:
// AppDelegate.swift
let cars: [CarViewModel] = {
let ferrariF12 = Car(model: "F12", make: "Ferrari", kilowatts: 544, photoURL: "http://auto.ferrari.com/en_EN/media/wp-content/uploads/sites/5/2013/07/Ferrari-F12berlinetta.jpg")
let zondaF = Car(model: "Zonda F", make: "Pagani", kilowatts: 449, photoURL: "http://storage.pagani.com/view/1024/BIG_zg-4-def.jpg")
let lamboAventador = Car(model: "Aventador", make: "Lamborghini", kilowatts: 522, photoURL: "http://cdn.lamborghini.com/content/models/aventador_lp700-4_roadster/gallery_2013/roadster_21.jpg")
return [CarViewModel(car: ferrariF12), CarViewModel(car: zondaF), CarViewModel(car: lamboAventador)]
}()
// MVVMTests.swift
let ferrariF12 = Car(model: "F12", make: "Ferrari", kilowatts: 544, photoURL: "http://auto.ferrari.com/en_EN/media/wp-content/uploads/sites/5/2013/07/Ferrari-F12berlinetta.jpg")
Bây giờ các bạn có thể chạy ứng dụng mà không gặp lỗi gì. Như các bạn biết thì hầu hết các ứng dụng sẽ có phần tương tác người dung, nó có nghĩa là Controller sẽ chịu trách nghiệm việc cập nhật View Model và nó sẽ tự động cập nhật Model của chính ViewModel đó, chúng ta sẽ sử dụng framework RxSwift để thực hiện việc bindling dữ liệu ở phần tiếp theo.
Nguồn bài viết: Link
Bình luận