swift: can't fetch entity-members from coredata - ios

I am new to programming and started with swift (xcode 9.4.1, swift 4.x) recently. Probably the solution is quite easy, but I don't get it. There are some similar questions and answers in the www but they have different code and I don't know how to adapt it. So I would like you to help me out. Thank you!
I have an iOS-app which saves statistics in sports. I created two entities called Stat and Game. Saving works so far with this code:
#IBAction func saveButtonPressed(_ sender: UIButton) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let stats = Stat(context: context)
stats.action = action
stats.playerNumber = Int32(playerOfAction)
stats.points = Int32(pointsOfAction)
let statsMinute = Game(context: context)
statsMinute.minute = Int32(currentMinute)
(UIApplication.shared.delegate as! AppDelegate).saveContext()
Now I want to fetch and display the last saved stat (and be able to edit it afterwards).
I am trying it with this code:
func lastStatInfo() {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
do {
let loadMin = try context.fetch(Game.fetchRequest()) as [Game]
lastStatMinuteLabel.text = "\(loadMin.minute)th minute"
} catch {
print("didn't load minute")
}
do {
let loadStat = try context.fetch(Stat.fetchRequest()) as [Stat]
lastStatActionLabel.text = loadStat.action
lastStatPlaylerLabel.text = "# \(loadStat.playerNumber)"
} catch {
print("didn't load stat")
}
}
There are three errors like
'Value of type '[Game]' has no member 'minute''.
Why aren't the attributes accessible? I think it's because the stats are stored as an array.
How do I fetch the data correctly?
Bonusquestion: I tried different types for my datamodels attributes. For saving jerseynumbers and points only Int32 does seem to work. Can you tell me why and if its the best way to store them?

Your fetch request returns an array as the result (even if only one object exist) so you need to loop the array or only get one from a specific index
for stat in loadMin {
var minute = stat.minute
//do stuff with minute
}
Or
if !loadMin.isEmpty {
lastStatMinuteLabel.text = "\(loadMin[0].minute)th minuteā€
}
I usually only use Int for integer values.

Related

ios firebase how to fetch multiple object inside collection?

I had a problem with firebase , i have 5 document IDs . I need to query those 5 documents , convert them in to object.
for oneID in allIDs {
self.db.collection("storecollection").document(oneID).getDocument {(snap,err) in
let oneobject = convertToObject(snap)
self.tempHolder.append(oneobject)
var newarray = [MyObjectClass]()
if allIDs.last == oneID {
// perform copy
for x in 0...(self.tempHolder.count -1){
newarray.append(self.tempHolder[x])
}
self.tempHolder.removeAll()
completion(newarray)
}
}
Something wrong with code above , the size of self.tempHolder always count = 1. (Only the last id fetched object exist) i have no idea how to make it right.
Whats the right way to fetch multiple document (with specifiedID) ???
There's a bit of extraneous code in the question so it's not exactly clear but it seems you want to iterate over an array of document keys, read each associated document and add properties to an array (or in your case create an object based on those properties and add it)
Here's a simple example reading in a series of posts, and appending the post text from each post in an array.
The structure is
posts //a collection
post_0
post_text: "A post"
post_1
post_text: "Another post"
post_2
post_text: "Cool post"
and the code to read in post_0 and post_2 and append the post text to an array
var postTextArray = [String]()
func readMultiplePosts() {
let postKeyArray = ["post_0", "post_2"]
for postKey in postKeyArray {
let docRef = self.db.collection("posts").document(postKey)
docRef.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("err fetchibng document")
return
}
guard let data = document.data() else {
print("doc was empty")
return
}
print("doc data: \(data)")
let post = document.get("post_text") as! String
self.postMsgArray.append(post)
}
}
}
then sometime later we want to print the post texts
for p in self.postMsgArray {
print(p)
}
and the output from console
A post
Cool post
While this solution works, a Firebaser will quickly point out that reading data like this in a tight loop is generally not recommended. It would be better to have some other correlation between the posts you want to read and then perform a query to read them in.

SQLite.Swift how to get random row never repeating

I'm experimenting with SQlite.Swift in iOS and I'm trying to randomly pick a row from a table in an SQLite database.
The table is movies and its columns are movieID, title genre and seen.
Problem:
Movies should be picked randomly but as soon as I set seen to "yes" they should no longer be picked.
func randomRow() -> Int {
let path = NSSearchPathForDirectoriesInDomains(
.documentDirectory,
.userDomainMask, true)
.first!
let database = try! Connection("\(path)/movies.sqlite3")
var randomRow = Int(arc4random_uniform(UInt32(try!
database.scalar(movies.where(seen != "yes")
.select(movieID.distinct.count)))))
while previousNumber == randomRow {
randomRow = Int(arc4random_uniform(UInt32(try!
database.scalar(movies.where(seen != "yes")
.select(movieID.distinct.count)))))
}
previousNumber = randomRow
return randomRow
}
let destRow = randomRow()
Now I want to use this random row number to be catched from the table to lead the title and genre of the random movie into let ("dispMovie" and "movieGenre") to be output into a UITextField.
let path = NSSearchPathForDirectoriesInDomains(
.documentDirectory,
.userDomainMask, true)
.first!
let database = try! Connection("\(path)/filme.sqlite3")
for movie in try! database.prepare(
self.movies.where(
seen != "yes" && movieID == "\(destRow)")) {
dispMovie = movie[title]
movieGenre = movie[genre]
}
outputTextField.text = "\(dispMovie), \(movieGenre)"
This code works fine. The only problem is that movies I have seen will still be diplayed because I have this .count in the random row function.
I've also tried to call Raw SQLite with this one:
let newMovie = try! database.scalar(
"SELECT title, genre FROM movies WHERE seen != yes
ORDER BY RANDOM() LIMIT 1") as! String
outputTextField.text = newMovie
But this only displays the title and not the genre and looking for the genre in an extra line is not possible because the movie is picked from a random row. I've tried to reference the movie title in the Raw code but that crashes the app.
Thanks for help and hints.
No Christmas 'till this is done!
After days of thinking and running against walls suddenly the good old while loop came to my mind. It turned out working so I'm leaving this as an answer to my own question:
Forget about the randomRow(). No need for this. I focused on the SQLite RAW in the first place and continued with a combination of scalar, where and select from the SQLite.Swift library. That finally got me this:
Posting my whole UIViewController so you can test yourself:
class TestView: UIViewController {
//Table
let movies = Table("movies")
//Expressions
let movieId = Expression<String>("ID")
let movieTitle = Expression<String>("movieTitle")
let genre = Expression<String>("genre")
let seen = Expression<String>("seen")
//to find with database connection
var randomMovie = String()
var movieDetails = [String]()
//Array of all movies seen
var seenMovies = [String]()
//TestLabel to check everything
#IBOutlet weak var testLabel: UILabel!
#IBAction func testButton(_ sender: Any) {
//Open database connection
let path = NSSearchPathForDirectoriesInDomains(
.documentDirectory, .userDomainMask, true
).first!
let database = try! Connection("\(path)/movies.sqlite3")
//Check column "seen" and add all movie Titles to Array seenMovies
for movie in try! database.prepare(self.movies.where(seen == "yes")) {
self.seenMovies.append(movie[movieTitle])
}
//Select random row from movies Table with SQLite RAW as String
self.randomMovie = "\(try! database.scalar("SELECT movieTitle FROM movies WHERE seen != yes ORDER BY RANDOM() LIMIT 1"))"
//Create Array of details for that randomly picked movie
//Use scalar, where and select to find details from the picked movie above
self.movieDetails = ["\(randomMovie!)",
"\(try! database.scalar(movies.where(movieTitle == "\(randomMovie!)").select(genre)))"]
//And here's the "unique" thing...
//while randomMovie is also an element in seenMovies, loop until it's not
while seenMovies.contains("\(randomMovie!)") {
randomMovie = "\(try! database.scalar("SELECT movieTitle FROM movies WHERE seen != yes ORDER BY RANDOM() LIMIT 1"))"
}
//let testLabel show the results and see that no seen movie will be picked
testLabel.text = "\(details)\n\n\(erraten)"
//To prevent the Array from being appended over and over:
self.seenMovies.removeAll()
}
}
Since I still consider myself a beginner (started coding in October '17) please feel free to optimize this if possible. All my tests went very well and no movie was ever repeated when turned to "seen".
Found a solution from SQLite.Swift issue filed in 2016.
let db = Connection(databasePath)
let movies = Table("movies")
let query = movies.order(Expressions<Int>.random()).limit(1)
for row in try! db.prepare(query) {
// handle query result here
}

FetchRequst issue with data fault

When I was inserting data to one entity of CoreData, All the rows are inserted successfully(Saved).
But when I try to fetch the data using FetchRequest, Only one row of data is coming even if number of rows inserted are 3 or 4 or anything(more than 1).
Remaining rows are not getting fetched. And when I print fetch results,
It says - Error
0:<EquipmentDetails: 0x6000000bad60>
(entity: EquipmentDetails; id: 0xd000000000040000
coredata:/EquipmentDetails/p1> **data:fault>)**
I didn't get what was going in backend of core data?
code for Insertion
func insertEqipToLocalDb()
{
let mobileNo : String = UserDefaults.standard.string(forKey: "phoneNumber")!
let equipDetailsItem = NSEntityDescription.insertNewObject(forEntityName: "EquipmentDetails", into:managedObjContext) as! EquipmentDetails
for (index,item) in array_IDEquip.enumerated()
{
equipDetailsItem.mobileNumber = mobileNo
equipDetailsItem.type = array_typeEquip[index]
equipDetailsItem.name = array_nameEquip[index]
equipDetailsItem.startDate = array_sDateEquip[index]
equipDetailsItem.endDate = array_eDateEquip[index]
equipDetailsItem.equpID = Int16(item)
equipDetailsItem.serviceDatesStr = array_serviceDateEquip[index]
}
do
{
try managedObjContext.save()
UserDefaults.standard.set("AlreadyInstalled", forKey: "statusInstallation")
}
catch
{
Exception.insertExceptionDetails(errorMsg: error as NSError, context: managedObjContext)
}
}
//code for fetching
let request = NSFetchRequest<NSFetchRequestResult>()
let entity = NSEntityDescription.entity(forEntityName:"EquipmentDetails", in: managedObjContext)
request.entity = entity
do
{
let fetchResults = try managedObjContext.fetch(request)
for r in fetchResults
{
typeEquipArray.append((r as AnyObject).value(forKey: "type") as! String)
}
}
catch let error as NSError
{
Exception.insertExceptionDetails(errorMsg: error, context: managedObjContext)
}
On this line:
let equipDetailsItem = NSEntityDescription.insertNewObject(forEntityName: "EquipmentDetails", into:managedObjContext) as! EquipmentDetails
You create one instance. In the loop that follows, you set values for the type, name, etc properties over and over again on that same instance. Then you save changes, which include just that one object. If you want a difference instance of EquipmentDetails for each pass through the loop, you need to create the instance inside the loop.
The "fault" message is not an error unless you tried to access the property values and found that they were not present. It's part of how Core Data works. See the answer that Harshal Valanda linked in the comments for more detail.

Couchbase lite, Search query taking very long time

When I try to search the couchbase documents of size around 10K, the searching is taking very long time. Below are the code snippet. Can anyone optimize it or suggest me any alternative approach. Thank you.
1) Search function
func search(keyword:String) -> [[String:AnyObject]] {
var results:[[String:AnyObject]]=[]
let searchView = database.viewNamed(AppConstants().SEARCH)
if searchView.mapBlock == nil {
startIndexing()
}
let query = searchView.createQuery()
var docIds = Set<String>()
let result = try query.run()
while let row = result.nextRow() {
let key = "\(row.key)"
let keyArr = keyword.characters.split(" ")
for (index, element) in keyArr.enumerate() {
let keyItem = String(element)
if key.lowercaseString.containsString(keyItem.lowercaseString) {
let value = row.value as! [String:AnyObject]
let id = value["_id"] as? String
if id != nil && !docIds.contains(id!) {
results.append(value)
docIds.insert(id!)
}
}
}
}
}
2) Indexing
func startIndexing() {
let searchView = database.viewNamed(AppConstants().SEARCH)
if searchView.mapBlock == nil {
searchView.setMapBlock({ (doc, emit) in
let docType = doc[AppConstants().DOC_TYPE] as! String
if AppConstants().DOC_TYPE_CONTACT.isEqual(docType) {
self.parseJsonToKeyValues(doc)
for value in self.fields.values {
emit(value, doc)
}
self.fields.removeAll()
}
}, version: "1")
}
}
self.parseJsonToKeyValues(doc) will return me the key value store of my documents to index.
You're emitting the entire document along with every field for your view. This could easily cause your queries to be slow. It also seems unlikely you want to do this, unless you really need to be able to query against every field in your document.
It's considered best practice to set your map function right after opening the database. Waiting until right before you query may or may not slow you down.
See https://developer.couchbase.com/documentation/mobile/current/guides/couchbase-lite/native-api/view/index.html for more, especially the section labeled "Development Considerations".

Faster way to check if entry exists in Core Data

In my app when data is synced i can get 20k entries (from given timestamp) from the server that should be synced to the local device. For every entry i try to fetch it (if it exist already) and if doesn't i create new. The problem is that the whole operation is too slow - for 20k on iphone 5 is 10+ mins. Another solution that i though is to delete all entries from the given timestamp and create new entries for all returned entries and there will be no need to perform fetch for every single entry ? If someone have any advice will be nice. Here is sample code for the current state:
var logEntryToUpdate:LogEntry!
if let savedEntry = CoreDataRequestHelper.getLogEntryByID(inputID: inputID, fetchAsync: true) {
logEntryToUpdate = savedEntry
} else {
logEntryToUpdate = LogEntry(entity: logEntryEntity!, insertInto: CoreDataStack.sharedInstance.saveManagedObjectContext)
}
logEntryToUpdate.populateWithSyncedData(data: row, startCol: 1)
Here is the actual request method:
class func getLogEntryByID(inputID:Int64, fetchAsync:Bool) ->LogEntry? {
let logEntryRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "LogEntry")
logEntryRequest.predicate = NSPredicate(format: "inputId == %#", NSNumber(value: inputID as Int64))
logEntryRequest.fetchLimit = 1
do {
let mocToFetch = fetchAsync ? CoreDataStack.sharedInstance.saveManagedObjectContext : CoreDataStack.sharedInstance.managedObjectContext
if let fetchResults = try mocToFetch.fetch(logEntryRequest) as? [LogEntry] {
if ( fetchResults.count > 0 ) {
return fetchResults[0]
}
return nil
}
} catch let error as NSError {
NSLog("Error fetching Log Entries by inputID from core data !!! \(error.localizedDescription)")
}
return nil
}
Another thing that i tried is to check the count for specific request but again is too slow.
class func doesLogEntryExist(inputID:Int64, fetchAsync:Bool) ->Bool {
let logEntryRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "LogEntry")
logEntryRequest.predicate = NSPredicate(format: "inputId == %#", NSNumber(value: inputID as Int64))
//logEntryRequest.resultType = .countResultType
logEntryRequest.fetchLimit = 1
do {
let mocToFetch = fetchAsync ? CoreDataStack.sharedInstance.saveManagedObjectContext : CoreDataStack.sharedInstance.managedObjectContext
let count = try mocToFetch.count(for: logEntryRequest)
if ( count > 0 ) {
return true
}
return false
} catch let error as NSError {
NSLog("Error fetching Log Entries by inputID from core data !!! \(error.localizedDescription)")
}
return false
}
Whether fetching the instance or getting the count, you're still doing one fetch request per incoming record. That's going to be slow, and your code will be spending almost all of its time performing fetches.
One improvement is to batch up the records to reduce the number of fetches. Get multiple record IDs into an array, and then fetch all of them at once with a predicate like
NSPredicate(format: "inputId IN %#", inputIdArray)
Then go through the results of the fetch to see which IDs were found. Accumulate 50 or 100 IDs in the array, and you'll reduce the number of fetches by 50x or 100x.
Deleting all the entries for the timestamp and then re-inserting them might be good, but it's hard to predict. You'll have to insert all 20,000. Is that faster or slower than reducing the number of fetches? It's impossible to say for sure.
Based on Paulw11's comment, I came up with the following method to evaluate Structs being imported into Core Data.
In my example, I have a class where I store search terms. Within the search class, create a predicate which describes the values of the stuff within my array of structs.
func importToCoreData(dataToEvaluateArray: [YourDataStruct]) {
// This is what Paul described in his comment
let newDataToEvaluate = Set(dataToEvaluateArray.map{$0.id})
let recordsInCoreData = getIdSetForCurrentPredicate()
let newRecords = newDataToEvaluate.subtracting(recordsInCoreData)
// create an empty array
var itemsToImportArray: [YourDataStruct] = []
// and dump records with ids contained in newRecords into it
dataToEvaluateArray.forEach{ record in
if newRecords.contains(record.id) {
itemsToImportArray.append(record)
}
}
// THEN, import if you need to
itemsToImportArray.forEach { struct in
// set up your entity, properties, etc.
}
// Once it's imported, save
// You can save each time you import a record, but it'll go faster if you do it once.
do {
try self.managedObjectContext.save()
} catch let error {
self.delegate?.errorAlert(error.localizedDescription, sender: self)
}
self.delegate?.updateFetchedResultsController()
}
To instantiate recordsInCoreData, I created this method, which returns a Set of unique identifiers that exist in the managedObjectContext:
func getIdSetForCurrentPredicate() -> Set<String> {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "YourEntity")
// searchQuery is a class I created with a computed property for a creating a predicate. You'll need to write your own predicate
fetchRequest.predicate = searchQuery.predicate
var existingIds: [YourEntity] = []
do {
existingIds = try managedObjectContext.fetch(fetchRequest) as! [YourEntity]
} catch let error {
delegate?.errorAlert(error.localizedDescription, sender: self)
}
return Set<String>(existingIds.map{$0.id})
}

Resources