SỬ DỤNG CLOSURE, PROTOCOL LÀM CODE GỌN GÀNG DỄ HIỂU HƠN.
Bài toán đặt ra Ta có 1 ứng dụng lấy dữ liệu song từ API theo các artist. Việc lấy dữ liệu thực hiện tuần tự theo các bước: lấy song artist 1 -> update UI -> lấy song artist 2 -> update UI. Ta có đoạn code như sau: func getDataSong ( ) { statusLabel . text = ...
Bài toán đặt ra
- Ta có 1 ứng dụng lấy dữ liệu song từ API theo các artist.
- Việc lấy dữ liệu thực hiện tuần tự theo các bước: lấy song artist 1 -> update UI -> lấy song artist 2 -> update UI.
- Ta có đoạn code như sau:
func getDataSong() { statusLabel.text = "Download Taylor Swift Song ..." AppServices.getSongByArtist("TaylorSwift", failureHandler: { (reason, errorMessage) in print(errorMessage) }, completion: {[weak self] songs in if let songs = songs { self?.taylorSwiftSongs.removeAll() self?.taylorSwiftSongs.appendContentsOf(songs) dispatch_async(dispatch_get_main_queue(), { self?.statusLabel.text = "Download Westlife Song ..." self?.statusProgressView.progress = 0.5 self?.tableView.reloadData() AppServices.getSongByArtist("Westlife", failureHandler: { (reason, errorMessage) in print(errorMessage) }, completion: { songs in if let songs = songs { self?.westlifeSongs.removeAll() self?.westlifeSongs.appendContentsOf(songs) dispatch_async(dispatch_get_main_queue(), { self?.statusLabel.text = "Done!" self?.statusProgressView.progress = 0.0 self?.tableView.reloadData() }) } }) }) } }) }
Sử dụng closure tối ưu lần 1
Ta định nghĩa 1 closure Action:
typealias Action = () -> Void
Từ đó ta tách phần get data song ra làm 2 hàm
func getTaylorSwiftSong(completion: Action) { AppServices.getSongByArtist("TaylorSwift", failureHandler: { (reason, errorMessage) in print(errorMessage) }, completion: {[weak self] songs in if let songs = songs { self?.taylorSwiftSongs.removeAll() self?.taylorSwiftSongs.appendContentsOf(songs) completion() } }) }
Và
func getWestlifeSong(complete: Action) { AppServices.getSongByArtist("Westlife", failureHandler: { (reason, errorMessage) in print(errorMessage) }, completion: {[weak self] songs in if let songs = songs { self?.westlifeSongs.removeAll() self?.westlifeSongs.appendContentsOf(songs) complete() } }) }
Ta cũng tách phần update UI ra thành 1 hàm riêng
func updateStatus(text: String, progress: Float = 0.0, reload: Bool = true) { self.statusLabel.text = text self.statusProgressView.progress = progress if reload { self.tableView.reloadData() } }
Giờ đây hàm getDataSong của chúng ta đã trở nên gọn hơn nhiều so với trước:
func getDataSong() { updateStatus("Download Taylor Swift Song ...") getTaylorSwiftSong { dispatch_async(dispatch_get_main_queue(), { self.updateStatus("Download Westlife Song ...") self.getWestlifeSong { dispatch_async(dispatch_get_main_queue(), { self.updateStatus("Done!") }) } }) } }
Giả sử chúng ta muốn get song từ nhiều artist nữa thì sao, sẽ có rất nhiều hàm lồng vào nhau, code nhìn sẽ rất rối.
Sử dụng closure tối ưu lần 2
Ta định nghĩa thêm 1 closure AsyncTask
typealias AsyncTask = (Action) -> Void
Ta tạo thêm 3 function:
func |>(lhs: AsyncTask, rhs: Action) -> AsyncTask { return { (action) -> Void in lhs { rhs() ; action() } } } func |>(lhs: AsyncTask, rhs: Action) -> Action { return { lhs { rhs() } } } func |>(lhs: AsyncTask, rhs: AsyncTask) -> AsyncTask { return { (action) -> Void in lhs { rhs(action) } } }
Chuyển hàm updateStatus từ (String, Float, Bool) -> Void thành (String, Float, Bool) -> Action
func updateStatus(text: String, progress: Float = 0.0, reload: Bool = true) -> Action{ return { self.statusLabel.text = text self.statusProgressView.progress = progress if reload { self.tableView.reloadData() } } }
Sử dụng closure chuyển hàm dispatch_async(dispatch_get_main_queue() thành 1 property
let switchToMainThread: AsyncTask = { (action) -> Void in dispatch_async(dispatch_get_main_queue(), action) }
Hàm getDataSong được viết lại như sau:
func getDataSong() { let syncTaylorSwiftSong: AsyncTask = switchToMainThread |> updateStatus("Download TaylorSwift Song ...") |> getTaylorSwiftSong let syncWestlifeSong: AsyncTask = switchToMainThread |> updateStatus("Download Westlife Song...") |> getWestlifeSong let endSync: Action = switchToMainThread |> updateStatus("Done!") let task: Action = syncTaylorSwiftSong |> syncWestlifeSong |> endSync task() }
Từ h nếu muốn lấy thêm song từ 1 artist nào khác ta chỉ việc thêm 1 asynTask khác rồi chèn vào trước endSync, nhìn rất gọn phải không
Có 1 vấn đền khác nữa nảy sinh, mỗi lần thêm 1 artist ta phải tạo thêm 1 hàm get song từ artist đó, công thêm ở phần tableView lại phải if else theo section từng artist.
Sử dụng protocol tối ưu lần 3
Tạo 1 protocol Synchronizable
protocol Synchronizable { associatedtype Element var items: [Element] { get } func synchronize(completion: Action) }
Tạo thêm 1 protocol TableViewSectionDataSource
protocol TableViewSectionDataSource { var sectionName: String { get } var rowCount: Int { get } subscript(i: Int) -> String { get } }
Tạo các data provider đảm nhiệm việc lấy song theo từng artist
class TaylorSwiftSongDataProvider: Synchronizable { typealias Element = SongModel private(set) var items = [SongModel]() func synchronize(completion: Action) { AppServices.getSongByArtist("TaylorSwift", failureHandler: { (reason, errorMessage) in print(errorMessage) }, completion: {[weak self] songs in if let songs = songs { self?.items.removeAll() self?.items.appendContentsOf(songs) completion() } }) } } extension TaylorSwiftSongDataProvider: TableViewSectionDataSource { var sectionName: String { return "Taylor Swift" } var rowCount: Int { return items.count } subscript(i: Int) -> String { return self.items[i].trackName! } }
class WestlifeSongDataProvider: Synchronizable { typealias Element = SongModel private(set) var items = [SongModel]() func synchronize(completion: Action) { AppServices.getSongByArtist("Westlife", failureHandler: { (reason, errorMessage) in print(errorMessage) }, completion: {[weak self] songs in if let songs = songs { self?.items.removeAll() self?.items.appendContentsOf(songs) completion() } }) } } extension WestlifeSongDataProvider: TableViewSectionDataSource { var sectionName: String { return "Westlife" } var rowCount: Int { return items.count } subscript(i: Int) -> String { return items[i].trackName! } }
Ở viewController ta khai báo thêm 1 mảng chứa các data provider trên
var tableViewSections = [TableViewSectionDataSource]()
Tạo 1 hàm add các data provider
func addTableViewSection<T: TableViewSectionDataSource where T: Synchronizable>(section: T, updatingText: String) { self.tableViewSections.append(section) if let preSyncTask = self.syncTask { let index = self.tableViewSections.count - 1 let updateProgress: Action = { [unowned self] in let progress = Float(index) / Float(self.tableViewSections.count) self.statusProgressView.progress = progress } self.syncTask = preSyncTask |> switchToMainThread |> updateStatus(updatingText) |> updateProgress |> section.synchronize } else { self.syncTask = switchToMainThread |> updateStatus(updatingText, reload: false) |> section.synchronize } }
Đến đây ta có thể viết lại hàm lấy song như sau:
func getDataSong() { addTableViewSection(TaylorSwiftSongDataProvider(), updatingText: "Download TaylorSwift Song ...") addTableViewSection(WestlifeSongDataProvider(), updatingText: "Download Westlife Song...") guard let performSync = self.syncTask else { return } let resetProgress: Action = { [unowned self] in self.statusProgressView.progress = 0.0 } let endSync: Action = switchToMainThread |> resetProgress |> updateStatus("Done!") let task: Action = performSync |> endSync task() }
Để lấy thêm song từ 1 artist khác ta chỉ việc tạo thêm 1 data provider cho artist đấy rồi add thêm vào hàm getDataSong.
Ở tableView ta cũng không cần if else theo từng section
extension ViewController: UITableViewDataSource { func numberOfSectionsInTableView(tableView: UITableView) -> Int { return tableViewSections.count } func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return tableViewSections[section].rowCount } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) let tableViewSection = tableViewSections[indexPath.section] cell.textLabel?.text = tableViewSection[indexPath.row] return cell } func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return tableViewSections[section].sectionName } }