How can I determine the index path for the currently focused UITableViewCell using Voice Over? - ios

I have a dynamic UITableView. For each cell, I add a UIAccessibilityCustomAction. When the action fires, I need to know the index path so I can respond accordingly and update my model.
In tableView(_:cellForRowAt:) I add my UIAccessibilityCustomAction like this...
cell.accessibilityCustomActions = [
UIAccessibilityCustomAction(
name: "Really Bad Name",
target: self,
selector: #selector(doSomething)
)
]
I have tried to use UIAccessibility.focusedElement to no avail...
#objc private func doSomething() {
let focusedCell = UIAccessibility.focusedElement(using: UIAccessibility.AssistiveTechnologyIdentifier.notificationVoiceOver) as! UITableViewCell
// Do something with the cell, like find the indexPath.
}
The problem is that casting to a cell fails. The debugger says that the return value type is actually a UITableTextAccessibilityElement, which I could find no information on.

When the action fires, I need to know the index path so I can respond accordingly and update my model.
The best way to reach your goal is to use the UIAccessibilityFocus informal protocol methods by overriding them in your object directly (the table view cell class in your case): you'll be able to catch the needed index path when a custom action is fired.
I suggest to take a look at this answer dealing with catching accessibility focus changed that contains a detailed solution with code snippets if need be.😉
Example snippet...
class SomeCell: UITableViewCell
override open func accessibilityElementDidBecomeFocused() {
// Notify view controller however you want (delegation, closure, etc.)
}
}

I ended up having to solve this myself to bodge an Apple bug. You've likely solved this problem, but this is an option similar to your first suggestion.
func accessibilityCurrentlySelectedIndexPath() -> IndexPath? {
let focusedElement:Any
if let voiceOverObject = UIAccessibility.focusedElement(using: UIAccessibility.AssistiveTechnologyIdentifier.notificationVoiceOver) {
focusedElement = voiceOverObject
} else if let switchControlObject = UIAccessibility.focusedElement(using: UIAccessibility.AssistiveTechnologyIdentifier.notificationSwitchControl) {
focusedElement = switchControlObject
} else {
return nil
}
let accessibilityScreenFrame:CGRect
if let view = focusedElement as? UIView {
accessibilityScreenFrame = view.accessibilityFrame
} else if let accessibilityElement = focusedElement as? UIAccessibilityElement {
accessibilityScreenFrame = accessibilityElement.accessibilityFrame
} else {
return nil
}
let tableViewPoint = UIApplication.shared.keyWindow!.convert(accessibilityScreenFrame.origin, to: tableView)
return tableView.indexPathForRow(at: tableViewPoint)
}
What we're essentially doing here is getting the focused rect (in screen coordinates) and then translating it back to the table view's coordinate space. We can then ask the table view for the indexpath which contains that point. Simple and sweet, though if you're using multi-window you may need to swap UIApplication.shared.keyWindow! with something more appropriate. Note that we deal with the issue you faced where the element was a UITableTextAccessibilityElement when we handle UIAccessibilityElement since UITableTextAccessibilityElement is a private, internal Apple class.

Related

Stop Diffable Data Source scrolling to top after refresh

How can I stop a diffable data source scrolling the view to the top after applying the snapshot. I currently have this...
fileprivate func configureDataSource() {
self.datasource = UICollectionViewDiffableDataSource<Section, PostDetail>(collectionView: self.collectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, userComment: PostDetail) -> UICollectionViewCell? in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PostDetailCell.reuseIdentifier, for: indexPath) as? PostDetailCell else { fatalError("Cannot create cell")}
cell.user = self.user
cell.postDetail = userComment
cell.likeCommentDelegate = self
return cell
}
var snapshot = NSDiffableDataSourceSnapshot<Section, PostDetail>()
snapshot.appendSections([.main])
snapshot.appendItems(self.userComments)
self.datasource.apply(snapshot, animatingDifferences: true)
}
fileprivate func applySnapshot() {
//let contentOffset = self.collectionView.contentOffset
var snapshot = NSDiffableDataSourceSnapshot<Section, PostDetail>()
snapshot.appendSections([.main])
snapshot.appendItems(self.userComments)
self.datasource.apply(snapshot, animatingDifferences: false)
//self.collectionView.contentOffset = contentOffset
}
store the offset, then reapply it. Sometimes it works perfectly and sometimes the view jumps. Is there a better way of doing this?
The source of this problem is probably your Item identifier type - the UserComment.
Diffable data source uses the hash of your item identifier type to detect if it is a new instance or an old one which is represented currently.
If you implement Hashable protocol manually, and you use a UUID which is generated whenever a new instance of the type is initialized, this misguides the Diffable data source and tells it this is a new instance of item identifier. So the previous ones must be deleted and the new ones should be represented. This causes the table or collection view to scroll after applying snapshot.
To solve that replace the uuid with one of the properties of the type that you know is unique or more generally use a technique to generate the same hash value for identical instances.
So to summarize, the general idea is to pass instances of the item identifiers with the same hash values to the snapshot to tell the Diffable data source that these items are not new and there is no need to delete previous ones and insert these ones. In this case you will not encounter unnecessary scrolls.
Starting from iOS 15
dataSource.applySnapshotUsingReloadData(snapshot, completion: nil)
Resets the UI to reflect the state of the data in the snapshot without computing a diff or animating the changes
First up: in most cases #Amirrezas answer will be the correct reason for the problem. In my case it was not the item, but the section identifier that caused the problem. That was Hashable and Identifiable with correct values, but it was a class, and therefore the hash functions were never called. Took me a while to spot that problem. Changing to a struct (and therefore adopting some things ;) ) helped in my case.
For reference here's a link to the topic on the Apple-Dev forums: https://developer.apple.com/forums/thread/657499
Hope my answer helps somebody :)
You'd think that any of these methods would work:
https://developer.apple.com/documentation/uikit/uicollectionviewdelegate/1618007-collectionview
https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617724-targetcontentoffset
But (in my case) they did not. You might get more mileage out of them, I am doing some crazy stuff with a custom UICollectionViewCompositionalLayout
What I did get to work is manually setting the offset in my custom layout class:
override func finalizeCollectionViewUpdates() {
if let offset = collectionView?.contentOffset {
collectionView?.contentOffset = targetContentOffset(forProposedContentOffset: offset)
}
super.finalizeCollectionViewUpdates()
}
where I have targetContentOffset also overridden and defined (I tried that first, didn't work, figured it was cleanest to just use that here. I suspect if you define targetContentOffset on the delegate without overriding it in the layout the above will also work, but you already need a custom layout to get this far so it's all the same.)

Not allowing touch interaction on certain views?

I've got a class where I handle my touches functions on buttons, and those buttons are also a Post class.
The logic I'm going for is "If the post.user is equal to the current firebaseuser, don't allow the user to move the button"
Here's what I tried:
In my "DragButtonsClass" I've got a notification being sent to my view controller on TouchesBegan.
In the view I've got
func touchesStarted(notif : NSNotification) {
if let dict = notif.userInfo! as? [NSObject: AnyObject] {
let tag = dict["tag"]!
let tagInt = Int(String((tag))) //this is probably super sloppy, don't know how else to change an AnyObject to an Int though.
let post = postArray[tagInt! - 1]
let postbutton = self.view.viewWithTag(tagInt!)
if post.user == currentUser {
postbutton?.userInteractionEnabled = false
print("Should be working")
} else {
print("Didn't work")
}
}
}
Basically I'm assigning tags to the buttons as they're created and those tags line up with my Posts array. I'm trying to check if the Post's user is the same as the current user by sending it over and then shut off user interaction.
I'm getting "Should be working" to print out but the button is still draggable.
This seems like a super sloppy and roundabout way to do this and most importantly it isn't working. I've read that turning off userInteraction is a way to stop touches from being recognized.
Any ideas?
Ended up just setting a bool "canBeDragged" in my button's class, and when the button is created the current user's ID is compared to the information being pulled from firebase and the bool is set. Then I just checked if the button could be dragged before my touchesMoved.

Recognizing subview's class

I decided to animate my objects manually and therefore made an extension for UIView class:
public extension UIView{
func slideOut(){
UIView.animateWithDuration(0.5, animations: { self.frame.origin.x = -self.frame.width }, completion: finishedDisposing)
}
func finishedDisposing(successfully: Bool){
if !successfully{
((UIApplication.sharedApplication().delegate as! AppDelegate).window!.rootViewController as! VC).showSystemMessage("Failed to dispose one or more subviews from superview", ofType: .NOTICE)
}
responder.viewDisposed()
}
}
Which works nice and I have no problems about it, BUT I have a method in VC (Custom UIViewController) viewDisposed() which is called whenever a view slides out of sight and it has such an implementation:
func viewDisposed() {
disposed++
print("Updated disposed: \(disposed) / \(self.view.subviews.count)")
if disposed == self.view.subviews.count - 1{
delegate.vcFinishedDisposing()
}
}
It shows that self.view.subviews contains all my custom views + 3 more (UIView, _UILayoutGuide x 2). They do extend UIView although do not callresponder.viewDisposed method. My decision was to figure out how to get classes of each subview and Mirror(reflecting: subView).subjectType if I print it does it wonderfully. Is there any way to actually compare this to anything or, better, get String representation? Basically, I want you to help me create a method which would create a stack of subviews which are not of type UIView (only subClasses) nor _UILayoutGuide. Thank you!
You'd probably be better off directly creating an array of just the subviews you care about, instead of starting with all subviews and trying to filter out the ones you don't care about. Those layout guides weren't always there—they were added in iOS 7. Who knows what else Apple will add in the future?
Anyway:
let mySubviews = view.subviews.filter {
!["UIView", "_UILayoutGuide"].contains(NSStringFromClass($0.dynamicType))
}

How is better to manage data details (Swift)

I want to do a little list of data in TableView, and a DetailView.User tap on cell and goes to DetailView. In DetailView I have to check, what kind of data user selected in TableView. I pass name of data (title of cell) and Index(by prepareForSegue method). I can check name of data by two ways: by index, or by name (by switch). Which Way is better. My code for IndexCheck is something like this:
let Details = ["first","second","third"]
var PassedIndex:Int
override func ViewDidLoad () {
super.ViewDidLoad()
self.DetailLabel.text = Details[PassedIndex]
}
And a check by name is something like this:
var PassedName:String?
var DetailText:String
switch PassedName {
case "NumberOne":
DetailText = "#1"
case "NumberTwo":
DetailText = "#2"
case "NumberThree":
DetailText = "#3"
default:
DetailText = "Unknown number"
}
override func ViewDidLoad () {
super.ViewDidLoad()
self.DetailLabel.text = DetailText
}
Which way is the best? Which makes system working faster? I am just a beginner, so I need an advice from more experienced programmer than I
The best way of your two methods to check Details is check-by-index method. Why? Because it takes O(1) to take value that you need. Check by String takes O(n) in the worst case. But i suggest you to create an Enum that contains your Details and pass exactly detail that you need.

Working with Multiple Segues | How to use Cell data to perform specific Segues

I am displaying data in a collection view, I know how to pass the data on with prepareForSegue function but am trying to have the app determine which segue to use depending on the cell property data. (Each segue goes to a different view controller to display relevant information.)
For e.g.
If the cell.type is equal to "1" then perform segueOne if it is of type "2" then perform segueTwo.
I was trying to do something like this;
func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! CollectionViewCell
if cell[indexPath].type = "1" {
performSegueWithIdentifier("showPage1", sender: self)
} else if self.cell[indexPath].type = "2" {
performSegueWithIdentifier("showPage2", sender: self)
} else { println("error when selecting cell to segue") }
}
However with this I get an error;
'CollectionViewCell' does not have a member named Subscript
Has anybody got any ideas ?
Assuming the items in your collection view can be re-arranged (or might be some time in the future), the indexPath will not be sufficient to give you the information which cell was selected. Thus, IMO your idea to give the cell a property is a feasible one.
The easiest "quick and dirty" way is to simply hardcode the segue identifier string into your cell. This is not the best design because you are introducing dependencies between app elements that should know of each other.
class MyCell : UICollectionViewCell {
var segue = "DefaultSegue"
}
Now calling the appropriate segue is really easy in didSelectItemAtIndexPath...
self.performSegueWithIdentifier(cell.segue, sender:cell)
It would of course be preferable to use an enum. Safer, more readable and better maintainable.
enum Segue : String {
case ToInfo = "SegueToInfo"
case ToLogin = "SegueToLogin"
// etc.
}
The ivar for MyCell would now be var : Segue = SomeDefaultValue and you can call it the same way.
BTW: Regarding your original question please note the following: as has been pointed out, you cannot subscript a cell. (UICollectionViewCell is not a Dictionary, so cell["key"] does not make sense.) Also, I am not a fan of dequeueing the cell in more than one place - instead you could call cellForItemAtIndexPath or do the work in that method in the first place, as I have suggested.
You're trying to index into a UICollectionViewCell, but of course that class is not an array, so you can't 'subscript' it.
My suggestion is to refactor your code. Whatever data you're storing in your cell you can presumably get from your data model, because that's where it originally came from. You are probably putting that in your cell in cellForIndexPath.
If that is the case, then there is no reason you can't get the same data from the same place in your func ... shouldSelectItemAtIndexPath ... -> Bool. I'd suggest doing it there. Your cell should only contain the data it needs to properly render itself to the screen.
See if that helps.

Resources