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.
Related
So I'm trying to store data retrieved from my Firestore database into an object. My database has a collection of users, and each user has a collection of classes. I want to be able to get the logged in users collection of classes and store them in an array of objects. Most of what I've tried so far can pull data but it won't save it into anything because its able access the data from within the completion handler. Any help would be great, here's the code I'm working with rn:
db.collection("users").whereField("uid", isEqualTo: uid).addSnapshotListener { (querySnapshot, error) in
if error == nil && querySnapshot != nil {
let docId = querySnapshot?.documents[0].documentID
db.collection("users").document(docId!).collection("classes").addSnapshotListener { (querySnap, error) in
guard let documents = querySnap?.documents else{print("No Classes");return}
var imageData:UIImage?
retrievedClasses = documents.map { (querySnap) -> UserClass in
let data = querySnap.data()
if let decodedData = Data(base64Encoded: data["class_img"] as! String, options: .ignoreUnknownCharacters){
imageData = UIImage(data: decodedData)
}
return UserClass.init(name: data["class_name"] as! String, desc: data["class_desc"] as! String, img: imageData!, color: data["class_color"] as! String, link: data["class_link"] as! String, location: data["class_location"] as! GeoPoint, meetingTime: data["meeting_time"] as! Dictionary<String,String>)
}
print(retrievedClasses[0].printClass())
}
}
}
As I understand you need to do something like this:
func getUserClasses(for userID: String, completion: #escaping (Result<[UserClass], Error>) -> Void) {
db.collection("users").whereField("uid", isEqualTo: userID).addSnapshotListener { (querySnapshot, error) in
if error == nil && querySnapshot != nil {
let docId = querySnapshot?.documents[0].documentID
db.collection("users").document(docId!).collection("classes").addSnapshotListener { (querySnap, error) in
guard let documents = querySnap?.documents else {
print("No Classes")
completion(.failure("No Classes"))
return
}
let retrievedClasses = documents.map { (querySnap) -> UserClass in
let data = querySnap.data()
var imageData: UIImage?
if let decodedData = Data(base64Encoded: data["class_img"] as! String,
options: .ignoreUnknownCharacters) {
imageData = UIImage(data: decodedData)
}
let user = UserClass(name: data["class_name"] as! String,
desc: data["class_desc"] as! String,
img: imageData!,
color: data["class_color"] as! String,
link: data["class_link"] as! String,
location: data["class_location"] as! GeoPoint,
meetingTime: data["meeting_time"] as! Dictionary<String,String>)
return user
}
completion(.success(retrievedClasses))
}
} else {
completion(.failure("Request Error"))
}
}
}
Then you will able to use data after request completion:
getUserClasses(for: "123") { result in
switch result {
case .success(let allClasses):
retrievedClasses = allClasses // retrievedClasses is a property in your class which you are going to use
if !retrievedClasses.isEmpty() {
print(retrievedClasses[0].printClass())
}
case .failure(let error):
print(error)
}
}
UPDATED WITH PROPOSED SOLUTION AND ADDITIONAL QUESTION
I'm officially stuck and also in callback hell. I have a call to Firebase retrieving all articles in the FireStore. Inside each article object is a an Image filename that translates into a storage reference location that needs to be passed to a function to get the absolute URL back. I'd store the URL in the data, but it could change. The problem is the ArticleListener function is prematurely returning the closure (returnArray) without all the data and I can't figure out what I'm missing. This was working fine before I added the self.getURL code, but now it's returning the array back empty and then doing all the work.
If anyone has some bonus tips here on chaining the methods together without resorting to PromiseKit or GCD that would be great, but open to all suggestions to get this to work as is
and/or refactoring for more efficiency / readability!
Proposed Solution with GCD and updated example
This is calling the Author init after the Article is being created. I am trying to transform the dataDict dictionary so it get's used during the Author init for key ["author"]. I think I'm close, but not 100% sure if my GCD enter/leave calls are happening in the right order
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
print("Error in setting up snapshot listener - \(error)")
} else {
let fireStoreDispatchGrp = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
//NEW EXAMPLE WITH ADDITIONAL TASK HERE
if let author = $0.data()["author"] as? DocumentReference {
author.getDocument() {(authorSnapshot, error) in
fireStoreDispatchGrp.enter() //1
if let error = error {
print("Error getting Author from snapshot inside Article getDocumentFunction - leaving dispatch group and returning early")
fireStoreDispatchGrp.leave()
return
}
if let newAuthor = authorSnapshot.flatMap(Author.init) {
print("Able to build new author \(newAuthor)")
dataDict["author"] = newAuthor
dataDict["authorId"] = authorSnapshot?.documentID
print("Data Dict successfully mutated \(dataDict)")
}
fireStoreDispatchGrp.leave() //2
}
}
///END OF NEW EXAMPLE
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
fireStoreDispatchGrp.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
dataDict["image"] = url.absoluteString
case .failure(let error):
print("Error getting URL for author: \n Error: \(error) \n forReference: \(reference) \n forArticleID: \(id)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
returnArray.append(newArticle)
}
fireStoreDispatchGrp.leave() ///3
}
}
}
//Completion block
print("Exiting dispatchGroup all data should be setup correctly")
fireStoreDispatchGrp.notify(queue: .main) { ///4
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
Original Code
Calling Setup Code
self.manager.SetupArticleListener() { [weak self] articles in
print("πππππππIn closure function to update articlesπππππππ")
self?.articles = articles
}
Article Listener
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
}
}
}
print("πππππππ sending back completion array \(returnArray)πππππππ")
completion(returnArray)
}
}
updateListeners(for: listener)
}
GetURL
private func getURL(reference: StorageReference, _ result: #escaping (Result<URL, Error>) -> Void) {
reference.downloadURL() { (url, error) in
if let url = url {
result(.success(url))
} else {
if let error = error {
print("error")
result(.failure(error))
}
}
}
}
You need dispatch group as the for loop contains multiple asynchronous calls
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
let g = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
g.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
g.leave() /// 3
}
}
}
g.notify(queue:.main) { /// 4
print("πππππππ sending back completion array \(returnArray)πππππππ")
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
I want to load all data from firebase, then show the data to the table view. But now, I can't show all the data to the table view. It is because call the finishLoading(realm) method is faster than the for loop get all the data. How can I do some show all data when for loop is finish in swift. I have to use the Closure, however the second of the loop is later than this "self.finishLoading(realm: realm)"
I have to try to add the DispatchGroup(), however, the leave() when having an error of EXC_BAD_INSTRUCTION. Can I put the leave() in the closure? How can I fix it?
func loopAllProduct(userId: String, finishLoadWhenErr:Bool, storedClosure: #escaping (DocumentSnapshot) -> Void){
let storage = Storage.storage()
let db = Firestore.firestore()
let userDocRef = db.collection("Users").document(userId).collection("Product")
userDocRef.getDocuments{(document, error) in
if let err = error {
print("Error getting documents: \(err)")
} else {
for document in document!.documents {
storedClosure(document)
}
}
}
}
func downloadData() {
let startTime = Date()
while updating {
let diffTime = Date(timeIntervalSinceReferenceDate: startTime.timeIntervalSinceReferenceDate)
if (diffTime.timeIntervalSinceNow < -5){
self.stopAnimating()
self.refreshControl?.endRefreshing()
print("Update Timeout")
return
}
}
updating = true
let storage = Storage.storage()
let db = Firestore.firestore()
let productLoading = NSMutableArray()
let realm = try! Realm()
print("all posts")
let group = DispatchGroup()
let addPosts: (DocumentSnapshot)->Void = {(document) in
try! realm.write {
if let resuls = self.realmResults {
realm.delete(resuls);
}
}
let product = Product()
product.id = document.documentID
product.userID = document.data()?["UserID"] as? String
product.userName = document.data()?["UserName"] as? String
product.descrition = document.data()?["Descrition"] as? String
product.postTime = document.data()?["PostTime"] as? Date
product.price = document.data()?["Price"] as? Double ?? 0.0
product.stat = (document.data()?["stat"] as? Int)!
product.productName = document.data()?["ProductName"] as? String
let productId = document.documentID
productLoading.add(productId)
try! realm.write {
realm.add(product)
}
group.leave()
}
let userDocRef = db.collection("Users")
userDocRef.getDocuments{(document, error) in
for document in document!.documents {
group.enter()
self.loopAllProduct(userId:document.documentID , finishLoadWhenErr: true, storedClosure: addPosts)
}
}
group.notify(queue: DispatchQueue.main) {
self.finishLoading(realm: realm)
}
}
I need to create an array of Categories that contains Questions array.
struct CategoryFB {
var title: String
var id: Int
var questions: [QuestionsFB]
var dictionary: [String : Any] {
return ["title" : title,
"id" : id]
}
}
extension CategoryFB {
init?(dictionary: [String : Any], questions: [QuestionsFB]) {
guard let title = dictionary["title"] as? String, let id = dictionary["id"] as? Int else { return nil }
self.init(title: title, id: id, questions: questions)
}
}
Firestore has a following structure
Collection("Categories")
Document("some_id")
Collection("Questions")
How to create array like this?
array = [Category(title: "First",
questions: [
Question("1"),
...
]),
... ]
My try was wrong:
db.collection("Categories").order(by: "id", descending: false).getDocuments {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
for document in querySnapshot!.documents {
print(document.documentID)
self.db.collection("Categories").document(document.documentID).collection("Questions").getDocuments(completion: { (subQuerySnapshot, error) in
if error != nil {
print(error!.localizedDescription)
} else {
var questionsArray: [QuestionsFB]?
questionsArray = subQuerySnapshot?.documents.compactMap({QuestionsFB(dictionary: $0.data())})
self.categoriesArray = querySnapshot?.documents.compactMap({CategoryFB(dictionary: $0.data(), questions: questionsArray!)})
print(self.categoriesArray![0].questions.count)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
})
}
}
}
Your main problem seems to stem from the fact that you're regenerating your categories array every time you run your subquery, and when you do, you're only supplying a single questions array to the entire thing.
There's lots of ways to fix this. I would probably break it up so that you a) First allow yourself to create a category array without any questions, and then b) Go back through each of your individual subQueries and insert them into your categories as you get them.
Your final code might look something like this. Note that this would mean changing your Category object so that you can first create it without a Questions array, and implementing this custom addQuestions:toCategory: method (which would be a whole lot easier if you stored your categories as a dictionary instead of an array)
db.collection("Categories").order(by: "id", descending: false).getDocuments {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
self.categoriesArray = querySnapshot?.documents.compactMap({CategoryFB(dictionary: $0.data()})
for document in querySnapshot!.documents {
print(document.documentID)
self.db.collection("Categories").document(document.documentID).collection("Questions").getDocuments(completion: { (subQuerySnapshot, error) in
if error != nil {
print(error!.localizedDescription)
} else {
var questionsArray: [QuestionsFB]?
questionsArray = subQuerySnapshot?.documents.compactMap({QuestionsFB(dictionary: $0.data())})
self.addQuestions(questionsArray toCategory: document.documentID)
print(self.categoriesArray![0].questions.count)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
})
}
}
}
Alternately, if you think you're going to be in a situation where you're always going to want to grab your questions every time you want to grab a category, you might consider not putting them in a subcollection at all, and just making them a map in the original category document.
This is the solution which I found by myself. Hopefully this will help someone in the future.
func getData(completion: #escaping (_ result: [Any]) -> Void) {
let rootCollection = db.collection("Categories")
var data = [Any]()
rootCollection.order(by: "id", descending: false).getDocuments(completion: {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
guard let topSnapshot = querySnapshot?.documents else { return }
for category in topSnapshot {
rootCollection.document(category.documentID).collection("Questions").getDocuments(completion: {
(snapshot, err) in
guard let snapshot = snapshot?.documents else { return }
var questions = [Question]()
for document in snapshot {
let title = document.data()["title"] as! String
let details = document.data()["details"] as! String
let article = document.data()["article"] as! String
let link = document.data()["link"] as! String
let id = document.data()["id"] as! String
let possibleAnswers = document.data()["possibleAnswers"] as! [String]
let rightAnswerID = document.data()["rightAnswerID"] as! Int
let newQuestion = Question(title: title, article: article, details: details, link: link, possibleAnswers: possibleAnswers, rightAnswerID: rightAnswerID, id: id)
questions.append(newQuestion)
}
let categoryTitle = category.data()["title"] as! String
let collectionID = category.data()["id"] as! Int
let newCategory = Category(title: categoryTitle, id: collectionID, questions: questions)
data.append(newCategory)
//Return data on completion
completion(data)
})
}
}
})
}
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()
}