Touches should cancel function for UITableView is not called - ios

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

Related

iOS / Swift : How can I scroll a view up or down when touch event starts on a UIButton?

Using UIKit:
I've got a view that has multiple UIButton, but I can only scroll the view up or down if I began my scroll touch event outside the UIButton.
So how can I make it so that I can scroll my view, regardless if I start my touch event on a UIButton?
If your view behaves like a scroll view, try solution as below
It has been proposed here: https://stackoverflow.com/a/26292829/9191054 and it works!
class UIButtonScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
if view.isKind(of: UIButton.self) {
return true
}
return super.touchesShouldCancel(in: view)
}
}

Highlighting button inside UITableView reacts slowly

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
}
}

incorrect order of Accessibility in UICollectionView

So I have a UICollectionView in a UIViewController which is one of the root view controllers in the tab bar. I set a contentInset for the UICollectionView so I can Add a Label to the top of the collectionView, at which point it would mean that the UILabel is part of the collectionView but is not part of the headerView of the collectionView. To achieve the addition of the UILabel to the UICollectionView, I use
collectionView.addSubview(theLabel)
and I turn voice over on and run the application. what happens is that the voiceover goes through all the UICollectionViewCells in the correct order all the way to the last CollectionViewCell to begin, then goes to the Label which is at the top of the collectionView and then goes to the tabBar. I tried the answer in this Change order of read items with VoiceOver, but had no luck, this solution did change the order of
self.accessibilityElements
to the way I want, except the voice over doesn't really follow the order in self.accsibilityElements and I am not really sure what is going on, has anyone come across the same trouble with the accessibility order being screwed up because "addsubView" was used on the UICollectionView. IF (and I say IF, because I don't think anyone would have added a subView to a collectionView this way) anyone has any thoughts please help me out here, been stuck with this bug the longest time.
Edit
class CollectionViewSubViewsAddedByTags: UICollectionView {
override func awakeFromNib() {
super.awakeFromNib()
}
var accessibilityElementsArray = [AnyObject]()
override var accessibilityElements: [AnyObject]?{
get{
return accessibilityElementsArray
}
set(newValue) {
super.accessibilityElements = newValue
}
}
override func accessibilityElementCount() -> Int {
return accessibilityElementsArray.count
}
override func accessibilityElementAtIndex(index: Int) -> AnyObject? {
return self.accessibilityElementsArray[index]
}
override func indexOfAccessibilityElement(element: AnyObject) -> Int {
return (accessibilityElementsArray.indexOf({ (element) -> Bool in
return true
}))!
}
override func didAddSubview(subview: UIView) {
super.didAddSubview(subview)
accessibilityElementsArray.append(subview)
accessibilityElementsArray.sortInPlace { $0.tag<($1.tag)}
}
override func willRemoveSubview(subview: UIView) {
super.willRemoveSubview(subview)
if let index = (accessibilityElementsArray.indexOf({ (element) -> Bool in
return true
})) {
accessibilityElementsArray.removeAtIndex(index)
}
}
}
Thanks,
Shabri
I've run into the same issue - I'm using a top inset on our UICollectionView to allow room for a header that slides on/off screen with scroll. If I use Voice Over with this layout then the entire system gets confused and the focus order is incorrect. What I've done to get around this is use an alternate layout when VO is activated - instead of placing the header over the collection view with an inset, I place the header vertically above the collection view and set 0 top inset on the collection view.

Load A UICollectionView's Data Before It's Added To The Screen

I have a situation where I need to have a UICollectionView have its data loaded (so I can fetch its content size) before it's been added to the view hierarchy.
I have tried calling reloadData and dispatching on the main_thread but to no avail
//The context manager needs to check with this to see if the scrollview can scroll (because otherwise it can't pan it up)
func hasEnoughContentForScrolling()->Bool
{
dispatch_sync(dispatch_get_main_queue(), {self.collectionView.reloadData()})
if(collectionView.contentSize.height > UIScreen.mainScreen().bounds.height)
{
return true
}
return false
}
Do you know how I can pre-load a UICollectionView before it's been added to the screen?
You can hide your collectionView in your viewDidLoad method and as collectionView is reloaded you can show your collection View with loaded data.
I tried to write the code in Swift. not sure it is the right code for a swift. but may be it can help you to understand-
override func viewDidLoad()
{super.viewDidLoad()self.collectionView.hidden = YES}
func hasEnoughContentForScrolling()->Bool
{
dispatch_sync(dispatch_get_main_queue(),{
self.collectionView.reloadData()self.collectionView.hidden = NO })
}

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