Last week, we took a custom look at the keyboard library that displays pre-Vietnam currency suggestions. Today I will show you how to create a library to call the network as a restful API. The library is called Qnetwork. You can download it on github at the link:
https://github.com/lexuanquynh/QNetwork
What is this library for?
First before proceeding with construction, download my demo section at the link:
https://github.com/lexuanquynh/QNetworkDemo
First, you need to know that I built this library best for MVVM architecture. However, you can still use it for other architectures you want. If you understand in a simple way, you use this library, you create services to call apIs. Then you use it to call wherever you want.
So how to use it? Take a look at my demo code. In this example I will proceed to call the GITHUB API:
curl
-H "Accept: application/vnd.github.v3+json"
https://api.github.com/search/repositories

In a previous article , Iintroduced quite carefully how to write an API for it. The steps are as follows:
- Create a Github module. Each module will serve a separate screen. In MVVM design I often do so, modules include View, Model, servive call API and view model. You can open the above souce code.
- Create a GithubAPI file. This file contains the API cluster to call. Usually in my application there will be many files in *API format. Each file contains 1 cluster, for example, AuthenAPI will contain a logon and logout cluster. FeedAPI contains feed-related API clusters. The naming will kill what it does. The description of this file is as follows:
// | |
// GithubAPI.swift | |
// QNetworkDemo | |
// | |
// Created by Xuân Quỳnh Lê on 2021/06/26. | |
// | |
import Foundation | |
import QNetwork | |
import Moya | |
enum GithubAPI { | |
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 { | |
switch self { | |
case .searchRepositories: | |
return "search/repositories" | |
} | |
} | |
var method: Moya.Method { | |
return .get | |
} | |
var sampleData: Data { | |
var dataUrl: URL? | |
switch self { | |
case .searchRepositories: | |
if let file = Bundle.main.url(forResource: "SearchRepositoriesResponse", withExtension: "json") { | |
dataUrl = file | |
} | |
} | |
if let url = dataUrl, let data = try? Data(contentsOf: url) { | |
return data | |
} | |
return Data() | |
} | |
var task: Task { | |
switch self { | |
case .searchRepositories: | |
if let parameters = parameters { | |
return .requestParameters(parameters: parameters, encoding: parameterEncoding) | |
} | |
} | |
return .requestPlain | |
} | |
var headers: [String: String]? { | |
return ["Content-type": "application/json"] | |
} | |
var parameters: [String: Any]? { | |
var params: [String: Any] = [:] | |
switch self { | |
case .searchRepositories(let q, let sort, let order, let page): | |
params["q"] = q | |
params["sort"] = sort | |
params["order"] = order | |
params["page"] = page | |
} | |
return params | |
} | |
// For json encode. Use in post request | |
var jsonEncoding: JSONEncoding { | |
return JSONEncoding.default | |
} | |
// For param encode. Use in get request | |
var parameterEncoding: ParameterEncoding { | |
return URLEncoding.default | |
} | |
} |
- Then you create the GithubSearchService file again. Here you need to transfer 1 Codable struct so that when the API has a response, you need to insert it and transfer json to this struct. Specifically, struct GithubSearchResponse:
// MARK: - GithubSearchResponse | |
struct GithubSearchResponse: Codable { | |
let totalCount: Int? | |
let incompleteResults: Bool | |
let items: [GithubSearchItem]? | |
enum CodingKeys: String, CodingKey { | |
case totalCount = "total_count" | |
case incompleteResults = "incomplete_results" | |
case items | |
} | |
} | |
// MARK: - Item | |
struct GithubSearchItem: Codable { | |
let id: Int | |
let name: String? | |
let htmlURL: String? | |
let itemDescription: String? | |
enum CodingKeys: String, CodingKey { | |
case id, name | |
case htmlURL = "html_url" | |
case itemDescription = "description" | |
} | |
} |
Many of you ask me how I can go from json to struct as above. I'm a lazy person, so I used
to go on this page to https://app.quicktype.io
Then you just need to edit the name to make sense. You do a lot of things that will get used to.
- After you've finished the service cluster above, you need to create GithubViewModel:
import Foundation | |
class GithubViewModel { | |
// Service call API | |
let service: GithubSearchService! | |
// Callback to view | |
var needReloadTableView: (() -> Void)? | |
var needShowError: ((String) -> Void)? | |
var needSetStateBottomIndicatorView: ((_ show: Bool) -> Void)? | |
private var page: Int = 0 | |
private var language = "" | |
private var incompleteResults = false | |
// Datasource | |
private var githubSearchItem: [GithubSearchItem] = [] | |
init() { | |
// Turn on is test is true if you need test for API | |
self.service = GithubSearchService(isTest: false) | |
} | |
/// Clear tableview data source | |
func clearTableView() { | |
self.page = 0 | |
self.incompleteResults = false | |
self.githubSearchItem.removeAll() | |
self.needReloadTableView?() | |
} | |
/// Request repositories | |
func requestRepositories(language: String, loadMore: Bool = false) { | |
// Check when load more | |
if self.incompleteResults { | |
return | |
} | |
if !loadMore { | |
self.page = 0 | |
self.githubSearchItem.removeAll() | |
} | |
self.language = language | |
// Default param | |
let sort = "stars" | |
let order = "desc" | |
self.service.searchRepositories(language: language, sort: sort, order: order, page: self.page) { [weak self] result in | |
guard let strongSelf = self else { return } | |
// Check when load more | |
if loadMore { | |
strongSelf.needSetStateBottomIndicatorView?(false) | |
} | |
switch result { | |
case .success(let githubResponse): | |
strongSelf.incompleteResults = githubResponse.incompleteResults | |
if let 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 element | |
if indexPath.row == self.githubSearchItem.count - 1 { | |
self.page += 1 | |
self.requestRepositories(language: language, loadMore: true) | |
self.needSetStateBottomIndicatorView?(true) | |
} | |
return githubSearchItem[indexPath.row] | |
} | |
} |
Many of you will be curious why I wrote the above. Similarly, I often download other people's sources. Then I clone exactly the same, what they write, I write. Gradually accustomed to hands, with reflexes. Finally, understand what people do. Practice is still the best way to learn programming anyway.
How to build QNetwork?
This is quite obscure without videos. To be honest, I watched another developer's video tutorial on this link:
https://www.youtube.com/watch?v=xu9oeCAS8aA
You watch the video carefully, follow him, you will create a swift package and can give it to others to use through a link. With Qnetwork, simply add this path to your swift package(Xcode ➞ File ➞ New ➞ Swift Package):
https://github.com/lexuanquynh/QNetwork.git
From now on, I will only need a link, then add, and can easily deploy my network service class.
If you have any questions, go to the page's fan page at:
https://www.facebook.com/codetoanbug
Share it with someone if you find the post helpful.