SQLite.Swift how to get random row never repeating - ios

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
}

Related

Coredata relationships how make ordered collection or added in particular index in iOS swift

I am working with core-data relationships add the student in profile entities.
Profile and Student entities are multiple relationship with each others.
Profile entities for create students, it successfully created.
I want to add or append some information that profile entities through student entities its also added.
(Its Like: Profile entities have a array of dictionary of student entities )
But when in display in UItableview added info of student it display in unordered.
I want to display the added student should be display in last or first.
Coredata is unordered collection of set. how to make it order.
Also selected ordered Arrangement. its shows error students not be ordered.
How can achieve this. Help me
Here my code:
func create(record: ProfileModel) {
let cdProfile = CDProfile(context: PersistentStorage.shared.context)
cdProfile.emailID = record.emailID
cdProfile.gender = record.gender
cdProfile.getDate = record.getDate
cdProfile.id = record.id
if(record.toStudent != nil && record.toStudent?.count != 0){
var studentSet = Set<CDStudent>()
record.toStudent?.forEach({ (student) in
let cdStudent = CDStudent(context: PersistentStorage.shared.context)
cdStudent.activity = student.activity
cdStudent.currentPage = Int16(student.currentPage ?? 0)
cdStudent.getPercentage = student.getPercentage
studentSet.insert(cdStudent)
})
cdProfile.toStudent = studentSet
}
PersistentStorage.shared.saveContext()
}
#IBAction func saveBtnClick(_ sender: Any) {
let studentArr = StudentModel(_activity: "S-\(self.sectionString)", _studentComments: self.infotextView.text, _getPercentage: "-", _result: String(self.audioValueKey.count), _sectionID: self.sectionString, _sessionDate: self.convertedDate, _timeSpend: self.timeSpendStr, _currentPage: self.allPageNumber, _selectedValue: self.audioValueKey)
if let getStudentData = userProfileArr![indexNumber].toStudent?.count{
self.personArrCount = getStudentData
let getArr = userProfileArr![indexNumber].toStudent!
if getArr.count == 0{
}else{
for j in 0..<getArr.count{
self.student.append(getArr[j])
// self.student.insert(getArr[j], at: 0)
}
self.student.append(studentArr)
}
}else{
self.personArrCount = 0
self.student.append(studentArr)
print("student-empty",student)
}
let getProfileData = userProfileArr![indexNumber]
let updatePerson = ProfileModel(_id: selectedUserIndex!.id, _profileComments: getProfileData.profileComments!, _emailID: getProfileData.emailID!, _gender: getProfileData.gender!, _profileImage: getProfileData.profileImage!, _getDate: "NO", _studentDOB: getProfileData.studentDOB!, _studentName: getProfileData.studentName!, _toStudent: student)
print("student",self.student)
if(dataManager.update(record: updatePerson))
{
print("Update added")
}else{
print("Not-- added")
}
}
How can i fix this issue help me... Thanks advance.
first of all Core data only provides sorting on parent table only
if you wanna sort data in a subtable you can do as below
First you need to add a field named student_id(int16) in student table
Then you need to assign value as count + 1
As core data does not provide autoincrement field need to manage manually.
Follow the below code to sort data as last to first
// assume you have fetched profile in stud_profile var
if let student_list = stud_profile.toStudent as? Set<CDStudent> {
let arrStudents = Array(student_list).sorted(by: {$0.student_id > $1.student_id})
}
4.You can use arrStudents as it will return sorted [CDStudent]

Realm - how to find a string in a List

Here's my object:
class Cat: Object {
let toys = List<String>()
}
How can I find a toy in the toys array and delete it?
if let foundToy = cat.toys.filter(???).first {
try! realm.write {
realm.delete(foundToy)
}
}
Realm does not support queries on a List of primities (yet).
EDIT: Release 10.7 added support for filters/queries as well as aggregate functions on primitives so the below info is no longer completely valid. However, it's still something to be aware of.
You will need to define a Realm ToyClass and have a property of String.
See Array of Primitives: Support queries #5361
So create a ToyClass
class ToyClass: Object {
#objc dynamic var toy_name = ""
}
and update your CatClass List
class CatClass: Object {
let toys = List<ToyClass>()
}
There are lots of way to delete but if you know the name of the toy, you can delete it directly from Realm.
IMPORTANT - this will remove the first object that matches the filter criteria completely from Realm which includes the object and the reference to it in the list. Note that it's going to delete whatever object matches the first object so if you have two objects called 'toy 1' it will delete one of them - data stored in realm is 'unsorted' so the result may not be what you want.
if let toyToDelete = realm.objects(ToyClass.self).filter("toy_name == 'toy 1'").first {
try! realm.write {
realm.delete(toyToDelete)
}
}
If you just want to remove the first object that matches the criteria (which may be dangerous) from the list but keep the object in Realm, you can do this
let cat = realm.objects(CatClass.self).first!
if let toyToDelete = cat.toys.filter("toy_name == 'toy 1'").first {
try! realm.write {
cat.toys.realm?.delete(toyToDelete)
}
}
You should really add a primary key to your objects so you can tell realm specifically which object to find/delete.
class ToyClass: Object {
#objc dynamic var toy_id = UUID().uuidString
#objc dynamic var toy_name = ""
override static func primaryKey() -> String? {
return "toy_id"
}
}
EDIT: Some testing code to demonstrate firstIndex potentially not working
Set up the cat and two toys
let cat0 = CatClass()
cat0.cat_name = "cat 0"
let toy0 = ToyClass()
toy0.toy_name = "toy 0"
let toy1 = ToyClass()
toy1.toy_name = "toy 1"
cat0.toys.append(toy0)
cat0.toys.append(toy1)
try! realm.write {
realm.add(cat0)
}
Then retrieve cat0 and attempt to get the index of toy 1
let cat = realm.objects(CatClass.self).filter("cat_name == 'cat 0'").first!
let toyToBeDeleted = cat.toys.filter("toy_name == 'toy 1'").first!
print(toyToBeDeleted) //prints toy 1
let index = cat.toys.firstIndex(of: toyToBeDeleted)
print(index) //prints nil
If you are keeping an array of distinct String (i.e., the toys array doesn't contain duplicates), you can just delete the first String found:
if let toyIndex = cat.toys.firstIndex(of: toyNameToBeDeleted) {
try! realm.write {
cat.toys.remove(at: toyIndex)
 }
}
If you are trying to delete all String objects == to a certain toy name, do this instead:
try! realm.write {
cat.toys = cat.toys.filter { $0 != toyNameToBeDeleted }
}

swift: can't fetch entity-members from coredata

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.

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".

Add and Delete CoreData based on attribute value search match

Setup: iOS 9, Swift, XCODE 7.1 Beta
Goal is to build shopping cart functionally for which I need unique values in CoreData.
I have a UITableView in which data (Product name, Cost, Quantity, ID) is uploaded from Parse backend. Each TableView custom cell has a button, tapping which saves the selected row data in CoreData.
I don't want to have duplicate product in the cart, so before saving I want to check if the cart already have the product. If it has, I want to replace the cart with currently selected data. If not, just want to add a new product in the cart.
My CoreData setup is simple.
Entity name = Cart
Attributes = pName(type String), pCost(type
Int), pQuantity(type Int), orderID(type String)
Add product to the Cart button code is as below:
// Product data is retrieved in these variable
var pNam = NSMutableArray()
var pCost = NSMutableArray()
var pQty = NSMutableArray()
var pObjectID = NSMutableArray()
// Add to Cart button ....
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
let cell = tableView.cellForRowAtIndexPath(indexPath) as! ProductMenuTableViewCell! // Custom cell in which the button add to Cart button is placed
let entityDescription = NSEntityDescription.entityForName("Cart", inManagedObjectContext: managedObjectContext!)
let cart = Cart(entity: entityDescription!, insertIntoManagedObjectContext: managedObjectContext)
cart.pName = pNam[indexPath.row].description
cart.pCost = pCst[indexPath.row].integerValue!
cart.pQuantity = pQty[indexPath.row].integerValue!
cart.orderID = pObjectID[indexPath.row].description
var error: NSError?
do {
try managedObjectContext?.save()
} catch let error1 as NSError {
error = error1
}
if let err = error {
print(err.localizedFailureReason)
} else {
print("Saved")
}
Should I directly use cell to add values to CoreData? For ex: cart.pName = cell.pName.text! or there is a better way to do it? Anyone know how to solve this?
Your data setup is absurd. You have 4 arrays for the 4 attributes of n objects. If for any reason the sorting of an array changes, or an element is dropped or added, you have to make sure the same happens with all the other arrays, a maintenance nightmare! How do you expand this model if later you have 15 attributes. Use 15 arrays? This is completely crazy.
Instead, you should have an array of objects - ideally Core Data objects - with the appropriate attributes grouped together. You can always keep a flag to indicate that you want to discard these items later rather then persist them in the database.
Now you do not have to decide to create or update: simply set the delete flag to false.
You need to search for an existing Cart object with the correct name; if there is no matching Cart, then create a new one. Something like this:
var cart : Cart
let requiredName = pNam[indexPath.row].description
let entityDescription = NSEntityDescription.entityForName("Cart", inManagedObjectContext: managedObjectContext!)
let fetchRequest = NSFetchRequest()
fetchRequest.entity = entityDescription!
fetchRequest.predicate = NSPredicate(format:"pName == %#",requiredName)
do {
let results = try self.managedObjectContext?.executeFetchRequest(fetchRequest) as! [Cart]
if results.count == 0 { // not found, so create new...
cart = Cart(entity: entityDescription!, insertIntoManagedObjectContext: managedObjectContext)
cart.pName = requiredName
} else { // found at least one, so use the first...
cart = results[0]
}
// Now update the other attribute values:
cart.pCost = pCst[indexPath.row].integerValue!
cart.pQuantity = pQty[indexPath.row].integerValue!
cart.orderID = pObjectID[indexPath.row].description
} catch {
print("Error fetching")
}
Regarding your secondary question, personally I would use your current approach: use the row as an index on your dataSource arrays, i.e. don't directly use the values from the cell's labels etc.

Resources