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

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)

Related

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

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
}

Dimming other cells in UICollectionView when a cell is selected

I'm trying to build a UICollectionView for a messaging application, where each UICollectionViewCell in the UICollectionView corresponds to a message. I am using MessageKit, which extends UICollectionView, to handle the messages. When the user taps a message, I want to reveal information about the message (time sent, user reactions to the message, etc.).
GroupMe does this in a very elegant way by (1) expanding the message vertically to reveal information, and (2) dimming the rest of the messages in the view and disabling user interaction for them. Note that the highlighted cell is still interaction enabled. See before/after images below.
Before tapping message
After tapping message
Here I am focused on achieving (2), i.e. when a message is tapped, I want to dim all other cells and disable user interaction for them. I also want to be able to animate this change so that it dims all the other cells over some short duration. When the user taps anywhere in the view, I'd like the highlighted cell to 'unhighlight', and user interaction to be re-enabled for all cells, and similarly animate the revert.
I'm a little stumped on how to achieve this behavior - any ideas on how to implement this in Swift? Any pointers that could get me started in the right direction would be greatly appreciated!
You could add a UIView for each Cell’s subview, except for the one selected. This view could be animated by changing the alpha value with fading/appearing effect.
You can override isSelected property in your custom UICollectionViewCell to handle the user interaction and alpha on cell selection, i.e.
class CollectionViewCell: UICollectionViewCell {
override var isSelected: Bool {
didSet {
self.isUserInteractionEnabled = self.isSelected
self.alpha = self.isSelected ? 1.0 : 0.5
}
}
//rest of the code...
}

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

Simultaneously change display parameters on all table view cells

I am trying to implement a table view design where a user can click a button outside of a table view cell and the display mode of all the buttons should change. However this is not the 'selected' mode for a given cell (that will be yet a third state that becomes accessible via switching to this second state). What's the proper way to accomplish this?
I am using dequeueReusableCellWith so I don't want to simply cycle through every cell because some that are out of sight probably shouldn't be modified. I simply want any cell that is visible, or becomes visible, while the table view cell is in this second display mode to follow a second design rather than the first design.
The second design, for now, is being modified via a method I added to a subclass of UITableViewCell like so:
- (void) p_refreshDisplay {
if (self.editing) {
self.buttonToClearWidth.constant = 20;
self.buttonToClearLeadingWidth.constant = 20;
} else {
self.buttonToClearWidth.constant = 0;
self.buttonToClearLeadingWidth.constant = 0;
}
}
However, I'm not sure how to trigger this p_refreshDisplay for every visible (and to become visible) cell. It seems unwise to call this many times and refresh the table view. What would be the proper way to accomplish what I want to do?
You do what should be done for any table view change:
Update your data model or some flag as needed.
Either call reloadData on the table view or call reloadRowsAtIndexPaths:withRowAnimation: passing in indexPathsForVisibleRows as the list of rows to reload.
Implement cellForRowAtIndexPath to provide appropriate cells for the given data/flags.
It sounds like you should have a custom cell class that has one or more properties that can be set on the cell in cellForRowAtIndexPath so the cell can render itself properly based on the specified state.
You can achieve this by doing three things:
Establish some property that indicates the "mode" of the table, either a boolean or perhaps an enum if there are more than three states
Ensure that cellForRowAtIndexPath configures the cell appropriately based on the value of this property. This will ensure that newly displayed cells are configured correctly.
When the "mode" changes you can use the tableview's visibleCells property to update any currently visible cells:
for cell in tableview.visibleCells {
if let myCell = cell as? MyCustomCellClass {
myCell.setButtonStyle()
}
}

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