Created
January 11, 2019 19:24
-
-
Save subdan/0df098f0b9f1991f09e4fd6e9ce08e16 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import CoreData | |
class StationService { | |
static var shared: StationService = StationService() | |
private init() {} | |
private let transport = StationTransport() | |
private let parser = StationParser() | |
private let persistence = StationPersistence() | |
private var stations: [Station]? | |
private var lastUpdated: Double = 0.0 | |
/** | |
Obtains stations from the local disk | |
- Returns: An array of Station | |
*/ | |
func obtainLocalStations() -> [Station] { | |
// 1. Get [StationEntity] | |
let stationEntities: [StationEntity] = persistence.load() | |
// 2. Map [StationEntity] to [Station] | |
stations = stationEntities.compactMap({ stationEntity -> Station? in | |
let id = Int(stationEntity.id) | |
guard let name = stationEntity.name else { | |
return nil | |
} | |
guard let urlString = stationEntity.url, let url = URL(string: urlString) else { | |
return nil | |
} | |
guard let countryString = stationEntity.country, let country = StationCountry(rawValue: countryString) else { | |
return nil | |
} | |
guard let imageURLString = stationEntity.image, let image = URL(string: imageURLString) else { | |
return nil | |
} | |
let station = Station(id: id, name: name, url: url, country: country, image: image) | |
station.isFavorite = stationEntity.isFavorite | |
return station | |
}) | |
// 3. Get info about when the local data was updated | |
lastUpdated = UserDefaults.standard.double(forKey: "lastUpdated") | |
return stations! | |
} | |
/// Returns loaded stations | |
func obtainCurrentStations() -> [Station]? { | |
return stations | |
} | |
/// Updates isFavorite property of Station | |
func updateIsFavorite(for station: Station, isFavorite: Bool) { | |
let fetch: NSFetchRequest<StationEntity> = StationEntity.fetchRequest() | |
fetch.predicate = NSPredicate(format: "url == %@", argumentArray: [station.url.absoluteString]) | |
do { | |
let result = try CoreDataStack.shared.context.fetch(fetch) | |
if let stationEntity = result.first { | |
stationEntity.isFavorite = isFavorite | |
CoreDataStack.shared.save() | |
} | |
} catch { | |
print(error.localizedDescription) | |
} | |
} | |
/** | |
Obtains stations from the web server and sync with local data | |
- Parameter completion: Calls when new stations available | |
*/ | |
func fetchStations(completion: @escaping ((FetchStationsResult) -> Void)) { | |
transport.getStations { [unowned self] data, error in | |
guard error == nil else { | |
DispatchQueue.main.async { | |
completion(.error()) | |
} | |
return | |
} | |
// If data exists | |
guard let data = data else { | |
DispatchQueue.main.async { | |
completion(.value(stations: nil)) | |
} | |
return | |
} | |
// If parse successfull | |
guard let parseResult = self.parser.parse(data: data) else { | |
DispatchQueue.main.async { | |
completion(.value(stations: nil)) | |
} | |
return | |
} | |
let currentLastUpdated = UserDefaults.standard.double(forKey: "lastUpdated") | |
// If lastUpdated changed | |
if currentLastUpdated != 0 && currentLastUpdated == parseResult.lastUpdated { | |
DispatchQueue.main.async { | |
completion(.value(stations: nil)) | |
} | |
return | |
} | |
if let stations = self.stations { | |
// Save IDs of favorite stations | |
let favoriteStationsIds = stations.compactMap({ station -> Int? in | |
return station.isFavorite ? station.id : nil | |
}) | |
self.stations = parseResult.stations.map({ station -> Station in | |
station.isFavorite = favoriteStationsIds.contains(station.id) | |
return station | |
}) | |
} else { | |
self.stations = parseResult.stations | |
} | |
UserDefaults.standard.set(parseResult.lastUpdated, forKey: "lastUpdated") | |
// Rewrite stations to disk | |
self.persistence.deleteAll() | |
DispatchQueue.main.async { | |
if let stations = self.stations { | |
self.persistence.save(stations: stations) | |
} | |
} | |
DispatchQueue.main.async { | |
completion(.value(stations: self.stations)) | |
} | |
} | |
} | |
} | |
enum FetchStationsResult { | |
case value(stations: [Station]?) | |
case error() | |
} | |
private class StationTransport { | |
fileprivate enum Constants { | |
static let stationListURLString = "##########" | |
} | |
func getStations(_ completion: @escaping (Data?, Error?) -> Void) { | |
guard let url = URL(string: Constants.stationListURLString) else { | |
return | |
} | |
let dataTask = URLSession.shared.dataTask(with: url) { (data, urlResponse, error) in | |
guard error == nil else { | |
completion(nil, error) | |
return | |
} | |
completion(data, nil) | |
} | |
dataTask.resume() | |
} | |
} | |
private class StationParser { | |
func parse(data: Data) -> (stations: [Station], lastUpdated: Double)? { | |
let decoder = JSONDecoder() | |
do { | |
let response = try decoder.decode(StationListResponse.self, from: data) | |
var country = StationCountry.UA | |
let mapper: (StationListResponse.Station) -> Station = { (rawStation) -> Station in | |
let station = Station(id: Int(rawStation.id_ex)!, | |
name: rawStation.name, | |
url: URL(string: rawStation.stream)!, | |
country: country, | |
image: URL(string: rawStation.icon)!) | |
return station | |
} | |
let stationsUA = response.items_ua.map(mapper) | |
country = StationCountry.RU | |
let stationsRU = response.items_ru.map(mapper) | |
let lastUpdated = Double(response.last_updated)! | |
return (stationsRU + stationsUA, lastUpdated) | |
} catch let error { | |
print(error.localizedDescription) | |
} | |
return nil | |
} | |
} | |
struct StationListResponse: Decodable { | |
let ads: String | |
let items_ru: [StationListResponse.Station] | |
let items_ua: [StationListResponse.Station] | |
let last_updated: String | |
struct Station: Decodable { | |
let name: String | |
let id_ex: String | |
let icon: String | |
let stream: String | |
} | |
} | |
private class StationPersistence { | |
/// Loads stations from the disk | |
func load() -> [StationEntity] { | |
let fetchRequest: NSFetchRequest<StationEntity> | |
if #available(iOS 10.0, *) { | |
fetchRequest = StationEntity.fetchRequest() | |
} else { | |
fetchRequest = NSFetchRequest(entityName: "StationEntity") | |
} | |
let sortByName = NSSortDescriptor(key: "id", ascending: true) | |
fetchRequest.sortDescriptors = [sortByName] | |
do { | |
let result = try CoreDataStack.shared.context.fetch(fetchRequest) | |
return result | |
} catch { | |
print(error.localizedDescription) | |
return [StationEntity]() | |
} | |
} | |
/// Deletes all stations from the disk | |
func deleteAll() { | |
let fetchRequest: NSFetchRequest<NSFetchRequestResult> | |
if #available(iOS 10.0, *) { | |
fetchRequest = StationEntity.fetchRequest() | |
} else { | |
fetchRequest = NSFetchRequest(entityName: "StationEntity") | |
} | |
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) | |
do { | |
try CoreDataStack.shared.context.execute(batchDeleteRequest) | |
} catch { | |
fatalError("Failed to execute request: \(error)") | |
} | |
} | |
/// Saves stations to the disk | |
func save(stations: [Station]) { | |
guard stations.count > 0 else { return } | |
for station in stations { | |
let stationEntity: StationEntity | |
if #available(iOS 10, *) { | |
stationEntity = StationEntity(context: CoreDataStack.shared.context) | |
} else { | |
let newEntity = NSEntityDescription.insertNewObject(forEntityName: "StationEntity", into: CoreDataStack.shared.context) | |
if let entity = newEntity as? StationEntity { | |
stationEntity = entity | |
} else { | |
continue | |
} | |
} | |
stationEntity.id = Int64(station.id) | |
stationEntity.name = station.name | |
stationEntity.url = station.url.absoluteString | |
stationEntity.country = station.country.rawValue | |
stationEntity.image = station.image.absoluteString | |
} | |
CoreDataStack.shared.save() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment