incorrect order of Accessibility in UICollectionView - ios

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.

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

Design UICollectionViewCell based on even and odd index of the cell in Swift iOS

I have been stuck on this problem for weeks. I am trying to design collectionview where an even cell has label on left side and odd cell has label on right side. Initially the design is ok and code runs fine but when user starts scrolling the indexPath changes and hence the label isn't aligned as wanted. The picture shows an simple desired layout. Anyone know a solution to this??
The desired UI:
You could do this in your UICollectionViewDataSource methods.
But I'd say the easiest way to do this is inside the cell's apply(_ layoutAttributes:) method.
Just create a cell subclass and override that method like so:
class MyCollectionViewCell: UICollectionViewCell {
var myLabel: UILabel!
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
let isEven = layoutAttributes.indexPath.row % 2 == 0
if isEven {
self.myLabel.textAlignment = .right
} else {
self.myLabel.textAlignment = .left
}
}
}

Self sizing UICollectionView with FlowLayout and variable number of cells

Please note that this is NOT a question about self sizing UICollectionViewCell.
Is it possible to create self sizing UICollectionView (with UICollectionViewFlowLayout) size of which depends on cells inside it?
I have a collection view with variable number of cells. I would like to constrain width of the collection view and then allow it to expand vertically depending on quantity of cells.
This question is similar to this one CollectionView dynamic height with Swift 3 in iOS but I have multiple cells per row.
Bonus points, if one could still use self sizing cells inside of collection view but it is ok if collection view delegate provides cell sizes.
I don't have enough reputation to comment, but I think
this is the answer
that you are looking for. Code below:
class DynamicCollectionView: UICollectionView {
override func layoutSubviews() {
super.layoutSubviews()
if bounds.size != intrinsicContentSize() {
invalidateIntrinsicContentSize()
}
}
override func intrinsicContentSize() -> CGSize {
return self.contentSize
}
}
#Michal Gorzalczany's answer led me to this answer (for my specific case):
Subclass UICollectionView
class DynamicCollectionView : UICollectionView {
weak var layoutResp : LayoutResponder?
override func invalidateIntrinsicContentSize() {
super.invalidateIntrinsicContentSize()
self.layoutResp?.updateLayout()
}
override var intrinsicContentSize : CGSize {
return self.contentSize
}
override var contentSize: CGSize {
get { return super.contentSize }
set {
super.contentSize = newValue
self.invalidateIntrinsicContentSize()
}
}
}
Protocol LayoutResponder should be adopted by the view which deals on a high level with layout of collection view (I have a relatively complex layout).
protocol LayoutResponder : class {
func updateLayout()
}
extension RespView : LayoutResponder {
func updateLayout() {
self.layoutResp?.setNeedsLayout()
self.layoutResp?.layoutIfNeeded()
}
}
In my case I actually forward updateLayout() even further up the chain.
I guess for simpler layouts you can skip step 2 alltogether.
This is in my opinion is a bit "hacky" so if someone has a better approach I would appreciate if you share.

iOS 11 scroll to top when using large titles doesn't work properly

When using large titles and tapping the status bar to scroll to the top of a UIScrollView or UITableView (probably also UICollectionView, haven't tested this) it always goes a little too far.
I have refresh enabled on my TableView and when tapping the status bar it appears like this and stays that way until I tap the screen.
I have a ScrollView in another ViewController and if I tap the status bar there it also scrolls a little bit too far, making the navigation bar too tall. This also returns to normal when I tap somewhere or scroll a tiny bit.
Normal:
After I tapped the status bar:
This also only happens when I have large titles activated, using normal titles everything works as it should.
Any ideas how to fix this?
How to recreate:
Create a new project with a navigation controller and a UIViewController with a TableView inside.
Set navigation controller to prefer large titles. Turn translucent off. Set title on UIViewController
Set constraints on TableView to pin to the edges of the ViewController
Create outlet for TableView in the ViewController
Implement delegates and set a number of rows, for example 100
Launch app
Scroll down so the large title becomes a normal title
Tap status bar so the tableView scrolls to the top
Now the title is not at the position it should be, if you now scroll a tiny bit up or down it snaps back to the normal position.
ViewController code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 100
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath)
return cell
}
}
Okay so I found why the problem occurs, but not how to fix it in that exact scenario.
If you're using large titles and a UITableViewController with the navigation bar translucency set to off the problem will occur.
When you turn translucent back on the problem goes away.
If you're using a TableView in a normal UIViewController the problem always occurs.
Edit
Turns out setting "extendedLayoutIncludesOpaqueBars = true" fixes the problem if you're using a translucent navigation bar!
Similar question: UIRefreshControl() in iOS 11 Glitchy effect
I faced the same issue. I had a collectionview in UIViewController embedded in UINavigationController. Collectionview had leading, trailing, top, bottom constraints to a safe area.
To fix this you need:
Change the top constraint of collectionview to supervises (NOT safe area)
Set extendedLayoutIncludesOpaqueBars = true in viewDidLoad method
After many hours of tests i found a solution that works with UIViewController.
In UIViewController with UITableView, you should:
in viewDidLoad set extendedLayoutIncludesOpaqueBars = true
in storyboard pin tableView top constraint to superview top with constant = 0 (tableView will be under navigationbar and statusbar when navigationbar is translucent)
After that if you tap on statusbar, tableview stops in the right place.
In your ViewController declare a var didBeginScrollToTop of Bool type.
Assign the controller as scrollView's delegate. Then
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
didBeginScrollToTop = true
return true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard didBeginScrollToTop, scrollToTopGestureTargetView?.contentOffset.y ?? 0 < -25 else { return }
scrollToTopGestureTargetView?.setContentOffset(.zero, animated: true)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
didBeginScrollToTop = false
}
What this does is adjusting your content offset when bouncing. The flag is there to catch the scrollToTop gesture and is reset on scroll. You can also set it off after setting the .zero offset. If you have implementation with child VCs, don't forget to call super when overriding those 3 delegate methods.
Now my first approach was to track when the content offSet is < 0, but on some devices it did not expand the navigation item and that magic -25 seemed to work on all simulators. You can combine this approach with an implementation that returns the size of the nav bar and replace it, but as far as I am aware there wasn't an easy way to get the nav bar frame/bounds so easily.
Note you may have to handle also this: Strange velocity prefers large title
I've fix it with maually setting the contentOffset to 0 after scroll to top.
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
return true
}
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
scrollView.contentOffset.y = 0.0
}

UITableViewCell - drag whole row to move?

I'm trying to use a UITableViewController and allow the user to reorganize my data by dragging the rows. This is working fine, except that the user has to tap on the relatively small UITableViewCellReorderControl on the right quarter of the cell to drag the row.
Here's an example:
As far as I can tell there's no way to get ahold of the UITableViewCellReorderControl or access its size or behavior through the UITableViewDelegate. Is there a way to customize drag behavior so that you can drag anywhere on the cell to move it up or down?
just expand the reorderControl to the size of cell.
extension UITableViewCell {
var reoderControl: UIView? {
for view in self.subviews {
if view.description.contains("UITableViewCellReorderControl") {
return view
}
}
return nil
}
}
class Cell: UITableViewCell {
override func layoutSubviews() {
super.layoutSubviews()
reoderControl?.frame = bounds
}
}

Resources