Getting Started With RxSwift and RxCocoa: Networking
Tiếp theo các phần trước Phần 1 - Getting Started With RxSwift and RxCocoa, Phần 2 - Getting Started With RxSwift and RxCocoa : Observable and the Bind hôm nay chúng ta sẽ tiếp tục tìm hiểu về RxSwift, cụ thể là tìm hiểu về networking, cách lấy data và kết nối dữ liệu đó với View. Rx có rất nhiều ...
Tiếp theo các phần trước Phần 1 - Getting Started With RxSwift and RxCocoa, Phần 2 - Getting Started With RxSwift and RxCocoa : Observable and the Bind hôm nay chúng ta sẽ tiếp tục tìm hiểu về RxSwift, cụ thể là tìm hiểu về networking, cách lấy data và kết nối dữ liệu đó với View. Rx có rất nhiều networking extentions bao gồm RxAlamofire, Moya và ở trong bài viết này chúng ta sẽ tập trung ở Moya.
Moya là một abstract layer giúp cho chúng ta thực hiện các tác vụ về networking, về cơ bản bằng cách sử dụng thư viện này chúng ta có thể tạo kết nối tới API một cách đơn giản, với phần mở rộng bao gồm RxSwift vào ModelMapper chúng ta đã có đầy đủ vũ khí cho việc thực hiện kết nối mạng một cách ngon lành.
Để setup Moya chúng ta cần Provider thiết lập stubbing, endpoint closure ... Cho trường hợp của chúng ta chugns ta chỉ cần thiết lập đơn giản Provider với RxSwift. Việc thứ 2 chúng ta cần làm là thiết lập cấu hình Endpoint - 1 enum định nghĩa ra các API cần gọi. Việc này rất đơn giản, chúng ta chỉ cần tạo ra enum conform TargetType là xong, nó là 1 protocol gồm url, method, task, parameter và parameterEncoding. Param cuối cùng chúng ta cần specify là sampleData sử dụng cho việc fake data trả về và test.
Trong example này chúng ta sẽ lấy ra các issues của 1 reposity xác định sử dụng GitHub API. Trước tiên chúng ta lấy về các reposity, kiểm tra xem nó tồn tại hay ko, sau đó gửi request lấy về các issuses cho repository này. Chúng ta sẽ map kết quả trả về từ JSON sang objects, xử lý lỗi, xử lý dupplicating requests, spamming API ... Ví dụ của chúng ta sẽ chạy như sau:
Chúng ta sẽ tạo project với nội dung file pod như sau:
platform :ios, '8.0' use_frameworks! target 'RxMoyaExample' do pod 'RxCocoa', '~> 3.0.0' pod 'Moya-ModelMapper/RxSwift', '~> 4.1.0' pod 'RxOptional' end post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['ENABLE_TESTABILITY'] = 'YES' config.build_settings['SWIFT_VERSION'] = '3.0' end end end
Chúng ta sẽ bắt đầu với UI, ở đây UI chỉ đơn giản gồm 1 UITableview và 1 UISearchbar. Tiếp theo chúng ta cần1 viewcontroller để quản lý, nó sẽ lấy data từ search bar, pass cho model, model sẽ lấy dữ liệu từ server và đẩy nó cho tableview. Chúng ta sẽ tạo file IssueListViewController.swift nội dung như sau:
import Moya import Moya_ModelMapper import UIKit import RxCocoa import RxSwift class IssueListViewController: UIViewController { @IBOutlet weak var tableView: UITableView! @IBOutlet weak var searchBar: UISearchBar! override func viewDidLoad() { super.viewDidLoad() setupRx() } func setupRx() { } }
Tiếp theo chúng ta sẽ tạo file GithubEndpoint.swift và tạo enum với 1 số target:
import Foundation import Moya enum GitHub { case userProfile(username: String) case repos(username: String) case repo(fullName: String) case issues(repositoryFullName: String) }
enum Github như mình đã nói nó cần conform TargetType, cái này cũng chỉ đơn giản là 1 enum. Chúng ta sẽ tạo extension cho GitHub enum, nó sẽ gồm tất cả các thuộc tính cần thiết:
import Foundation import Moya private extension String { var URLEscapedString: String { return self.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlHostAllowed)! } } enum GitHub { case userProfile(username: String) case repos(username: String) case repo(fullName: String) case issues(repositoryFullName: String) } extension GitHub: TargetType { var baseURL: URL { return URL(string: "https://api.github.com")! } var path: String { switch self { case .repos(let name): return "/users/(name.URLEscapedString)/repos" case .userProfile(let name): return "/users/(name.URLEscapedString)" case .repo(let name): return "/repos/(name)" case .issues(let repositoryName): return "/repos/(repositoryName)/issues" } } var method: Moya.Method { return .get } var parameters: [String: Any]? { return nil } var sampleData: Data { switch self { case .repos(_): return "{{"id": "1", "language": "Swift", "url": "https://api.github.com/repos/mjacko/Router", "name": "Router"}}}".data(using: .utf8)! case .userProfile(let name): return "{"login": "(name)", "id": 100}".data(using: .utf8)! case .repo(_): return "{"id": "1", "language": "Swift", "url": "https://api.github.com/repos/mjacko/Router", "name": "Router"}".data(using: .utf8)! case .issues(_): return "{"id": 132942471, "number": 405, "title": "Updates example with fix to String extension by changing to Optional", "body": "Fix it pls."}".data(using: .utf8)! } } var task: Task { return .request } var parameterEncoding: ParameterEncoding { return JSONEncoding.default } }
Chúng ta ko cần truyền bất kỳ param nào ở đây nên return nil, method luôn luôn là .get, baseURL cũng ko thay đổi, sampleData thì có trả về các dữ liệu khác nhau trong các trường hợp khác nhau do đó chúng ta để trong switch. Chúng ta đã thực thi xong Moya Provider. Chúng ta cũng cần thực hiện việc ẩn bàn phím khi click cell, việc này đương nhiên cũng được thực hiện với RxSwift, chúng ta sẽ cần 1 DisposeBag, thêm nữa chúng ta sẽ tạo ra 1 Observable mới để lấy dữ liệu text từ search bar, filter, ...
class IssueListViewController: UIViewController { ... let disposeBag = DisposeBag() var provider: RxMoyaProvider<GitHub>! var latestRepositoryName: Observable<String> { return searchBar .rx.text .orEmpty .debounce(0.5, scheduler: MainScheduler.instance) .distinctUntilChanged() } ... func setupRx() { // First part of the puzzle, create our Provider provider = RxMoyaProvider<GitHub>() // Here we tell table view that if user clicks on a cell, // and the keyboard is still visible, hide it tableView .rx.itemSelected .subscribe(onNext: { indexPath in if self.searchBar.isFirstResponder() == true { self.view.endEditing(true) } }) .addDisposableTo(disposeBag) } ... }
Chúng ta cần model để lấy dữ liệu từ text tương ứng lấy ở search bar, nhưng đầu tiên chúng ta cần parse object trước khi chúng ta truyền bất kì dữ liệu nào vào nó, việc này được thực hiện dễ dàng nhờ ModelMapper, chúng ta sẽ cần 2 entities, 1 cho Repository và 1 cho Issuse.
import Mapper struct Repository: Mappable { let identifier: Int let language: String let name: String let fullName: String init(map: Mapper) throws { try identifier = map.from("id") try language = map.from("language") try name = map.from("name") try fullName = map.from("full_name") } }
import Mapper struct Issue: Mappable { let identifier: Int let number: Int let title: String let body: String init(map: Mapper) throws { try identifier = map.from("id") try number = map.from("number") try title = map.from("title") try body = map.from("body") } }
Chúng ta ko cần quá nhiều thuộc tính, nếu bạn muốn có thể tự thêm thuộc tính dựa theo tài liệu GitHub API Tiếp theo chungs ta sẽ chuyển tới phần thú vị nhất của tutorial này, IssueTrackerModel - core of our Networking. Trước tiên, model của chúng ta cần có Provider property để pass nó vào hàm init. Sau đó chúng ta cần có property để observable text để lấy tên reposityNames
Let’s create the IssueTrackerModel.swift:
import Foundation import Moya import Mapper import Moya_ModelMapper import RxOptional import RxSwift struct IssueTrackerModel { let provider: RxMoyaProvider<GitHub> let repositoryName: Observable<String> func trackIssues() -> Observable<[Issue]> { } internal func findIssues(repository: Repository) -> Observable<[Issue]?> { } internal func findRepository(name: String) -> Observable<Repository?> { } }
Chúng ta thêm 2 functions:
- findRepository(