Highlighting button inside UITableView reacts slowly - ios

I have a UITableView with dynamically populated rows, but also there's a section at the top that contains one special cell (with a different identifier) which is always the same.
I've added two buttons to this cell and they do work, however they react poorly. That is, the highlighting occurs only after about 0.25s.
I'm using the following slightly customized button:
import UIKit
class HighlightingButton: UIButton {
override var isHighlighted: Bool {
didSet {
if isHighlighted {
backgroundColor = UIColor.lightGray
} else {
backgroundColor = UIColor.white
}
}
}
}
It's important that the user gets a clear feedback that they tapped the button. However with the slow highlighting this isn't satisfying, although the events seem to be triggered quickly (juding by printing some output).
In a normal view this HighlightingButton works as expected and the highlighting flashes as quickly as I can tap.
Is there something in the event handling of the UITableViewCell that leads to this slowness?
Update
I created a minimalistic example project that demonstrates the problem.
There aren't any GestureRecognizers and still there's this very noticable delay.

Take a look at delaysContentTouches property of UIScrollView.
I fixed your problem by setting it to false on tableView and all of it's scrollview subviews.
So you should just add a tableView IBOutlet and override viewDidLoad method like this:
override func viewDidLoad() {
super.viewDidLoad()
tableView.delaysContentTouches = false
for case let subview as UIScrollView in tableView.subviews {
subview.delaysContentTouches = false
}
}

Related

Touches should cancel function for UITableView is not called

I have a table view that has a UIImage and some UIButton objects in each TableView cell. When I scroll the table view, it works quite well overall. However, if I touch one of the UIButton items to scroll the table view, the UIButton seems to steal the touches and the table view does not scroll. Instead the UIButton items appears to be selected instead. I would like to be able to scroll the table view even when the user touches buttons when starting to scroll. So, I searched for solutions here, tried the following.
extension UITableView {
override public func touchesShouldCancel(in view: UIView) -> Bool {
print("the touchesShouldCancel function is called.")
if view is UIButton {
return true
}
return super.touchesShouldCancel(in: view)
}
}
However, it doesn't work. The function does not even get called whenever I scroll the table view. What am I missing here? I would greatly appreciate your input. Thanks all.
Subclass UITableView Set tableView canCancelContentTouches to true as per Apple docs
The scroll view does not call this method if the value of the
canCancelContentTouches property is false
class YourTableView:UITableView {
override func awakeFromNib() {
canCancelContentTouches = true
delaysContentTouches = false
}
override func touchesShouldCancel(in view: UIView) -> Bool {
}
}
You need to make a UITableView subclass
class SubTbl:UITableView {
// add your method
}
Then assign it to that table in IB or use it in code

UITableViewCell selection style changing background color for all subviews

I'm writing in swift, using the storyboard, and I'm working with iOS 10.
I have a UITableViewCell composed of some subviews (they're UIViews). Some of those subviews have a background color.
The selection style on the cell is Default (gray).
When I tap the cell, the cell becomes gray. Good. However, the background color of each subview in the cell is also changed automatically to match the selection color (gray) of the cell. Not good.
How can I best prevent this behavior? I don't want the background colors of the subviews in the cell to change.
NOTE: I don't consider it a very good solution to do my own selection by listening for the didSelectRowAtIndexPath delegate method and then setting the background color of the cell's content view. Perhaps this is the only way, but I want to see if there are other options first.
UPDATE:
Just to clarify, another reason I don't like the above solution of listening for the delegate is that it fixes the issue per cell. So if I have other custom cells that use the same subviews, I would have to implement the same fix. I want something that fixes this at the view level, rather than the cell level. So that when I use those same views in other custom cells for different table views, I don't have to worry about it.
This way is better than listening to the didSelectRowAtIndexPath for sure.
Play with these properties in your custom cell class:
override var isSelected: Bool {
didSet {
if isSelected {
//play with colors
print("selected")
} else {
//play with colors
print("deselected")
}
}
}
override var isHighlighted: Bool {
didSet {
if isHighlighted {
//play with colors
print("highlighted")
} else {
//play with colors
print("not highlighted")
}
}
}
Disable selection style:
cell.selectionStyle = UITableViewCellSelectionStyleNone
And tricky one:
cell.selectedBackgroundView = UIView()

UIButton not responding used in a custom UITableViewCell

I know this issue is already been asked few times in SO. Despite trying those out, I am still unable to solve my problem.
I am using a UITableView inside a UIViewController. I have a custom UITableViewCell which has couple of buttons in it. However, I am not able to make the Button respond to Click event.
The development environment is iOS 9 and Swift 2
Snippets used :
BranchNearMeTableViewCell.swift contains
#IBOutlet weak var btnDetails: UIButton!
view controller class
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("branchNearTableCell") as! BranchNearMeTableViewCell
cell.btnDetails.tag = indexPath.row
cell.btnDetails.addTarget(self, action: "showDetails:", forControlEvents: .TouchUpInside)
}
func showDetails(sender: UIButton){
print("Button Pressed:")
}
Additional Info:
TableView and TableCellView has User interaction disabled in Interface builder since don't want the entire cell to be clickable.
UIButton inside TableViewCell has User Interaction enabled.
Being an iOS noob, I may be making a silly mistake which I might have overlooked.
Similar questions that I checked include:
SO1
SO2
SO3
I Deeply appreciate any help regarding this question.
I faced a similar issue. I was programmatically adding an UIButton to the UITableViewCell via addSubview. The button would not respond to touch events. Using Debug View Hierarchy, I finally discovered that any subviews added to the UITableViewCell was behind contentView, which was blocking user input from reaching the UIButton. The issue was resolved by adding the UIButton to contentView instead of the UITableViewCell.
I would have userInteractionEnabled set to true on the table view cell as well. I would prevent taps using the UITableView allowsSelection to false
Also remember to remove the target and action in tableView:cellForRowAtIndexPath: since the cells are recycled, the button might already have the target and action, it might add a second.
I found a simple solution:
Inherits UITableViewCell, and override init()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
//init subviews, eg. self.switch = UISwitch()
super.init(style: style, reuseIdentifier: reuseIdentifier)
// add this line magic code
contentView.isUserInteractionEnabled = true
//add subviews, e.g. self.addSubView(self.switch)
}
You only have to do (in ViewDidLoad):
mTableView.delaysContentTouches = false
For programmatically created views, the only thing to remember is to declare buttons using lazy var in UITableViewCell. And also add subviews to contentView instead of the cell itself For example:
class CounterCell: UITableViewCell {
lazy var incrementButton: UIButton = {
let button = UIButton()
button.setTitle("+", for: .normal)
button.addTarget(self, action: #selector(incrementAction), for: .touchUpInside)
return button
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(incrementButton)
// Your constrains here
}
#objc func incrementAction() {
}
}
When using programmatically views, there's no need to add .userInteractionEnabled flags.
Then to take the action out of the cell, just add a delegate and assign it from the UITableViewDataSource.
I came across this issue today, with a button inside a static UITableview cell, that was not responding to user events.
I realised the 'Content View' of the cell also has a 'User Interaction Enabled' tick box. Make sure you select the 'Content View' inside the UITableview cell in your Document Outline menu, then tick the box for 'User Interaction Enabled' in the Attributes Inspector - see attached photo for reference. 'User Interaction Enabled' also needs to be checked for the cell for this to work.
Hope this helps. XCcode screen shot
Also, make sure you are adding target actions to your buttons outside their setup. So instead of
let button: UIButton = {
//addTarget...
}()
you can have a function to set up your buttons after something happens:
func setButtonsUp() {
// myButton.addTarget
}
For anyone else struggling, here's my solution:
sendSubviewToBack(cell.contentView)
The thing that there's now an extra UITableViewCellContentView layer which blocks interaction with views behind it.
Related issue: An extra UITableViewCellContentView overlay appears in a TableView on iOS 14 preventing taps, but works fine on iOS 13
Ad a first sight nothing seems to be wrong with your code.
So I suggest you to add a background color to the superview of the button, why? because if the button is outside the frame of its superview it will never receive touches.
If you see that the button is not inside the background color probably you have an issue positioning the item, check constraints or whatever you are using.
Check also the frame of the button.
You can also do both by inspecting the view at runtime, here a tutorial.
I dont know what wrong in the code but i can suggest which i personally use and it works for me
In BranchNearMeTableViewCell.swift
#IBOutlet var btnDetails: UIButton!
#IBAction func btnDetailsClick(sender: AnyObject) {
tapButton?(self)
}
var tapButton: (UITableViewCell -> Void)?
In Table view controller
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("branchNearTableCell") as! BranchNearMeTableViewCell
cell.tapButton = {(user) in
//User will be tablecell here do whatever you want to do here
}
}
So if you click on button in table cell this cell.tapButton will be called you can do whatever you want to do here
The only things we need to do is in cellForRowAt just put:
cell.selectionStyle = .none
in this way, UITableview will bypass the touch of selecting cells and allow buttons inside our cells to be clickable.
set cell and cell content view isUserInteractionEnabled = true
Add Tapgesture to the button
Add a closure to handle gesture action
Add target for that button.
button.addTarget(self, action: #selector(connected(sender:)), for: .touchUpInside)
Set tag of that button since you are using it.
button.tag = indexPath.row
Achieve this by subclassing UITableViewCell. button on that cell, connect it via outlet.
Make sure button.isUserInteractionEnabled = true
To get the tag in the connected function:
#objc func connected(sender: UIButton){
let buttonTag = sender.tag
}
Make sure that ALL of tableView's superviews do have isUserInteractionEnabled set to true
User interaction was already enabled for my UIButton. The thing that worked for me is
switching the stackView distribution to "Fill".

Disable flashing scroll bar of a UITableViewController?

I have a UITableViewController, and I'd like to make it not flash the vertical scroll bar when I go back from a push action segue on one of it's cells (popping the view controller and going back to the UITableViewController).
It seems that, if the table has many rows (mine has around 20 with 60 points height each, so bigger than the screen), when I go back, it always flashes the vertical scroll bar once to show where it is in the table. However, I don't want that to happen, but I do want to keep the scrollbar around so it shows when the user scrolls. Therefore, disabling it completely is not an option.
Is this default behavior and can I disable it temporarily?
There is a simpler solution that doesn't require avoiding using a UITableViewController subclass.
You can override viewDidAppear: as stated by http://stackoverflow.com/users/2445863/yonosoytu, but there is no need to refrain from calling [super viewDidAppear:animated]. Simply disable the vertical scrolling indicator before doing so, and then enable it back afterwards.
- (void)viewDidAppear:(BOOL)animated {
self.tableView.showsVerticalScrollIndicator = NO;
[super viewDidAppear:animated];
self.tableView.showsVerticalScrollIndicator = YES;
}
If you're using Interface Builder, you can disable the Shows Vertical Indicator option on the tableView for your UIViewController and enable it in code as shown above.
To get Cezar's answer to work for iOS10 I had to include a (sizeable) delay before re-enabling the scroll indicator. This looks a bit strange if someone tries to scroll before the second is up, so you can re-enable the scroll indicator as soon as someone scrolls.
override func viewDidAppear(_ animated: Bool) {
tableView.showsVerticalScrollIndicator = false
super.viewDidAppear(animated)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.tableView.showsVerticalScrollIndicator = true
}
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !tableView.showsVerticalScrollIndicator {
tableView.showsVerticalScrollIndicator = true
}
}
Actually, on thinking about it, you don't even need the delay, just do this:
override func viewDidAppear(_ animated: Bool) {
tableView.showsVerticalScrollIndicator = false
super.viewDidAppear(animated)
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !tableView.showsVerticalScrollIndicator {
tableView.showsVerticalScrollIndicator = true
}
}
Update: Please, look at Cezar’s answer below, which gives a nice workaround without any of the drawbacks of my proposals.
According to the documentation it is a behaviour of UITableViewController:
When the table view has appeared, the controller flashes the table view’s scroll indicators. The UITableViewController class implements this in the superclass method viewDidAppear:.
So I think you have two options:
You can avoid using UITableViewController and start using a naked UIViewController. Rebuilding the functionality of UITableViewController from UIViewController is not that hard (you can follow this old article as reference).
Override viewDidAppear: and don’t call [super viewDidAppear:animated]. The problem here is that you don’t know what else does UITableViewController do when viewDidAppear: is called, so you might break something.

Leaving inputAccessoryView visible after keyboard is dismissed

What I'm trying to do is to create something similar to the "find on page" search function in Safari on iPad.
I'm using a UIToolbar with some items in it and attached it to the keyboard by setting it as an inputAccessoryView on the UITextField. Works like a charm, but there is one thing I can't figure out. In Safari, when you search for something, the keyboard disappears but the tool bar remains on the bottom of the screen.
Does anyone have a clue on how to accomplish this? The only solution I can think of is to respond to a keyboard dismissed event and then pull out the UIToolBar and create a custom animation that moves it to the bottom of the screen. But this is hacky. I am looking for a more elegant solution. Something that can make me decide what to do with the input accessory view when the keyboard gets dismissed.
It's done like this:
Assign your UIToolbar to a property in your view controller:
#property (strong, nonatomic) UIToolbar *inputAccessoryToolbar;
In your top view controller, add these methods:
- (BOOL)canBecomeFirstResponder{
return YES;
}
- (UIView *)inputAccessoryView{
return self.inputAccessoryToolbar;
}
And then (optionally, as it usually shouldn't be necessary), whenever the keyboard gets hidden, just call:
[self becomeFirstResponder];
That way, your inputAccessoryToolbar will be both your view controller's and your text view's input accessory view.
I've ended up with UIToolBar that is not assigned as input accessory view, and slide up and down on UIKeyboardWillShowNotification / UIKeyboardWillHideNotification
Update to Swift 4, based on prior answers. If you add toolbar via storyboards you can do this
class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
#IBOutlet var toolbar: UIToolbar!
override var canBecomeFirstResponder: Bool {
get {
return true
}
}
override var inputAccessoryView: UIView {
get {
return self.toolbar
}
}
override func viewDidLoad() {
super.viewDidLoad()
textField.inputAccessoryView = toolbar
}
}
In this case, whenever text field resigns first responder, it defaults first responder to main view. Keep in mind, you might want to explicitly resign first responder, and set main view as first responder if there are multiple UI elements and first responder defaults to undesired view after resignation.
Adding to #arik's answer, here is the Swift version:
class ViewController: UIViewController {
#IBOutlet var textField: UITextField!
// Input Accessory View
private var inputAccessoryToolbar: UIToolBar?
override func canBecomeFirstResponder() -> Bool {
return true
}
override var inputAccessoryView: UIView? {
return inputAccessoryToolbar
}
override func viewDidLoad() {
super.viewDidLoad()
inputAccessoryToolbar = UIToolbar(frame: CGRectMake(0, 0, view.frame.size.width, 50))
textField.inputAccessoryView = inputAccessoryToolbar
}
// UITextFieldDelegate
func textFieldShouldReturn(textField: UITextField) -> Bool {
becomeFirstResponder()
return true
}
}
Thanks for the clean solution!
You may also need to work around the bug with the inputAccessoryView not respecting the safe area margins and thus not making room for home indicator thing on iPhone X: iPhone X how to handle View Controller inputAccessoryView?
I found the easiest solution when you have a UIToolbar from a xib and you are also using that UIToolbar as the inputAccessoryView of a text field is to embed the toolbar in a UIView when you return it from your overridden inputAccessoryView, and make the containing UIView taller by the safeAreaInsets.bottom. (Other solutions suggest constraining the bottom of the toolbar to the safe area in a subclass, but this leads to constraint conflicts and also means the area under the toolbar is the wrong colour.) However, you have to also bear in mind that the text field can have focus even when there is no keyboard on the screen (for instance if there is an external keyboard), so you need to change the inputAccessoryView of the text view to this toolbar-within-a-UIView in that case as well. In fact it will probably make things simpler to just always use the containing view and adjust the size of it appropriately. Anyway, here's my override of inputAccessoryView:
override var inputAccessoryView: UIView? {
if toolbarContainerView == nil {
let frame=CGRect(x: toolBar.frame.minX, y: toolBar.frame.minY, width: toolbar.frame.width, height: toolBar.frame.height+view.safeAreaInsets.bottom)
toolbarContainerView = UIView(frame: frame)
}
if (toolbar.superview != toolbarContainerView) {
//this is set to false when the toolbar is used above the keyboard without the container view
//we need to set it to true again or else the toolbar will appear at the very top of the window instead of the bottom if the keyboard has previously been shown.
toolbar.translatesAutoresizingMaskIntoConstraints=true
toolbarContainerView?.addSubview(toolbar)
}
return toolbarContainerView
}
It would probably be a good idea to override viewSafeAreaInsetsDidChange to adjust the size of toolbarContainerView in that case, too.

Resources