I am having a peculiar problem when I refresh my tableview. (I am using UIRefreshControl.) If I were to slow-mo what is happening:
Suppose there are 3 cells visible, A, B, and C, in that order from top to bottom.
1) I pull down and the tableview shows that it is refreshing.
2) A gets the text that should go in B. B gets the text that should go in C. C is off-screen.
3) Refresh ends and the table snaps back into place.
4) Incorrect text lingers for a second or so.
5) Each cell's text flips to the right thing.
The flip-flopping is pretty annoying to look at visually. Anyway, I feel like this is the kind of issue that can be fixed with a line of code that I just don't know about.
Here are excerpts of my code:
Function called when refresh occurs (gets records from CloudKit):
func refreshTable(sender: UIRefreshControl) {
var postsPredicate = NSPredicate(format: "%K = %#", VISIBILITY_CODE, WORLD) // default
if sender.tag == 0 {
postsPredicate = NSPredicate(format: "%K = %#", VISIBILITY_CODE, WORLD)
}
else if sender.tag == 1 {
postsPredicate = NSPredicate(format: "%K = %#", VISIBILITY_CODE, PRIVATE)
}
else {
// report problem
}
let query = CKQuery(recordType: POST, predicate: postsPredicate)
let sort = NSSortDescriptor(key: "creationDate", ascending: false)
query.sortDescriptors = [sort]
self.db.perform(query, inZoneWith: nil) { records, error in
if error == nil {
self.list = [records!]
DispatchQueue.main.async(execute: {
self.postTableView.reloadData()
self.refreshControl.endRefreshing()
})
}
else {
if let error = error as? CKError {
print(error)
}
}
}
}
Here's my cellForRowAtIndexPath function:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if self.list.count == 1 && self.list[0].count == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: NO_POSTS, for: indexPath)
return cell
}
let post = self.list[indexPath.section][indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: POST_IN_FEED_CELL, for: indexPath)
if let postCell = cell as? PostInFeedTableViewCell {
// deleted the assignment of values to other UI elements in my custom cell
let predicate = NSPredicate(format: "%K = %#", LIBRARY_CODE, post.object(forKey: LIBRARY_CODE) as! String)
let query = CKQuery(recordType: LIBRARY_ITEM, predicate: predicate)
self.db.perform(query, inZoneWith: nil) { records, error in
if error == nil {
let libraryItem = records?[0]
DispatchQueue.main.async(execute: {
postCell.title.text = libraryItem?.object(forKey: NAME) as! String
})
}
else {
if let error = error as? CKError {
print(error)
}
}
}
}
return cell
}
Related
I've been struggling with this for several days already. There are similar problems in this website, but not very the same. And I didn't manage to go forward. I will try to simplify it with one variable.
Problem:
After filtering records in UITableView (records are taken from core data) and trying to push data to another viewcontroller, I get unfiltered index for data, so incorrect data is pushed to new view controller.
My code is below:
I set global variable for core data:
var events : [Event] = []
#objc func textFieldDidChange(_ textField: UITextField) {
if searchField.text == "" {
filterAdded = false
} else {
filterAdded = true
let request:NSFetchRequest<Event> = Event.fetchRequest()
let predicate = NSPredicate(format: "name CONTAINS[c] %# AND nearestDate >= %#", searchField.text!, currentCorrectDate! as CVarArg)
request.predicate = predicate
let sortDescriptor = NSSortDescriptor(key: "nearestDate", ascending: true)
request.sortDescriptors = [sortDescriptor]
do {
events = try DatabaseController.getContext().fetch(request)
}
catch {
print("Error: \(error)")
}
mainListOfDates.reloadData()
}
}
}
It is triggered every time some character is added to search field. UITableView name is "mainListOfDates".
This function works properly and calculated only filtered events:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return events.count }
This function shows all records from core data in UITableView cells:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "eventCell", for: indexPath) as! EventTableViewCell
let event = events[indexPath.row]
cell.eventNameLabel.text = event.value(forKeyPath: "name") as? String
return cell
}
And with "didSelectRowAt" I would like to push filtered or unfiltered (works perfectly with unfiltered) data to new view controller:
let Storyboard = UIStoryboard(name: "Main", bundle: nil)
let eventStoryboard = Storyboard.instantiateViewController(withIdentifier: "EventViewController") as! EventViewController
let cell = tableView.dequeueReusableCell(withIdentifier: "eventCell", for: indexPath) as! EventTableViewCell
eventStoryboard.getEventName = events[indexPath.row].name ?? "nil"
self.navigationController?.pushViewController(eventStoryboard, animated: false) }
How to solve this issue and send filtered correct data to new view controller?
Thanks in advance.
I have been implementing code that is to enable paging scroll to fetch data by a certain amount of data from firebase database.
Firstly, then error says
Terminating app due to uncaught exception 'InvalidQueryParameter',
reason: 'Can't use queryEndingAtValue: with other types than string in
combination with queryOrderedByKey'
The below here is the actual code that produced the above error
static func fetchPostsWith(last_key: String?, completion: #escaping (([PostModel]?) -> Void)) {
var posts = [PostModel]()
let count = 2
let ref = Database.database().reference().child(PATH.all_posts)
let this_key = UInt(count + 1)
let that_key = UInt(count)
let this = ref.queryOrderedByKey().queryEnding(atValue: last_key).queryLimited(toLast: this_key)
let that = ref.queryOrderedByKey().queryLimited(toLast: that_key)
let query = (last_key != nil) ? this : that
query.observeSingleEvent(of: .value) { (snapshot) in
if snapshot.exists() {
for snap in snapshot.children {
if let d = snap as? DataSnapshot {
let post = PostModel(snapshot: d)
print(post.key ?? "")
posts.append(post)
}
}
posts.sort { $0.date! > $1.date! }
posts = Array(posts.dropFirst())
completion(posts)
} else {
completion(nil)
}
}
}
What it tries to do is to fetch a path where all posts are stored by auto id. But the error keeps coming out so I do not know what is wrong. Do you have any idea?
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// index is the last and last fetched then
print(self.feeds.count - 1 == indexPath.row, "index ", self.hasFetchedLastPage, "has fetched last")
if self.feeds.count - 1 == indexPath.row {
let lastKey = self.feeds[indexPath.row].key
if lastKey != self.previousKey {
self.getFeeds(lastKey: lastKey)
} else {
self.previousKey = lastKey ?? ""
}
}
print("Cell Display Number", indexPath.row)
}
func getFeeds(lastKey: String?) {
print(self.isFetching, "is fetching")
guard !self.isFetching else {
self.previousKey = ""
return
}
self.isFetching = true
print(self.isFetching, "is fetching")
FirebaseModel.fetchPostsWith(last_key: lastKey) { ( d ) in
self.isFetching = false
if let data = d {
if self.feeds.isEmpty { //It'd be, when it's the first time.
self.feeds.append(contentsOf: data)
self.tableView.reloadData()
print("Initial Load", lastKey ?? "no key")
} else {
self.hasFetchedLastPage = self.feeds.count < 2
self.feeds.append(contentsOf: data)
self.tableView.reloadData()
print("Reloaded", lastKey ?? "no key")
}
}
}
}
I want to implement a paging enabled tableView. If you can help this code to be working, it is very appreciated.
You're converting your last_key to a number, while keys are always strings. The error message tells you that the two types are not compatible. This means you must convert your number back to a string in your code, before passing it to the query:
let this = ref.queryOrderedByKey().queryEnding(atValue: last_key).queryLimited(toLast: String(this_key))
let that = ref.queryOrderedByKey().queryLimited(toLast: String(that_key))
I am using CloudKit in my app and facing problem showing data in table view. In viewDidLoad() I am fetching data from CloudKit database.
Then in table view functions I do CKRecord object count for number of rows.
But count returns 0 to table view and after few seconds returns number of row. Because of this table view does not show the results.
override func viewDidLoad() {
super.viewDidLoad()
loadNewData()
}
func loadNewData() {
self.loadData = [CKRecord]()
let publicData = CKContainer.default().publicCloudDatabase
let qry = CKQuery(recordType: "Transactions", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
qry.sortDescriptors = [NSSortDescriptor(key: "Transaction_ID", ascending: true)]
publicData.perform(qry, inZoneWith: nil) { (results, error) in
if let rcds = results {
self.loadData = rcds
}
if error != nil {
self.showAlert(msg: (error?.localizedDescription)!)
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return loadData.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell2", for: indexPath) as! ViewAllTransactionsTVCell
let pn = loadData[indexPath.row].value(forKey: "Party_Name") as! String
let amt = loadData[indexPath.row].value(forKey: "Amount") as! String
let nrt = loadData[indexPath.row].value(forKey: "Narattions") as! String
let dt = loadData[indexPath.row].value(forKey: "Trans_Date") as! String
cell.partyNameLabel.text = pn
cell.dateLabel.text = dt
cell.narationLabel.text = nrt
cell.amountLabel.text = amt
return cell
}
You shouldn't wait, but instead trigger the reloading of the data when the perform completion handler is called:
publicData.perform(qry, inZoneWith: nil) { (results, error) in
if let rcds = results {
DispatchQueue.main.async {
self.loadData = rcds
self.tableView.reloadData()
}
}
if error != nil {
self.showAlert(msg: (error?.localizedDescription)!)
}
}
Note, I'm dispatching the reload process to the main queue, because you're not guaranteed to have this run on the main thread. As the documentation says:
Your block must be capable of running on any thread of the app ...
And because UI updates must happen on the main thread (and because you want to synchronize your access to loadData), just dispatch this to the main queue, like above.
I want to trigger an else statement if there is no object at the indexPath i look for in an array.
The array is
let exerciseSets = unsortedExerciseSets.sorted { ($0.setPosition < $1.setPosition) }
Then i use let cellsSet = exerciseSets[indexPath.row]
There is a chance, when a user is adding a new cell, that it wont already have an exerciseSet at the indexPath to populate it (adding a new set to an existing array of sets) and so I need to run an else statement to set up a blank cell rather than attempting to populate it and crashing my app.
However if i use if let then i get this error:
Initializer for conditional binding must have Optional type, not
'UserExerciseSet'
Here is the whole function for context if needed:
func configure(_ cell: NewExerciseTableViewCell, at indexPath: IndexPath) {
if self.userExercise != nil {
print("RESTORING CELLS FOR THE EXISTING EXERCISE")
let unsortedExerciseSets = self.userExercise?.exercisesets?.allObjects as! [UserExerciseSet]
let exerciseSets = unsortedExerciseSets.sorted { ($0.setPosition < $1.setPosition) }
if let cellsSet = exerciseSets[indexPath.row] { // this causes a creash when user adds new set to existing exercise as it cant populate, needs an else statement to add fresh cell
cell.setNumber.text = String(cellsSet.setPosition)
cell.repsPicker.selectRow(Int(cellsSet.setReps), inComponent: 0, animated: true)
let localeIdentifier = Locale(identifier: UserDefaults.standard.object(forKey: "locale") as! String)
let setWeight = cellsSet.setWeight as! Measurement<UnitMass>
let formatter = MassFormatter()
formatter.numberFormatter.locale = localeIdentifier
formatter.numberFormatter.maximumFractionDigits = 2
if localeIdentifier.usesMetricSystem {
let kgWeight = setWeight.converted(to: .kilograms)
let finalKgWeight = formatter.string(fromValue: kgWeight.value, unit: .kilogram)
let NumericKgResult = finalKgWeight.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789.").inverted)
cell.userExerciseWeight.text = NumericKgResult
} else {
let lbsWeight = setWeight.converted(to: .pounds)
let finalLbWeight = formatter.string(fromValue: lbsWeight.value, unit: .pound)
let NumericLbResult = finalLbWeight.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789.").inverted)
cell.userExerciseWeight.text = NumericLbResult
}
} else {
cell.setNumber.text = String((indexPath.row) + 1)
}
You could do something crazy like this:
if let cellSet = (indexPath.row < exerciseSets.count ? exerciseSets[indexPath.row] : nil) {
//
}
but it would be more straightforward to do:
if indexPath.row < exerciseSets.count {
let cellSet = exerciseSets[indexPath.row]
...
}
OK, so your problem is that you are trying to access a value in an array that may or may not be there.
If you just try to access the value based on indexPath you can crash because indexPath may refer to a value not there. On the other hand, the array does not return an optional so you can't use if let either.
I kind of like the idea of using an optional, so how about introducing a function that could return an optional to you if it was there.
Something like:
func excerciseSet(for indexPath: IndexPath, in collection: [UserExcerciseSet]) -> UserExcerciseSet? {
guard collection.count > indexPath.row else {
return nil
}
return collection[indexPath.row]
}
and then you can say:
if let cellsSet = exerciseSet[for: indexPath, in: excerciseSets] {
//It was there...showtime :)
} else {
cell.setNumber.text = String((indexPath.row) + 1)
}
Hope that helps you.
Just check the index against your array count:
if indexPath.item < exerciseSets.count {
// Cell exists
let cellsSet = exerciseSets[indexPath.row]
} else {
// cell doesn't exists. populate new one
}
I'm writing an app in Swift where the first scene has a TableView, I have it setup to display the title and it works fine, I also have it setup to count occurrences in a CloudKit database(or whatever its called) but it performs the count in async so the table defaults to show 0 in the detail pane.
I need to know how to make the app wait before it sets the value for the right detail until the count is completed or how to change them afterwards.
I have attached the code I used to perform the count etc, if I am doing this wrong or inefficiently please let me know
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.hidesBackButton = true;
self.textArray.addObject("Link 300")
self.textArray.addObject("Link 410")
self.textArray.addObject("Link 510")
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
let query = CKQuery(recordType: "Inventory", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
publicData.performQuery(query, inZoneWithID: nil){results, error in
if error == nil {
for res in results {
let record: CKRecord = res as! CKRecord
if(record.objectForKey(("TrackerModel")) as! String == "Link 300"){
self.count300 = self.count300++
}else if(record.objectForKey(("TrackerModel")) as! String == "Link 410"){
self.count410 = self.count410++
}else if(record.objectForKey(("TrackerModel")) as! String == "Link 510"){
self.count510 = self.count510++
}
}
}else{
println(error)
}
}
self.detailArray.addObject(self.count300.description)
self.detailArray.addObject(self.count410.description)
self.detailArray.addObject(self.count510.description)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.textArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) ->UITableViewCell {
var cell: UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
cell.textLabel?.text = self.textArray.objectAtIndex(indexPath.row) as? String
cell.detailTextLabel?.text = self.detailArray.objectAtIndex(indexPath.row) as? String
return cell
}
Many thanks - Robbie
The closure associated with the performQuery will complete asynchronously - that is after viewDidLoad has finished. You need to make sure that you reload your table view once the query has completed and you have the data. You also have a problem because you are updating your totals outside the closure - this code will also execute before the data has loaded.
Finally, make sure that any update to the UI (such as reloading the table view) is dispatched on the main queue
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.hidesBackButton = true;
self.textArray.addObject("Link 300")
self.textArray.addObject("Link 410")
self.textArray.addObject("Link 510")
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
let query = CKQuery(recordType: "Inventory", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
publicData.performQuery(query, inZoneWithID: nil){results, error in
if error == nil {
for res in results {
let record: CKRecord = res as! CKRecord
if(record.objectForKey(("TrackerModel")) as! String == "Link 300"){
self.count300++
}else if(record.objectForKey(("TrackerModel")) as! String == "Link 410"){
self.count410++
}else if(record.objectForKey(("TrackerModel")) as! String == "Link 510"){
self.count510++
}
}
self.detailArray.addObject(self.count300.description)
self.detailArray.addObject(self.count410.description)
self.detailArray.addObject(self.count510.description)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}else{
println(error)
}
}
}