iOS 13 UISegmentedControl inside UITableView caches previous selection - ios

I have a screen with UITableView which displays one row per data element available, count of data element can be in hundreds. Depending on the data element type, each rows shows either an input field or segmented control. There is problem with the segmented control in iOS 13, when user taps first or second segment of a segmented control row on the top of the view and scrolls down, it auto select first segment for some random segmented control row in the subsequent scrolling view/page. It looks as if the control state is not refreshed.
Here is the code snippet of cellForRowAtIndexPath function where it creates the UISegmentedControl row.
QuestionOnTopTableViewCell *cell = [aTableView dequeueReusableCellWithIdentifier:#"YesNoInputIdentifier"];
if (!cell) {
cell = [[QuestionOnTopTableViewCell alloc] initWithCellStyle:QuestionCellStyleYesNo reuseIdentifier:#"YesNoInputIdentifier"];
}
cell.titleLabel.text = row.title;
cell.segmentControl.selectedSegmentIndex = row.answer ? row.answer.boolValue : UISegmentedControlNoSegment;
Any one ran into this issue?
Note: This issue happens only on iOS13.

The system will only allocate enough cells to visually display, and then when one scrolls off it reuses it for the new cell scrolling in. As a result - when this happens it remembers the previous selection as you're seeing.
Not sure why this wasn't happening pre iOS 13 (it does look like your cellForRowAt: function should set the correct value if you're looking at the right row variable, but you should just be able to reset it by overriding the UITableViewCells prepareForReuse() method.
Swift
class QuestionOnTopTableViewCell: UITableViewCell {
// stuffs
override func prepareForReuse() {
super.prepareForReuse()
self.segmentControl.selectedSegmentIndex = .noSegment
}
}
Obj-C
This should work for you:
-(void)prepareForReuse{
[super prepareForReuse];
self.segmentControl.selectedSegmentIndex = UISegmentedControlNoSegment;
}
Edit:
I actually did just see another question claiming .noSegment doesn't work anymore after a value has been set on iOS13/Xcode 11 builds: UISegmentedControl.noSegment stopped working with Xcode 11, iOS 13
What you may need to do instead, unfortunately, is recreate a local version of the segmented control every time and overriding the control: cell.segmentControl = myNewJustCreatedSegmentControl
you can perhaps still do that in the prepareForReuse method

Related

UICollectionView Cells Are Not Retaining Data --- How Can I Make Their Data Persistent?

I have a UICollectionView full of custom cell objects.
When the user selects one I draw a circle and put a number in the middle.
However as the user scrolls down and then back up, selected cells have their number data all messed up.
I dove deeper into what the problem is, and it turns out that cells are not cached, and when you scroll up with a UICollectionView, its creating new cell assets, as such data inside them is destroyed (and hence I can't store a number in them). So my thought is to put the number back each time that cell is back on the screen, but I can't tell when a cell is being displayed or not.
I've made a map which records the NSIndexPath of selected cells. However, I don't know how to tell if that path (i.e. a cell at that path) is currently being displayed.
Do you know how I can tell if a cell is being displayed from an NSIndexPath at any point along a scroll?
Here is my cell code for selection (my UICollectionView calls this).
override public var selected: Bool
{
didSet
{
_checkMark?.hidden = !selected
_number?.hidden = !selected
println("did set")
}
}
func setNumberText(number:UInt8)
{
if(_number == nil)
{
_number = UILabel(frame: CGRectMake(0,0,1,1))
addSubview(_number)
}
_number.text = String(number)
_number.sizeToFit()
_number.frame = CGRectMake(_checkMark.frame.width/2 - _number.frame.width/2, _checkMark.frame.height/2 - _number.frame.height, _checkMark.frame.width, _checkMark.frame.height)
_numberInt = number
}
Is there some "cellAtPathIsNowOnScreen" event I can track?
You're doing it wrong. Cells get reused as you scroll, and the method by which this happens is an implementation detail that you don't get to know.
Instead you should store the data about what the cell should draw, in this case the number, in the data source for your collection view. Then in -
collectionView:cellForItemAtIndexPath: you can tell the cell what number to draw after you dequeue it, or clear the value if the number shouldn't be drawn for this particular cell.
Additionally, UICollectionView does have a - visibleCells method which will return an array of all the cells that are currently visible, but that's really not what you want to do.

Deleting cell at edge of UICollectionView - cells not appearing immediately after scroll

Consider an standard, vertically scrolling flow layout populated with enough cells to cause scrolling. When scrolled to the bottom, if you delete an item such that the content size of the collection view must shrink to accommodate the new number of items (i.e. delete the last item on the bottom row), the row of cells that scroll in from the top are hidden. At the end of the deletion animation, the top row appears without animation - it's a very unpleasant effect.
In slow motion:
It's really simple to reproduce:
Create a new single view project and change the default ViewController to be a subclass of UICollectionViewController
Add a UICollectionViewController to the storyboard that uses a standard flow layout, and change its class to ViewController. Give the cell prototype the identifier "Cell" and a size of 200x200.
Add the following code to ViewController.m:
#interface ViewController ()
#property(nonatomic, assign) NSInteger numberOfItems;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.numberOfItems = 19;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return self.numberOfItems;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
self.numberOfItems--;
[collectionView deleteItemsAtIndexPaths:#[indexPath]];
}
#end
Additional Info
I've seen other manifestations of this problem when dealing with collection views, it's just that the above example seems the simplest to demonstrate the issue. UICollectionView seems to go into some kind of paralysed state of panic during the default animations, and refuses to unhide certain cells until after the animation completes. It even prevents manual calls to cell.hidden = NO on hidden cells from having an effect (hidden is still YES afterwards). Dropping down to the underlying layer and setting hidden there works, provided you can get a reference to the cell you want to unhide, which is non-trivial when dealing with cells that haven't been displayed yet.
-initialLayoutAttributesForAppearingItemAtIndexPath is being called for every item visible at the time of the call to deleteItemsAtIndexPaths:, but not for the ones that are scrolled into view. It is possible work around the issue by calling reloadData inside a batch update block immediately afterwards, which appears to make the collection view realise that the top row is about to appear:
[collectionView deleteItemsAtIndexPaths:#[indexPath]];
[collectionView performBatchUpdates:^{
[collectionView reloadData];
} completion:nil];
But unfortunately this is not an option for me. I am trying to implement some custom animation timing by manipulating the cell layers & animations, and calling reloadData really throws things out of whack by causing unnecessary layout callbacks.
Update: A bit of investigation
I added log statements to a lot of layout methods and looked through some stack frames to try and find out what's going wrong. Crucially, I'm checking when layoutSubviews is called, when the collection view asks for layout attributes from the layout object (layoutAttributesForElementsInRect:) and when applyLayoutAttributes: is called on the cells.
I would expect to see a sequence of methods like this:
// user taps cell (to delete it)
-deleteItemsAtIndexPaths:
-layoutAttributesForElementsInRect:
-finalLayoutAttributes...: // Called for the item being deleted
-finalLayoutAttributes...: // \__ Called for each index path visible
-initialLayoutAttributes...: // / when deletion started
-applyLayoutAttributes: // Called for the item being deleted, to apply final layout attributes
// collection view begins scrolling up
-layoutSubviews: // Called multiple times as the
-layoutAttributesForElementsInRect: // collection view scrolls
// ... for any new set of
// ... attributes returned:
-collectionView:cellForItemAtIndexPath:
-applyLayoutAttributes: // Sets the standard attributes for the new cell
// collection view finishes scrolling
Most of this is happening; layout is correctly triggered as the view scrolls, and the collection view properly queries the layout for the attributes of cells to be displayed. However, collectionView:cellForItemAtIndexPath: and the corresponding applyLayoutAttributes: methods are not being called until after the deletion, when layout is invoked one last time causing the hidden cells to be assigned their layout attributes (sets hidden = NO).
So it seems that despite receiving all the correct responses from the layout object, the collection view has some kind of flag set to not update the cells during the update. There is a private method on UICollectionView called from within layoutSubviews that seems responsible for refreshing the cells' appearance: _updateVisibleCellsNow:. This is from where the data source eventually gets asked for a new cell before applying the cells starting attributes, and it seems this is the point of failure, as it is not being called when it should be.
Additionally, this does seem to be related to the update animation, or at least cells are not updated for the duration of the insertion/deletion. For example the following works without glitches:
- (void)addCell
{
NSIndexPath *indexPathToInsert = [NSIndexPath indexPathForItem:self.numberOfItems
inSection:0];
self.numberOfItems++;
[self.collectionView insertItemsAtIndexPaths:#[indexPathToInsert]];
[self.collectionView scrollToItemAtIndexPath:indexPathToInsert
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:YES];
}
If the above method is called to insert a cell while the inserted cell is outside the current visible bounds, the item is inserted without animation and the collection view scrolls to it, properly dequeuing and displaying cells on the way.
Problem occurs in iOS 7 & iOS 8 beta 5.
Adjust your content insets so that they go beyond the bounds of the device's screen size slightly.
collectionView.contentInsets = UIEdgeInsetsMake(-5,0,0,0); //Adjust this value until it looks ok

Keep the color of text's cell when come back to table

I changed the color of text for the cell clicked in the table. But after the cell is clicked, when i come back to table the text of cell has the original color. Could you give me an advice?
This is the code in "didSelectRowAtIndexPath"
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.textLabel.highlightedTextColor = [UIColor blueColor];
Thank you
after the cell is clicked, when i come back to table the text of cell has the original color. Could you give me an advice?
You need to have the color for each cell stored somewhere other than in the table, so that you can reproduce the colors you want anytime the table redraws itself. Typically, you'll have some sort of data structure that stores the table's data, and that's usually the right place to save any changes the user makes. The table view's data source should have a -tableView:cellForRowAtIndexPath: method that sets the color according to what you've saved, along with any other cell attributes.
This is happen because the cells are reused, so lets say when you change text colour property of some cell it will be affected as you expect but when you scroll and that cell disappear off the screen it will be put to reuse pool and if it appears again on the screen table view takes some cell from the reuse pool but it's properties will be different so the colour won't persist.
You should keep somewhere, for example in NSMutableArray, info about which table was clicked.
You can add an index path to the array when you click the cell and in cellForRowAtIndexPath: check is this indexPath in the array and if it is change appropriate property.
The problem is that iOS throws away your cell if you scroll away and recreates it when it's needed (you scroll back to the cell).
If I were you, I would subclass UITableViewCell and overwrite
- (void)setSelected:(BOOL)selected animated:(BOOL)animated;
In there you would have
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected: selected animated: animated];
self.textLabel.textColor = selected ? [UIColor blueColor] : [UIColor blackColor];
}
Since iOS UITableView remembers which cell is selected, this should work fine, even when it's recreated.
The reason it's happening is what others are saying: cells are reused.
Storing selection state or color will work, however if you just need to make sure that selected cells have a different color for a label than non-selected cells, there's a way that does not require to use a supporting data structure.
You just need to check if the cell being setup at - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath is currently selected or not, and that can be achieved with [tableView indexPathForSelectedRow] if your table uses single selection, or [tableView indexPathsForSelectedRows] if it uses multiple selection.
The last case requires you to find the current indexPath in the returned array, and might be slower than using the supporting array.
But if the selection is simple, then this solution is probably faster, uses less memory and is easier to read (IMO).

UITableViewCell showing UIImageView in some cells and different cells when scrolling - in use with CoreData and NSFetchedResultsController

I have a simple application whereby a UITableView is populated by a user tapping on an item in the UINavigationBar and then getting taken to another view to fill in UITextFields and UITextView. When the user clicks save, the information is saved to Core Data and then represented in this UITableView with the use of NSFetchedResultsController.
One of the attributes is a "Notes" attribute on a "Transaction" Entity. Filling in the Notes in the UITextView is completely optional, but if the user adds in a note, I want to show a custom image that I've created on the cell for the entry that has the note.
When the app is run in this version alone (so deleted and installed with this developer release), it works very well and the cells show the notes only for the cells that have the notes. However, when updating from a previous version of the app, this is where the problem occurs.
The previous version of the app didn't have a Notes attribute and so I used CoreData lightweight migration to set up a new model with a Notes attribute. That part works.
The Problem
Because of the reusing of cells, I'm noticing that when I've updated from an old version to this new version, none of the cells have the custom image and that's good because the notes doesn't exist. However, if I go in and add a new entry with a note and then scroll through my UITableView, I notice the cells start showing the custom image randomly, based on scrolling. So it disappears from one cell and shows up on another. This is a big mis-representation for my users and I'm not quite sure what to do to fix this.
In my cellForRow I have the following code:
self.pin = [[UIImageView alloc] initWithFrame:CGRectMake(13, 30, 24, 25)];
self.pin.image = [UIImage imageNamed:#"pin"];
self.pin.tag = 777;
if (!transaction.notes) {
dispatch_async (dispatch_get_main_queue (), ^{
[[customCell viewWithTag:777] removeFromSuperview];
});
}
if ([transaction.notes isEqualToString:#""] || ([transaction.notes isEqualToString:#" "] || ([transaction.notes isEqualToString:#" "] || ([transaction.notes isEqualToString:#" "]))))
{
[[customCell viewWithTag:777] removeFromSuperview];
}
else
{
[customCell addSubview:self.pin];
}
So the first if statement is to check whether the notes exist and that returns true when updating from an old version of an app to this version. The second if statement just checks if the value of the notes is equal to a few spaces and if so, then to remove the note.
I just can't figure out what's going on here.
Possible Solution
In the same UITableView cell, I also have a green/red dot UIImageView which is displayed depending on whether the user selected a Given or Received Status when adding a new entry. With this in mind, one image is ALWAYS displayed, whether it's the green or red dot. So what I'm thinking about here is creating a transparent square and just changing the if statement to say "If note, show pin image and if not, show transparent image".
That feels a bit like a hack though and I'd prefer a proper way to fix this.
Any thoughts on this would really be appreciated.
First of all, bad practice to allocate views in cellForRow. If you really need to allocate views in cellForRow do it just when it's needed, in your case in the else statement.
Second, do not use dispatch_async to dispatch on main thread if you are already on main thread (cellForRow it's on main thread).
The above points are just some suggestions for performance improvement.
As a solution of your problem, I would create a custom UITableViewCell and in it's method prepareForReuse I would remove the imageView.
EDIT
YourCustomCell.m
- (void)prepareForReuse {
[super prepareForReuse];
[[self viewWithTag:YOUR_TAG] removeFromSuperview];
}
This is a straightforward implementation, but you have to take in consideration that is more expensive to alloc/dealloc the UIImageView than keep a reference to the image and hide it when you don't need it. Something like:
YourCustomCell.h
#interface YourCustomCell : UITableViewCell {
IBOutlet UIImageView *theImageView; // make sure you link the outlet properly ;)
}
YourCustomCell.m
- (void)prepareForReuse {
[super prepareForReuse];
theImageView.hidden = YES;
}
And in cellForRow you just have to check if you have notes and make the imageView visible (probably you will make theImageView a property)
Because table view cells are reused you must have a default value for your image. So for example set your image to transparent by default and change it under some condition. This will stop your image being shown in reused cells.
Why do you have a dispatch_async here?
if (!transaction.notes) {
dispatch_async (dispatch_get_main_queue (), ^{
[[customCell viewWithTag:777] removeFromSuperview];
});
}
Because you cannot be sure when the function inside it will execute. Suppose that transaction.notes is nil. All the isEqualToString functions will return false and the else condition of addSubView will be called. But sometime after this function is exited the code inside dispatch_async will be run and remove the pin view. I'm not whether this is the intended behavior.

Why is my UILabel in a UIView in a UITableCellView not updating after a correct database call, (it seems cell reuse related)?

I have a UITableview cell that gets a tally from a core data database. The tallyTable is in a view controller inside a UITab view. I have an NSLog statement that prints out the tally value whenever it gets updated. Another tab has a list to change the source (different day) for the tallies. I am using iOS5 with ARC targeting iOS 4.2.
Here's the problem. When I load the application, the correct tallies for whatever the last selected day show up in the table tab. If I then go to the day tab and change the day and return to the tally tab there is no change in the display. However, the viewWillAppear on the tally tab runs and as the table cycles through cellForIndexPath, my NSLog statement prints out all the correct new values. If I then scroll the top label off the screen and back the label updates to the new value.
I've tried setNeedsLayout and setNeedsDisplay on the UILabel, the UITableViewCell, the UITableView and the view controller loading the table. I tried changing the CellReuse identifier so that it would never reuse a cell.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
CollectionItemTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[CollectionItemTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
NSUInteger row = [indexPath row];
cell.textLabel.text = [[self.collectionKeys objectAtIndex:row] valueForKey:#"collectionTitle"];
NSInteger test1 = indexPath.row + 150;
NSLog(#"tag = %i", test1);
cell.tallyButton.tag = test1;
NSNumber * questionID = [[self.collectionKeys objectAtIndex:row] valueForKey:#"answerID"];
cell.tallyLabel.text = [NSString stringWithFormat:#"%i",[self updatePointTotal:questionID]];
NSLog(#"Collection text should be = %#", [NSString stringWithFormat:#"%i",[self updatePointTotal:questionID]]);
[cell setNeedsLayout];
return cell;
}
I've read over a half dozen other similar questions. Got about three hours invested so far in trying to solve this.
EDIT: I thought I fixed it by using the navigation controller to repush the top level view controller on to the view again. I'll admit now this feels like a classically kludgy hack in every way. When the view is PUSHED everything updates and it is seamless. However, in order to have a fixed footer to make selection settings for the table buttons, I used a UIView with two subviews, a UITableView on top and a simple UIView with four buttons below.
The captions on the buttons need to change with the data source. Now when the view controller is pushed onto the view it obscures my fixed footer view. So, I inserted the fixed footer into the UITableview and everything appeared fine until I scrolled the UITableView and the footer scrolled up with it. The table is basically a tally sheet with buttons next to each item and in the footer is four buttons to note the color of the tallied item. Say the next item was a green lego, you would tap "green" in the footer and the button next to "lego" in the table. When I push the view controller with the two subviews the UITableview labels do not update. Thus the tableview needs to be pushed itself (as far as I can tell).
ANSWER: see comment below but ultimately I needed to reload both the visible UITableView data and the delegate UITableView controller data behind it.
I'll give it a shot. First, are you using ARC? If not, you need to add autorelease when you alloc/init a new cell. Otherwise, it's fine as is.
If I'm understanding your question correctly:
The tableView displays the correct data at app launch
You switch away from the tab with the tableView and change the tableView dataSource
You switch back to the tab with the tableView and you see (via NSLog) that the table cells are reloaded with the correct data yet the old data is still visible in the cells
If you scroll a cell off the display and back forcing it to refresh it contains the correct data
Some thoughts:
the tableView will not reload itself automatically when it's view appears. You need to call [tableView reloadData] whenever the dataSource changes. This is independent of whether the tableView is currently displayed or not. My guess is this alone will solve your problem.
You don't need to call setNeedsLayout on the cell unless you want the cell to relayout its subviews based on the data. You also don't need setNeedsDisplay.
I'm assuming there aren't other complicating factors (such as multiple tableViews displaying the same data) that could confuse things.
If you use prepare for reuse method, remember to over the original method with [super prepareForReuse];
Another method if the above way does not work is re setup cell as describe here.
I use the same method i applied for some of my collection view : we should remove/reset your subview where you create/add it to cell 's content. That mean we need set up data each cell completely for each row.
I move the code reset data value from prepare reuse to where i set value and I worked simply !
In my CustomCell.m :
- (void)configCellWith:(id)item At:(NSUInteger)row {
if (_scrollView) {
[[_scrollView subviews]
makeObjectsPerformSelector:#selector(removeFromSuperview)];
_scrollView = nil;
[_scrollView removeFromSuperview];
}
else {
CGFloat y = labelHeight+15;
float scrollHeight = _imgArray.count*200;
_scrollView=[[UIScrollView alloc]initWithFrame:CGRectMake(10, y,SCREEN_WIDTH-20, scrollHeight)];
_scrollView.scrollEnabled=YES;
_scrollView.userInteractionEnabled=YES;
_scrollView.layer.masksToBounds = YES;
[self.contentView addSubview:_scrollView]; } }
Remember to change your data source appropriately too.

Resources