I have the text label of a cell set by URL task. But when I first load up the app the label text is not set to the task results until I either scroll the table view or I select the cell itself.
I am assuming my code needs to manually "update" the view of the cell? Here is a task I am using inside of the tableView cellForRowAtIndexPath function:
if indexPath.row == self.homeLabels.count - 1 {
let task1 = NSURLSession.sharedSession().dataTaskWithURL(url1!) { data, response, error in
if error != nil {
println(error)
return
}
let parser = NSXMLParser(data: data)
parser.delegate = self
if parser.parse() {
println(self.results)
if indexPath.row == 0 {
cell.detailTextLabel!.text = self.parseResults
}
}
}
I've tried adding a tableView.reloadData() but that seems to not be the correct function.
Actually, it is not a good way to do this.
First, NSURLSession is an asynchronous networking, that means they run the networking off of the main thread.
Second, you should have a data model, like array to hold these strings from self.parseResults, and handle these requests in viewDidLoad.
Finally, check all the requests done or not. If done, than reload the tableview, feeding all data to UI.
You should do all UI updates on the main queue.
dispatch_async(dispatch_get_main_queue()) {
// Update UI
}
Related
I'm trying to build a tableView witch has many cells with a button, what I want to do is when I click the button in a cell, the cell should be go to the bottom of the table, here's my code:
let datasource = RxTableViewSectionedAnimatedDataSource<ToDoListSection>(
configureCell: { [weak self] _, tableView, indexPath, item in
guard let self = self else { return UITableViewCell() }
let cell = tableView.dequeueReusableCell(withIdentifier: ToDoTableViewCell.reuseID, for: indexPath) as? ToDoTableViewCell
cell?.todoTextView.text = item.text
cell?.checkBox.setSelect(item.isSelected)
cell?.checkBox.checkBoxSelectCallBack = { selected in
if selected {
var removed = self.datasList[indexPath.section].items.remove(at: indexPath.row)
removed.isSelected = selected
self.datasList[indexPath.section].items.append(removed)
self.datasList[indexPath.section] = ToDoListSection(
original: self.datasList[indexPath.section],
items: self.datasList[indexPath.section].items
)
self.sections.onNext(datasList)
} else {
// Todo
}
}
return cell ?? UITableViewCell()
}, titleForHeaderInSection: { dataSource, section in
return dataSource[section].header
})
sections.bind(to: table.rx.items(dataSource: datasource))
.disposed(by: disposeBag)
however, because I sent a onNext event in the configureCell closure, I received a waring:
⚠️ Reentrancy anomaly was detected.
Debugging: To debug this issue you can set a breakpoint in /Users/me/Desktop/MyProject/Pods/RxSwift/RxSwift/Rx.swift:96 and observe the call stack.
Problem: This behavior is breaking the observable sequence grammar. next (error | completed)?
This behavior breaks the grammar because there is overlapping between sequence events.
Observable sequence is trying to send an event before sending of previous event has finished.
Interpretation: This could mean that there is some kind of unexpected cyclic dependency in your code,
or that the system is not behaving in the expected way.
Remedy: If this is the expected behavior this message can be suppressed by adding .observe(on:MainScheduler.asyncInstance)
or by enqueuing sequence events in some other way.
and the action on the screen is not I want.
what should I do?
how to reload the TableView correctly?
The fundamental problem here is that you are calling onNext inside an Observer that is observing the emission. Another way to word this is that you are emitting a new value before the system is finished processing the current value.
As the warning says, the simplest way to deal with this (and likely the best way in this case) is to insert .observe(on:MainScheduler.asyncInstance) between sections. and bind(to:). What this does is stall the emission for a cycle so your configureCell function has a chance to return.
I have a horizontal scrolling collection view inside a UITableView cell, achieving the view same as that of Netflix.
Currently, I am loading URL data in my view controller containing table view and passing the array of data in UITableViewCell which contains the collectionView, and then rendering collection view cells.
But I'm feeling lack of controls using this method. For e.g, UI management, hiding, showing views depending on URL data load and error, etc.
I tried loading URL data inside table view cell and that works
perfectly fine for me but I don't think that's appropriate to do, as
only controllers should control everything.
The closure I'm using to load data in my controller is -
private func fetchData() {
let id = UserDefaults.standard.getUserId()
Service.shared.fetch(userId: id) { (data, error) in
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: {
guard error == nil else {
print(error?.localizedDescription ?? "Error")
return
}
let result = data?.count != 0 ? "Success" : "Failure"
switch result {
case ResultType.Failure.rawValue:
print("Failure")
case ResultType.Success.rawValue:
if let data = data {
self.data = data
}
default: break
}
})
}
}
Back to the Question, Is it fine loading the data inside
UITableViewCell in order to hide/show or animate UICollectionView inside that UITableViewCell?
Moreover, Assume a scenario where I have to load 4-5 URL data and render them in each custom table view cell which may or may not contain a collection view.
Complicated!
You can load data in a view, but it wouldn’t be a good architecture. It hampers reusabilty and mixes responsibilities. Also, table view cells will be reused, which will eventually lead to weird data loading behavior and probably bugs.
I suggest extracting data loading into a custom class, and using that class in the view controller. This way your data loading is decoupled from the controller and the view, giving the most flexibility.
I'm trying to implement a tableView that has 4 different possible prototype cells. They all inherit from base UITableViewCell class and implement its protocol.
For two of the cells there's asynchronous data fetching but one in particular has been giving me fits. The flow is as follows:
1) Dequeue reusable cell
2) Call configure
func configure(someArguments: ) {
//some checks
process(withArguments: ) { [weak self in] in
if let weakSelf = self {
weakSelf.reloadDelegate.reload(forID: id)
}
}
}
3) If the async data is in the cache, configure the cell using the image/data/stuff available and be happy
4) If the async data is NOT in the cache, fetch it, cache it, and call the completion
func process(withArguments: completion:) {
if let async_data = cache.exists(forID: async_data.id) {
//set labels, add views, etc
} else {
fetch_async_data() {
//add to cache
//call completion
}
}
}
5) If the completion is called, reload the row in question by passing the index path up to the UITableViewController and calling reloadRows(at:with:)
func reload(forID: ) {
tableView.beginUpdates()
tableView.reloadRows(at: indexPath_matching_forID with: .automatic)
tableView.endUpdates()
}
Now, my understanding is that reloadRows(at:with:) will trigger another dataSource/delegate cycle and thus result in a fresh resuable cell being dequeued, and the configure method being called again, thereby making step #3 happy (the async data will now be in the cache since we just fetched it).
Except...that's not always happening. If there are cells in my initial fetch that require reloading, it works - they get the data and display it. Sometimes, though, scrolling down to another cell that requires fetching DOES NOT get the right data...or more specifically, it doesn't trigger a reload that populates the cell with the right data. I CAN see the cache being updated with the fresh data, but it's not...showing up.
If, however, I scroll completely past the bad cell, and then scroll back up, the correct data is used. So, what the hell reloadRows?!
I've tried wrapping various things in DispatchQueue.main.async to no avail.
reloadData works, ish, but is expensive because of potentially many async requests firing on a full reload (plus it causes some excessive flickering as cells come back)
Any help would be appreciated!
Reused cells are not "fresh". Clear the cell while waiting for content.
func process(withArguments: completion:) {
if let async_data = cache.exists(forID: async_data.id) {
//set labels, add views, etc
} else {
fetch_async_data() {
// ** reset the content of the cell, clear labels etc **
//add to cache
//call completion
}
}
}
I have a tableView.reloadData() call in a loop in several places in my app. the basic idea is like this:
let dataSourceArray = []
for arrayItem in anotherArray {
dataSourceArray.append(arrayItem)
self.tableView.reloadData()
}
What I'm trying to achieve is to have the tableView draw each cell as it is loaded into the model array.
What is currently happening is that all the cells appear once the loop is complete.
This is all being done in the main thread, however I tried anyway to dispatch to main thread for the reloadData call. That made no difference. I tried tableView.layoutIfNeeded() after reloadData and that also made no difference.
Placing break points in the cellForRowAtIndexPath and heightForRow... methods indicate normal tableView calls as expected at each pass in the loop. However for some reason the cells don't draw on screen until the loop is completed.
Is there anyway to force the tableView to draw the cell at each pass other than what I have tried above?
I belive the reason is because the looping of your elements will be done very quickly (milliseconds) therefore you will not notice the incremental changes.
You need to run outer loop in global queue like below and dispatch reloadData to main queue.
var dataSourceArray = [ String ]()
#IBAction func
DoA( _: Any? ) {
let anotherArray = [ "A", "B", "C" ]
DispatchQueue.global().async {
for arrayItem in anotherArray {
self.dataSourceArray.append( arrayItem )
DispatchQueue.main.async {
self.tableView.reloadData()
}
Thread.sleep( forTimeInterval: 1 )
}
}
}
If you want a neat animation thats visible then you can probably force a timer on it, between each append; with a UIView.animate or UIView.transition... I'd prefer the transition method, it gives you a few options that make your changes even neater.
Here is a sample of what you can do (dont copy paste it directly, adjust it to your own, i wrote it in playground without testing it)
var i = 0
func addItem(){
if i == anotherArray.count {
return
}
UIView.transition(with: tableView, duration: 0.3, options: [.allowAnimatedContent, .transitionCrossDissolve], animations: { // content that is going to animate
dataSourceArray.append(anotherArray[i])
tableView.reloadData()
}) { // completion block
i += 1
addItem()
}
}
Other than that, you can change the content mode of your tableView (most probably from the storyboard) to be set to .top so on every change your tableView keeps on sticking to the top rather than jumping around (https://developer.apple.com/documentation/uikit/uiview/1622619-contentmode). This specially helps if you're using autolayouts
I added 2 UIButton in my custom UITableViewCell.
When pressed something is done with the displayed object(in this case a User). Now I want the row/cell to disappear. My Idea was to reload the screen via triggering the viewDidLoad() and viewDidAppear() functions,
since i use PFQueries to obtain user data and display them in my table view.
What happens is, that other than deleting that row since my query shouldn't find the data, it just adds the same things again.
Is there a better way to solve this? I want to delete the row and redo my Query.
To delete a row from a table, you can use a function like this:
// Add this function to your ViewController
func tableDeleteRow(indexPath: NSIndexPath) {
// IMPLEMENT ME:
// first, remove the item from the data that drives the tableView.
// This is what I do. Yours will be different.
// self.tableData.removeAtIndex(indexPath.row)
// tell the table to delete the row
dispatch_async(dispatch_get_main_queue()) {
// Code
print("remove from table")
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
}
If you are looking to reload the entire table and redo the data you got from the server, do this:
// This is pseudo code.
func tableRefresh() {
// this is a pseudo code function.
// replace it with your own.
get_data_from_server() {
(data, response, error) in
// do something with data
dispatch_async(dispatch_get_main_queue()) {
print("refresh table")
self.tableView.reloadData()
}
}
}