voice over can only see a page of a uicollectionview - ios

So i have a UICollectionView with a set of UICollectionViewCells displayed using a custom UILayout.
I've configured the UILayout to lay out all the UICollectionViewCells almost exactly the same as how they are laid out in the photos app on ios.
The problem is, it seems when voice over is turned on, and the user is traversing through the UICollectionViewCells using swipe, when the user gets to the last visible cell on the page, and tries to swipe forward to the next cell, it simply stops.
I know that in UITableView the cells will just keep moving forward, and the table view will scroll down automatically.
Does anyone know how to get this behaviour?

This answer worked for me, too. Thanks!
There is one other call you must have enabled to get this to work. Otherwise your method (void)accessibilityElementDidBecomeFocused will never get called. You must enable accessibility on the object Cell.
Option 1: In ViewController, set the cell instance to have accessibility.
Cell *cell = [cv dequeueReusableCellWithReuseIdentifier:kCellID forIndexPath:indexPath];
[cell setIsAccessibilityElement:YES];
Option 2: Implement the accessibility interface in the cell object:
- (BOOL)isAccessibilityElement
{
return YES;
}
- (NSString *)accessibilityLabel {
return self.label.text;
}
- (UIAccessibilityTraits)accessibilityTraits {
return UIAccessibilityTraitStaticText; // Or some other trait that fits better
}
- (void)accessibilityElementDidBecomeFocused
{
UICollectionView *collectionView = (UICollectionView *)self.superview;
[collectionView scrollToItemAtIndexPath:[collectionView indexPathForCell:self] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally|UICollectionViewScrollPositionCenteredVertically animated:NO];
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self);
}

After hours and hours of headache, the solution was really simple. If anyone else comes across a similar problem, this is what i did:
In the subclass of UICollectionViewCell that you are using for your CollectionView, override accessibilityElementDidBecomeFocused and implement it like this:
- (void)accessibilityElementDidBecomeFocused
{
UICollectionView *collectionView = (UICollectionView *)self.superview;
[collectionView scrollToItemAtIndexPath:[collectionView indexPathForCell:self] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally|UICollectionViewScrollPositionCenteredVertically animated:NO];
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
}

Stephen's answer worked for me! Thanks.
I want to add that this seems to affect only iOS6; it looks like they fixed it in iOS7.
Also, you can make the scrolling slightly faster and cleaner by passing self instead of nil to UIAccessibilityPostNotification -- like so:
- (void)accessibilityElementDidBecomeFocused {
UICollectionView *collectionView = (UICollectionView *)self.superview;
[collectionView scrollToItemAtIndexPath:[collectionView indexPathForCell:self] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally|UICollectionViewScrollPositionCenteredVertically animated:NO];
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self);
}

This is how you would do it in Swift:
override func accessibilityElementDidBecomeFocused() {
guard let collectionView = superview as? UICollectionView,
let indexPath = collectionView.indexPath(for: self) else {
return
}
collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
UIAccessibility.post(notification: .layoutChanged, argument: nil)
}

Related

Correct way to setting a tag to all cells in TableView

I'm using a button inside a tableView in which I get the indexPath.row when is pressed. But it only works fine when the cells can be displayed in the screen without scroll.
Once the tableView can be scrolleable and I scrolls throught the tableview, the indexPath.row returned is a wrong value, I noticed that initially setting 20 objects, for example Check is just printed 9 times no 20.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
lBtnWithAction = [[UIButton alloc] initWithFrame:CGRectMake(liLight1Xcord + 23, 10, liLight1Width + 5, liLight1Height + 25)];
lBtnWithAction.tag = ROW_BUTTON_ACTION;
lBtnWithAction.titleLabel.font = luiFontCheckmark;
lBtnWithAction.tintColor = [UIColor blackColor];
lBtnWithAction.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
[cell.contentView addSubview:lBtnWithAction];
}
else
{
lBtnWithAction = (UIButton *)[cell.contentView viewWithTag:ROW_BUTTON_ACTION];
}
//Set the tag
lBtnWithAction.tag = indexPath.row;
//Add the click event to the button inside a row
[lBtnWithAction addTarget:self action:#selector(rowButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
//This is printed just 9 times (the the number of cells that are initially displayed in the screen with no scroll), when scrolling the other ones are printed
NSLog(#"Check: %li", (long)indexPath.row);
return cell;
}
To do something with the clicked index:
-(void)rowButtonClicked:(UIButton*)sender
{
NSLog(#"Pressed: %li", (long)sender.tag);
}
Constants.h
#define ROW_BUTTON_ACTION 9
What is the correct way to get the indexPath.row inside rowButtonClicked or setting a tag when I have a lot of of cells in my tableView?
My solution to this kind of problem is not to use a tag in this way at all. It's a complete misuse of tags (in my opinion), and is likely to cause trouble down the road (as you've discovered), because cells are reused.
Typically, the problem being solved is this: A piece of interface in a cell is interacted with by the user (e.g. a button is tapped), and now we want to know what row that cell currently corresponds to so that we can respond with respect to the corresponding data model.
The way I solve this in my apps is, when the button is tapped or whatever and I receive a control event or delegate event from it, to walk up the view hierarchy from that piece of the interface (the button or whatever) until I come to the cell, and then call the table view's indexPath(for:), which takes a cell and returns the corresponding index path. The control event or delegate event always includes the interface object as a parameter, so it is easy to get from that to the cell and from there to the row.
Thus, for example:
UIView* v = // sender, the interface object
do {
v = v.superview;
} while (![v isKindOfClass: [UITableViewCell class]]);
UITableViewCell* cell = (UITableViewCell*)v;
NSIndexPath* ip = [self.tableView indexPathForCell:cell];
// and now we know the row (ip.row)
[NOTE A possible alternative would be to use a custom cell subclass in which you have a special property where you store the row in cellForRowAt. But this seems to me completely unnecessary, seeing as indexPath(for:) gives you exactly that same information! On the other hand, there is no indexPath(for:) for a header/footer, so in that case I do use a custom subclass that stores the section number, as in this example (see the implementation of viewForHeaderInSection).]
I agree with #matt that this is not a good use of tags, but disagree with him slightly about the solution. Instead of walking up the button's superviews until you find a cell, I prefer to get the button's origin, convert it to table view coordinates, and then ask the table view for the indexPath of the cell that contains those coordinates.
I wish Apple would add a function indexPathForView(_:) to UITableView. It's a common need, and easy to implement. To that end, here is a simple extension to UITableView that lets you ask a table view for the indexPath of any view that lies inside one of the tableView's cells.
Below is the key code for the extension, in both Objective-C and Swift. There is a working project on GitHub called TableViewExtension-Obj-C that illustrates the uses of the table view extension below.
EDIT
In Objective-C:
Header file UITableView_indexPathForView.h:
#import <UIKit/UIKit.h>
#interface UIView (indexPathForView)
- (NSIndexPath *) indexPathForView: (UIView *) view;
#end
UITableView_indexPathForView.m file:
#import "UITableView_indexPathForView.h"
#implementation UITableView (UITableView_indexPathForView)
- (NSIndexPath *) indexPathForView: (UIView *) view {
CGPoint origin = view.bounds.origin;
CGPoint viewOrigin = [self convertPoint: origin fromView: view];
return [self indexPathForRowAtPoint: viewOrigin];
}
And the IBAction on the button:
- (void) buttonTapped: (UIButton *) sender {
NSIndexPath *indexPath = [self.tableView indexPathForView: sender];
NSLog(#"Button tapped at indexpPath [%ld-%ld]",
(long)indexPath.section,
(long)indexPath.row);
}
In Swift:
import UIKit
public extension UITableView {
func indexPathForView(_ view: UIView) -> IndexPath? {
let origin = view.bounds.origin
let viewOrigin = self.convert(origin, from: view)
let indexPath = self.indexPathForRow(at: viewOrigin)
return indexPath
}
}
I added this as a file "UITableView+indexPathForView" to a test project to make sure I got everything correct. Then in the IBAction for a button that is inside a cell:
func buttonTapped(_ button: UIButton) {
let indexPath = self.tableView.indexPathForView(button)
print("Button tapped at indexPath \(indexPath)")
}
I made the extension work on any UIView, not just buttons, so that it's more general-purpose.
The nice thing about this extension is that you can drop it into any project and it adds the new indexPathForView(_:) function to all your table views without having do change your other code at all.
You are running into the issue of cell-reuse.
When you create a button for the view you set a tag to it, but then you override this tag to set the row number to it.
When the cell get's reused, because the row number is longer ROW_BUTTON_ACTION, you don't reset the tag to the correct row number and things go wrong.
Using a tag to get information out of a view is almost always a bad idea and is quite brittle, as you can see here.
As Matt has already said, walking the hierarchy is a better idea.
Also, your method doesn't need to be written in this way. If you create your own custom cell, then the code you use to create and add buttons and tags isn't needed, you can do it in a xib, a storyboard, or even in code in the class. Furthermore, if you use the dequeue method that takes the index path, you will always get either a recycled cell, or a newly created cell, so there is no need to check that the cell returned is not nil.

My gesture recognizer is attached to the wrong view

I have a UICollectionView that has elements that can be dragged and dropped around the screen. I use a UILongPressGestureRecognizer to handle the dragging. I attach this recognizer to the collection view cells in my collectionView:cellForItemAtIndexPath: method. However, the recognizer's view property occasionally returns a UIView instead of a UICollectionViewCell. I require some of the methods/properties that are only on UICollectionViewCell and my app crashes when a UIView is returned instead.
Why would the recognizer that is attached to a cell return a plain UIView?
Attaching the recognizer
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
EXSupplyCollectionViewCell *cell = (EXSupplyCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
UILongPressGestureRecognizer *longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:cell action:nil];
longPressRecognizer.delegate = self;
[cell addGestureRecognizer:longPressRecognizer];
return cell;
}
Handling the gesture
I use a method with a switch statement to dispatch the different states of the long press.
- (void)longGestureAction:(UILongPressGestureRecognizer *)gesture {
UICollectionViewCell *cell = (UICollectionViewCell *)[gesture view];
switch ([gesture state]) {
case UIGestureRecognizerStateBegan:
[self longGestureActionBeganOn:cell withGesture:gesture];
break;
//snip
default:
break;
}
}
When longGestureActionBeganOn:withGesture is called if cell is actually a UICollectionViewCell the rest of the gesture executes perfectly. If it isn't then it breaks when it attempts to determine the index path for what should be a cell.
First occurrence of break
- (void)longGestureActionBeganOn:(UICollectionViewCell *)cell withGesture:(UILongPressGestureRecognizer *)gesture
{
NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell]; // unrecognized selector is sent to the cell here if it is a UIView
[self.collectionView setScrollEnabled:NO];
if (indexPath != nil) {
// snip
}
}
I also use other properties specific to UICollectionViewCell for other states of the gesture. Is there some way to guarantee that the recognizer will always give me back the view that I assigned it to?
Views like UICollectionView and UITableView will reuse their cells. If you blindly add a gestureRecognizer in collectionView:cellForItemAtIndexPath: you will add a new one each time the cell is reloaded. If you scroll around a bit you will end up with dozens of gestureRecognizers on each cell.
In theory this should not cause any problems besides that the action of the gestureRecognizer is called multiple times. But Apple uses heavy performance optimization on cell reuse, so it might be possible that something messes up something.
The preferred way to solve the problem is to add the gestureRecognizer to the collectionView instead.
Another way would be to check if there is already a gestureRecognizer on the cell and only add a new one if there is none. Or you use the solution you found and remove the gestureRecognizer in prepareForReuse of the cell.
When you use the latter methods you should check that you remove (or test for) the right one. You don't want to remove gestureRecognizers the system added for you. (I'm not sure if iOS currently uses this, but to make your app proof for the future you might want to stick to this best practice.)
I had a similar problem related to Long-Touch.
What I ended up doing is override the UICollectionViewCell.PrepareForReuse and cancel the UIGestureRecognizers attached to my view. So everytime my cell got recycled a long press event would be canceled.
See this answer

Implement UICollectionView in a UICollectionViewCell

I want to have a UICollectionView where the cells are also UICollectionViews. Is this possible and if so, could someone give me a guide on how to start this? I already have my main UICollectionView, but I’m having trouble implementing another UICollectionView in its UICollectionViewCell.
I did something similar some time ago. Inside your cellForItemAtIndexPath: of your parent CollectionView you have to do something like below. I havent tested this so you might have to add in some code:
ChildCollectionViewController * cv = //Initialize your secondCVController here
if (!cv) {
cv = [self.storyboard instantiateViewControllerWithIdentifier:#"collectionViewID”]; //set your class’s storyboard ID before doing this
cv.view.frame = cell.contentView.bounds;
[self addChildViewController:cv];
[cell.contentView addSubview:cv.view];
[cv didMoveToParentViewController:self];
}
//Some code here for cell contents
return cell;
}

How to zoom a UIScrollView inside of a UICollectionViewCell?

I'm trying to add a UIScrollView inside of a UICollectionViewCell. The idea is that you can use pinch to zoom the UIScrollView (and with it, the image within), but the scrollview doesn't seem to handle any gesture. I'm guessing they are being caught by the UICollectionView.
I've set the delegate of the UIScrollView to be the UICollectionViewCell, but none of the delegate methods are being called.
EDIT:
I've made a github repo with the code (simplified as much as I could). Even though it's just a few lines of code, I cannot see what I did wrong.
EDIT2: After the answer was found, I added the fixes to the above-mentioned repo, hope others find it helpful too :)
https://github.com/krummler/gallery-pinchzoom-example
I just made an implementation for SWIFT 3 on iOS 9.3+ and all i done was:
1. Place the image view inside a scrollview
2. Connect the scrollview delegate to collectionview cell class
3. Implement the code below on the collectionview subclass
class FullScreenImageTextDetailCollectionViewCell: UICollectionViewCell, UIScrollViewDelegate {
#IBOutlet var scrollview: UIScrollView!
#IBOutlet weak var backgroundImageView: UIImageView!
override func awakeFromNib() {
self.scrollview.minimumZoomScale = 0.5
self.scrollview.maximumZoomScale = 3.5
self.scrollview.delegate = self
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.backgroundImageView
}
}
No gesture recognizer adding or removing on the parent collectionview controller was necessary, worked like a charm!
Thanks for previous examples for reaching this!
You might want to try manipulating the UIGestureRecognizers in order to do that. In the GalleryViewController:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
GalleryImageCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"galleryImageCell" forIndexPath:indexPath];
ImageContext *imageContext = [self.images objectAtIndex:indexPath.row];
cell.imageContext = imageContext;
[self.collectionView addGestureRecognizer:cell.scrollView.pinchGestureRecognizer];
[self.collectionView addGestureRecognizer:cell.scrollView.panGestureRecognizer];
return cell;
}
From Apple's documentation on UIView:
Attaching a gesture recognizer to a view defines the scope of the represented gesture, causing it to receive touches hit-tested to that view and all of its subviews. The view retains the gesture recognizer.
So you'll also want to make sure to remove them when the cell is not showing anymore.
- (void)collectionView:(UICollectionView *)collectionView
didEndDisplayingCell:(UICollectionViewCell *)cell
forItemAtIndexPath:(NSIndexPath *)indexPath {
// Get the cell instance and ...
[self.collectionView removeGestureRecognizer:cell.scrollView.pinchGestureRecognizer];
[self.collectionView removeGestureRecognizer:cell.scrollView.panGestureRecognizer];
}
Since you're not modifying the UIGestureRecognizer's delegate, only its scope, it will still control the zooming of just that cell's scrollview.
EDIT:
I added the panGestureRecognizer to the above examples, following a suggestion from the OP that it was needed. The zooming itself is completely handled by the pinchGestureRecognizer, but it's true that in most cases, after zooming an image to a point where only a subset of it is visible, you'll want to pan to move the visible portion around. That is, it's part of a proper zooming experience.
Please add scrollview in your cell and your current cell image view add in scrollview.
then use below code:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
ImageContext *context = [self.images objectAtIndex:indexPath.row];
ImageCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"imageCell" forIndexPath:indexPath];
cell.cellScrollView.autoresizesSubviews = YES;
cell.cellScrollView.multipleTouchEnabled =YES;
cell.cellScrollView.maximumZoomScale = 4.0;
cell.cellScrollView.minimumZoomScale = 1.0;
cell.cellScrollView.clipsToBounds = YES;
cell.cellScrollView.delegate = self;
cell.cellScrollView.zoomScale = 1.0;
[cell.imageView setImage:[UIImage imageNamed:context.thumbImageUrl]];
return cell;
}
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{ NSLog(#"%i",scrollView.subviews.count);
for (UIView *v in scrollView.subviews) {
if ([v isKindOfClass:[UIImageView class]]) {
return v;
}
}
}
Check if all related view's multitouch is on. iOS disabled multitouch by default on most views to save energy.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if (touch.view == [self myScrollView]) //<- whatever your scrollView is called
{
return YES;
}
return NO;
}
Don't know your code but try playing around with the above code to see if you can filter your touches to the objects you want. The above code is from the UIGestureRecognizerDelegate Protocol Reference.
The delegates are not getting called because you added it inside the UICollectionview.The touch events are availabel for the collection view which is the superview and hence not obtainable by the view inside.You may have to think of some other way to achieve this model.
UICollectionView is having the same pattern as UITableView,The problem occours in tableview inside scrollview ,that the touch event is accepted by scrollview[which is the superview] and to make the tableview scrollable in that case ,The scroll of scrollview must be disabled.This is another version of that problem
The apple docs says that the above said case make unpredictable results.So This may be same for your problem.
In my opinion you must go for better design which can achieve what you look for
I've set the delegate of the UIScrollView to be the UICollectionViewCell, but none of the delegate methods are being called.
For that just define one function in collectionViewCell
#IBOutlet weak var mainScrollView:UIScrollView!
func setup(){
mainScrollView.minimumZoomScale=1
mainScrollView.maximumZoomScale=10.0
mainScrollView.delegate=self
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return yourimageviewinside of scrollview
}
call this fuction in
collectionview(_,cellForItem ) method of collectionview

Turning off Voice-over focus for a cell in table view and not for others

The first cell in my table view is a dummy cell and thus, when Voice-over mode is ON, I want to skip that cell so that the focus does not come to that control
and thus, none of its traits are spoken by the Voice over. I wrote the code pasted below to acheive the same, thinking that isAccessibilityElement alone is sufficient for this. But it seems that is not the case.
Even though this element i said to be non-accessible in the code, still its getting focus by right/left flicks in Voice-over mode. Any idea as to how this can be achieved?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
....
UITableViewCell *cell = (UITableViewCell *)[tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if(indexPath.row == 0)
{
cell.isAccessibilityElement = 0;
}
}
use some cutom cell, and within that cell definition implement this:
- (NSInteger)accessibilityElementCount {
NSIndexPath *indexPath = [(UITableView *)self.superview indexPathForCell: self];
if(indexPath.row==0){
return 0;
}
else{
return 1;
}
}
The current way to do this seems to be setting accessibilityElementsHidden on the cell to true/YES (depending on whether using Swift or Obj-C.
Seems cleaner than other answers proposed and seems effective in my very brief testing.
It’s not ideal but could you only display the cell when VoiceOver is not activated? You can use the
UIAccessibilityIsVoiceOverRunning()
Function to see if VoiceOver is on when your app loads, and register the
#selector(voiceOverStatusChanged)
Notification so you can be informed if the user enables or disables voiceover. For more on this see the following blog post.
< http://useyourloaf.com/blog/2012/05/14/detecting-voiceover-status-changes.html>
Just override the cell's function accessibilityElementCount like this:
Swift 4.2:
override func accessibilityElementCount() -> Int {
return 0
}

Resources