How to scroll UICollectionView cells before animated insert of new cell? - swift

I have a horizontal scrolling UICollectionView to which I want to add multiple cells that will cause a 2 stage animation to occur:
UICollectionView scrolls existing cells to the left, showing
the empty space where the new cell will appear, then...
The new cell to be inserted then scrolls up vertically from off screen into the empty space.
Imagine the Cells as cards appearing from the bottom of the screen when a user triggers an action.
Currently I have a custom UICollectionViewFlowLayout with an overridden initialLayoutAttributesForAppearingItem which will animate the scroll up element of the transition, however after the first item this animation occurs offscreen. I cannot figure out how to scroll the UICollectionView to the left to show the space where the future cell will appear before the slide up animation occurs.
I have dabbled with contentOffset changes, however this results in some very jerky and glitchy behaviour.
Whats the best way to approach this?
Current `initialLayoutAttributesForAppearingItem' implementation:
override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attr = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath)
attr?.alpha = 0.0
attr?.transform = CGAffineTransform(
translationX: 0,
y: self.collectionView!.bounds.height
)
return attr
}

Related

How to scroll UICollectionView without touching it by dragging another element in Swift?

Hello! I'm trying to set up a CollectionView, that scrolls only when user scrolls another element (view with arrows on the image). Also, depending on which cell is in the middle of the screen, text in label should change to match this cell.
I've done the scrolling part this way: when user pans view with arrows, collection also moves, but not scrolls:
else if sender.state == .changed {
if scrollButtonImageView.frame.maxY < scrollRangeView.frame.maxY {
scrollButtonImageView.center = CGPoint(x: originalPosition.x, y: originalPosition.y + translation.y)
collectionView.center.y = scrollButtonImageView.center.y
My question is: Which is the proper way to scroll CollectionView without touching it and how to observe which cell is in the center of the screen and change data in labels instantly, when another cell got into center?
UPD: Now I'm using UISlider with same number of values as collection. But it is not very smooth. My code:
let currentValue = Int(sender.value)
collectionView.scrollToItem(at: IndexPath(row: currentValue, section: 0), at: .top, animated: true)
One plan might be to describe a rectangle for the area that you'd like to consider as in view (maybe as a fixed rectangle above the collection view) and then use the CGRect functions contains or intersection to identify when a child view of the collection is within the target area.

Paging with left inset on small cells in UICollectionView

I have a UICollectionView filled with dates ranging from one date to another. I have implemented the UICollectionView with IGListKit. I would Like to enable paging on each cell so that each cell will land in the middle of the circle when it scrolls past it. (Like below) I have tried a few ways of paging, but these don't centre directly in the middle of the circle (As seen in the second image). What is happening is, the cells are getting paged to the left-hand side, I would like there to be an inset of 20 so that it fits into the circle
To enable the current date to show on the left-hand side in the circle when the view first loads, I have the semantic set to Force Right-to-Left and I also have edgeInsets which are:
left: screenWidth-62, right: 20)
The width of the cell is 42 and the inset at the side is 20, therefore the first cell will sit in the circle with these insets. So when the view first loads, the collection view looks like this:
I basically want each cell to land inside of this circle when it scrolls past (if the scrolling stops)
This is what I have so far:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView == self.collectionView {
var currentCellOffset = self.collectionView.contentOffset
currentCellOffset.x += (self.collectionView.frame.width / 2) + 20
if let indexPath = self.collectionView.indexPathForItem(at: currentCellOffset) {
self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
}
and this:
collectionView.decelerationRate = UIScrollView.DecelerationRate.fast
Does anyone know how to solve this?
Thank you
i created a custom collection view layout for paging by cell instead of screen
i also outline a few different ways to customize it with examples in the README
check it out here
let me know if you have any questions about it or need more help

First use of tableView.reloadRows is different

My cells stores an item and the amount of this item increases whenever tapped. To delete the amount the cell can be panned to the left and the amount is zero.
Whenever a cell has an item with amount > 0, a blue line (a CAShapelayer) is visible at the left side of the cell.
To set the amount back to 0 a function is called:
func deleteItem(_ index: IndexPath) {
// Amount is set to 0
tableView.reloadRows(at: [index], with: .none)
}
When the cell is reloaded, the blue line will be moved out of the cell. And because the blue line is a CAShapeLayer, it's implicit animation will kick in and animates the blue line going out of the cell.
Implicit animation:
Problem:
However, the first time (after the view is loaded) this function is called, the blue line (CAShapeLayer) doesn't do the implicit animation.
No animation:
If I use tableView.reloadData() I won't have this problem, but instead it will reload the whole table even when I'm panning other cell's causing it to reset so I can't use tableView.reloadData().
Question:
Why is it only the first time after the view has been loaded that the implicit animation of the blue line (`CAShapeLayer) isn't working? How can I make sure this implicit animation will always work?

Tracking the position of a NSCell on change

I have a NSTableView and want to track the position of its containing NSCells when the tableView got scrolled by the user.
I couldn’t find anything helpful. Would be great if someone can lead me into the right direction!
EDIT:
Thanks to #Ken Thomases and #Code Different, I just realized that I am using a view-based tableView, using tableView(_ tableView:viewFor tableColumn:row:), which returns a NSView.
However, that NSView is essentially a NSCell.
let cell = myTableView.make(withIdentifier: "customCell", owner: self) as! MyCustomTableCellView // NSTableCellView
So I really hope my initial question wasn’t misleading. I am still searching for a way how to track the position of the individual cells/views.
I set the behaviour of the NSScrollView (which contains the tableView) to Copy on Scroll in IB.
But when I check the x and y of the view/cells frame (within viewWillDraw of my MyCustomTableCellView subclass) it remains 0, 0.
NSScrollView doesn't use delegate. It uses the notification center to inform an observer that a change has taken place. The solution below assume vertical scrolling.
override func viewDidLoad() {
super.viewDidLoad()
// Observe the notification that the scroll view sends out whenever it finishes a scroll
let notificationName = NSNotification.Name.NSScrollViewDidLiveScroll
NotificationCenter.default.addObserver(self, selector: #selector(scrollViewDidScroll(_:)), name: notificationName, object: scrollView)
// Post an intial notification to so the user doesn't have to start scrolling to see the effect
scrollViewDidScroll(Notification(name: notificationName, object: scrollView, userInfo: nil))
}
// Whenever the scroll view finished scrolling, we will start coloring the rows
// based on how much they are visible in the scroll view. The idea is we will
// perform hit testing every n-pixel in the scroll view to see what table row
// lies there and change its color accordingly
func scrollViewDidScroll(_ notification: Notification) {
// The data's part of a table view begins with at the bottom of the table's header
let topEdge = tableView.headerView!.frame.height
let bottomEdge = scrollView.bounds.height
// We are going to do hit-testing every 10 pixel. For best efficiency, set
// the value to your typical row's height
let step = CGFloat(10.0)
for y in stride(from: topEdge, to: bottomEdge, by: step) {
let point = NSPoint(x: 10, y: y) // the point, in the coordinates of the scrollView
let hitPoint = scrollView.convert(point, to: tableView) // the same point, in the coordinates of the tableView
// The row that lies that the hitPoint
let row = tableView.row(at: hitPoint)
// If there is a row there
if row > -1 {
let rect = tableView.rect(ofRow: row) // the rect that contains row's view
let rowRect = tableView.convert(rect, to: scrollView) // the same rect, in the scrollView's coordinates system
let visibleRect = rowRect.intersection(scrollView.bounds) // the part of the row that visible from the scrollView
let visibility = visibleRect.height / rowRect.height // the percentage of the row that is visible
for column in 0..<tableView.numberOfColumns {
// Now iterate through every column in the row to change their color
if let cellView = tableView.view(atColumn: column, row: row, makeIfNecessary: true) as? NSTableCellView {
let color = cellView.textField?.textColor
// The rows in a typical text-only tableView is 17px tall
// It's hard to spot their grayness so we exaggerate the
// alpha component a bit here:
let alpha = visibility == 1 ? 1 : visibility / 3
cellView.textField?.textColor = color?.withAlphaComponent(alpha)
}
}
}
}
}
Result:
Update based on edited question:
First, just so you're aware, NSTableCellView is not an NSCell nor a subclass of it. When you are using a view-based table, you are not using NSCell for the cell views.
Also, a view's frame is always relative to the bounds of its immediate superview. It's not an absolute position. And the superview of the cell view is not the table view nor the scroll view. Cell views are inside of row views. That's why your cell view's origin is at 0, 0.
You could use NSTableView's frameOfCell(atColumn:row:) to determine where a given cell view is within the table view. I still don't think this is a good approach, though. Please see the last paragraph of my original answer, below:
Original answer:
Table views do not "contain" a bunch of NSCells as you seem to think. Also, NSCells do not have a position. The whole point of NSCell-based compound views is that they're much lighter-weight than an architecture that uses a separate object for each cell.
Usually, there's one NSCell for each table column. When the table view needs to draw the cells within a column, it configures that column's NSCell with the data for one cell and tells it to draw at that cell's position. Then, it configures that same NSCell with the data for the next cell and tells it to draw at the next position. Etc.
To do what you want, you could configure the scroll view to not copy on scroll. Then, the table view will be asked to draw everything whenever it is scrolled. Then, you would implement the tableView(_:willDisplayCell:for:row:) delegate method and apply the alpha value to the cells at the top and bottom edges of the scroll view.
But that's probably not a great approach.
I think you may have better luck by adding floating subviews to the scroll view that are partially transparent, with a gradient from fully opaque to fully transparent in the background color. So, instead of the cells fading out and letting the background show through, you put another view on top which only lets part of the cells show through.
I just solved the issue by myself.
Just set the contents view postsBoundsChangedNotifications to true and added an observer to NotificationCenter for NSViewBoundsDidChange. Works like a charm!

UIButton focus in UITableView

I am trying to understand how the focus is working when I have button inside Table.
The goal is to create a table view with custom cell, that has the same size as the table view size ( I want to see only one cell at the time ). When the button is getting focused, I want to scroll ( the table view is doing it automatically ), to the next cell and to see only the next cell.
I made sure that the button will be in focus and not table view cell, by override this method in the Table cell:
override var canBecomeFocused: Bool{
return false
}
The button is focused and when I am swipe up/down, the focus is moving between the buttons and not the Table it self ( great success ) ... BUT, the table view is being scrolling automatically, and I want to understand why is some cases ( when the Table and Table Cell are bigger then 360 pixel ) it's scrolling to the next cell and I can see only the next cell ( the table view and the cell in the same size ) , and sometimes ( when the Table and Cell are Smaller then 360 pixel ) the table view scrolling just a little bit, until the next cell is showing only the button ( meaning the cell is not fully shown )
So , I have:
Cell and table view cell size are Greater then 360, we can see that when the button is focused, the table view scrolling to the center of the cell, and we can see only 1 cell:
Cell and table view cell size is Smaller then 360, we can see that when the button is focused, the table view scrolling to some point, when the button is shown
FYI: I think the size of the button is not effecting the scroll, only the table view and table view cell size is effect the scrolling.
Some one? any idea ?
This is the test Project: in order to see it better please change the table view size and the cell size to be smaller then 360, and make sure the UIButton is centered: Project
You can put those methods in UITableViewCell's subclass. see
override var preferredFocusEnvironments: [UIFocusEnvironment]{
// Condition
}
override func shouldUpdateFocus(in context: UIFocusUpdateContext) -> Bool {
// Condition
}
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
// Condition
}
you can see more at this link: enter link description here