Swift UIcollectionView change content of second UICollectionView on same ViewController? - swift

I’m new to collection views and I’m struggling to get a collection views selected cell to change the content of another collection views displayed content which both collection views are on same ViewController.
I have two working collection views on one view controller and want my selection on first collection view to change the content of the second collection view.
Didn’t feel I need to post my code of collectionViews as I set up the collectionViews like normal, as per Apple documentation.
So both collection views work, just this requested function doesn’t yet.
An example of the data would be:
Collection view one
Let arrayDrinks = [“beer”, “spirit”, “wine”]
Collection view two ( changes depending what is selected in collection view one)
Let arrayBeer = [“Carling”, “fosters”, “Carlsberg”]
Let arraySpirit = [“vodka”, “whiskey”]
Let arrayWine = [“red wine”, “white wine”, rose]
Any help would be great. Thanks

Implement the collection view's delegate method.
extension YourViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if collectionView == firstCollectionView {
// Identify the selected cell here and change in your second collection view accordingly.
}
}
}
And don't forget to set the delegate in your viewcontroller:
firstCollectionView.delegate = self
secondCollectionView.delegate = self

Related

Header view of UICollectionView keeps duplicating on scroll - How to create fixed (non-sticky), single collection view header?

I wanted to implement a view hierarchy like the following so that the ENTIRE view would be scrollable:
UIScrollView
Image View
Collection view
But a lot of people on here have said that it is better to use the header that comes along with the collection view. I've done that but now I have a new problem: as I scroll the collection view, any configurations I've done to the header cell in the viewForSupplementaryElementOfKind function is duplicating (Eg: If I programmatically create a new view in viewForSupplementaryElementOfKind function, this view will keep creating as I scroll)
I kind of get that this is happening because I'm dequeuing the header using dequeueReusableSupplementaryView. But I've tried searching on Apple docs and there are no other codes I can use to instantiate the header view without making it reusable.
Is there any way I can create a view controller as described above without using UICollectionView?
I've tried setting the number of sections to 1 hoping that it would only be reused ONCE but it doesn't work.
Edit: Also tried setting header size using UICollectionViewDelegateFlowLayouta and using UICollectionView instead of UIViewController and UICollectionViewDataSource etc.
func collectionView(_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
{
switch kind {
case UICollectionView.elementKindSectionHeader:
guard
let headerView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: "DiscoverHeader",
for: indexPath) as? DiscoverHeader
else {
fatalError("Invalid view type")
}
// Rotating arrow image
headerView.arrowImg.transform = headerView.arrowImg.transform.rotated(by: CGFloat.pi/0.67)
return headerView
default:
assert(false, "Invalid element type")
}
}
Why not UIImageView and UICollectionView inside UIScrollView?
You can definitely create a UIScrollView and add an UIImageView and a UICollectionView in it.
But that won't work as expected. This is because you're embedding a scrollView(UICollectionView) inside another scrollView. Scrolling the collectionView vertically will hamper the scrolling of the outer scrollView.
Solution:
Try giving the height of the header view using UICollectionViewDelegateFlowLayout's method,
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.bounds.width, height: 100.0) //give the height as per your requirement...
}
Also, set the contentMode of imageView as Aspect Fit / Aspect Fill.

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.

UISplitViewController - Lost reference to detail view controller on resize

I have seen plenty of questions similar to this - but not quite the same..
So I have a UISplitViewController, and the detail view controller has a lot of heavy drawing in it, so I don't want to reinit it every time I select a new row on the master view. So I observe when the user selects a row, and only perform a segue if it doesn't see a detail view controller.
Here's where the problem lies... On the iPad, if I resize the view via multi-tasking, eventually the UISplitViewController only shows the master. But when I select a row, it thinks the detail view controller is nil and allocates a new detail view controller (performs the segue). The thing is that the detail view controller still exists, It just doesn't show up on the split view controller's childViewControllers. I want to perform a segue when in the compact size, but I don't want it to recreate a new view - I want it to use the view that the split view controller is holding.
Sorry if this seems confusing.
Thanks
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// try to get my custom detail view controller
if let myVC = splitViewController?.detailViewController?.childViewControllers.first as? MyViewController {
let selectedRow = indexPath.row
myVC.doSomething(selectedRow)
} else {
// perform segue if it's not available
performSegue(withIdentifier: "showMyVC", sender: self)
}
}

Cell loaded in cellForItemAt, but not in visibleCells

I have a custom UICollectionView and the cells are loaded in cellForItemAt but when I try to get all the visible cells by using visibleCells I'm not getting all the cells.
For example, in cellForItemAt, I'm setting the alpha of the labels in the cells to 0. When panned, I want the alpha of those labels change to 1:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
handleLabel(scrollView, active: true)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if pickerIsActive { handleLabel(scrollView, active: false) }
}
private func handleLabel(_ scrollView: UIScrollView, active: Bool) {
guard let pickerView = scrollView as? UICollectionView else { return }
let cells = pickerView.visibleCells.flatMap { $0 as? CustomCell }
panningIsActive = active
UIView.animate(duration: 0.3) {
cells.forEach { $0.label.alpha = $0.isSelected || active ? 1 : 0 }
}
}
And cellForItemAt:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CustomCell
cell.label.alpha = 0
return cell
}
What can I do to change all the "loaded" cells instead of just the "visible" cells?
The visibleCells are only the on screen cells. This used to be everything initialized in cellForItem:at: but as of iOS 10 UICollectionView now prefetches to improve scrolling performance (see WWD 2016 video) which maybe why you are having this problem. Anyways it sounds like all you want to do is animate the cells to fade in when they come on screen. You can either move your animation logic to willDisplayCell or subclass UICollectionViewCell. UIColectionViewCell inherits from UIView, so you can override didMoveToSuperView in your UICollectionViewCell and call your animation method there, which will cause the cell to animate as it appears.
I am using Xcode 11.4 and Swift 5, and I had the exactly the same issue: .visibleCells is not giving me all the loaded cells.
By reading #Josh Homann's answer and the comments below, I figured out 2 solutions.
The first solution is same as the solution you reached at: customize cell appearance in collectionView(_:willDisplay:_:) after it's loaded but before it's displayed on the screen.
Another quick and dirty solution is to simply uncheck UICollectionView's 'Prefetch' option in attributes inspector.
This fixes the issue because by disabling prefetching, UICollectionView will stop pre-loading cells that are not displayed on the screen, so .visibleCells are now all the loaded cells. This solution will work fine if you're simply loading static or small local data in the cells. If you're prefetching large data (e.g. images) from network for upcoming cells, you probably need Prefetching Enabled, then solution 1 is your go-to option.
It sounds like you might want to try using layoutAttributesForElements(in:).
You'll need to implement your own collection view layout subclass (rather than using the delegate methods) but I think it will be worth it in the long term.
Rather than manually managing the animations (via UIView.animateWithDuration) you use this method to tell the collection view what properties cells should have at different positions, and as people pan the collection view, the correct properties are automatically applied.
I tried to find a good Swift reference for this, but I could't, but here's a post in Objective-C that you can follow if you want to try this approach:
https://bradbambara.wordpress.com/2014/05/24/getting-started-with-custom-uicollectionview-layouts/

How to determine which cell is being viewed or clicked in a collection view

I want to be able to edit records that are displayed in a collection view.
I've added an "Edit" button to the collection view. What is the best way to determine which cell is to be edited?
I thought of doing something like so:
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
visibleCell = collectionView.visibleCells().first as MessageCell
}
I didn't want to add the edit button to the xib itself as I don't want the button to scroll when the cell scrolls
One possibility is to have a variable in your ViewController called something like isEditing. This will be set to true when the edit button is clicked.
Then, set your collection view's delegate to self.
Implement this method: func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath), A UICollectionViewDelegate method.
In the method, write something like:
if isEditing{
// What happens when selected and editing
} else{
// What happens when selected and not editing
}