Is there a way to change the focused index path in a collection view? - ios

I am building an app like Photos where you can scroll through thumbnails of your photos in a UICollectionView and you can tap on one to view that photo full-screen then swipe to move between photos. I'm working to add support for keyboard navigation so you can use the arrow keys to select a photo, hit space to view it full-screen, use the arrow keys to move between full-screen photos, then hit space to dismiss it. This works well in the Photos app, but in my app when you dismiss the full-screen view controller, the focus does not update in the underlying view controller to the index path of the photo you just dismissed - it's obviously only aware of the index path that was last focused in that view controller, the one focused prior to pressing space. It seems I need to manually move focus to a potentially different index path when the full screen view controller is dismissed. How do you accomplish that?
To enable focus I set these in the UICollectionViewController:
collectionView.allowsFocus = true
collectionView.allowsFocusDuringEditing = true
collectionView.remembersLastFocusedIndexPath = true
restoresFocusAfterTransition = true
I have tried the following but the focus is not moved to that cell, even if I set remembersLastFocusedIndexPath and restoresFocusAfterTransition to false:
cell.focusGroupPriority = .currentlyFocused
cell.setNeedsFocusUpdate()
cell.updateFocusIfNeeded()
cell.becomeFirstResponder()

It is possible to change the focused index path if there is already a focused index path.
To do this, implement the collection view delegate method indexPathForPreferredFocusedView(in:) to return the index path you want to be focused. When you want to change the focus, call collectionView.setNeedsFocusUpdate() and that function will be called by the system giving you the opportunity to specify the index path to focus. Note iOS will now be asking your app to tell it which index path to focus initially and as focus states change. You can return nil to let the system decide which to focus.
Note you must not set collectionView.remembersLastFocusedIndexPath to true otherwise this will not work. To have that functionality, you'll need to manually keep track of the last focused index path using collectionView(_:didUpdateFocusIn:with:) and return that in indexPathForPreferredFocusedView(in:).
func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? {
return lastFocusedIndexPath
}
func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
lastFocusedIndexPath = context.nextFocusedIndexPath
}
private func moveFocus(to indexPath: IndexPath) {
lastFocusedIndexPath = indexPath
collectionView.setNeedsFocusUpdate()
//collectionView.updateFocusIfNeeded() //can update it now if you need it to
}

Related

How to retain previous selection in collectionview after reload in Apple TV

Hi in my Apple TV application i have one left collectionview right collectionview.Like splitview.When ever i focus cell on left data will refresh on right and when i select any cell in right collection view i am refreshing left and right collectionviews with new data (Like next level).And when on click on menu i will refresh both collectionviews with old data (Like coming to previous level). I want to highlight cell in left collectionview with red colour but i am reloading left collectionview while going forward or coming backward so always first cell is highlighting with Red colour. Can anyone suggest how to maintain previous selection in left collection-views because i am using only one collectionview for left menu and just reloading data.
The easiest way to retain focus in a UITableView or UICollectionView is to use UICollectionView.remembersLastFocusedIndexPath = true. This will automatically restore focus to the last focused item in a collection/table view and also automatically focus on the first item if there was no previously focused item or if the collection view data is reloaded.
If you need more control, the next level is to set UICollectionView.remembersLastFocusedIndexPath = false and use UICollectionViewDelegate.indexPathForPreferredFocusedView from your UIViewController instead. This method is only called whenever focus changes to a collection view programmatically though (but not if focus changes to a collection view as a result of TV remote interaction).
Now to ensure that indexPathForPreferredFocusedView is called when you switch between the left and right collection views using a TV remote, you will need to intercept shouldUpdateFocusInContext to override focus switches between the left and right collection view programmatically:
override func shouldUpdateFocusInContext( ... ) -> Bool {
if let nextView: UIView = context.nextFocusedView, let previousView: UIView = context.previouslyFocusedView{
if (nextView.isDescendant(of:leftCollectionView) && previousView.isDescendant(of:rightCollectionView)){
setFocusTo(leftCollectionView) // will invoke delegate indexPath method
return false // prevent system default focus change in favor of programmatic change
}
else if (nextView.isDescendant(of:rightCollectionView && previousView.isDescendant(of:leftCollectionView){
setFocusTo(rightCollectionView) // will invoke delegate indexPath method
return false
}
}
return true
}
internal var focusedView: UIView?
internal func setFocusTo(_ view:UIView){
focusedView = view
setNeedsFocusUpdate()
}
override var preferredFocusEnvironments -> [UIFocusEnvironment]{
return focusedView != nil ? [focusedView!] : super.preferredFocusEnvironments
}
func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? {
...
}
Alternatively, instead of using setFocusTo( collectionView ) + indexPathForPreferredFocusedView, you can just use setFocusTo( collectionViewCell ). Overriding indexPathForPreferredFocusedView is more robust though since it catches all cases where focus shifts for reasons other than user interaction (ex: system focus update due to an alert displaying + dismissing)

Is there any way to scroll to the selected UICollectionViewCell in a collection?

I've got a UICollectionView in a modal view controller in my app. When the modal view is brought up, one of the collection's cells is set as selected based on certain data I pass into the modal view from my home view.
I need to programmatically scroll the selected collection cell into view once the modal view with the collection appears... but using scrollToItem(at:at:animated:) while the collection view is being populated (in cellForItemAt) doesn't seem to work.
So while I could easily just use the indexPath available to me in cellForItemAt to scroll to the selected cell while populating the collection, since that isn't possible, I can't figure out how to scroll to the currently selected collection cell after the collection is fully populated and presented.
I can't even use a heavy-handed approach like looping through all collection cells and checking which is selected manually, since it doesn't appear possible to loop through cells that aren't currently visible.
Help?
Add a variable on Top.
private var didLayoutFlag: Bool = false
Just pass the index number (indexNO) in viewDidLayoutSubviews method. I am using horizontal scrolling.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if self.colllecitonView != nil {
if !self.didLayoutFlag {
self.colllecitonView.scrollToItem(at:IndexPath(item: indexNO, section:0), at:.right, animated: false)
self.didLayoutFlag = true
}
}
}

How to move focus at button in collectionview cell in tvos application?

I am working on apple TV application. In my app, I have made a screen which has a collection view. In that case, I am able to move focus at collection view cell but not able to move focus to the button which is in collection view cell so can anyone help me to solve this issue ?
please give me an answer if anyone knows this answer.
I am able to solve this problem by adding below methods in collectionViewCell subclass.
In CollectionViewCell Subclass
override var preferredFocusEnvironments: [UIFocusEnvironment]{
// Condition
}
override func shouldUpdateFocus(in context: UIFocusUpdateContext) -> Bool {
// Condition
}
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
// Condition
}
you can see more at this link:
enter link description here.
I think this page will guide to achieve what you want. https://developer.apple.com/library/content/documentation/General/Conceptual/AppleTV_PG/WorkingwiththeAppleTVRemote.html#//apple_ref/doc/uid/TP40015241-CH5-SW4
It give good explaination of how focus engine will decide which object should get next focus. Below is great explanation step by step at above link.
Here is an example showing how focus might be determined:
The focus engine asks the root window for its preferredFocusedView,
which returns its root view controller’s preferredFocusedView
object.
The root view controller, a tab view controller, returns its
select view controller’s preferredFocusedView object.
The select view controller overrides its preferredFocusedView method to return a specific UIButton instance.
The UIButton instance returns self(the default), and is focusable, so it is chosen by the focus engine as the next focused view.
You have to override the preferredFoucsedView property of your UIView or UIViewController.
override weak var preferredFocusedView: UIView? {
if someCondition {
return theViewYouWant
} else {
return defaultView
}
}
Thanks to Slayter's answer

UIButton never fires off code in Action

I have the following screen:
The X is the image of a UIButton, I have add the appropriate action to the button. Yet when I click on the button it never fires off the code in the action.
Here is some code:
#IBAction func CloseProfilePage(sender: AnyObject) {
self.removeAnimate();
}
This is the code that is used to launch the view controller seen:
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let selectedAppointment = self.dayDatasource!.appointmentAtIndex(indexPath.item);
let profilePageViewController = ProfilePageViewController.init(withAppointment: selectedAppointment);
profilePageViewController.view.frame = self.view.frame
self.view.addSubview(profilePageViewController.view)
profilePageViewController.didMoveToParentViewController(self)
}
The button is definitely connected in the xib to the action:
When I check in the view hierachy, there isn't anything else on top of the button that would prevent the button but registering clicks. I'm assuming here that the imageView in the UIButton is clickable as its part of the button iteself.
The X in the image is not an image view I added, it is the image view that comes with the UIButton. With that said I've also resorted to the following:
self.profilePageClosePopUpButton.imageView?.userInteractionEnabled = true;
Still the button remains unclickable. Where am I going wrong?
It pains me to say this but I'm only writing a solution here just in case someone in the future struggles with the same issue and maybe this post could help them.
In the view, seen in the image below, I had some how unintentionally switched off User Interaction Enabled in interface builder. Because this was off, all other children didn't have interaction enabled on them and hence why the button was not clickable...
Moral of the story is, check your parent views and make sure their user interaction is enabled as well.

UICollectionView does not scroll after it has been initialized

I have a subclass of UICollectionViewController that is nested inside a UINavigationController. The collection contains several cells (currently, 3) and each cell is as big as the full screen.
When the whole thing is shown, the collection view initally scrolls to a specific cell (which works flawlessly for each cell):
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let path = currentlyPresentedPhotoCellIndexPath { // this is set in the beginning
collectionView?.scrollToItemAtIndexPath(path, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: false)
}
}
However, the collection view refuses to scroll horizontally, hereafter, as if the user interaction was disabled. I am not sure what is happening, but this is what I have checked so far:
user interaction is enabled for the collection view
the next cell (right or left, depending on the scroll direction) is requested correctly which I found out by inspecting collectionView:cellForItemAtIndexPath:
the requested imagePath is the right one
scrollToItemAtIndexPath... does not work either if I try to trigger a scroll programmatically after everything has been loaded (nothing happens)
scrollRectToVisible... does neither
setting collectionView?.contentInset = UIEdgeInsetsZero before the programmatic scroll attempts take place does not change anything
the content size of the collection view is 3072x768 (= 3 screens, i.e. 3 cells)
Which bullet points are missing, here?
Although the post did not precisely tackle the root of my problem it forced me to ponder the code that I posted. If you look at it you will see that it basically says: Whenever the views need to be layouted, scroll to the cell at position currentlyPresentedPhotoCellIndexPath. However, and this you cannot see without any context, this variable is only set once, when the whole controller is being initialized. Thus, when you try to scroll, the layout changes, the controller then jumps back to the initial cell and it looks like nothing happens, at all.
To change this, you just have to enforce a single scroll, e.g. by doing this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let path = currentlyPresentedPhotoCellIndexPath { // only once possible
collectionView?.scrollToItemAtIndexPath(path, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: false)
currentlyPresentedPhotoCellIndexPath = nil // because of this line
// "initiallyPresentedPhotoCellIndexPath" would probably a better name
}
}
A big thanks to Mr.T!

Resources