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/
Related
how are you
I need to know how can i determine table view cell fully visible for playing auto play movie in cell and also detect hiding the table view cell.
I have apply this code below in table view but not give me the correct solution.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let cellRect = tableView.rectForRow(at: indexPath)
let isFullyVisible = tableView.bounds.contains(rectInFull)
if isFullyVisible {
// Play video
}
}
So please can you tell me how can i get the correct table view cell visible
cell.frame.origin.y >= tableview.contentOffset.y && cell.frame.origin.y + cell.frame.size.height <= tableview.contentOffset.y + tableview.bounds.size.height
You might want to implement the UIScrollViewDelegate method scrollViewDidScroll(_:) and in there you could do the check for visible cells in your table view:
func scrollViewDidScroll(_ scrollView: UIScollView) {
guard let tv = scrollView as? UITableView else { return }
tv.visibleCells.forEach { $0.playVideo() }
}
Of course assuming that method on the cell (playVideo()) exists and it takes care of being called multiple times when the video is already playing (i.e. must ignore the call if is already playing).
Otherwise if you need the indexPaths of visible cells or any fine tuning use either the property indexPathsForVisibleRows or the method indexPathForRows(in:) on the table view, then those index paths you'll optionally obtain inside an array will point to the model's objects (which might implement the play video logic).
I have a collectionView embedded in a subview of the ViewController. The collectionView has 12 cells. Each cell takes up the whole width and height of the collection view, so that I can achieve the pagination affect. However, when the app starts, I want to show the middle cell like the 6th or 7th one of my collectionView.
P.S. I have the collectionView in a wrapper view, not in my viewController.
In my WrapperView, I added the following method but as this method is called after the collectionView is added, it shows a sudden jump.
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
let indexPath = IndexPath(row: 6, section: 0)
DispatchQueue.main.async {
self.calendarCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false)
}
}
If I could do it before the collection view appear on the screen, I may be able to fix that problem, but I can't find which method is called before the didAddSubview(_:) method in UIView Life Cycle.
Can anyone give me hint on how to solve this.
After your data source has loaded use:
func selectItem(at indexPath: IndexPath?,
animated: Bool,
scrollPosition: UICollectionView.ScrollPosition)
https://developer.apple.com/documentation/uikit/uicollectionview/1618057-selectitem
Probably best used before the view appears so trigger in viewWillAppear or viewDidLoad
I have a project where I have to show a lot of information on one screen. It is not too much information, but it is complicated to achieve. For the purpose of this question I suggest to look at this screenshot that will illustrate what I want to achieve.
I currently set up this screen with a UIViewController containing a UITableView that is pinned to the top, bottom, leading and trailing anchor of the view using AutoLayout for the middle section. This works great. I then added a tableHeaderView which contains the information about the user on top. Now I have just added a second UITableView in the tableFooterView, and surprisingly, this works like a charm too. I had a bit of trouble with the height, but I managed to get it done in the viewDidLayoutSubviews() method. But that's bit off topic.
I am now at the final stage where I want to add a UITextView together with a UIButton (in a UIView container) to allow users to add comments. At first, I added this in the tableFooterView and it worked, but as soon as the content was too short (e.g. only one row in the middle and no comments yet), the UITextView would appear in the middle of the screen (directly under the contents of the UITableView). I read up on this and figured it is the expected behavior of the tableFooterView, so I am now trying to figure out a way on how to add this custom view to add comments that will always be on the bottom if the content is not filling the entire screen, but will also scroll with the contents if the content is larger than the screen size. (Ideally, I would want to be able to grow or shrink the UITextView when a user enters text - might be relevant in case someone suggests contentInsets).
Any suggestions? Should I add a subview directly to the UITableView (which is, as far as I read, not recommended)? Should I work with contentInsets on the UITableView and add the UIView container as a subview of my main UIViewController? I'm a bit lost after searching multiple solutions without finding the right one, so I hope you guys can help me out. Cheers!
Adding a UITextView is tricky because it's subclass of UIScrollView, just like UITableView, and adding a scroll view to another scroll view is not only difficult for the gesture recognisers to handle, but also for auto-layout.
What would make your life easier is to have just 1 table view in your view controller with different sections, and each section has a different type of cell.
What I like to do for this is declare an enum in my view controller:
class MyViewController: UIViewController {
private enum Section: Int, CaseIterable {
case foo
case bar
}
}
extension MyViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return Section.allCases.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let section = Section(rawValue: section) else { preconditionFailure() }
switch section {
case .foo:
return 1
case .bar:
return 4
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let section = Section(rawValue: indexPath.section) else { preconditionFailure() }
let cell: UITableViewCell
switch section {
case .foo:
cell = ...
case .bar:
cell = ...
return cell
}
}
For the cells with long text you can just use a default cell style (in your XIB/storyboard) and set
cell.textLabel?.numberOfLines = 0
The table view should be able to automatically size itself to fit the entire text.
The only reason you should need to use a UITextView in your scenario is if you want the user to be able to edit the text. Otherwise save yourself a lot of pain and go with a UILabel.
You should add separate UIView at the bottom of main screen and add UITextView as a subview in it.
Add TableView above this container view so that it scrolls and textview is always visible at the bottom.
Is there any way to avoid the tableview keep scrolling when reaching the bottom cell?
So far I managed to do this:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == dataArray.count - 1 {
tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
and that allows me only to stop the scroll when the user "rolled" the table to bottom. But after that the user can scroll again. Only if I could disable the "down" scrolling I could achieve it. Enabling it again as soon as it moved.
But it seems a lot of work just to do that, maybe I'm missing some property that fixes the last cell bottom to tableview's bottom.
Any ideas?
There are 3 things you need to do.
tableView.footerView = UIView()
tableView.bounces = false
And after the tableView has been reloaded.
tableViewHeight(your constraint).constant = tableView.contentSize.height
This will cause the tableView to only display the populated cells, you will restrict the height to be as high as the content and you can't bounce the content as you want it to stop at last cell.
Remember that an UITableView is a subclass of UIScrollView
Starting here, you have 2 options :
1 - Disable the bounces effect : tableView.bounces = false
2 - Prevent scroll further than the bottom of your tableView :
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y + scrollView.bounds.size.height > scrollView.contentSize.height {
scrollView.contentOffset.y = scrollView.contentSize.height - scrollView.bounds.size.height
}
}
You can do by disabling it directly from Storyboard:
Disable the Bounce on scroll to Untick
Also if u want to disable the lines coming at bottom when data is less you can use:
tableView.footerView = UIView()
So I want a signature view within a table cell. Obviously whenever somebody tries to draw in the cell, the table scrolls.
How would I stop the scrolling but ONLY when the user is writing in the signature box?
I found better solution for this issue rather than putting button. Implement the delegate methods in viewController,
class mainVC: UIViewController,YPSignatureDelegate {
Than set delegate of signature view to this view controller
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SignatureCell", for: indexPath) as! SignatureCell
cell.signatureView.delegate = self
return cell
}
And then add these code. This are two delegates of YPSignature. Add in Main view controller
func didStart() {
tableView.isScrollEnabled = false
}
// didFinish() is called rigth after the last touch of a gesture is registered in the view.
// Can be used to enabe scrolling in a scroll view if it has previous been disabled.
func didFinish() {
tableView.isScrollEnabled = true
}
I would solve this with a button covering the cell, and when the user taps it, the cell displays the YPDrawSignatureView. Just before the signature view is shown, disable the scrolling:
tableView.scrollEnabled = false
Later when you save the signature, enable scrolling again by setting scrollEnabled to true.
I added a uitableview and custom cells. In one of the custom cells contain a button(ex. addSignatureButton) on the top of signatureView.
I used delegate method to communicate between uitableviewcell and uiviewcontroller. A delegate is added to UITableViewCell to notify whether the addSignatureButton is tapped. Once it is tapped, addSignatureButton is hidden, signatureView is visible and the tableview's scroll is disabled. When user finishes adding signature, signatureView is hidden, addSignatureButton is visible and tableview scroll is enabled.
https://github.com/alvinvgeorge/DrawSignatureOnTableViewCell