Update UITableViewCell based on alert view button pressed - uitableview

I have a tableview with custom cells. Each cell contains a button you press, that prompts an alert.
The alert uses the UIAlertView delegate as expected, and it checks the title of the button to determine what the user pressed. This in turn calls a web service that submits data. Once that submission completes, I need to have the button in that cell change to another button image.
Reloading the data so that it reflects the updates sent to the server and then reloading the tableview is having no effect.
Is there a way to access the cell outside of the cellForRowAtIndexPath? I have it setting a global variable named selectedIndex based on what button I press, but that is as far as I can get. Is it even plausible (or a good idea) to access a cell this way.
Thanks for any help!

There are quite a few ways to reload the data in a tableview. But for your case, if you can get a reference to the cell, it would suffice to do little UI related changes(changing images or something like that).
If you think your table's content size is going to vary after the web service returns response, then you must reload the table. Ofcourse you can reload the whole table's content, or just a few rows or sections. Check out the official documentations for the detailed information on them
The global variable is not a good way to get the index of the cell, of which button is pressed. A better approach would be if you use the tag property of the button. Set the tag of the button with the row(or section whatever your schema is) of indexPath. Then you can fetch the tag in your button's selector method.
Get a reference to the cell when the web service call finishes, get an instance of NSIndexPath via indexPathForRow:inSection:.
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:button.tag inSection:0];
Check if the cell is still visible and go forward with that.
MyCustomCell *cell = (MyCustomCell *)[self.tblView cellForRowAtIndexPath:indexPath];
and do your changes on that cell there.

UITableView has a method (not the delegate method) called -cellForRowAtIndexPath:. Provided that cell is still visible, and you have stored the index path off, you can use that to obtain the cell. It will return nil if the cell is not visible (but in that case the delegate method will be called later on to provide a cell for that index path again before it becomes visible, so you would configure it then).
The caveats are that a web services call is presumably asynchronous, and lots of things can change between when the user presses the button and you get the web response. The user could have scrolled the cell offscreen such that it is no longer visible, in which case you would need to store off the updated value somehow so you can properly configure the cell later on. Also, if your code calls reloadData for any reason, even keeping the NSIndexPath may not make any sense -- if your underlying model has changed at all, the model record associated with the user's click may now be at a completely different NSIndexPath (or may not exist in the data set at all). Depending on your situation those may or may not be issues, but if they are, it may be best to keep a reference to the selected record (or the primary key of that record), and when the call comes back, determine what the index path to that record now is, and see if the cell for that index path is visible.
Lastly, I'd recommend using properties of your view controller to store off these values, not globals. If you ever create a new instance of the view controller, and you change behavior based on the value of the global variable, it may still be set (with completely bogus values) from the previous usage of that view controller class.

Related

Avoid UITableViewCell updating content when scrolled

I've found some similar questions already on SO, but nothing that seems to address this specific problem.
I'm using a UITableView with around 25 dynamic cells. Each cells contains a hidden UIProgressView. When the user taps on the download button within the cell to download that item, the UIProgressView is displayed and it indicates the progress of the download.
I've achieved this by assigning a tag to each cell which is equivalent to its corresponding downloadItemID. Each instance of MyCell has its own progressBar property.
This works fine as long as the table view is not scrolled during the download. It works also when the user is downloading multiple items at the same time. The code looks like this:
UIProgressView *theProgressBar;
for (MyCell *cell in self.tableView.visibleCells)
{
if (cell.tag == downloadItemID) theProgressBar = cell.progressBar;
}
float progressPercentage = (float)completedResources / expectedResources;
[theProgressBar setProgress:progressPercentage animated:YES];
The problem is that when the user scrolls the table view, the cell and progress view are transferred to another cell. It's simple enough to reset and hide the progress view for the new cell, but when the original/downloading cell is scrolled back into view, no progress is visible.
I've tried caching active progress bars into a dictionary and reallocating them back to the original cell in cellForRowAtIndexPath, but this is giving me the same result: no visible progress after scrolling the cell off and on the screen. Even if I can get them to show up, I'm doubtful I can get this to work seamlessly by rolling my own caching method.
What I need is to keep cells in memory. But can I work around dequeueReusableCellWithIdentifier? This whole problem has arisen because I had to switch to a dynamic system of allocating the cells, but it is too dynamic. Can I create the cells dynamically, but keep them in memory all the time once created, or at least keep the ones that are currently downloading?
(I understand the reasons why cell reuse is the preferred way to go with table views.)
You are working against the framework. As vadian says in the comment, you need to separate the logic and state information from the cells and put them elsewhere.
Here is one way of doing it.
Create a class to hold each download state, like a download ongoing flag, download progress, title, URL, etc.
In your view controller, create and add all your download objects to an array when the view controller is created. The first object corresponds to the first row in the table.
Whenever you dequeue a cell, populate it with data from the array. The NSIndexPath row property serves as the index into the array.
All your updates (download progress) updates the download objects in the array, then update the cell content using the updated object. You can use UITableView cellForRowAtIndexPath to get the cell for a specific array index, if you get nil there is no need to update it.

Is there a way to directly access UICollectionView elements without reloading?

I have another question open where I'm trying to figure out how to reload the collectionView without auto-scrolling. I was also realizing there are a lot of other situations where I will need to change things in the collection view. Also I have some items that I will want to change the .alpha on and change the text of. Is there a way to do all of this in Swift? For example (to be specific) if I have a collection view with a view in each cell and that view has a textField in it, can I change the alpha and text, (change alpha with animation even) without reloading entire table?
Look at the documentation for UICollectionView. There are several "reload" methods:
reloadData()
reloadSections(_:)
reloadItems(at:)
If you just want to reload a single item, update your data source's data and then call reloadItems(at:) passing in the index path for the item.
Another option, if a cell is currently visible, is to use the cellForItem(at:) method to get a reference to an existing cell. Then you can directly access UI components of the cell as needed. You should also update your data model as needed so if the user scrolls and comes back, the cell will be rendered properly.
Most appropriate where you can update your custom view of a particular UIcollectionViewcell is reloadItemsAtIndexPaths.
You would be handling a particular item than whole collectionview with reloadData.
You can handle it via notifications or some call backs in your code where you can make decision when to update which cell.
Hope it will help you.

What is a better way to deal with data after an async call using NSURLConnection?

This is the current logic that I'm using to populate a table view with NSURLConnection. It doesn't seem elegant to me.
Table View Controller's viewDidLoad method calls "sendConnection" method in my api wrapper class with the URL string as a parameter. This method makes the NSURLConnection. In connectionDidFinishLoading (which is in my wrapper class), another method is called (also in the wrapper class) with the connection as a parameter. This method extracts the URL from the connection object and examines it. It then uses a switch statement to deal with the data depending on the URL. The data is stored in variables in the wrapper class itself. By the time cellForRowAtIndexPath is called, the async call has finished and the data has been processed.
Is there a better way of doing this?
My reason for asking this is as follows:
I want to refresh a cell with a new height and a new text label when it is clicked. The data for this text label will be retrieved from the server upon the cell being tapped. Each cell will have slightly different data in the label (each cell represents a 'user' and the label will display how many mutual friends you have with the user). I want to store the data in the cell itself when the data is retrieved and then place it into the text label. This doesn't seem possible with my current way of making URL calls.
Any help with how to achieve this would be appreciated.
Here is some pseudo code for a pattern I like to use in these situations. Maybe it will help you as well.
- (void)viewDidLoad {
//1. put up some type of progressHud or spinner
//2. call your NSURL wrapper
//3. in the completion block of your wrapper, set your datasource variables
//example: #property (nonatomic,strong) NSArray *listOfData;
//4. create a custom setter for your datasource that calls tableview reload
//5. enable a refresh function; like "pull to refresh" or a bar button
//6. when pull to refresh is tapped or called, just repeat these steps
}
- (void)setListOfData:(NSArray*)listOfData {
_listOfData = listOfData;
if (_listOfData) {
[self.tableView reloadData];
}
}
As I read your question again, here are a couple more thoughts:
the pattern above will work for your initial load, to create the list of people or friends, etc.
If you plan on making another round trip after the cell is tapped, then you have to consider a number of issues. This is similar to a common problem with lazy loading images into tableview cells. There are issues like scrolling to consider - what if the cell is scrolled off the view before the data returns, for example, what if the cell has been reused, now the data is not tied to that cell any longer.
There are many async image libraries available on Github that would be good to look at to see how they solved those issues. Generally they are keeping track of the item in the cell and then checking if the cell is still in view and if so, they set the image.
You have a similar issue to solve. Tap the cell, get the new data, then update the cell. Resizing the cell will require you to reload it.
Look into [tableview reloadRowsAtIndexPaths:(NSArray*) with RowAnimation:(UITableViewRowAnimation)];
hope that helps
best wishes;
You should have a "Data Model" which represents the content (that is the cells) of your Table View.
Since you have "rows" in your table view, it makes sense this data model is a kind of array (possibly a NSArray) whose elements keep the data and state of the cell.
The data for each cell should not only have all the "data" properties rendered in your cell (e.g. the label) but also its state:
When a user tabs on a cell it will start an asynchronous task. This task may take a while to finish since it fetches data from a remote server. Think of several seconds, or even longer. You need to keep track of pending update tasks, since your implementation should prevent the user to update a cell again before the corresponding pending update task has been finished.
There are several techniques to accomplish this. One way is to have a property in your "Cell Data" class which reflects this state, for example:
#interface CellModel : NSObject
#property (atomic) BOOL hasPendingUpdate;
...
When the cell will be rendered, you retrieve the value of the property and render the cell appropriately.
When the update task finishes, it updates its cell model data.
This model update will eventually update your Table View. There are several techniques to accomplish this. You should take care about thread-safety here and the "synchronization" of your Data Model and the table view cells. For example ensure the value of the hasPendingUpdate only changes on the main thread - since otherwise your rendered cell may become out of sync with the data model (not to mention race conditions in case you modify and access the property on different threads without synchronization primitives).
While the cell waits for an update, it should visually indicate this state (using a spinner for example) and disable the action to start an update task.
Very much recommended is a "Cancel" button, which either cancels a certain cell update task or all pending update tasks.
When the user moves away from this view, you may consider to cancel all pending tasks.

How to manage selected cell

I'm building a blog reader app with a list of post objects in an NSArray.
cell.postTitle.text=post.Title;
I'm trying to find out which post has been selected and then set the corresponding cell.postTitle.text to a lighter font.
Somehow I have to remember this state when the navigationcontroller segue back to the top level (and destroyed?).
Can anyone tell me the best way to do this? Each post object has a postID. Maybe I can store this in NSUserDefault, but the list of read postID might grow too big over time.
Also there is didSelectRowAtIndexPath but in this method, I can't access the cell properties directly right?
If you are using a UITableView to display your information you should use the delgate didSelectRowAtIndexPath.
In order to make this work with a custom interface (I assume you have), you should create your own custom cell, subclass UITableViewCell and create a controller and xib. You can then register your xib to the table and use that. Then you can call the needed attributes.
Somehow I have to remember this state when the navigationcontroller segue back to the top level (and destroyed?).
This is best handled by using a delegate method by creating a custom protocol:
https://developer.apple.com/library/ios/documentation/cocoa/conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html
When your user clicks the cell, you would call a delegate method which could set the postID value back in the top controller.
Also there is didSelectRowAtIndexPath but in this method, I can't access the cell properties directly right?
Yes, you can get at the cell. Check out this question and answer:
How to reach the current selected cell in didSelectRowAtIndexPath?
Alternatively, if you want to get at the data you used to create the table cells in the first place, in your didSelectRowAtIndexPath method you can used the indexPath.row value to get the value from your array that is at that index position and pass that via a delegate method.
Found a solution.
Just add the postId of the selected post to an NSMutableArray in didSelectRowAtIndexPath. Then in cellForRowAtIndexPath, I check to make sure that the postID doesn't exist in my self.readPostID array.
If it does I set the font to Helvetica Light, else set it to Helvetica Bold.
First I had to make sure to [self.tableview reloadData] every time viewdidappear.
Second, I just save my NSMutableArray of readPostId to NSUserDefault everytime viewDidDisappear and reload the array in viewDidLoad.
Also I had to make sure that cell reuse doesn't affect the font. The else condition fixed that.

How can I get the value of a UITableViewCell that is not visible?

I have a table view cell that has many rows with a UITextView. In those UITextView the user can enter values.
The problem that I have is that if I want to get the value of a row when the row is hidden I get nil.
NSString *cellValue = ((UITextField *)[[[self.table cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]] contentView] viewWithTag:[self.table cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]].tag]).text;
I was thinking to save the value that the user enters in the UITextField, but if the user clicks a row that performs the calculate the delegate textFieldDidEndEditing is not being called.
I don't know how can I get this values in a secure way.
You should target the data source that is behind the UITableView. The UITableView is just a display mechanism and not the primary resource for getting to data. You can get the index for the cell based on the title, but then I would look tot he data source to get your actual data. Better yet I would just handle all operations against the data source itself if that is possible. When you build the table initially your data source is a list or array of some kind I am sure so you can just use it. You may need to create a variable on the view and retain the list/array there to make sure you have it and you should be all set.
Good Luck!
As mentioned, you would generally use the UITableView data source to access this.
That said, you are probably not seeing the data you expect because the you are using dequeued cells. If the number of cells is small, you could consider caching your own cells in an NSDictionary whose keys are some combination of the section and row, and accessing their view hierarchy via your own cache.
You can access rows that are not currently displayed by directly referring the tableView's dataSource with the wanted IndexPath.
You can achieve this by doing something like this (when using Swift 3):
UITableViewCell cell = self.tableView(self.tableView, cellForRowAt: indexPath)
What is happening most likely is your reusing cells. So when your cell with the textfield goes off screen it gets reused so you cannot access it anymore. What you will need to do is grab the value out the the textfield right after they enter it and store it somewhere. You can use – textField:shouldChangeCharactersInRange:replacementString: so that when ever the user enters a character into the textfield you can save the string.

Resources