Core data how to use NSMangedObjectContext in multithreaded - swift

Okay, I've been going at this for a day and can't seem to figure out what I am doing wrong. This is how my data model looks like for core data.
This is how my code looks like.
class Service {
static let shared = Service()
private let numberOfPokemons = 151
func downloadPokemonsFromServer(completion: #escaping ()->()) {
let urlString = "https://pokeapi.co/api/v2/pokemon?limit=\(numberOfPokemons)"
guard let url = URL(string: urlString) else { return }
var id: Int16 = 0
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let err = error {
print("Unable to fetch pokemon", err)
}
guard let data = data else { return }
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let pokemonJSON = try decoder.decode(PokemonsJSON.self, from: data)
pokemonJSON.pokemons.forEach { (JSONPokemon) in
id += 1
let pokemon = Pokemon(context: privateContext)
pokemon.name = JSONPokemon.name
pokemon.url = JSONPokemon.detailUrl
pokemon.id = id
}
try? privateContext.save()
try? privateContext.parent?.save()
completion()
} catch let err {
print("Unable to decode PokemonJSON. Error: ",err)
completion()
}
}.resume()
}
private var detailTracker = 0
func fetchMoreDetails(objectID: NSManagedObjectID) {
guard let pokemon = CoreDataManager.shared.persistentContainer.viewContext.object(with: objectID) as? Pokemon, let urlString = pokemon.url else { return }
print(pokemon.name)
print()
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let err = error {
print("Unable to get more details for pokemon", err)
}
guard let data = data else { return }
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let pokemonDetailJSON = try decoder.decode(PokemonDetailJSON.self, from: data)
pokemonDetailJSON.types.forEach { (nestedType) in
let type = Type(context: privateContext)
type.name = nestedType.type.name
type.addToPokemons(pokemon)
}
try? privateContext.save()
try? privateContext.parent?.save()
} catch let err {
print("Unable to decode pokemon more details", err)
}
}.resume()
}
private var imageTracker = 0
func getPokemonImage(objectID: NSManagedObjectID) {
guard let pokemon = CoreDataManager.shared.persistentContainer.viewContext.object(with: objectID) as? Pokemon else { return }
let id = String(format: "%03d", pokemon.id)
let urlString = "https://assets.pokemon.com/assets/cms2/img/pokedex/full/\(id).png"
print(urlString)
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let err = error {
print("Unable to load image from session.", err)
}
guard let data = data else { return }
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext
pokemon.image = data
self.imageTracker += 1
if self.imageTracker == self.numberOfPokemons {
try? privateContext.save()
try? privateContext.parent?.save()
}
}.resume()
}
}
I have 3 entities, which are Pokemon, Type & Ability. I am not doing nothing with ability right now, so we can just ignore that. The first func downloadPokemonFromServer just grabs the first 151 pokemon, saves the name and a url of pokemon. I then use that url to go into another URLSession and grab more information about that pokemon. Which is what the fetchMoreDetails func does. However, this func crashes my app. I don't know what I am doing wrong here, it crashes when I try to save it.
The third func getPokemonImage I go into another URLSession, get the data and save it to my pokemon image attribute. The thing is this works perfectly fine. It saves to my CoreData and it doesn't crash my app.
This is how I call it in my ViewController.
#objc func handleRefresh() {
if pokemonController.fetchedObjects?.count == 0 {
Service.shared.downloadPokemonsFromServer {
let pokemons = self.pokemonController.fetchedObjects
pokemons?.forEach({ (pokemon) in
Service.shared.getPokemonImage(objectID: pokemon.objectID)
//If I uncomment the line below it will crash my app.
//Service.shared.fetchMoreDetails(objectID: pokemon.objectID)
})
}
}
tableView.refreshControl?.endRefreshing()
}
Will someone pls help me figure out what I am doing wrong. Would really appreciate the help.

You need to make sure you're doing all the Core Data work on the same thread as the private context you've created. To do so please use:
privateContext.perform {
//Core data work: create new entities, connections, delete, edit and more...
}
This can prevent you a lot of headaches and troubles down the road

I think the problem is that you are trying to set a relationship between two objects from different contexts. Your pokemon object is registered with the view context:
guard let pokemon = CoreDataManager.shared.persistentContainer.viewContext.object(with: objectID) as? Pokemon, let urlString = pokemon.url else { return }
whereas your type object is registered with the private context:
let type = Type(context: privateContext)
type.name = nestedType.type.name
so this line will not work:
type.addToPokemons(pokemon)
I would try amending the code to use only the privateContext, something like this:
func fetchMoreDetails(objectID: NSManagedObjectID) {
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext
guard let pokemon = privateContext.object(with: objectID) as? Pokemon, let urlString = pokemon.url else { return }
print(pokemon.name)
print()
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let err = error {
print("Unable to get more details for pokemon", err)
}
guard let data = data else { return }
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let pokemonDetailJSON = try decoder.decode(PokemonDetailJSON.self, from: data)
pokemonDetailJSON.types.forEach { (nestedType) in
let type = Type(context: privateContext)
type.name = nestedType.type.name
type.addToPokemons(pokemon)
}
try? privateContext.save()
try? privateContext.parent?.save()
} catch let err {
print("Unable to decode pokemon more details", err)
}
}.resume()
}

Related

Error when working with NSManagedObjectContext in background thread

So I keep getting this error when I save to core data.
Coredata[21468:13173906] [error] error: SQLCore dispatchRequest: exception handling request: , ** -_referenceData64 only defined for abstract class. Define -[NSTemporaryObjectID_default _referenceData64]! with userInfo of (null)
I recently finished a course on core data, the instructor brush a bit on the topic that core data isn't thread safe. So what he suggested is a child/parent context, so that is what I tried doing but kept getting the error from above. This is how my code looks.
struct Service {
static let shared = Service()
func downloadPokemonsFromServer(completion: #escaping ()->()) {
let urlString = "https://pokeapi.co/api/v2/pokemon?limit=9"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let err = error {
print("Unable to fetch pokemon", err)
}
guard let data = data else { return }
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let pokemonJSON = try decoder.decode(PokemonsJSON.self, from: data)
pokemonJSON.pokemons.forEach { (JSONPokemon) in
//Works fine with privateContext here
let pokemon = Pokemon(context: privateContext)
pokemon.name = JSONPokemon.name
pokemon.url = JSONPokemon.detailUrl
}
//Why does the privateContext work here
//and doesn't crash my app.
try privateContext.save()
try privateContext.parent?.save()
completion()
} catch let err {
print("Unable to decode PokemonJSON. Error: ",err)
completion()
}
}.resume()
}
func fetchMoreDetails(pokemon: Pokemon, urlString: String, context: NSManagedObjectContext, completion: #escaping ()->()) {
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let err = error {
print("Unable to get more details for pokemon", err)
}
guard let data = data else { return }
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let pokemonDetailJSON = try decoder.decode(PokemonDetailJSON.self, from: data)
pokemonDetailJSON.types.forEach { (nestedType) in
// I can just change this to context and it works
// Why doesnt it work with privateContext and crashes my app
let type = Type(context: privateContext)
type.name = nestedType.type.name
pokemon.addToTypes(type)
}
try privateContext.save()
try privateContext.parent?.save()
completion()
} catch let err {
print("Unable to decode pokemon more details", err)
completion()
}
}.resume()
}
}
So in the downloadPokemonsFromSever, I make a call to an api that parse the json and saves it into Coredata. I followed my instructor instructions, by creating a privateContext and then setting its parent to my mainContext. Then I create a new Pokemon that has a name & url using my privateContext and NOT my mainContext. When I completely parse my Pokemon I go into another api that has more details on that Pokemon.
This is where my app starts to crash. As you can see from the fetchMoreDetails there is a parameter that is context. When I try to create a new Type with privateContext it crashes my app. When I use the context that is passed through it works fine. I would like to know why privateContext works inside downloadPokemonFromServer and not in fetchMoreDetails. I left a comment above the line that I think that crashes my app. This is how I call it in my ViewController, using this action.
#objc func handleRefresh() {
Service.shared.downloadPokemonsFromServer {
let context = CoreDataManager.shared.persistentContainer.viewContext
self.pokemonController.fetchedObjects?.forEach({ (pokemon) in
Service.shared.fetchMoreDetails(pokemon: pokemon, urlString: pokemon.url ?? "", context: context) {
}
})
}
tableView.refreshControl?.endRefreshing()
}

How to add to an NSSet using Core Data in Swift 5

So I'm practicing a little more with core data after finishing a course. So I am still a little new to it. So I Have 3 entities named Pokemon, Type & Ability. So a Pokemon can have many types like Fire,Water,Flying and so on. Type can also have multiple Pokemon that are Fire,Water,Flying and so on. Same goes for the Ability, so I made a many-to-many relationship. Here is how it looks like.
I am parsing some JSON form an api and trying to save it into core data. Now here is where I am having a bit of trouble. This is how my code looks and it just basically parse the JSON.
struct Service {
static let shared = Service()
func downloadPokemonsFromServer(completion: #escaping ()->()) {
let urlString = "https://pokeapi.co/api/v2/pokemon?limit=9"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let err = error {
print("Unable to fetch pokemon", err)
}
guard let data = data else { return }
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let pokemonJSON = try decoder.decode(PokemonsJSON.self, from: data)
pokemonJSON.pokemons.forEach { (JSONPokemon) in
let pokemon = Pokemon(context: privateContext)
pokemon.name = JSONPokemon.name
pokemon.url = JSONPokemon.detailUrl
//Would want to set pokemon types here but
//When i call fetchMoreDetails(pokemon:,urlString:,completion:)
//The pokemon is always nil inside fetchMoreDetails
}
try privateContext.save()
try privateContext.parent?.save()
completion()
} catch let err {
print("Unable to decode PokemonJSON. Error: ",err)
completion()
}
}.resume()
}
func fetchMoreDetails(pokemon: Pokemon, urlString: String, completion: #escaping ()->()) {
guard let url = URL(string: urlString) else { return }
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let err = error {
print("Unable to get more details for pokemon", err)
}
guard let data = data else { return }
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let pokemonDetailJSON = try decoder.decode(PokemonDetailJSON.self, from: data)
pokemonDetailJSON.types.forEach { (nestedType) in
let type = Type(context: privateContext)
type.name = nestedType.type.name
//How do I add type to pokemon.types this does work
//pokemon.types?.adding(type)
}
try privateContext.save()
try privateContext.parent?.save()
completion()
} catch let err {
print("Unable to decode pokemon more details", err)
completion()
}
}.resume()
}
}
I am able to parse everything fine and all but I just can't seem to add a new type to pokemons.types. I have look on stack overflow but most of the solutions seem to be in Objective C.
This is how my ViewController looks like and I am also using a NSFetchResultController.
class PokemonTableVC: UITableViewController {
lazy var pokemonController: NSFetchedResultsController<Pokemon> = {
let context = CoreDataManager.shared.persistentContainer.viewContext
let request: NSFetchRequest<Pokemon> = Pokemon.fetchRequest()
let nameSort = NSSortDescriptor(key: "name", ascending: true)
request.sortDescriptors = [nameSort]
let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
controller.delegate = self
return controller
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
tableView.refreshControl = refreshControl
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Delete", style: .done, target: self, action: #selector(handleDelete))
try? pokemonController.performFetch()
}
#objc func handleDelete() {
print("Deleting")
let context = CoreDataManager.shared.persistentContainer.viewContext
guard let pokemons = pokemonController.fetchedObjects else { return }
pokemons.forEach { (pokemon) in
context.delete(pokemon)
}
do {
try context.save()
} catch let err {
print("Unable to save data", err)
}
}
#objc func handleRefresh() {
print("DDDDD")
Service.shared.downloadPokemonsFromServer {
self.pokemonController.fetchedObjects?.forEach({ (pokemon) in
print(pokemon.name)
Service.shared.fetchMoreDetails(pokemon: pokemon, urlString: pokemon.url ?? "") {
print(pokemon.abilities?.count)
}
})
}
tableView.refreshControl?.endRefreshing()
}
}
I can provide my other structs if needed. But basically I am trying to add a type to pokemon.types would also like to add fetchMoreDetails when I fetch pokemons where I put the comment at. Would
really appreciate any feedback.
When you add a relationship to an entity Xcode creates methods for getting and setting values for that relationship using a pre-defined naming standard. So you should have some methods in your Pokemon class for setting Type instances (and code completion should be able to help here):
addToTypes(value:) // single object
addToTypes(values:) //set of objects
So in your code it should be
pokemon.addToTypes(value: type)
You also have the same methods on Type for the opposite direction

Data tasks outside ViewController

I'm gonna start with I'm currently learning swift + iOS so I'm by no means an experienced developer or one for that matter.
My goal is to separate any network calls that are currently done in my view controller to a dedicated class outside of it.
In this view controller i have a IBAction with the following code inside of it:
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: loginRequest) {
(data, response, error) in
guard let _ = response, let data = data else {return}
do {
let apiData = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
let token = apiData.data?.token
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
} catch let decodeError as NSError {
print("Decoder error: \(decodeError.localizedDescription)\n")
return
}
}.resume()
What I want, or I think I want to achieve is something like this:
let apiData = "somehow get it from outside"
Then when apiData has info stored in it, execute this next bit of code:
let token = apiData.data?.token
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
How would I achieve this? Thank you.
You can try
class API {
static func userLoginWith(email:String,password:String,completion:#escaping(_ token:String?) -> ()) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: loginRequest) {
(data, response, error) in
guard let _ = response, let data = data else { completion(nil) ; return }
do {
let apiData = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
completion(apiData.data?.token)
} catch {
print("Decoder error: ",error")
completion(nil)
}
}.resume()
}
}
Inside the VC
API.userLoginWith(email:<##>,password:<##>) { (token) in
if let token = token {
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
}
}

Converting Swift ios Networking to use Alamofire

I got a source code from a github page written in swift and implementing GoogleMaps. I now want to refactor the codes to use Alamofire and SwiftyJSON so that I can improve the code but I got confused because through my learning of swift I used Alamofire and swiftyJSON for every networking process so I am confused currently. the code below
typealias PlacesCompletion = ([GooglePlace]) -> Void
typealias PhotoCompletion = (UIImage?) -> Void
class GoogleDataProvider {
private var photoCache: [String: UIImage] = [:]
private var placesTask: URLSessionDataTask?
private var session: URLSession {
return URLSession.shared
}
let appDelegate = UIApplication.shared.delegate as! AppDelegate
func fetchPlacesNearCoordinate(_ coordinate: CLLocationCoordinate2D, radius: Double, types:[String], completion: #escaping PlacesCompletion) -> Void {
var urlString = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=\(coordinate.latitude),\(coordinate.longitude)&radius=\(radius)&rankby=prominence&sensor=true&key=\(appDelegate.APP_ID)"
let typesString = types.count > 0 ? types.joined(separator: "|") : "food"
urlString += "&types=\(typesString)"
urlString = urlString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) ?? urlString
guard let url = URL(string: urlString) else {
completion([])
return
}
if let task = placesTask, task.taskIdentifier > 0 && task.state == .running {
task.cancel()
}
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
}
placesTask = session.dataTask(with: url) { data, response, error in
var placesArray: [GooglePlace] = []
defer {
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
completion(placesArray)
}
}
guard let data = data,
let json = try? JSON(data: data, options: .mutableContainers),
let results = json["results"].arrayObject as? [[String: Any]] else {
return
}
results.forEach {
let place = GooglePlace(dictionary: $0, acceptedTypes: types)
placesArray.append(place)
if let reference = place.photoReference {
self.fetchPhotoFromReference(reference) { image in
place.photo = image
}
}
}
}
placesTask?.resume()
}
func fetchPhotoFromReference(_ reference: String, completion: #escaping PhotoCompletion) -> Void {
if let photo = photoCache[reference] {
completion(photo)
} else {
let urlString = "https://maps.googleapis.com/maps/api/place/photo?maxwidth=200&photoreference=\(reference)&key=\(appDelegate.APP_ID)"
guard let url = URL(string: urlString) else {
completion(nil)
return
}
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
}
session.downloadTask(with: url) { url, response, error in
var downloadedPhoto: UIImage? = nil
defer {
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
completion(downloadedPhoto)
}
}
guard let url = url else {
return
}
guard let imageData = try? Data(contentsOf: url) else {
return
}
downloadedPhoto = UIImage(data: imageData)
self.photoCache[reference] = downloadedPhoto
}
.resume()
}
}
}
any help to refactor the codes to use Alamofire and swiftyJSON would be appreciated.
Both Alamofire and SwiftyJSON have pretty decent instructions, and there are plenty of examples online to look for. However, this would be a decent starting point - you need to replace your session.dataTask and session.downloadTask with Alamofire methods. For example, instead of:
session.downloadTask(with: url) { url, response, error in
var downloadedPhoto: UIImage? = nil
defer {
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
completion(downloadedPhoto)
}
}
guard let url = url else {
return
}
guard let imageData = try? Data(contentsOf: url) else {
return
}
downloadedPhoto = UIImage(data: imageData)
self.photoCache[reference] = downloadedPhoto
}
.resume()
use this skeleton and implement your models and logic:
Alamofire
.request(url)
.responseJSON { dataResponse in
switch dataResponse.result {
case .success:
guard let json = JSON(dataResponse.data) else {
return
}
// Continue parsing
case .failure(let error):
// Handle error
print("\(error)")
}
}

Nested dataTaskWithRequest in Swift tvOS

I'm a C# developer convert to Swift tvOs and just starting to learn. I've made some progress, but not sure how to handle nested calls to json. The sources are from different providers so I can't just combine the query.
How do I wait for the inner request to complete so the TVSeries has the poster_path? Is there a better way to add the show to the collection and then process the poster path loading in another thread so it doesn't delay the UI Experience?
func downloadTVData() {
let url_BTV = NSURL(string: BTV_URL_BASE)!
let request_BTV = NSURLRequest(URL: url_BTV)
let session_BTV = NSURLSession.sharedSession()
//get series data
let task_BTR = session_BTV.dataTaskWithRequest(request_BTV) { (data_BTV, response_BTV, error_BTV) -> Void in
if error_BTV != nil {
print (error_BTV?.description)
} else {
do {
let dict_BTV = try NSJSONSerialization.JSONObjectWithData(data_BTV!, options: .AllowFragments) as? Dictionary<String, AnyObject>
if let results_BTV = dict_BTV!["results"] as? [Dictionary<String, AnyObject>]{
for obj_BTV in results_BTV {
let tvshow = TVSeries(tvDict: obj_BTV)
//for each tv series try to load a poster_path from secondary provider
if let str = obj_BTV["title"] as? String!{
let escapedString = str?.stringByAddingPercentEncodingWithAllowedCharacters(.URLQueryAllowedCharacterSet())!
if let url = NSURL(string: self.SEARCH_URL_BASE + escapedString!) {
let request = NSURLRequest(URL: url)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
if error != nil {
print (error?.description)
} else {
do {
let dict = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments) as? Dictionary<String, AnyObject>
if let results = dict!["results"] as? [Dictionary<String, AnyObject>] {
//iterate through the poster array
for obj in results {
if let path = obj["poster_path"] as? String {
tvshow.posterPath = path
break
}
}
}
} catch let error as NSError {
print(error.description)
}
}
}
task.resume()
}
}
self.tvSeries.append(tvshow)
}
dispatch_async(dispatch_get_main_queue()){
self.collectionView.reloadData()
}
}
} catch let error as NSError {
print(error.description)
}
}
}
task_BTR.resume()
}
Thanks for your help!
I would recommend breaking things apart into multiple methods, with callbacks to sequence the operations, and utilizing Swift's built-in throws error handling mechanism. Here's an example, not perfect, but might help as a starting point:
class TVSeries
{
let title: String
var posterPath: String?
enum Error: ErrorType {
case MalformedJSON
}
init(tvDict: [String: AnyObject]) throws
{
guard let title = tvDict["title"] as? String else {
throw Error.MalformedJSON
}
self.title = title
}
static func loadAllSeries(completionHandler: [TVSeries]? -> Void)
{
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: BTV_URL_BASE)!) { data, response, error in
guard let data = data else {
print(error)
completionHandler(nil)
return
}
do {
completionHandler(try fromJSONData(data))
}
catch let error {
print(error)
}
}.resume()
}
static func fromJSONData(jsonData: NSData) throws -> [TVSeries]
{
guard let dict = try NSJSONSerialization.JSONObjectWithData(jsonData, options: .AllowFragments) as? [String: AnyObject] else {
throw Error.MalformedJSON
}
guard let results = dict["results"] as? [[String: AnyObject]] else {
throw Error.MalformedJSON
}
return try results.map {
return try TVSeries(tvDict: $0)
}
}
func loadPosterPath(completionHandler: () -> Void)
{
guard let searchPath = title.stringByAddingPercentEncodingWithAllowedCharacters(.URLQueryAllowedCharacterSet()) else {
completionHandler()
return
}
let url = NSURL(string: SEARCH_URL_BASE)!.URLByAppendingPathComponent(searchPath)
NSURLSession.sharedSession().dataTaskWithURL(url) { [weak self] data, response, error in
defer { completionHandler() }
guard let strongSelf = self else { return }
guard let data = data else {
print(error)
return
}
do {
strongSelf.posterPath = try TVSeries.posterPathFromJSONData(data)
}
catch let error {
print(error)
}
}.resume()
}
static func posterPathFromJSONData(jsonData: NSData) throws -> String?
{
guard let dict = try NSJSONSerialization.JSONObjectWithData(jsonData, options: .AllowFragments) as? [String: AnyObject] else {
throw Error.MalformedJSON
}
guard let results = dict["results"] as? [[String: AnyObject]] else {
throw Error.MalformedJSON
}
for result in results {
if let path = result["poster_path"] as? String {
return path
}
}
return nil
}
}
It might also be worth your time to look into something like RxSwift or Alamofire, which help you with these kinds of data-conversion / sequencing operations.