Bài viết được sự cho phép của tác giả Lê Xuân Quỳnh
Tuần trước, chúng ta đã xem qua 1 cách custom về thư viện bàn phím hiển thị gợi ý tiền Việt Nam. Hôm nay tôi sẽ hướng dẫn cách tạo 1 thư viện để gọi network dạng restful API. Thư viện tên là Qnetwork. Bạn có thể tải trên github tại đường dẫn:
Đầu tiên, bạn cần biết rằng tôi xây dựng thư viện này tốt nhất cho kiến trúc MVVM. Tuy nhiên bạn vẫn có thể sử dụng nó cho các architecture khác mà bạn muốn. Nếu hiểu theo cách đơn giản, bạn dùng thư viện này, bạn tạo các service để gọi API. Sau đó, bạn dùng nó để gọi bất cứ ở đâu bạn muốn.
Trong 1 bài viết trước đây, tôi đã giới thiệu khá kỹ càng cách viết API cho nó. Các bước tiến hành như sau:
Tạo 1 module Github. Mỗi module sẽ phục vụ cho 1 màn hình riêng biệt. Trong thiết kế MVVM tôi hay làm như vậy, module bao gồm View, Model, servive gọi API và view model. Bạn có thể mở souce code ở trên ra xem.
Tạo 1 file GithubAPI. File này chứa cụm API cần gọi. Thông thường trong ứng dụng của tôi sẽ có nhiều file dạng *API. Mỗi 1 file chứa 1 cụm, ví dụ AuthenAPI sẽ chứa cụm đăng nhập, đăng xuất. FeedAPI chứa cụm API liên quan đến feed. Cách đặt tên sẽ sát nghĩa nó làm gì. Mô tả file này như sau:
//// GithubAPI.swift// QNetworkDemo//// Created by Xuân Quỳnh Lê on 2021/06/26.//importFoundationimportQNetworkimportMoyaenumGithubAPI{case searchRepositories(q:String, sort:String, order:String, page:Int)}
extension GithubAPI:TargetType{var baseURL: URL {let url = URL(string:Configs.Network.baseUrl)!return url
}var path:String{switchself{case.searchRepositories:return"search/repositories"}}var method:Moya.Method{return.get}var sampleData:Data{var dataUrl: URL?switchself{case.searchRepositories:iflet file =Bundle.main.url(forResource:"SearchRepositoriesResponse", withExtension:"json"){
dataUrl = file
}}iflet url = dataUrl,let data =try?Data(contentsOf: url){return data
}returnData()}var task:Task{switchself{case.searchRepositories:iflet parameters = parameters {return.requestParameters(parameters: parameters, encoding: parameterEncoding)}}return.requestPlain
}var headers:[String:String]?{return["Content-type":"application/json"]}var parameters:[String:Any]?{varparams:[String:Any]=[:]switchself{case.searchRepositories(let q,let sort,let order,let page):params["q"]= q
params["sort"]= sort
params["order"]= order
params["page"]= page
}returnparams}// For json encode. Use in post requestvar jsonEncoding:JSONEncoding{returnJSONEncoding.default}// For param encode. Use in get requestvar parameterEncoding:ParameterEncoding{returnURLEncoding.default}}
Sau đó bạn tạo tiếp file GithubSearchService. Ở đây bạn cần truyền 1 struct Codable để lúc API có response, bạn cần đưa nó vào và chuyển json sang struct này. Cụ thể là struct GithubSearchResponse:
Nhiều bạn hỏi tôi sao có thể từ json chuyển sang struct được như trên. Tôi là 1 người lười biếng, nên tôi hay vào trang này để chuyển: https://app.quicktype.io
Sau đó bạn chỉ cần chỉnh sửa tên cho hợp lý là được. Bạn làm nhiều sẽ quen tay thôi.
Sau khi xong cụm service ở trên, bạn cần tạo GithubViewModel:
importFoundationclassGithubViewModel{// Service call APIlet service:GithubSearchService!// Callback to viewvar needReloadTableView:(()->Void)?var needShowError:((String)->Void)?var needSetStateBottomIndicatorView:((_ show:Bool)->Void)?privatevar page:Int=0privatevar language =""privatevar incompleteResults =false// Datasourceprivatevar githubSearchItem:[GithubSearchItem]=[]
init(){// Turn on is test is true if you need test for APIself.service =GithubSearchService(isTest:false)}/// Clear tableview data source
func clearTableView(){self.page =0self.incompleteResults =falseself.githubSearchItem.removeAll()self.needReloadTableView?()}/// Request repositories
func requestRepositories(language:String, loadMore:Bool=false){// Check when load moreifself.incompleteResults {return}if!loadMore {self.page =0self.githubSearchItem.removeAll()}self.language = language
// Default paramlet sort ="stars"let order ="desc"self.service.searchRepositories(language: language, sort: sort, order: order, page:self.page){[weak self] result in
guard let strongSelf =selfelse{return}// Check when load moreif loadMore {
strongSelf.needSetStateBottomIndicatorView?(false)}switch result {case.success(let githubResponse):
strongSelf.incompleteResults = githubResponse.incompleteResults
iflet items = githubResponse.items {
items.forEach({strongSelf.githubSearchItem.append( $0 )})}
strongSelf.needReloadTableView?()case.failure(let error):
strongSelf.needShowError?(error.description)}}}
func numberOfRowsInSection(section:Int)->Int{return githubSearchItem.count
}
func cellForRowAt(indexPath:IndexPath)->GithubSearchItem{// Check if the last row number is the same as the last current data elementif indexPath.row ==self.githubSearchItem.count -1{self.page +=1self.requestRepositories(language: language, loadMore:true)self.needSetStateBottomIndicatorView?(true)}return githubSearchItem[indexPath.row]}}
Nhiều bạn sẽ tò mò tại sao tôi lại viết được như trên. Cũng tương tự, tôi hay tải source của người khác về xem. Sau đó tôi clone y hệt, người ta viết gì, tôi viết nấy. Lâu dần quen tay, có phản xạ. Cuối cùng là hiểu được ý nghĩa mà người ta làm. Dù sao thì thực hành vẫn là cách tốt nhất để học lập trình.
Bạn xem kỹ video, làm theo anh ấy, bạn sẽ tạo được 1 swift package và có thể đưa cho người khác sử dụng thông qua 1 đường dẫn. Với Qnetwork, đơn giản bạn chỉ cần thêm đường dẫn này vào phần swift package của bạn(Xcode ➞ File ➞ New ➞ Swift Package):