CollectionView: Unable to animate ReloadItems - swift

ReloadItems does not seem to allow for animation.
I tried using collectionView:( willDisplay) but the reloadItems() actually overtakes the animation (i.e. cutting it off and reloading the cell)
I tried performBatchUpdates() but I get outOfRange error in this block:
collectionView.performBatchUpdates({
// reload one or more items
collectionView.reloadItems(at: [tappedArray[0], tappedArray[1]])
}) { (_) in
// do animations here
if let cell1 = collectionView.cellForItem(at: self.tappedArray[1]) {
self.animateCell(cell1)
}
}
If a single item in a collectionView is reloaded, how can I add animation(s) to the re-display of that item?

Issue resolution: Animation done in a closure is NOT on the main thread. Since all animation should be on the main thread, this was causing my UI to glitch, or "not allow for animation".

Related

UICollectionView inside UITableViewCell slow srolling

I know that this question was asked a couple of times, but other solutions didn't work for me.
I have this model:
var notifications = [Notification]()
Notification:
let user: User (name, profileimage, etc)
let items: [Movies] (movie has image, name, etc)
So i display my notifications inside a tableview, each cell has profile info at the top and collectionview with items bellow.
Inside my tableviewcell, i have to reloadData of my collectionview to display correct movies. I know that probably reloadData method causes this lagging, but are there any solutions to avoid it?
TableViewcell:
var notification: Notification! {
didSet {
collectionView.reloadData();
}
}
I also tried this thing inside tablecell and call this method in willDisplayTableViewcell, but it doesn't help at all:
func setCollectionViewDataSourceDelegate(forRow row: Int) {
collectionView.delegate = self
collectionView.dataSource = self
collectionView.reloadData()
}
Images are loading using kingfisher, so it's fine in other places in my project.
userProfileImage.kf.setImage(
with: URL(string: profileImage),
options: [
.processor(DownsamplingImageProcessor(size: CGSize(width: 175, height: 175))),
.scaleFactor(UIScreen.main.scale),
.transition(.fade(0.2)),
]
)
I would comment out the code you have shown above as it will only get in the way and give false readings. My gut feeling is somehow you have managed to directly wire your notifications to your tableview / collection view and they are running on a background thread. What I would do to test this theory is wrap code anywhere where the UI is going to be updated with a perform on main thread like this
DispatchQueue.main.async { [unowned self] in
//Your UI update code within cellforRow / cellForItem (both table and collection view) Here
}
Try doing this one by one until you find the culprit and for a better solution try to refactor so you don't have to force this Main thread directive.

UICollectionView's prefetchItemsAt not being called

I'm trying to implement data prefetching for my UICollectionView using the UICollectionViewDataSourcePrefetching protocol; however, the respective method is not being called.
The collection view has a custom layout. I have also tried with the regular flow layout, same results.
Upon data reload, I execute the following code to make the collection view have the size of its content to prevent scrolling within that collection view but in a scroll view outside the collection view:
func reloadData() {
viewDidLayoutSubviews()
collectionView.reloadData()
collectionView.layoutIfNeeded()
viewDidLayoutSubviews()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionViewHeightConstraint.constant = collectionView.collectionViewLayout.collectionViewContentSize.height
collectionView.layoutIfNeeded()
view.layoutIfNeeded()
}
Maybe that has something to do with it?
What I have done:
my UIViewController does inherit from UICollectionViewDataSourcePrefetching
collectionView.prefetchDataSource = self (also tried using storyboard)
collectionView.isPrefetchingEnabled = true (also tried using storyboard)
I have implemented collectionView(_:prefetchItemsAt:)
Issue:
The prefetchItemsAt method is not being called. I determined that by placing a print statement in the method & setting up a breakpoint.
Like requested in the comments, I'll share my implementation for this issue here:
I created the tracker prefetchState which determines whether I'm prefetching at the moment, or not:
enum PrefetchState {
case fetching
case idle
}
var prefetchState: PrefetchState = .idle
Then, I hooked up the scroll view's delegate (the scroll view my collection view is in) to my view controller and implemented the scrollViewDidScroll method:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView == self.scrollView else { return }
let prefetchThreshold: CGFloat = 100 // prefetching will start 100pts above the bottom of the scroll view
if scrollView.contentOffset.y > scrollView.contentSize.height-screenBounds.height-prefetchThreshold {
if prefetchState == .idle {
prefetchItems()
}
}
}
In there, you can see that I check whether we're already prefetching. If not, I call prefetchItems(), as implemented here:
func prefetchItems() {
guard prefetchState == .idle else { return }
prefetchState = .fetching
someDownloadFuncWithCompletionBlock { (newItems) in
self.dataSource += newItems
self.collectionView.reloadData()
self.prefetchState = .idle
}
}
I execute the following code to make the collection view have the size of its content to prevent scrolling within that collection view but in a scroll view outside the collection view:
This sounds very broken.
Why are you doing this?
Prefetching on the collection view (from the docs) is triggered when the user scrolls the collection view. By making the collection view frame the same as the content size you are essentially disabling the scrolling of the collection view itself.
The collection view calls this method as the user scrolls
If you are forcing the collection view frame to be the same as the content size then you are entirely breaking UICollectionView.
The reason the prefetch isn't called is because every cell has been loaded already. Nothing is in prefetch any more. Because your collection view is displaying every cell at the same time.
If you want to scroll a collection view... let the collection view handle it. You shouldn't need to place the collection view inside another scroll view.

collectionview stops scrolling when calling performBatchUpdates on new items

I'm inserting new items as the user scrolls down the collection view... I'm having an issue where if they scroll too fast the collection view stops scrolling when the new items are inserted.
My original issue was that I was just calling self.collectionView.reloadData() instead of performing batch updates but it's currently doing the same exact thing...
This is my code
let postIndex = self.posts.count
let newPostIndex = newPosts.count
let indexArray = (postIndex...postIndex+newPostIndex-1).map{IndexPath(item: $0, section: 0)}
self.posts.append(contentsOf: newPosts)
self.collectionView.performBatchUpdates({
self.collectionView.insertItems(at: indexArray)
}, completion: nil)
I already have posts so I create an array of the indexes that I need to update and insert them in the performBatchUpdates but it looks really choppy and lacks a smooth feel.
I tried CATransaction as suggested here https://stackoverflow.com/a/32691888/6513002 and it literally scrolled all the way down completely bypassing everything new and going to the end..
Please assist

Add custom recognizer delay

I've disabled delaysContentTouches in my tableview subclass using:
delaysContentTouches = false
subviews.forEach { ($0 as? UIScrollView)?.delaysContentTouches = false }
But in one of my sections, I still want to keep the delay. Is there a way to cancel the delay for certain sections or perhaps I can add a custom recognizer delay to a section?
Sections are not actual objects within a tableView, so my answer to your first question is no. The .delaysContentTouches applies to the entire tableView.
For your second inquiry, I believe that one way it could be possible is through setting a delay for desired cells' scrollView subview. In your tableView(cellForRowAt: indexPath) func, you could have something like this:
if indexPath.section == 3 { //or whatever your desired section is
for view in cell.subviews {
if view is UIScrollView {
let currentView = view as! UIScrollView
currentView.delaysContentTouches = true
}
}
}
This will find the UIScrollView in your cell's subviews in your desired section. It will then set the .delaysContentTouches property accordingly.
I have not personally executed this code, just researched it, so let me know if it works.
Edit
Apparently the UIScrollView in UITableViewCell has been deprecated, so the above method will not work anymore.
My next best suggestion to you is to use a UILongPressGuestureRecognizer. This will not be quite the same thing as delaying the touch, but could have a similar effect in real execution.
You could use it in the same tableView(cellForRowAt: indexPath) func as so:
let press = UILongPressGestureRecognizer(target: self, action: #selector(handlePress))
press.minimumPressDuration = 2.0 //however long you want
cell.addGestureRecognizer(press)
Whatever you are trying to achieve by selecting certain rows of your tableView could be placed in the handlePress func above which would be trigged upon the long press.

How to dismiss keyboard in UISearchController when changing focus in tvos?

I'm completely new to tvos and I'm trying to implement a UISearchController view where, in my SearchResultsViewController, I have two UICollectionViews displayed one above the other:
The problem is that when the user swipes down to select one of the items in the UICollectionView, the keyboard doesn't dismiss. Even swiping back up to select the keyboard doesn't fully scroll up and it's impossible to see what you're typing. The resulting view is this:
Ideally, I'd like to dismiss the keyboard when the user swipes down to focus on anything else in the interface. I looked at Apple's tvos UIKit Catalog and their example shows a UISearchController which dismisses the keyboard when changing focus, but I don't see that they're doing anything differently.
Here is the code I'm using to setup my UISearchController when the user clicks on a button:
#IBAction func onSearchButton(sender: AnyObject) {
guard let resultsController = storyboard?.instantiateViewControllerWithIdentifier(SearchResultsViewController.storyboardID) as? SearchResultsViewController else { fatalError("Unable to instantiate a SearchResultsViewController.") }
// Create and configure a `UISearchController`.
let searchController = UISearchController(searchResultsController: resultsController)
searchController.searchResultsUpdater = resultsController
searchController.hidesNavigationBarDuringPresentation = false
let searchPlaceholderText = NSLocalizedString("Search for a Show or Movie", comment: "")
searchController.searchBar.placeholder = searchPlaceholderText
// Present the search controller from the root view controller.
guard let rootViewController = view.window?.rootViewController else { fatalError("Unable to get root view controller.") }
rootViewController.presentViewController(searchController, animated: true, completion: nil)
}
After quite a bit of trial and error, I was able to figure out the solution.
The keyboard will automatically dismiss itself as long as:
1) The item the user focuses on is inside of a scrollview
2) The scrollview content size is larger than the screen height by at least 1px (1081px).
After quite a lot of trial and error, finally I figured out.
The reason is that you have nested ScrollViews in searchResultsController.
"ScrollViews" of-course includes UICollectionView, UITableView, and UIScrollView.
According to my investigation, UISearchController behaves as follows.
If the first view which gets focused in searchResultsController is subview of the inner scrollView (which is the horizontal UICollectionView, in your case), then you won't get keyboard hidden as expected.
Interestingly, if the first view which gets focused in searchResultsController is subview of outer scrollView, then you will get keyboard hidden completely, animated, just as expected (!).
I think this is sort of UIKit's bug.
I had exactly same layout and wasn't able to achieve this so far. I believe you return false in tableView(tableView: UITableView, canFocusRowAtIndexPath indexPath: NSIndexPath) -> Bool so that each cells in collection view can scroll horizontally with proper focus behavior. I think it's actually causing the issues. If you make the first cell in the tableview focusable the problem goes away but of course focus behavior is not desired. I just found that out today and will try more tomorrow to find out what I can do about this. I sense that I will need a new design that allows me to use a single collectionview or tableview that has its cells focusable in resultsController. Hope this is easily achievable in tvOS 10.
Work Around Solution: Add one dummy cell at indexPath.row == 0 with height as 1 pixel and enable the focus on it.