Networking with RxSwift
For the purposes of this guide we will create a small app that search universities using Hipolabs API. The core of network communication will be based on URLSession. I assume that you know basics of iOS programing, so I will focus to explain only Rx parts of the project.
Prepare project
First step in our journey will be preparing the project, after creating it in Xcode, we need to add two external libraries:
I used for that Cocoapods, but feel free to import libraries via Carthage or manually. For Instructions head to RxSwift
Simple Layout
When our project is ready for coding we need to create place where received data will be presented. For this I created simple UITableView
and UISearchController
in main ViewController
which should be embed in UINavigationController
Here you have code that do the work:
private let tableView = UITableView()
private let cellIdentifier = "cellIdentifier"
private let searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.searchBar.placeholder = "Search for university"
return searchController
}()
private func configureProperties() {
tableView.register(TableViewCell.self, forCellReuseIdentifier: cellIdentifier)
navigationItem.searchController = searchController
navigationItem.title = "University finder"
navigationItem.hidesSearchBarWhenScrolling = false
navigationController?.navigationBar.prefersLargeTitles = true
}
private func configureLayout() {
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor),
tableView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
tableView.contentInset.bottom = view.safeAreaInsets.bottom
}
And here is the result:
What we want to receive? How we want to receive it?
If our layout is ready we can try to handle some data from REST API. For that we will use Hipolabs API. We want to get informations about universities which names contain search phrase from our UISearchController
.
Example
Here you have example of request and response for finding universities with middle
as name parameter.
Request |
---|
http://universities.hipolabs.com/search?name=middle |
Response |
[{"name": "Middlesex University", "domains": ["mdx.ac.uk"], "web_pages": ["http://www.mdx.ac.uk/"], "alpha_two_code": "GB", "state-province": null, "country": "United Kingdom"}, ...] |
Now when we know how API works we can create request and model objects.
Model
For working on data that came from server we can use JSON dictionary like [String: Any]
, but I prefer to create data model which is much clearer and easier to use. For purpose of receiving universities objects I created struct UniversityModel
, which conform to Codable
protocol and because of that we don't need to be bothered by parsing data, let's leave that to swift engine.
struct UniversityModel: Codable {
let name: String
let webPages: [String]?
let country: String
private enum CodingKeys: String, CodingKey {
case name
case webPages = "web_pages"
case country
}
}
Requests
For making this more universal we need to create APIRequest
protocol, so different requests could be handle by the same APIClient
.
APIRequest
class consists of two parts:
Protocol itself where are defined necessary properties:
protocol APIRequest {
var method: RequestType { get }
var path: String { get }
var parameters: [String : String] { get }
}
Protocol extension that will create URLRequest
from instance of APIRequest
:
extension APIRequest {
func request(with baseURL: URL) -> URLRequest {
guard var components = URLComponents(url: baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: false) else {
fatalError("Unable to create URL components")
}
components.queryItems = parameters.map {
URLQueryItem(name: String($0), value: String($1))
}
guard let url = components.url else {
fatalError("Could not get url")
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.addValue("application/json", forHTTPHeaderField: "Accept")
return request
}
}
I also created a small enum inside APIRequest
class to improve declaring httpMethod:
public enum RequestType: String {
case GET, POST
}
When APIRequest
protocol is ready we can make real request specify for searching universities by names. For that we need to create another class inherited from APIRequest
protocol where is defined method, endpoint path and parameters.
class UniversityRequest: APIRequest {
var method = RequestType.GET
var path = "search"
var parameters = [String: String]()
init(name: String) {
parameters["name"] = name
}
}
Ok, there was a lot of it, but where is this RxSwift?
Time for magic
Now it is time for the most important piece of this puzzle, part that will change our request for data from server. Now it is time for APIClient
!
APIClient
is a class where we will make a request by using rx
extension on URLSession
from RxCocoa and then map
the response data to already parsed model of data if only model is Codable
. The .data(request: URLRequest)
function will make sure for us that status code is 200..<300
and only then return data, if not then it will throw an error.
class APIClient {
private let baseURL = URL(string: "http://universities.hipolabs.com/")!
func send<T: Codable>(apiRequest: APIRequest) -> Observable<T> {
let request = apiRequest.request(with: baseURL)
return URLSession.shared.rx.data(request: request)
.map {
try JSONDecoder().decode(T.self, from: data)
}
.observeOn(MainScheduler.asyncInstance)
}
}
One more last thing...
After creating APIClient
the last part is connecting everything together.
Result that we expect:
Typing search phrase in search field → Instance of request created with search phrase → Array of models of university → Refreshed UITableView
filled by new data and all of that in 10 lines!!!
searchController.searchBar.rx.text.asObservable()
.map { ($0 ?? "").lowercased() }
.map { UniversityRequest(name: $0) }
.flatMapLatest { [unowned self] request -> Observable<[UniversityModel]> in
return self.apiClient.send(apiRequest: request)
}
.bind(to: tableView.rx.items(cellIdentifier: cellIdentifier)) { index, model, cell in
cell.textLabel?.text = model.name
}
.disposed(by: disposeBag)
Please remember to import RxSwift
and RxCocoa
and create two variable:
private let apiClient = APIClient()
private let disposeBag = DisposeBag()
Extra feature
If you would like to present a website of university when user will tap on cell you can do this in 7 lines, by taking advantage from model and reactive binding.
tableView.rx.modelSelected(UniversityModel.self)
.map { URL(string: $0.webPages?.first ?? "")! }
.compactMap { URL(string: $0) }
.map { SFSafariViewController(url: $0) }
.subscribe(onNext: { [weak self] safariViewController in
self?.present(safariViewController, animated: true)
})
.disposed(by: disposeBag)
Final effect
That is all for today. You can find the whole project at this repository. I hope that you enjoyed.