I'm trying to make an array from my Viewcontroller equal to, the objects my core data has saved. I'm using core data and created an entity named Pokemon which has 3 attributes name, id and generation. In the app delegate, I use the following function to get Pokemon from this API. This is what I do to parse the data and save the context:
typealias DownloadCompleted = () -> ()
var pokemonId: Int16 = 0
func fetchPokemon(url: String, completed: #escaping DownloadCompleted) {
let context = coreData.persistentContainer.viewContext
let url = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: url) { (data, repsonse, error) in
if error != nil {
print(error!)
}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! NSDictionary
let jsonArray = jsonResult.value(forKey: "results") as! [[String: Any]]
for pokemonData in jsonArray {
self.pokemonId += 1
if self.pokemonId > 721 {
self.coreData.saveContext()
return
}
guard let name = pokemonData["name"] as? String else {
return
}
let pokemon = Pokemon(context: context)
pokemon.name = name
pokemon.id = self.pokemonId
print("Name: \(pokemon.name) Id:\(self.pokemonId)")
if self.pokemonId <= 151 {
pokemon.generation = 1
} else if self.pokemonId <= 251 {
pokemon.generation = 2
} else if self.pokemonId <= 386 {
pokemon.generation = 3
} else if self.pokemonId <= 493 {
pokemon.generation = 4
} else if self.pokemonId <= 649 {
pokemon.generation = 5
} else if self.pokemonId <= 721 {
pokemon.generation = 6
}
}
guard let nextURL = jsonResult.value(forKey: "next") as? String else {
self.coreData.saveContext()
return
}
DispatchQueue.main.async {
self.fetchPokemon(url: nextURL, completed: {
self.coreData.saveContext()
})
completed()
}
} catch let err {
print(err.localizedDescription)
}
}
task.resume()
}
This is how I call it in the appDelegate. Really don't know what to do in the middle of the fetchPokemon or how to call it in another view controller. So I left it blank, not sure if this has something to do with the problem I'm having.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let context = self.coreData.persistentContainer.viewContext
let pokemonListVC = self.window?.rootViewController as! PokemonListVC
pokemonListVC.context = context
fetchPokemon(url: pokemonAPI) {
}
return true
}
Im using this SQL-Light read-only app from the app store. I check the data and all 721 pokemon are saving. Now, I don't know how I would be able to make the array in my view controller equal to all 721 Pokemon saved. I added this code into my viewController.
class PokemonListVC: UIViewController {
weak var context: NSManagedObjectContext! {
didSet {
return pokemon = Pokemon(context: context)
}
}
var pokemon: Pokemon? = nil
lazy var pokemons = [Pokemon]()
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}
func loadData() {
pokemons = pokemon!.loadPokemon(generation: 1, context: context)
}
}
I've created an extension of my Pokemon entity and added a function loadPokemon that filters the Pokemon by generation. Here is the code.
extension Pokemon {
func loadPokemon(generation: Int16 = 0, context: NSManagedObjectContext) -> [Pokemon] {
let request: NSFetchRequest<Pokemon> = Pokemon.fetchRequest()
request.predicate = NSPredicate(format: "generation = %#", generation)
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
do {
let pokemons = try context.fetch(request)
print("My Pokemon count: \(pokemons.count)")
return pokemons
} catch let err {
print(err.localizedDescription)
}
return []
}
}
When I call the loadData in my ViewController it crashes. The array count is 0 and so is the one in the hero extension. So I don't how to make my array equal the Pokemon saved from coreData.
Would really appreciate any help provided. :)
Here is my deleteRecords code, which is also in my appDelegate. This deletes all records when app launches. I call this method at the very beginning of didFinishLaunchingWithOption function before the fetchPokemons.
func deleteRecords() {
let context = coreData.persistentContainer.viewContext
let pokemonRequest: NSFetchRequest<Pokemon> = Pokemon.fetchRequest()
var deleteRequest: NSBatchDeleteRequest
var deleteResults: NSPersistentStoreResult
do {
deleteRequest = NSBatchDeleteRequest(fetchRequest: pokemonRequest as! NSFetchRequest<NSFetchRequestResult>)
deleteResults = try context.execute(deleteRequest)
} catch let err {
print(err.localizedDescription)
}
}
As you are saying that you have sure that all the pockemon records are stored correctly in your coredata you can simply fetch records from your codedata by providing fetch request. I have created demo for contact storing and I can get all the contact by this fetch request you can try this code in your ViewController where you want to fetch all the record.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject> (entityName: "Pokemon")
do {
arrPockemon = try managedContext.fetch(fetchRequest)
}catch let error as NSError {
showAlert(string: error.localizedDescription)
}
try to get all records first and if you get all then work for filtering extension and all. hope it will help you. you can learn from here https://code.tutsplus.com/tutorials/core-data-and-swift-core-data-stack--cms-25065
save flag on userDefault.
//check for first time when app is installed first time(first time flag is not present so)
let userDefault = UserDefaults.standard.dictionaryRepresentation()
if userDefault.keys.contains("isDataAvailable") {
//key is availebe so check it
if userDefault["isDataAvailable"] as! String == "1"{
//no need to call server for data
}else{
//fetch data from server
// once you get data from server make isDataAvailable flage as 1
UserDefaults.standard.setValue("1", forKey: "isDataAvailable")
UserDefaults.standard.synchronize()
}
}
else{
//flag is not avalable so call server for data
// once you get data from server make isDataAvailable flage as 1
UserDefaults.standard.setValue("1", forKey: "isDataAvailable")
UserDefaults.standard.synchronize()
}
Related
I've have an entity called Places and an attribute(placeMark) which is and of Transformable.
I'm having an issue when I close the app and re-launch it, it queries CoreData and the records are there however the timestamp of each of these places is exactly the same.
**Code to write to CoreData **
private func store(_ location: CLLocation) {
if let context = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext {
let newData = Places(context: context)
CLGeocoder().reverseGeocodeLocation(location, completionHandler:
{ (placeMarks, error) in
if (error != nil) {
print("reverse geodcode fail: \(error!.localizedDescription)")
}
let placeMark = placeMarks! as [CLPlacemark]
if placeMark.count > 0 {
// add to the database
newData.placeMark = placeMark[0]
print(newData.placeMark?.location?.timestamp as Any)
self.places.append(newData)
self.lastPlaceForAllLocations = newData
}
//Save the context
(UIApplication.shared.delegate as? AppDelegate)?.saveContext()
// test if this works if we clear all placemarks
//self.getLocationFromDatabase()
})
}
}
Once I get a location(ClLocation) I am able to store, erase my context and read back time(different timestamps).
I use the code below to read the database.
**Core to read from CoreData **
private func getLocationFromDatabase() {
places.removeAll()
if let context = (UIApplication.shared.delegate as? AppDelegate)?
.persistentContainer.viewContext {
let request: NSFetchRequest<Places> = Places.fetchRequest()
/*let sort = NSSortDescriptor(key: "placeMark.location.timestamp", ascending: false)
request.sortDescriptors = [sort]*/
if let placesCoreData = try?
context.fetch(request) {
if let placesHere = placesCoreData as? [Places] {
print(placesHere)
for item in placesHere {
print(item.placeMark?.location?.timestamp)
self.places.append(item)
}
lastPlaceForAllLocations = places.last
// TODO
//tableView.reloadData()
}
}
}
lastPlaceForAllLocations = places.first
}
I try storing a Placemark and then I clear my buffer and read and its ok(I am able to see two places with different timestamps), however once, launch the app again, I am able to read to read both placemarks's but they have the same timestamp.
Any help would be appreciated!
I am using CoreData to display cached data while new data is loaded and then updated onto a tableView
The cached data is loaded fine but the problem is as soon as API is called to load the new data , the tableView becomes unresponsive i.e user can't scroll on the table or click anything and when the new data has completely loaded , it updates the tableView and it becomes responsive again
What I want to achieve is the app displays the cache data right away and the api is called and data loaded in the background.The tableView shouldn't be unresponsive while the data is being loaded and when the data loading from API is complete User can click a refresh button or swipe up to update the data
// DID LOAD
override func viewDidLoad() {
super.viewDidLoad()
print("did load")
getAvatar()
tableView.separatorStyle = .none
updateTableContents()
self.tableView.delegate = self
self.tableView.dataSource = self
}
My function where I display cache Data and call the API
func updateTableContents()
{
do {
try self.fetchedhResultController.performFetch()
print("COUNT FETCHED FIRST: \(self.fetchedhResultController.sections?[0].numberOfObjects)")
} catch let error {
print("ERROR: \(error)")
}
print("function called")
let retrievedToken: String? = KeychainWrapper.standard.string(forKey: "acessTokenKey")
let headers = [
"Authorization" : "Bearer "+retrievedToken!,
"Content-Type" : "application/json"
]
let url = "someURL"
Alamofire.request(url, method: .get , headers: headers).responseJSON { response in
switch response.result {
case .success:
let json = response.result.value as! [String:Any]
let data = json["data"] as! [[String : Any]]
self.clearData()
self.saveInCoreDataWith(array: data)
self.nextToken = json["nextPageToken"] as? String ?? "empty"
print("Token = "+self.nextToken!)
for dic in data{
self.news.append(News(dictionary: dic))
print(self.news.count)
}
DispatchQueue.main.async {
self.loadingIndicator.stopAnimating()
self.tableView.reloadData()
}
case .failure: break
}
}
}
My tableView Code
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let count = fetchedhResultController.sections?.first?.numberOfObjects {
return count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "NewsCell") as! NewsCell
if let fetchedNews = fetchedhResultController.object(at: indexPath) as? NewsObject {
cell.test(object : fetchedNews)
print(self.count)
self.count+=1;
}
and all the Core Data storing and fetching functionality
// Creating an Object
private func createNewsEntityFrom(dictionary: [String: Any]) -> NewsObject {
let context = CoreDataStack.sharedInstance.managedObjectContext
let newsEntity = NewsObject(context: context)
newsEntity.newsAuthor = dictionary["author"] as? String ?? "default"
newsEntity.newsTitle = dictionary["title"] as? String ?? "default"
let images = dictionary["image"] as? [String: Any]
newsEntity.newsImageURL = images?["link"] as? String ?? "default"
newsEntity.newsID = dictionary["_id"] as? String ?? "default"
newsEntity.newsPublisher = dictionary["publisher"] as? String ?? "default"
newsEntity.newsPublishorIconURL = dictionary["shortenedLogo"] as? String ?? "default"
newsEntity.liked = dictionary["liked"] as? Bool ?? false
newsEntity.bookmarked = dictionary["bookmarked"] as? Bool ?? false
return newsEntity
}
// Saving Data in Core Data
private func saveInCoreDataWith(array: [[String: Any]]) {
for dict in array {
_ = self.createNewsEntityFrom(dictionary: dict)
}
do {
try CoreDataStack.sharedInstance.persistentContainer.viewContext.save()
} catch let error {
print(error)
}
}
// Fetching Data from Core Data
lazy var fetchedhResultController: NSFetchedResultsController<NSFetchRequestResult> = {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "NewsObject")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "newsID", ascending: true)]
let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataStack.sharedInstance.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
return frc
}()
// Function used to Clear Data from Core Data
private func clearData() {
do {
let context = CoreDataStack.sharedInstance.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "NewsObject")
do {
let objects = try context.fetch(fetchRequest) as? [NSManagedObject]
_ = objects.map{$0.map{context.delete($0)}}
CoreDataStack.sharedInstance.saveContext()
} catch let error {
print("ERROR DELETING : \(error)")
}
}
}
I was following this tutorial to know how to implement CoreData if that helps https://medium.com/#jamesrochabrun/parsing-json-response-and-save-it-in-coredata-step-by-step-fb58fc6ce16f
EDIT :
Tried calling Alamofire completion handler in background thread
func updateTableContents()
{
do {
try self.fetchedhResultController.performFetch()
print("COUNT FETCHED FIRST: \(self.fetchedhResultController.sections?[0].numberOfObjects)")
} catch let error {
print("ERROR: \(error)")
}
print("function called")
let retrievedToken: String? = KeychainWrapper.standard.string(forKey: "acessTokenKey")
let headers = [
"Authorization" : "Bearer "+retrievedToken!,
"Content-Type" : "application/json"
]
let url = "https://api.tapin.news/v1/posts/home"
Alamofire.request(url, method: .get , headers: headers).responseJSON { response in
DispatchQueue.global(qos: .background).async {
switch response.result {
case .success:
let json = response.result.value as! [String:Any]
let data = json["data"] as! [[String : Any]]
self.nextToken = json["nextPageToken"] as? String ?? "empty"
print("Token = "+self.nextToken!)
self.clearData()
self.saveInCoreDataWith(array: data)
case .failure: break
}
}
self.loadingIndicator.stopAnimating()
self.tableView.reloadData()
}
}
EDIT 2 : So I was testing around a bit and I don't think its an issue of background Tasks , since I moved the fetching code from updateTableContents to viewDidLoad and removed the function from viewDidLoad. So background API calling and saving to coreData is not even being performed
Still the UI takes a couple seconds to become responsive during which time I see no images , and then as soon as images load it becomes responsive
override func viewDidLoad() {
super.viewDidLoad()
print("did load")
getAvatar()
tableView.separatorStyle = .none
self.tableView.delegate = self
self.tableView.dataSource = self
do {
try self.fetchedhResultController.performFetch()
print("COUNT FETCHED FIRST: \(self.fetchedhResultController.sections?[0].numberOfObjects)")
} catch let error {
print("ERROR: \(error)")
}
}
// Fetching Data from Core Data
lazy var fetchedhResultController: NSFetchedResultsController<NSFetchRequestResult> = {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "NewsObject")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "newsID", ascending: true)]
let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataStack.sharedInstance.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
return frc
}()
EDIT 3 : Here is my CoreDataStack
import UIKit
import CoreData
class CoreDataStack: NSObject {
static let sharedInstance = CoreDataStack()
private override init() {}
lazy var managedObjectContext : NSManagedObjectContext = {
return self.persistentContainer.viewContext
}()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyAppName")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
func applicationDocumentsDirectory() {
if let url = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).last {
print(url.absoluteString)
}
}
}
In your Alamofire's completion handler, you assume it is being run in background thread, and then you call the main thread and update the tableView.
The reality is: Alamofire's completions handlers already run in the main thread.
This is causing your UI blockings, because you have expensive methods there, like these ones:
self.clearData()
self.saveInCoreDataWith(array: data)
Also this one might be expensive:
for dic in data {
self.news.append(News(dictionary: dic))
print(self.news.count)
}
Try wrapping your Alamofire completion handler in a background thread:
DispatchQueue.global(qos: .background).async {
<#code#>
}
coreData returns empty data when there should not be any, even if you uninstall the application and reinstall it and make a request to Сore Data, the context.fetch returns the data
get all Data in Сore Data
func getMyLoadBook(){
words.removeAll()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let fetchRequest:NSFetchRequest<Favorite> = Favorite.fetchRequest()
fetchRequest.returnsObjectsAsFaults = false
do {
let result = try! context.fetch(fetchRequest)
print(result)
if result.isEmpty {
emptyBookMark()
return
} else {
tableView.isHidden = false
}
for data in result as [NSManagedObject] {
if let _ = data.value(forKey: "word"){
let initData = Words(word: (data.value(forKey: "word") as? [String]) ?? [""], wordDesc: (data.value(forKey: "wordDesc") as? [String]) ?? nil, translation: (data.value(forKey: "translation") as? [String]) ?? [""], translDesc: (data.value(forKey: "translDesc") as? [String]) ?? nil)
words.append(initData)
}
}
}
tableView.reloadData()
}
I have these functions, but they are not called when I get data from coreData
// creates a path and checks for the presence of an element
static func coreDataResult(data: [[String?]?]?, completion: #escaping (NSFetchRequest<NSFetchRequestResult>, Favorite?, NSManagedObjectContext) -> ()){
guard let w = data?.first, let word = w, let t = data?.last, let transl = t else { return }
DispatchQueue.main.async {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
guard let entity = NSEntityDescription.entity(forEntityName: "Favorite", in: context) else { return }
guard let taskObject = NSManagedObject(entity: entity, insertInto: context) as? Favorite else { return }
let predicate = NSPredicate(format: "word == %#", word)
let predicate2 = NSPredicate(format: "translation == %#", transl)
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Favorite")
let andPredicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, predicate2])
fetchRequest.predicate = andPredicate
completion(fetchRequest, taskObject, context)
}
}
// remove data from Сore Data
static func deleteFromCoreData(data: [[String?]?]?){
coreDataResult(data: data, completion: { (result, taskObject, context) in
do {
let fetchedEntities = try context.fetch(result) as! [Favorite]
if let entityToDelete = fetchedEntities.first {
context.delete(entityToDelete)
}
do {
try context.save()
if let data = getDataFromContext(result:fetchedEntities){
Analytics.logEvent("RemovedFavorite", parameters: ["word": data.0, "translation": data.1])
YMMYandexMetrica.reportEvent("RemovedFavorite", parameters: ["word": data.0, "translation": data.1], onFailure: nil)
}
} catch {
print(error)
}
} catch { print(error) }
})
}
// add data to Сore Data
static func saveWithModelToCoreData(_ words: Words){
DispatchQueue.main.async {
coreDataResult(data: [words.word, words.translation], completion: { (result, taskObject, context) in
do {
let fetchedEntities = try context.fetch(result) as! [Favorite]
if let _ = fetchedEntities.first?.word {
print("the element already have in coreData")
} else {
taskObject?.setValue(words.word, forKey: "word")
taskObject?.setValue(words.translation, forKey: "translation")
taskObject?.setValue(words.descript, forKey: "wordDesc")
taskObject?.setValue(words.translDesc, forKey: "translDesc")
do {
try context.save()
} catch {
print(error)
}
}
} catch {
print(error)
}
})
}
}
that's what result returns
[<Favorite: 0x283478500> (entity: Favorite; id: 0x281306ee0 <x-coredata:///Favorite/t722DD7F9-8DD7-4AC4-AA20-02324AB1B08713> ; data: {
translDesc = nil;
translation = nil;
word = nil;
wordDesc = nil;
})
It seems that you are you using a simple core-data setup, where all read and write are done on the main thread to the viewContext. This setup is fine for simple application where you don't expect to do a bulk import, or have a huge amount of entities. It should simplify a lot of multithread issues so I am a little confused why you have such a complex setup with callbacks and DispatchQueue.main.async when everything should just simply run on the main thread. (Perhaps you are planing for a future with a more complex setup?).
In any event, one of the consequences of this is that any changes to the viewContext will appear in your app for the lifetime of the app, even if you don't call save. This is because there is a single context - so even it is not saved, it has still been changed.
In the method coreDataResult you create an empty object, and then in saveWithModelToCoreData it is either set with values and the context saved or it is found to already exist and no further action is taken. If coreDataResult returned on a background context that would be fine. The empty object would disappear when the background context. The problem is that you are writing to the viewContext so the context does not go away, and the object sticks around.
If the application would quit right then, you wouldn't see it in the next launch. But if save is called any time after, then the empty object will also be saved.
I would suggest not creating objects unless you already know that you want them. I would refactor so that there is a single function that checks for duplicate, and then creates and set or does nothing. As it is I don't see the value of the two different methods.
I am attempting to pull data from Firebase and then save it to CoreData but am having trouble with the async operation. I have a custom function that returns [ConversationStruct] upon completion. I then do a forEach to save it to CoreData.
However, my current implementation saves the object multiple times, ie Firebase have 10 entries, but CoreData would somehow give me 40 over entries which most are repeated. I suspect the problem is in my completionHandler.
//At ViewDidLoad of my VC when I pull the conversations from Firebase
FirebaseClient.shared.getConversationsForCoreData(userUID) { (results, error) in
if let error = error {
print(error)
} else if let results = results {
print(results.count)
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext
results.forEach({ (c) in
let conversation = Conversation(context: privateContext)
conversation.conversationStartTime = c.conversationStartTime
conversation.recipientID = c.recipientID
conversation.shoutoutID = c.shoutoutID
conversation.unreadMessagesCount = Int32(c.unreadMessagesCount!)
conversation.profileImage = c.profileImage
conversation.recipientUsername = c.recipientUsername
})
do {
try privateContext.save()
} catch let error {
print(error)
}
}
}
//At FirebaseClient
func getConversationsForCoreData(_ userUID: String, _ completionHandler: #escaping (_ conversations: [ConversationStruct]?, _ error: Error?) -> Void) {
var conversations = [ConversationStruct]()
ref.child("conversations").child(userUID).observeSingleEvent(of: .value) { (snapshot) in
for snap in snapshot.children {
let snapDatasnapshot = snap as! DataSnapshot
let snapValues = snapDatasnapshot.value as! [String: AnyObject]
let recipientUID = snapDatasnapshot.key
for (key, value) in snapValues {
//Some other logic
self.getUserInfo(recipientUID, { (results, error) in
if let error = error {
print(error.localizedDescription)
} else if let results = results {
let username = results["username"] as! String
let profileImageUrl = results["profileImageUrl"] as! String
URLClient.shared.getImageData(profileImageUrl, { (data, error) in
if let error = error {
print(error.localizedDescription)
} else if let imageData = data {
let convo = ConversationStruct(conversationStartTime: conversationStartTime, shoutoutID: shoutoutID, recipientID: shoutoutID, unreadMessagesCount: unreadMessagesCount, recipientUsername: username, profileImage: imageData)
conversations.append(convo)
}
completionHandler(conversations, nil)
})
}
})
}
}
}
}
struct ConversationStruct {
var conversationStartTime: Double
var shoutoutID: String
var recipientID: String
var unreadMessagesCount: Int?
var recipientUsername: String?
var profileImage: Data?
}
The print statement would print the count as and when the operation completes. This seems to tell me that privateContext is saving the entities when the results are consistently being downloaded which resulted in 40 over entries. Would anyone be able to point me out in the right direction how to resolve this?
Also, the implementation does not persist.
I'm trying to make 2 API calls on Segue invoke and ultimately pass Array of Data from Second Call to CollectionView. With first call I'm getting one value catID, which I need in order to make the other call:
let searchEndpoint: String = MY_ENDPOINT
// Add auth key
let serviceCallWithParams = searchEndpoint + "?PARAMETER"
guard let url = URL(string: serviceCallWithParams) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
// setting up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// making the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// error check
guard error == nil else {
print("error")
print(error)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse JSON
do {
guard let catData = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: AnyObject] else {
print("error converting data to JSON")
return
}
if let data = catData["data"] as? [String: Any] {
if let array = data["categories"] as? [Any] {
if let firstObject = array.first as? [String: Any] {
if let catId = firstObject["catId"] as? Int {
getTitles(catId: catId)
}
}
}
}
} catch {
print("error converting data to JSON")
return
}
}
task.resume()
And then getTitles function looks like this:
func getTitles(catId: Int) {
let catIdString = String(catId)
let titlesEndPoint: String = MY_ENDPOINT + catIdString
// Add auth key
let titlesEndPointWithParams = titlesEndPoint + "?PARAMETER"
guard let titlesUrl = URL(string: titlesEndPointWithParams) else {
print("Error: cannot create URL")
return
}
let titlesUrlRequest = URLRequest(url: titlesUrl)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: titlesUrlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET on listCategoryTitles")
print(error)
return
}
// make sure we got data
guard let titlesData = data else {
print("Error: did not receive data")
return
}
// parse the JSON
do {
guard let allTitles = try JSONSerialization.jsonObject(with: titlesData, options: []) as? [String: AnyObject] else {
print("error converting data to JSON")
return
}
if let titlesJson = allTitles["data"] as? [String: Any] {
if let titlesArray = titlesJson["titles"] as? Array<AnyObject> {
self.books = []
for (index, value) in titlesArray.enumerated() {
var book = Book()
book.bookTitle = value["title"] as? String
book.bookAuthor = value["author"] as? String
if let imageSource = value["_links"] as? Array<AnyObject> {
book.bookImageSource = imageSource[1]["href"] as? String
}
self.books?.append(book)
}
}
}
} catch {
print("error converting data to JSON")
return
}
}
task.resume()
}
Now when I put:
let resultsVC = segue.destination as? CollectionViewController
resultsVC?.books = self.books
outside function, in target controller I'm getting an empty array as output on first click, but on every next one I'm getting proper data.
When I try putting this inside function "getTitles" the output in CollectionViewController is "nil" every time.
Worth mentioning could be that I have "books" variable defined like so:
Main Controller:
var books: [Book]? = []
Collection Controller:
var books: [Book]?
and I have created type [Book] which is basically object with 3 string variables in separate struct.
All of the code above is encapsulated in
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowResults" {
Any help/guideline would be much appreciated!
When you make api call it will execute in background means asynchronously where as prepare(for:sender:) will call synchronously.
Now from your question it is looks like that you have create segue in storyboard from Button to ViewController, so before you get response from your api you are moved to your destination controller, to solved your issue you need to create segue from your Source ViewController to Destination ViewController and set its identifier. After that inside getTitles(catId: Int) method after your for loop perform segue on the main thread.
for (index, value) in titlesArray.enumerated() {
var book = Book()
book.bookTitle = value["title"] as? String
book.bookAuthor = value["author"] as? String
if let imageSource = value["_links"] as? Array<AnyObject> {
book.bookImageSource = imageSource[1]["href"] as? String
}
self.books?.append(book)
}
DispatchQueue.main.async {
self.performSegue(withIdentifier: "ShowResults", sender: nil)
}
After that inside your prepare(for:sender:) make changes like below.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowResults" {
let resultsVC = segue.destination as? CollectionViewController
resultsVC?.books = self.books
}
}