I Have a details page with collectionView. The collection view has a header view which has some buttons(play buttons).
When the page first opens the focus will be on the play button. after loading the header view we are inserting rail cells for showing related items in the collectionView. But whenever the rails get loaded the focus automatically moves to the rail cell.
There is no focus handling done for collectionView only one is this code
func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool {
return true
}
how can I remove this behavior?
I tried "should update focus" to get this focus change and block it
override func shouldUpdateFocus(in context: UIFocusUpdateContext) -> Bool {
<#code#>
}
but this function is getting triggered only when focus change is through remote and no automatic focus change (like my case)
Now am handling the situation with
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if let headerView = getHeaderView(), let collectionViewCell = detailsCollectionView.cellForItem(at: IndexPath(item: 0, section: 0)) as? CollectionViewCell {
let previouslyFocusedView = context.previouslyFocusedView
let nextFocusedView = context.nextFocusedView
if previouslyFocusedView?.isDescendant(of: headerView.playButton) ?? false, nextFocusedView?.isDescendant(of: collectionViewCell.getInnerCollectionView()) ?? false {
cancelFocusChange(previouslyFocusedView: previouslyFocusedView)
}
}
super.didUpdateFocus(in: context, with: coordinator)
}
but I don't think it is a good idea. There must be a reason why this focus change is happening. If anyone knows the answer to this, please do help.
Related
I need to store a view to use as a UICollectionView header. I don't want it to cycle out of memory though, because it needs to keep its state/data, etc.
With a table view you can just do tableView.tableHeaderView = view.
Here's what I'm trying:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case MagazineLayout.SupplementaryViewKind.sectionHeader:
if let t = headerView { //headerView is an instance var
return t
} else {
let view = collectionView.dequeueReusableSupplementaryView(ofKind: MagazineLayout.SupplementaryViewKind.sectionHeader, withReuseIdentifier: "MyHeaderView", for: indexPath) as! MyHeaderView
view.titleLabel.text = "test"
view.switch.addAction(for: .valueChanged, { [weak self] in
self?.switchValueChanged()
})
headerView = view
return view
}
...
}
I don't want to re-create it every time the user scrolls it away and then back, so I'm trying to store a reference to it. This isn't working though. Hard to explain but the view it displays is cut off and the switch isn't responsive. If I comment out the "if" part and just create a new one every time, it looks correct but state is lost (i.e. the switch gets turned off) What's the best way to do this?
Since you're keeping the reference and not letting it deallocate when it scrolls out of the view, remove the register and dequeuing entirely. It worked fine for me, here's how:
let view = MyHeaderView()
override func viewDidLoad() {
super.viewDidLoad()
view.titleLabel.text = "test"
view.switch.addAction(for: .valueChanged, { [weak self] in
self?.switchValueChanged()
})
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case MagazineLayout.SupplementaryViewKind.sectionHeader:
return view
//...
}
}
I have adopted the new UICollectionViewDiffableDataSource. I am applying a datasource snapshot everytime I delete an item:
var snapshot = NSDiffableDataSourceSnapshot<Int, Item>()
snapshot.appendSections([0])
snapshot.appendItems(items)
apply(snapshot, animatingDifferences: true)
The delete is offered through the built in collection view configuration option:
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return nil
}
let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
let delete = UIAction(title: "Delete", image: UIImage(systemName: "trash.fill"), attributes: .destructive) { _ in
self.deleteItem(item)
}
return UIMenu(title: "", image: nil, identifier: nil, children: [delete])
}
return configuration
}
If I delete the item from outside the context menu, the animation works great. If I delete from the context menu then one cell disappears and then causes the next to flash. I suspect there is some sort of conflict between closing the context menu & running the delete animation. I'm looking for a way to work around this.
edit: This was very stable resulting in freezing of the UI and other weird glitches where it sometimes didn't respond to long-press, do not use this. I wish this API didn't have that glitch, it's very annoying.
I found this ugly glitch too and was very close on giving up until I thought I'd give it a shot implementing the other interaction API instead of the built-in on UICollectionView, weirdly enough the animation went away.
First add the interaction to your cells contentView
let interaction = UIContextMenuInteraction(delegate: self)
cell.contentView.addInteraction(interaction)
Then implement the delegate, presumably where you present the cell, in my case in the viewController
extension ViewController : UIContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
// loop through all visible cells to find which one we interacted with
guard let cell = collectionView.visibleCells.first(where: { $0.contentView == interaction.view }) else {
return nil
}
// convert it to an indexPath
guard let indexPath = collectionView.indexPath(for: cell) else {
return nil
}
// continue with your magic!
}
}
And voila, that's it, no more glitchy animation.
I don't know if there's some edge cases where wrong cell will be selected or other weird stuff, but this seems to work just fine.
I have a ViewController with a CollectionView that loads four possible answers to a question asked in the ViewController.
When the user selects an item, the collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) gets called in which the question changes along with the array of answers (always 4) then a collectionView.reloadData is called to redraw the new question and possible answers.
Everything is working perfectly, except for when the user taps on an item quickly 2 times in a row.
in that case, the first selection registers the answer, and then the collectionview is taking another tap (as if the user has tapped again on the next question) and thus answering another question.
What i would like to do, if possible, is the following:
1. disable touch events on the first touch (while reloading a new question)
2. re-enable touch events once the reloadData of the collectionView has finished loading. Which is another problem that i solved using a custom Collection View Class taken from this thread How to tell when UITableView has completed ReloadData?
I have tried disabling touch events using: view.userInteractionEnabled = false/true and UIApplication.shared.beginIgnoringInteractionEvents() and UIApplication.shared.endIgnoringInteractionEvents() with no luck.
Here is what I've tried so far:
func loadNewQuestion {
//UIApplication.shared.beginIgnoringInteractionEvents()
//self.view.isUserInteractionEnabled = false
//change the question, answer, and array of possible answers
answers = answers.shuffled() //simply shuffle the answers
//pick a random answer
let number = Int.random(in: 0 ... 3)
answer = answers[number] //shuffle again and take first value
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if stillAnswering {
print("still answering, so skipping touch events")
return
}
stillAnswering = true
print("not answering")
let a = answers[indexPath.row]
if a.descript.lowercased() == questionAnswer.descript.lowercased() //questionAnswer is the correct answer to the question {
self.loadNewQuestion()
self.collectionView.reloadData(onComplete: {
//UIApplication.shared.endIgnoringInteractionEvents()
//self.view.isUserInteractionEnabled = true
stillAnswering = false
})
} else {
//warn about wrong answer
stillAnswering = false
}
}
I have tagged both objective-c and swift because i don't mind the language used for the solution, and also i believe that the solution/problem is similar for uitableview vs uicollectionview.
Any Hints?
You can use flag and set it to true when you make a tap on the correct answer cell, then set it to false in the onComplete block of reloadData().
Ex:
var answerChosen: Bool = false
...
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if answerChosen { return }
let a = answers[indexPath.row]
if a.descript.lowercased() == questionAnswer.descript.lowercased() //questionAnswer is the correct answer to the question {
answerChosen = true
self.loadNewQuestion()
self.collectionView.reloadData(onComplete: {
answerChosen = false
})
} else {
//warn about wrong answer
}
}
I have finally managed to solve the problem. The trick that solved it is to put the reloadData() inside a dispatch async block.
Here's the final code.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if UIApplication.shared.isIgnoringInteractionEvents {
return //for extra safety
}
UIApplication.shared.beginIgnoringInteractionEvents()
let a = answers[indexPath.row]
if a.descript.lowercased() == questionAnswer.descript.lowercased() //questionAnswer is the correct answer to the question {
self.loadNewQuestion()
DispatchQueue.main.async(execute: {
self.collectionView.reloadData(onComplete: {
UIApplication.shared.endIgnoringInteractionEvents()
})
})
} else {
//warn about wrong answer
DispatchQueue.main.async(execute: {
self.collectionView.reloadData(onComplete: {
UIApplication.shared.endIgnoringInteractionEvents()
})
})
}
}
I am implementing a simple messenger for my app where the users can chat among themselves. The messenger is based on UICollectionView (JSQMessagesViewController) where each message is represented by one UICollectionView row. Each message also has a top label that is used to display when the message was sent. This label is initially hidden (height=0) and when the user taps the particular message (row), the label gets displayed by setting the height correspondingly. (height=25)
The problem I am facing is the actual animation of displaying the label. (height change). Part of the row overlays the row bellow by several pixels before it gets to it's position. Also when hiding the label back, the animation first sets the height to zero and then the text fades out overlaying part of the message bellow which looks really bad.
So basically what I am trying to achieve is to get rid of those two previously mentioned problems.
Code:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForCellTopLabelAt indexPath: IndexPath!) -> CGFloat {
if indexPath == indexPathTapped {
return 25
}
let messageCurrent = messages[indexPath.item]
let messagePrev: JSQMessage? = indexPath.item - 1 >= 0 ? messages[indexPath.item - 1] : nil
if messageCurrent.senderId == messagePrev?.senderId || messagePrev == nil {
return 0
}
else{
return 25
}
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
if let indexPathTapped = indexPathTapped, indexPathTapped == indexPath {
self.indexPathTapped = nil
}
else{
indexPathTapped = indexPath
}
collectionView.reloadItems(at: [indexPath])
// UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveLinear, animations: {
// collectionView.performBatchUpdates({
// collectionView.reloadItems(at: [indexPath])
// }, completion: nil)
// }, completion: nil)
}
Demo: (Sorry for the quality)
I would really appreciate if somebody could help me with this as I have already spent several hours trying to figure it out without getting anywhere.
Thank you in advance!
EDIT:
I tried the solution proposed by #jamesk as following:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
if let indexPathTapped = indexPathTapped, indexPathTapped == indexPath {
self.indexPathTapped = nil
}
else{
indexPathTapped = indexPath
}
UIView.animate(withDuration: 0.25) {
collectionView.performBatchUpdates(nil)
}
}
And override the apply of JSQMessagesCollectionViewCell:
extension JSQMessagesCollectionViewCell {
override open func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
layoutIfNeeded()
}
}
However those changes resulted in:
I also tried the second solution with invalidating the layout:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
if let indexPathTapped = indexPathTapped, indexPathTapped == indexPath {
self.indexPathTapped = nil
}
else{
indexPathTapped = indexPath
}
var paths = [IndexPath]()
let itemsCount = collectionView.numberOfItems(inSection: 0)
for i in indexPath.item...itemsCount - 1 {
paths.append(IndexPath(item: i, section: 0))
}
let context = JSQMessagesCollectionViewFlowLayoutInvalidationContext()
context.invalidateItems(at: paths)
UIView.animate(withDuration: 0.25) {
self.collectionView?.collectionViewLayout.invalidateLayout(with: context)
self.collectionView?.layoutIfNeeded()
}
}
Which resulted in the following:
There seem to be two issues. The first issue is that the call to reloadItems(at:) is limited to cross-fading between the old cell and the new cell—it won't interpolate between the layout attributes for the old cell and the layout attributes for the new cell. The second issue is that there doesn't seem to be any code that instructs your selected cell to perform a layout pass if needed upon new layout attributes being applied to it.
The JSQMessagesViewController framework uses subclasses of UICollectionViewFlowLayout and UICollectionViewFlowLayoutInvalidationContext, so we can leverage the invalidation behaviour of the flow layout when updating and animating items. All that is needed is to invalidate the layout attributes (i.e. position) and delegate metrics (i.e. size) for the items affected by the change in cell height.
The code below was written for use with the Swift example project included in the release_7.3 branch of JSQMessagesViewController:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
// Determine the lowest item index affected by the change in cell size.
// Lesser of previous tapped item index (if any) and current tapped item index.
let minItem = min(tappedIndexPath?.item ?? indexPath.item, indexPath.item)
// Update tapped index path.
tappedIndexPath = (tappedIndexPath == indexPath ? nil : indexPath)
// Prepare invalidation context spanning all affected items.
let context = JSQMessagesCollectionViewFlowLayoutInvalidationContext()
let maxItem = collectionView.numberOfItems(inSection: 0) - 1
let indexPaths = (minItem ... maxItem).map { IndexPath(item: $0, section: 0) }
context.invalidateItems(at: indexPaths) // Must include all affected items.
context.invalidateFlowLayoutAttributes = true // Recompute item positions (for all affected items).
context.invalidateFlowLayoutDelegateMetrics = true // Recompute item sizes (needed for tapped item).
UIView.animate(withDuration: 0.25) {
collectionView.collectionViewLayout.invalidateLayout(with: context)
collectionView.layoutIfNeeded() // Ensure layout pass for visible cells.
}
}
The above code should be reasonably performant.
While the positions of affected items must always be recomputed, it isn't necessary to recompute the sizes of all affected items as is done above. It would be sufficient to recompute only the size of the tapped item. But as the effect of the invalidateFlowLayoutDelegateMetrics property is always applied to every invalidated item, to implement that narrower approach, you would need to use two flow layout invalidation contexts and divide the items between them (or implement a custom invalidation context with corresponding invalidation behaviour). It is probably not worth it unless Instruments tells you otherwise.
After inserting the data try adding this piece of code.
collectionView.reloadItems(at: [indexPath])
UIView.animate(withDuration: 0.6) {
self.view.layoutIfNeeded()
}
I have working uicollectionview codes with CustomCollectionViewLayout , and inside have a lot of small cells but user cannot see them without zoom. Also all cells selectable.
I want to add my collection view inside zoom feature !
My clear codes under below.
class CustomCollectionViewController: UICollectionViewController {
var items = [Item]()
override func viewDidLoad() {
super.viewDidLoad()
customCollectionViewLayout.delegate = self
getDataFromServer()
}
func getDataFromServer() {
HttpManager.getRequest(url, parameter: .None) { [weak self] (responseData, errorMessage) -> () in
guard let strongSelf = self else { return }
guard let responseData = responseData else {
print("Get request error \(errorMessage)")
return
}
guard let customCollectionViewLayout = strongSelf.collectionView?.collectionViewLayout as? CustomCollectionViewLayout else { return }
strongSelf.items = responseData
customCollectionViewLayout.dataSourceDidUpdate = true
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
strongSelf.collectionView!.reloadData()
})
}
}
}
extension CustomCollectionViewController {
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return items.count
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items[section].services.count + 1
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CustomCollectionViewCell
cell.label.text = items[indexPath.section].base
return cell
}
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath cellForItemAtIndexPath: NSIndexPath) {
print(items[cellForItemAtIndexPath.section].base)
}
}
Also my UICollectionView layout properties under below you can see there i selected maxZoom 4 but doesnt have any action !
Thank you !
You don't zoom a collection like you'd zoom a simple scroll view. Instead you should add a pinch gesture (or some other zoom mechanism) and use it to change the layout so your grid displays a different number of items in the visible part of the collection. This is basically changing the number of columns and thus the item size (cell size). When you update the layout the collection can animate between the different sizes, though it's highly unlikely you want a smooth zoom, you want it to go direct from N columns to N-1 columns in a step.
I think what you're asking for looks like what is done in the WWDC1012 video entitled Advanced Collection Views and Building Custom Layouts (demo starts at 20:20) https://www.youtube.com/watch?v=8vB2TMS2uhE
You basically have to add pinchGesture to you UICollectionView, then pass the pinch properties (scale, center) to the UICollectionViewLayout (which is a subclass of UICollectionViewFlowLayout), your layout will then perform the transformations needed to zoom on the desired cell.