static func request<T: Decodable>(endpoint: String, query: [String: Any] = [:], contentIdentifier: String) -> Observable<T> {
do {
guard let url = URL(string: API)?.appendingPathComponent(endpoint),
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
throw EOError.invalidURL(endpoint)
}
components.queryItems = try query.compactMap { (key, value) in
guard let v = value as? CustomStringConvertible else {
throw EOError.invalidParameter(key, value)
}
return URLQueryItem(name: key, value: v.description)
}
guard let finalURL = components.url else {
throw EOError.invalidURL(endpoint)
}
let request = URLRequest(url: finalURL)
return URLSession.shared.rx.response(request: request)
.map { (result: (response: HTTPURLResponse, data: Data)) -> T in
let decoder = self.jsonDecoder(contentIdentifier: contentIdentifier)
let envelope = try decoder.decode(EOEnvelope<T>.self, from: result.data)
return envelope.content
}
} catch {
return Observable.empty()
}
}
static var categories: Observable<[EOCategory]> = {
let request: Observable<[EOCategory]> = EONET.request(endpoint: categoriesEndpoint, contentIdentifier: "categories")
return request
.map { categories in categories.sorted { $0.name < $1.name } }
.catchErrorJustReturn([])
.share(replay: 1, scope: .forever)
}()
private static func events(forLast days: Int, closed: Bool) -> Observable<[EOEvent]> {
let query: [String: Any] = [
"days": days,
"status": (closed ? "closed" : "open")
]
let request: Observable<[EOEvent]> = EONET.request(endpoint: eventsEndpoint, query: query, contentIdentifier: "events")
return request.catchErrorJustReturn([])
}
static func events(forLast days: Int = 360) -> Observable<[EOEvent]> {
let openEvents = events(forLast: days, closed: false)
let closedEvents = events(forLast: days, closed: true)
// return openEvents.concat(closedEvents)
// Downloading in parallel
return Observable.of(openEvents, closedEvents)
.merge()
.reduce([]) { running, new in
running + new
}
}
CategoriesViewController
import UIKit
import RxSwift
import RxCocoa
class CategoriesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet var tableView: UITableView!
let categories = BehaviorRelay<[EOCategory]>(value: [])
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
categories
.asObservable()
.subscribe(onNext: { [weak self] _ in
DispatchQueue.main.async {
self?.tableView?.reloadData()
}
})
.disposed(by: disposeBag)
startDownload()
}
func startDownload() {
// let eoCategories = EONET.categories
// eoCategories
// .bind(to: categories)
// .disposed(by: disposeBag)
let eoCategories = EONET.categories
let downloadedEvents = EONET.events(forLast: 360)
let updatedCategories = Observable
.combineLatest(eoCategories, downloadedEvents) {
(categories, events) -> [EOCategory] in
return categories.map { category in
var cat = category
cat.events = events.filter {
$0.categories.contains(where: { $0.id == category.id })
}
return cat
}
}
eoCategories
.concat(updatedCategories)
.bind(to: categories)
.disposed(by: disposeBag)
}
// MARK: UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return categories.value.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "categoryCell")!
let category = categories.value[indexPath.row]
cell.textLabel?.text = "\(category.name) (\(category.events.count))"
cell.accessoryType = (category.events.count > 0) ? .disclosureIndicator : .none
cell.detailTextLabel?.text = category.description
return cell
}
}
EventsViewController
import UIKit
import RxSwift
import RxCocoa
class EventsViewController: UIViewController, UITableViewDataSource {
@IBOutlet var tableView: UITableView!
@IBOutlet var slider: UISlider!
@IBOutlet var daysLabel: UILabel!
let events = BehaviorRelay<[EOEvent]>(value: [])
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 60
events.asObservable()
.subscribe(onNext: { [weak self] _ in
self?.tableView.reloadData()
})
.disposed(by: disposeBag)
}
@IBAction func sliderAction(slider: UISlider) {
}
// MARK: UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return events.value.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "eventCell") as! EventCell
let event = events.value[indexPath.row]
cell.configure(event: event)
return cell
}
}
CategoriesViewController
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let category = categories.value[indexPath.row]
tableView.deselectRow(at: indexPath, animated: true)
guard !category.events.isEmpty else { return }
let eventsController = storyboard!.instantiateViewController(withIdentifier: "events") as! EventsViewController
eventsController.title = category.name
eventsController.events.accept(category.events)
navigationController!.pushViewController(eventsController, animated: true)
}
import UIKit
import RxSwift
import RxCocoa
class EventsViewController: UIViewController, UITableViewDataSource {
@IBOutlet var tableView: UITableView!
@IBOutlet var slider: UISlider!
@IBOutlet var daysLabel: UILabel!
let events = BehaviorRelay<[EOEvent]>(value: [])
let disposeBag = DisposeBag()
let days = BehaviorRelay<Int>(value: 360)
let filteredEvents = BehaviorRelay<[EOEvent]>(value: [])
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 60
events.asObservable()
.subscribe(onNext: { [weak self] _ in
self?.tableView.reloadData()
})
.disposed(by: disposeBag)
Observable.combineLatest(days, events) { days, events -> [EOEvent] in
let maxInterval = TimeInterval(days * 24 * 3600)
return events.filter { event in
if let date = event.date {
return abs(date.timeIntervalSinceNow) < maxInterval
}
return true
}
}
.bind(to: filteredEvents)
.disposed(by: disposeBag)
filteredEvents.asObservable()
.subscribe(onNext: { _ in
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
}
})
.disposed(by: disposeBag)
days.asObservable()
.subscribe(onNext: { [weak self] days in
self?.daysLabel.text = "Last \(days) days"
})
.disposed(by: disposeBag)
}
@IBAction func sliderAction(slider: UISlider) {
days.accept(Int(slider.value))
}
// MARK: UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredEvents.value.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "eventCell") as! EventCell
let event = filteredEvents.value[indexPath.row]
cell.configure(event: event)
return cell
}
}