clipToBounds doesn't work when swiping cells - uitableview

I have a custom cell with several labels hidden by default (the user sees all content when clicking on the cell). I'm changing the height of the cell and cell.clipToBounds = true to hide the labels. But this doesn't work for editable cells. When I start swiping the cell to left, the hidden content appears. I noticed that it works if I first expand the cell, collapse and the swipe, but stops working if I swipe another cell.
Any suggestions how to hide it when the cell is swiped?

Add the labels to an array.
Set hidden to true:
let labels = [firstLabel, secondLabel, etc.…]
for label in labels {
label.hidden = true
}

I think that is a bug with the setEditing overriding or ignoring clipsToBounds during the animation and edit.
The solution:
You should hide all the elements which should not appear when the table is "closed", and unhide them when the table is expanded.
1) Create a UITableViewCell method to hide/unhide all elements you need:
func hideAll(hide: Bool) {
myLabel.hidden = hide
myUISwitch.hidden = hide
}
2) Call the above method when you initialize your cells. Usually will be called somewhere inside the cell, on an initializer, or will be called on cellForRowAtIndexPath. I check for the cell.bounds.height to determine if the cell is expanded or not:
//cell init code
//I'm calling this method from inside the cell, so I use self here.
//If you're calling this method from your controller you should
//target the cell with cell.bounds.height, as well as targeting the
//cell to call the method: cell.hideAll(Bool)
var hidden = (self.bounds.height < 80) //change this number depending on your collapsed and expanded cell heights
hideAll(hidden)
3) At this point (if the above method was called inside cellForRowAtIndexPath or on a cell initializer) your method will be called when the cells are created and every time the cell expands. But not when the cell contracts (more on this ahead). So add some code in your editingStyleForRowAtIndexPath:
override func tableView(tableView: UITableView,
editingStyleForRowAtIndexPath indexPath: NSIndexPath)
-> UITableViewCellEditingStyle{
if let cell = tableView.cellForRowAtIndexPath(indexPath) as? CustomTableViewCell {
cell.hideAll(cell.bounds.height < 80) //calls hideAll with a Bool argument, depending on height of cell
}
return .Delete
}
editingStyleForRowAtIndexPath is called only on the cell you swipe, and doesn't call any of the delegate methods such as heightForRowAtIndexPath, so I believe this is the best place to hide your view items.
Ok, that was the solution. Now the WHY:
The true problem when you want to hide these cells is when to hide them. First you have to understand the cycle that swift goes through with tableViewCells.
I'm assuming you're expanding/contracting the cells on the heightForRowAtIndexPath. Which semantically makes total sense. But let's suppose you have 3 cells (all expandable). I'm doing a println() whenever cellForRowAtIndexPath, didSelectRowAtIndexPath and heightForRowAtIndexPath are called. In the following example, I tapped on the first row, then on the second, then on the third. Observe the results on the console:
>>>> called didSelectRow at index 0
???? called heightForRow at index 0
???? called heightForRow at index 1
???? called heightForRow at index 2
???? called heightForRow at index 0
==== called cellForRow at index 0
???? called heightForRow at index 0
>>>> called didSelectRow at index 1
???? called heightForRow at index 0
???? called heightForRow at index 1
???? called heightForRow at index 2
???? called heightForRow at index 1
==== called cellForRow at index 1
???? called heightForRow at index 1
>>>> called didSelectRow at index 2
???? called heightForRow at index 0
???? called heightForRow at index 1
???? called heightForRow at index 2
???? called heightForRow at index 2
==== called cellForRow at index 2
???? called heightForRow at index 2
As you can see, when you didSelectRowAtIndexPath, heightForRowAtIndexPath is called multiple times, cyclically, until swift is sure it has all the heights right. Meanwhile, cellForRow is called only on the cell you selected.
So if your cells auto-collapse when you select another cell, you have to realize they're not running any code (such as the method to hide elements) when they collapse, because cellForRowAtIndexPath was called only on the tapped cell.
You could hide/unhide inside your heightForRow code, but that code is called so many times that it's really not optimal. Code should be called only once when you need it. So the best solution IMO is to call it inside the editingStyleForRowAtIndexPath

In your UITableViewCell subclass, override the willTransitionToState(state: UITableViewCellStateMask) method and set contentView.clipToBounds to true like this:
override func willTransitionToState(state: UITableViewCellStateMask) {
super.willTransitionToState(state)
contentView.clipsToBounds = true
}
From now on, it will clip the cell's subviews correctly.

try setting the contentView clipsToBounds property to true.
normally you shouldn't mess with the cell as a view.
Swift
cell.contentView.clipsToBounds = true
Obj-C
cell.contentView.clipsToBounds = YES

Related

UICollectionView custom layout invalidate when section count is 0 on viewWillAppear

I have a custom layout for uiCollectionView. Now if collectionView has 0 sections. And i push from collectionViewController to another controller and then pop. Now if section count is 0 collectionView will automatically invalidateLayout. How to stop this behaviour.
I have also override shouldInvalidateLayout but it never gets called.

Disabling all UITableViewCells user interaction

Hey I have a UITableView. There are 4 large cells (so they are not all displayed on the screen). At various points through the app I want to disable user interaction for all of them however I am getting a nil unwrap error when I run this code:
for row in 0...3 {
let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: 2)) as! AnswerTextChoiceCell
cell.userInteractionEnabled = reEnable ? true : false
}
I'm guessing it's because it can't fetch the cell because it's not displayed on the screen. How Would I go about disabling all user interaction for all the cells?
I don't want to disable the user interaction on the tableView as it will prevent the user from scrolling.
Any pointers on this would be really appreciated!
Try once.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//Configure the cell
if indexPath.row == yourcell {
cell.userInteractionEnabled = false
}
return cell!
}
Hope this helps.
Without seeing the rest of your code and your TableView the only two things I can see that would cause that crash would be:
When you are casting the cell from 'cellForRowAtIndexPath' to your 'AnswerTextChoiceCell' type
You have the wrong section passed in to your 'cellForRowAtIndexPath' function
To try and see which of the two it is I would first try omitting the 'AnswerTextChoiceCell' cast completely and see if the problem goes away, or at least that it stops crashing. The function will return a UITableViewCell anyway and the userInteractionEnabled property is available on that object anyway so this will still allow you to test out what you're after.
If that doesn't work then we'll need a bit more information about your table view such as, how many sections do you have, is it storyboard based, etc.

Accessibility collectionView focus changes when reloaded, iOS

I'm working on the accessibility of a calendar which is actually a collectionView. Whenever a cell is tapped, the collectionView will be reloaded by calling
[self.collectionView reloadData];
The problem is if the voiceOver is running, the focus will move to another place after the cell tapped because that cell is reused on somewhere else.
Is there anyway to keep the focus where it was after the reloadData? Thanks!
Just find a workaround for this. The focus is changed because the focused cell is reused somewhere else when doing [colleciontView reloadData].
So if we reload the collectionViewCells one by one, that focused cell will not be used anywhere else. I call this method to reload the collectionView when VoiceOver is running.
- (void)reloadCalendarCollectionView {
NSInteger items = [self.calendarItems count];
for (NSInteger i = 0; i < items; i++) {
[self.collectionView reloadItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:i inSection:1] ]];
}
}
You could try doing self.collectionView.accessibilityElementsHidden = YES before reloading data. Then, when it completes, you will have to do the inverse & then post a notification for the cell you're looking for.
We have a collection view and this collection view reloading data in every second. When user taps cell, focus changing after every reload, so collectionview cell selects wrong cell at indexPath.
Create a protocol for delegation in cell:
protocol AccessibilityCellProtocol{
func accessibilityFocused(cell:UICollectionViewCell)
}
Override accessibilityElementDidBecomeFocused in your cell:
override class func accessibilityElementDidBecomeFocused(){
self.delegate.accessibilityFocused(cell:self)
}
In view controller create an selectedIndexPath variable. Assign it in delegation method.
func accesibilityFocused(cell:UITableViewCell){
selectedIndex = collectionView.indexPath(for: cell)
}
And in your didSelectItemAtIndexPath method:
if UIAccessibility.isVoiceOverRunning{
cellTappedWith(indexPath:selectedIndex)
return
}
cellTappedWithIndexPath(indexPath:indexPath)

How to calculate tableView row height and pass value to heightForRowAtIndexPath

I've a tableView with custom cells.
Each cell has different interface, elements. All they have different sizes.
Each cell has different type of elements and their count.
All these elements are created dynamically, so I'm creating them, making their frames and adding as subviews.
So the problem that heightForRowAtIndexPath executes before cellForRowAtIndexPath where I'm creating the row and constructing its interface and I don't know how to pass calculated height to heightForRowAtIndexPath
How can I count row height before and pass its value to heightForRowIndexPath for correct drawing of my tableView?
If your cell view is really very complex and every component's height are depending on data source. You can try to create the view in heightForRowIndexPath method and then cache the created view to a dictionary in your view controller and use it directly in cellForRowAtIndexPath. In this way you only need to create the view once when user scrolling the table. If the datasource is not changing very frequently, you can reuse the cached view in heightForRowIndexPath as well.
And if the tableview has a lot of rows, you should return an approximate value for height in estimatedHeightForRowAtIndexPath to speed up the loading process of the view controller. Otherwise during loading tableview, it will try to calculate all row's height which may requires a lot of time.
But I really don't think your cell would be so complex. If only some UITextLabels that depends on datasource for the height, you can simply only calculate the height for the label, then add it to other components' height which is fixed.
You really should consider subclassing UITableViewCell and adding your custom logic inside your subclass.
Starting from there, you'll have such options:
Create static method in your cell that will receive all data necessary to draw your cell (e.g heithtForCellWithFirstString:secondString:accessoryImage etc) and calculate height using string size computation methods. Use this method inside heightForRowAtIndexPath.
Use autolayout for laying out subviews of your cell and then set table view's row height property to UITableViewAutomaticDimension. This way you won't need heightForRow delegate method at all. There are plenty of tutorials on this, for example: http://www.raywenderlich.com/87975/dynamic-table-view-cell-height-ios-8-swift
Try to create a subclass of UITableViewCell.
All elements created by you put inside UIView, which is put in a cell.
Create three NSLayoutConstraint, one from UIView.top to top of the cell, the second from UIView.bottom to the bottom of the cell and the third - the height of the UIView.
In the viewDidLoad method of tableViewController set row height as the UITableViewAutomaticDimension
:
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.rowHeight = UITableViewAutomaticDimension
}
In a subclass of UITableViewCell create a method that will calculate the height of the cell and change NSLayoutConstraint, which sets the height of the cell. For example:
:
func adjustHeightOfInnerView() {
let height = 100
self.myInnerCellViewHeightConstraint.constant = height
self.contentView.setNeedsUpdateConstraints()
}
In the method cellForRowAtIndexPath call the adjustHeightOfTableview:
:
func tableView (tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("myCell", forIndexPath: indexPath) as! myTableViewCellSubclass
cell.adjustHeightOfTableview()
return cell
}

UITableView didSelectRowAtIndexPath: not being called on first tap

I'm having an issue with UITableView's didSelectRowAtIndexPath.
My table is setup so that when I select row it initializes a new view controller and pushes it.
The first time I tap any row in the table, the method does not get called. Once I select another row, it begins to work as normal.
I have verified this by setting a breakpoint on didSelectRowAtIndexPath. When adding an NSLog to the method I see that when I select the second row that finally pushes the new view controller, I see two log statements appear in the console at the same time.
Any suggestions?
Any chance you accidentally typed didDeselectRowAtIndexPath?
Also check the selection property of your table view in xib file. Use 'Single Selection' or 'Multiple Selection' as required.
I experienced the following issue:
first tap in row -> no effect, no selection, never
second tap and following -> correct selection behavior, always
In my case, my error was checking Show Selection on Touch in Interface Builder. You can uncheck it in IB here:
Hope that helps someone
Check If you have set any Gesture recognisers in your class. Removing gesture worked for me.
I debated even posting this answer because I think the stars kind of aligned in order for this to manifest itself.
I am having a variation of this problem and have checked the other solutions. On my table view it isn't processing the very last row of my table on the first tap. It highlights it, but didSelectRowAtIndexPath isn't being called. All the other rows work fine. But if I turn the tableview bounce on then it seems to solve the problem (but then you have to deal with a tableview bounce).
UITableViewCellSelectionStyleNone was set for the cell displaying that problem
(ios9).
I have ended up calling
[tableView deselectRowAtIndexPath:indexPath animated:NO];
first thing in
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
which is not the same, but is good enough.
makes me wonder what kind of table breakage ios10 brings.
this issue happens also when you are working with gesture recogniser within a tableView in this case you don't need to remove them, you need only to make sure that your gesture property cancelsTouchesInView = false this is a boolean value affecting whether touches are delivered to a view when a gesture is recognised.
SWIFT 2
Make sure you have this set to true:
self.tableView.allowsSelection = true
Put this above your right after your viewDidLoad() and before the super.viewDidLoad()
SWIFT 3
If you are working with Swift 3 in a class which isn't a UITableViewController and you are using UITableViewDelegate, then you may need to use the method title:
#objc(tableView:didSelectRowAtIndexPath:) func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){...}
This worked for me, although I have just migrated my project to Swift 3. It's fairly new so this may be fixed later.
SWIFT 3
None of the other answers worked in my case, what fixed it for me was:
tableView.delaysContentTouches = false
If you have set any UITapGestureRecognizer in your class, you can add this line in your didSelectRowAtIndexPath:
[tableView deselectRowAtIndexPath:indexPath animated:NO];
This worked for me.
Swift 4.2
Disabling "Delay Touch Down" in the attributes inspector solved the issue.
After that the clicks are smooth and didSelectRowAt fires immediately.
didSelectRowAt function was not called in my app in first or second tap...
I was trying to solve the problem could not find any solution. But suddenly I was recognise, I was using view.animation color change... Delegate method was not called while animation persist
In my case I had a UITableView section header with a gesture recognizer. When the header is tapped, it should insert few rows into that section and animate the insertion (like expand/collapse section).
First time when expanded and tapped, the didSelectRow delegate method was not fired. For further taps and expand/collapse actions, it was working as expected.
Per answer by #Ayoub Nouri I set cancelsTouchesInView to false, and this resolved the issue.
tapGestureRecognizer?.cancelsTouchesInView = false
What worked for me was to start typing "didSelect..." and then let autocorrect fill in the rest. Apparently some detail of my syntax was wonky. As others have said, Use the IDE!!
I had the same issue on my tableView(swift 4.2), it could be fixed with allowsMultipleSelection property on tableView.
If allowsMultipleSelection is set to true the table view selection mechanism will change in a way that by selecting each cell the tableView didSelectRowAtIndexPath: is called for the first time and by selecting the same cell for the second time the tableView didDeselectRowAtIndexPath: is called.
It means that if the number of times a cell tapped are odd (1, 3, 5, ...) then always tableView didSelectRowAtIndexPath: will be called and if the number of times a cell tapped are even (2, 4, 6, ...) then always tableView didDeselectRowAtIndexPath: will be called.
This makes tableView didSelectRowAtIndexPath: function to be called on the third selection for the same cell and so the result is double tap for calling didSelectRowAtIndexPath:!!!
If you want the tableView didSelectRowAtIndexPath: to be called on each selection for a cell then the tableView multiple selection has to be set false, tableView.allowsMultipleSelection = false.
By doing this, every time the cell is tapped tableView didSelectRowAtIndexPath: will be called on table view and by selecting another cell tableView didDeselectRowAtIndexPath: will be called for the cell was selected before and then the tableView didSelectRowAtIndexPath: will be called for the newly selected cell.
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.allowsMultipleSelection = false
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("This will be called for each cell tap")
}

Resources