How to retrieve data from CloudKit using SwiftUI - swift

I am using SwiftUI to make an app and storing the data on ICloud. Because all the code I can find relates to Swift and viewDidLoad and TableView, however this do not apply. I have written code and seems to retrieve it but will not return it to the ObservableObject to be able to display in the SwiftUI
The ObservableObject file:-
import SwiftUI
import Combine
final class ObservedData: ObservableObject {
#Published var venues = venuesData
}
The query to retrieve data
import SwiftUI
import CloudKit
var venuesData: [Venue] = loadVenues()
func loadVenues() -> [Venue] {
var data = [Venue]()
let pred = NSPredicate(value: true)
let sort = NSSortDescriptor(key: "id", ascending: true)
let query = CKQuery(recordType: "DeptfordHopData", predicate: pred)
query.sortDescriptors = [sort]
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["id","name", "address"]
operation.resultsLimit = 50
var newVenues = [Venue]()
operation.recordFetchedBlock = { record in
let venue = Venue()
venue.racordID = record.recordID
venue.id = record["id"]
venue.name = record["name"]
venue.address = record["address"]
newVenues.append(venue)
}
operation.queryCompletionBlock = {(cursor, error) in
DispatchQueue.main.async {
if error == nil {
data = newVenues
} else {
print(error!.localizedDescription)
}
}
}
CKContainer.default().publicCloudDatabase.add(operation)
return data
}
I have got data showing when do break in data but does not pass to venueData

The query operation runs asynchronously. When you add the operation to the database, the framework runs the query in the background and immediately returns control to your function, which promptly returns data, which is still set to the empty array.
To fix this, you either need to wait for the operation to complete using Grand Central Dispatch (not recommended if this is called directly from UI code, as it will block your application), or you need to rewrite loadVenues so it accepts a callback that you can then invoke inside queryCompletionBlock. The callback you provide to loadVenues should save the array of venues somewhere where SwiftUI will detect the change.

Related

Swift code is reading Firestore DB using addSnapshotListener but no snapshot or error is created

I have some very simple data in Firestore I am trying to read into my iOS app. There is one collection called "matches" with one document containing two fields "id" and "name".
With the code below I am trying to load the Firestore data into my array of matches, but it is not working. I can see in the Firestore usage data that the DB is being read, but no data is being saved to the local variables. Upon execution of this code I expected the matches array to have one object, but it remains empty. When I debug the code line by line, nothing is executing after this line:
collection.addSnapshotListener { (querySnapshot, error) in
Which to me indicates no snapshot or error are being produced, but I don't know why.
Full code:
import Foundation
import Firebase
class ContentModel: ObservableObject {
let db = Firestore.firestore()
// List of matches
#Published var matches = [Match]()
init() {
// Get database matches
getMatches()
}
func getMatches() {
// Specify path
let collection = db.collection("matches")
// Get documents
collection.addSnapshotListener { (querySnapshot, error) in
print("test") // this print statement never executes
if let error = error {
print("Error retrieving collection: \(error)")
}
else {
// Create an array for the matches
var matches = [Match]()
// Loop through the documents returned
for doc in querySnapshot!.documents {
// Create a new Match instance
var m = Match()
// Parse out the values from the document into the Match instance
m.id = doc["id"] as? String ?? UUID().uuidString
m.name = doc["name"] as? String ?? ""
// Add it to our array
matches.append(m)
}
// Assign our matches to the published property
DispatchQueue.main.async {
self.matches = matches
}
}
}
}
The ContentModel is instantiated in the main .swift file for the project as an environment object. Code below:
import SwiftUI
import Firebase
#main
struct AppName: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
MatchTabView().environmentObject(ContentModel())
}
}
}
Found the issue. The view was loading before the data could be read into my array. I'm not sure why the code is not executed when initially debugged, but the data is be read in once I added error handling so the view can load without data.

Swift & Core Data unwrapping fetchedresults

I'm pretty new to iOS and trying to understand how to work with Core Data. The basic challenge I struggle with is accessing the data from a fetchResult outside of a view, as if it were in an array or a database query result (which I thought this was)
Example:
Core Data Entity Foo has a 1:1 relationship with Core Data Entity Bar. I create a new Foo that needs to attach to an existing Bar.
I do a fetch<Bar> with a predicate that returns a fetchedResult object. I need to get access to the Bar object inside the fetchedResult object so that I can assign it:
newFoo.bar = fetched<Bar> doesn't work, and I can't figure out how to get to the Bar - I've spent probably 10 hours going through tutorials and Apple docs and can't find any way to unwrap it or access it - convert it or access it as if it were just a simple array of objects.
At this point I'm about to give up - I'm creating duplicate classes for the entities and when the app inits I load ALL of the Core Data from every entity and map it to Arrays that can be easily accessed, then just update Core Data with changes. That can't be how this is supposed to work.
What am I missing? This should be easy.
EDIT===========================
Added a simplified code section:
import Foundation
import CoreData
struct Create: View {
#Environment(\.managedObjectContext) var viewContext
#EnvironmentObject var show: ViewLogic
func newOnDemand() {
let newT = T(context: viewContext)
let newS = S(context: viewContext)
let existingPlace = Place.idRequest(id: placeId) <-- returns fetchedResults<Place>, I need Place or [Place]
newT.place = existingPlace <--- Place object required here. HOW?
newT.S = newS <--- New Objects. Simple.
do {
// print("Saving session...")
try viewContext.save()
} catch let error as NSError {
print("Failed to save session data! \(error): \(error.userInfo)")
}
=========================== fetch
class Place:
static func idRequest(id: UUID) -> NSFetchRequest<Place> {
let request: NSFetchRequest<Place> = Place.fetchRequest()
request.predicate = NSPredicate(format: "id == %#", id)
request.sortDescriptors = [NSSortDescriptor(key: "sortOrder", ascending: true)]
request.fetchLimit = 1 // use to specify specific queries. Position.
return request
=========================== entity class
import Foundation
import CoreData
extension T {
#nonobjc public class func fetchRequest() -> NSFetchRequest<T> {
return NSFetchRequest<T>(entityName: "T")
}
#NSManaged public var id: UUID
#NSManaged public var place: Place?
}
According to the code the method idRequest returns a fetch request, not any fetchresults. You have to call fetch on the managed object context passing the request to fetch the data
let request = Place.idRequest(id: placeId)
do {
if let existingPlace = try viewContext.fetch(request).first {
newT.place = existingPlace
// do other things
}
} catch {
print(error)
}

How to write to a variable from within the Firebase getDocument function (Swift)

I want to read a document, get a field from that document, and set a variable to that field's value
I expected my variable declared outside the Firebase getDocument function to be written to. The actual results are that the variable is being written to within the Firebase getDocument function but outside the function it is empty.
Here's what I have tried:
[1]: Modifying variable within firebase function - this didn't work for me because I can't translate it well with my current Swift skills
[2]: How to set variables from a Firebase snapshot (swift) - this didn't work for me because the implementation deviates by a lot from what I have right now
//open the user Firebase database by documentID
let userDocument = userdb.collection("users").document(userDocumentId)
var userjobsData = [[String:Any]]()
userDocument.getDocument { (docums, error) in
if let docum = docums, docum.exists {
//grab all jobs data
userjobsData = docum.get("jobData") as! [[String:Any]]
//sort jobs by category in alphabetical order
userjobsData = (userjobsData as NSArray).sortedArray(using: [NSSortDescriptor(key: "category", ascending: true)]) as! [[String:AnyObject]]
}
//Here userjobsData contains data
print(userjobsData)
}
//Here userjobsData is empty
print(userjobsData)
Actually what is happening in your case Firebase fetching data is asynchronous task and takes some time to fetching data , in mean time you are reading your userjobsData which is empty since Firebase request has not been completed .
What you can do is actually perform needed operation after fetching data from firebase .
Adding Sample code for your reference.
private func fetchDataFromFirebase(){
let userDocument = userdb.collection("users").document(userDocumentId)
var userjobsData = [[String:Any]]()
userDocument.getDocument { (docums, error) in
if let docum = docums, docum.exists {
//grab all jobs data
userjobsData = docum.get("jobData") as! [[String:Any]]
//sort jobs by category in alphabetical order
userjobsData = (userjobsData as NSArray).sortedArray(using: [NSSortDescriptor(key: "category", ascending: true)]) as! [[String:AnyObject]]
self.perfomAction(firebaseResult : userjobsData)
// now pass this data to your need function like
}
}
}
private func perfomAction(firebaseResult : [[String:Any]]){
// perform your job here
}

Core Data Object was written to, but never read

As I try to update an existing entry in my Core Data DB, I fetch the desired item by id, change it to a new item and save in context.
However, when I fetch the object and replace it, I get the warning "Core Data Object was written to, but never read." It does make sense since I'm not really using that object, but as I understand it, just giving it a value saves it in Core Data.
static var current: User? {
didSet {
if var userInCoreData = User.get(with: current?.id), let current = current { //userInCoreData is the value with the warning
userInCoreData = current
}
CoreDataManager.saveInContext()
}
}
static func get(with id: String?) -> User? {
guard let id = id else { return nil }
let request: NSFetchRequest = User.fetchRequest()
let predicate = NSPredicate(format: "id = %#", id)
request.predicate = predicate
do {
let users = try CoreDataManager.managedContext.fetch(request)
return users.first
} catch let error {
print(error.localizedDescription)
return nil
}
}
I want to make sure, is this the recommended process to overwrite a value in Core Data, or am I doing something wrong?
This section
if var userInCoreData = User.get(with: current?.id), let current = current { //userInCoreData is the value with the warning
userInCoreData = current
}
seems just updating local variable userInCoreData, not User object in Core Data.
So the warning says "you fetched data from core data and set to a variable, but you set another value to the variable soon, never use the first value from core data. Is it OK?"
What you really want to do is something like this?
if var userInCoreData = User.get(with: current?.id), let current = current {
userInCoreData.someValue = current.someValue
userInCoreData.anotherValue = current.anotherValue
}

CKQuery with CloudKit Issue

I'm very new to CloudKit and am simply trying to run the most basic query. I want to pull from RecordType "Users" and field type "Name" and have the name equal to my nameText label!
Please see my code below:
func getUserInformation() {
let Pred = NSPredicate(value: true)
let Query = CKQuery(recordType: "Namer", predicate: Pred)
let AgeOp = CKQueryOperation(query: Query)
AgeOp.database = self.Pub
AgeOp.recordFetchedBlock = {(record : CKRecord!) in
let Recorded : CKRecord = record!
self.nameText.text = Recorded.objectForKey("Name") as? String
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.Container = CKContainer.defaultContainer() as CKContainer
self.Pub = Container.publicCloudDatabase as CKDatabase
self.Container.accountStatusWithCompletionHandler {
accountStatus, error in
if error != nil {
print("Error: \(error?.localizedDescription)")
} else {
self.getUserInformation()
}
}
}
Users is a system table. You could add new fields to that, but it's advised not to do so. One of the main reasons is that you cannot query the Users recordType.
Besides that I see in your code that you query the recordType Namer and not Users. Is your question wrong or is your code wrong?
In the recordFetchedBlock you directly put the result in the .text. That block will be executed in a background thread. You should first go to the mainQueue. You could do that like this:
AgeOp.recordFetchedBlock = {(record : CKRecord!) in
NSOperationQueue.mainQueue().addOperationWithBlock {
let Recorded : CKRecord = record!
self.nameText.text = Recorded.objectForKey("Name") as? String
}
}
When there is no query match, then the .recordFetchedBlock will never be called. You should also set the .queryCompletionBlock
The query could return multiple records (if there are more people with the same name). In the recordFetchedBlock you should add records to an array. Then in the .queryCompletionBlock you could use that array.