I'm having an issue with my application architecture...
I have a UITableView filled with custom UITableViewCells.
Of course, I use dequeuing so there are only 8-10 cell instances ever generated.
Moving forward to my problem... I have added a theming feature to my application.
When the user long touches the main UINavigationBara notification is posted app wide that informs each viewController to update their UI.
When the viewController hosting the tableView with the custom cells receives the notification, it calls tableView.reloadData()
This works well, as I have implemented func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
Inside willDisplay I call a method on my custom tableViewCell which animates, using UIViewAnimation, appropriate colour changes to the cell.
It looks like this:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let newsFeedItemCell = cell as? NewsFeedItemCell {
if (self.nightModeEnabled) {
newsFeedItemCell.transitionToNightMode()
} else {
newsFeedItemCell.transitionFromNightMode()
}
}
}
Inside the custom cell implementation those methods look like this:
func transitionToNightMode() {
UIView.animate(withDuration: 1, animations: {
self.backgroundColor = UIColor.black
self.titleTextView.backgroundColor = UIColor.black
self.titleTextView.textColor = UIColor.white
})
}
func transitionFromNightMode() {
UIView.animate(withDuration: 1, animations: {
self.backgroundColor = UIColor.white
self.titleTextView.backgroundColor = UIColor.white
self.titleTextView.textColor = UIColor.black
})
}
This works fine... heres the issue:
Upon scrolling the tableView, any cells that weren't on screen have their colour update/animation code called as they scroll onto the screen, which leads to a jarring user experience.
I understand why this is happening of course, as willDisplay is only called as cells display.
I can't think of an elegant way to avoid this.
I'm happy that cells on screen are animating for the user experience to be pleasant, however, for cells off screen, I'd rather they skipped the animation.
Possible solutions (though inelegant):
Keep a reference to each of the 8-10 cells created by cellForRow, and check if they are off screen, if they are set their state immediately.
However, I don't like the idea of keeping a reference to each cell.
Any ideas?
I would not use a reloadData to animate this, since your data model for the tableview is not actually changing.
Instead, I would give the UITableView a function, like so:
class myClass : UITableViewController {
....
func transitionToNightMode() {
for visible in visibleCells {
UIView.animate(withDuration: 1, animations: {
visible.backgroundColor = UIColor.black
visible.titleTextView.backgroundColor = UIColor.black
visible.titleTextView.textColor = UIColor.white
})
}
}
}
and then in your willDisplay or cellForItemAt, set the correct appearance without animation.
I would try the following.
Instead of using UIView.animate, in transition(To/From)NightMode I would create a UIViewPropertyAnimator object that would do the same animation. I would keep the reference to that object around and then in prepare for reuse, if the animation is still running, I would simply finish that animation and reset the state. So something like this:
fileprivate var transitionAnimator: UIViewPropertyAnimator?
fileprivate func finishAnimatorAnimation() {
// if there is a transitionAnimator, immediately finishes it
if let animator = transitionAnimator {
animator.stopAnimation(false)
animator.finishAnimation(at: .end)
transitionAnimator = nil
}
}
override func prepareForReuse() {
super.prepareForReuse()
finishAnimatorAnimation()
}
func transitionToNightMode() {
transitionAnimator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut, animations: {
self.backgroundColor = UIColor.black
self.titleTextView.backgroundColor = UIColor.black
self.titleTextView.textColor = UIColor.white
})
transitionAnimator?.startAnimation()
}
func transitionFromNightMode() {
transitionAnimator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut, animations: {
self.backgroundColor = UIColor.white
self.titleTextView.backgroundColor = UIColor.white
self.titleTextView.textColor = UIColor.black
})
transitionAnimator?.startAnimation()
}
Related
I'm trying to handle the dequeue reusable cell for my situation. I'm not sure I'm doing things the best way here, I was able to handle the weird stuff about dequeueing the cells for the text labels in my custom cells. I handled that with override func prepareForReuse() in the cell xib code. However when I try to handle the cell background in the same way it does not work as its supposed to. I believe this is because I'm changing cell background elsewhere than the tableView function...
So the problem is the cell background isn't changing as its supposed to, typical dequeue reusable cell issues. If I put backgroundColor = nil inside prepareForReuse() then the background does not show at all. If I leave it out then the background color jumps all over the tableView when I scroll.
FYI I have registered the cells properly in viewDidLoad()
I think this is enough code to explain the situation. Appreciate any pointers :)
Mark cell background RED
func markRED(cell: EntrantCell) {
var timeOut: String {
let currentDateTime = Date()
let formatter = DateFormatter()
formatter.timeStyle = .medium
return formatter.string(from: currentDateTime)
}
if let indexPath = self.entrantsTable.returnIndexPath(cell: cell) {
entryStatusGrid[indexPath.row] = 2
entrantTime[indexPath.row].append("OUT- \(timeOut)")
entrantsTable.cellForRow(at: indexPath)?.backgroundColor = #colorLiteral(red: 0.7647058964, green: 0.01910983652, blue: 0.008289327978, alpha: 1)
entrantsTable.reloadData()
}
print(entrantTime)
print(entryStatusGrid)
// TODO: Handle proper space status for global button
Entrant.updateStatus(entryStatusGrid: entryStatusGrid, button: spaceStatus)
}
TableView extension
extension EntrySession: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return entryGrid.count
}
// TODO: Handle proper deque for reusable cells
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let addEntrantCell = entrantsTable.dequeueReusableCell(withIdentifier: Global.headerCellIdentifier, for: indexPath) as! AddEntrantCell
let entrantCell = entrantsTable.dequeueReusableCell(withIdentifier: Global.bodyCellIdentifier, for: indexPath) as! EntrantCell
if indexPath.row == 0 {
addEntrantCell.delegate = self
addEntrantCell.entrantNameTextField.text = entryGrid[indexPath.row].name
addEntrantCell.entrantCompanyTextField.text = entryGrid[indexPath.row].company
return addEntrantCell
} else {
entrantCell.delegate = self
entrantCell.nameLabel.text = entryGrid[indexPath.row].name
entrantCell.companyLabel.text = entryGrid[indexPath.row].company
return entrantCell
}
}
}
Custom cell reuse()
override func prepareForReuse() {
super.prepareForReuse()
name.text = nil
status.text = nil
backgroundColor = nil // When i do this the background color does not change...
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
#IBAction func inButton(_ sender: UIButton) {
delegate?.markRED(cell: self)
}
#IBAction func outButton(_ sender: UIButton) {
delegate?.markBLUE(cell: self)
}
You need to always set it — to what's appropriate.
if shouldSetBackgroundColor {
backgroundColor = .brown
} else {
backgroundColor = nil
}
Let's assume you initialize only 4 cells.
Then show 4 cells. Assume all have shouldSetBackgroundColor set to true. As a result you see all 4 cell's backgroundColor to brown. So far so good.
Now comes your 5th cell. If you don't set shouldSetBackgroundColor to false on the cell or don't have some logic to change the color of the backgroundColor then because the cell gets re-used, the backgroundColor won't change. It will remain set to brown...
I need to expand or collapse a table view cell and its contents.
For that I'm using NSLayoutConstraints. Though it gets the work done, I'm facing some pesky layout issues with other views in the cell.Here is a video.And here is my code:
ViewController:
extension ViewController: TableViewCellDelegate {
func tableView(shouldExpand cell: TableViewCell) {
tableView.performBatchUpdates({
cell.expand()
})
}
func tableView(shouldCollapse cell: TableViewCell) {
tableView.performBatchUpdates({
cell.collapse()
})
}
}
TableViewCell:
func expand() {
UIView.animate(withDuration: 0.57) {
self.viewHeight.constant = 320
self.view.alpha = 1
self.layoutIfNeeded()
}
}
func collapse() {
UIView.animate(withDuration: 0.57) {
self.viewHeight.constant = 0
self.view.alpha = 0
self.layoutIfNeeded()
}
}
How to stop the upper view stop resizing?
You can use UIStackView and just show and hide view for collapse and expand.
UIStackView will be easy to handle and will work for you.
performBatchUpdates is used to animate changes in tableView based on changes in the data source. Since you don't have any change in datasource, there is no need to do the animation inside tableView.performBatchUpdates block. Try it without performBatchUpdates.
func tableView(shouldExpand cell: TableViewCell) {
cell.expand()
}
func tableView(shouldCollapse cell: TableViewCell) {
cell.collapse()
}
I've built an expandable Tableview and I want an arrow which points to the left when the cell is not expanded and down if the cell is expanded.
The first animation (To point the arrow down) Works like a charm. When trying to rotate back (while closing the expanded Cell), it just jumps back to normal without any animation.
In my cellForRow I do this:
cell.image1.rotate(item.opened ? -.pi/2 : 0, duration: 0.4)
I have an extension for it:
extension UIView {
func rotate(_ toValue: CGFloat, duration: CFTimeInterval) {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.toValue = toValue
animation.duration = duration
animation.isRemovedOnCompletion = false
animation.fillMode = CAMediaTimingFillMode.forwards
self.layer.add(animation, forKey: nil)
}
}
This one works like a charm outside of the tableview. But inside the Tableview it always goes back to normal without the animation.
Any idea where this comes from?
Call rotation in the action method instead of func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
Try the code below.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! CustomTableViewCell
cell.rotateArrow()
}
In CustomTableViewCell
class CustomTableViewCell: UITableViewCell {
#IBOutlet weak var arrowImageView: UIImageView!
var isOpened = false
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
func rotateArrow() {
isOpened = !isOpened
arrowImageView.rotate(isOpened ? -.pi/2 : 0, duration: 0.4)
}
}
Ok I found the answer.
It jumped back (without any animation) because of how my rotate function was executed. Doing the animation when inputting 0 didn't work, because the image was set in the cell when it reloaded. So there was nothing to animate. Now I just set the picture and animate it afterwards.
if item.opened{
cell.image1.image = UIImage(named: "back")
cell.image1.rotate(-.pi/2, duration: 0.3)
} else {
cell.image1.image = UIImage(named: "down")
cell.image1.rotate(.pi/2, duration: 0.3)
}
I have table view. Inside cell I have method that animates view inside cell.
func animateCell() {
UIView.animate(withDuration: 0.4, animations: { [weak self] in
self?.quantityBackround.transform = CGAffineTransform(scaleX: 25, y: 25)
}) { [weak self] _ in
UIView.animate(withDuration: 0.1) {
self?.quantityBackround.transform = CGAffineTransform.identity
}
}
}
Also I have prepareForReuse()
override func prepareForReuse() {
quantityBackround.transform = CGAffineTransform.identity
}
The animation must work only for last cell when array of datasource changes and I do this in property observer like this (fires when something is being added to array)
guard let cell = checkTableView.cellForRow(at: IndexPath(row: viewModel.checkManager.check.count - 1, section: 0)) as? CheckItemTableViewCell else { return }
cell.animateCell()
All of this works fine.
One problem, is that I encounter is that when tableView is reloaded, all background views in all cells expand from zero size to its initial. Last cell animates ok.
I think that i miss something in prepareForReuse and because of this i see this glitch of inreasing from zero to initial size.
How to fix it ?
You need to implement this method of UITableViewDelegate
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
//here check if your this cell is the last one, something like this
if (indexPath.row == yourDataSourceArray.count - 1)
{
if let customCell = cell as? CheckItemTableViewCell{
customCell.animateCell()
}
}
}
Hope this helps
I am programmatically adding a UITableView as a subview of a view that uses UIView.animateWithDuration to expand the view when a button is clicked from a single point to a full window. Basically, a box that starts as a point and expands to full size with an animation. I am having difficulties getting the table to populate with cells. At first, a cell was being created, but would disappear after quickly after the animation completed, after playing around with it, I have gotten the cell to remain after the animation is complete, but now the cell disappears when I tap on it. I don't understand what is going on here. Can someone please help?
Here is my code. Note, I have removed what I believe to be irrelevant to this problem to make the code easier to read.
class PokerLogSelectionView: UIViewController {
let logSelectionTableViewController = LogSelectionTableViewController()
let logSelectionTableView = UITableView()
// Irrelevant class variables removed
init(btn : PokerLogSelectionButton){
// Irrelevant view initialization code removed
// Display the subviews
self.displayLogListScrollView()
}
func displayLogListScrollView() {
// Frame is set to (0,0,0,0)
let frame = CGRect(x: self.subviewClosed, y: self.subviewClosed, width: self.subviewClosed, height: self.subviewClosed)
logSelectionTableView.delegate = self.logSelectionTableViewController
logSelectionTableView.dataSource = self.logSelectionTableViewController
// Set the frame of the table view
logSelectionTableView.frame = frame
// Give it rounded edges
logSelectionTableView.layer.cornerRadius = 10
// Remove the cell divider lines
logSelectionTableView.separatorStyle = UITableViewCellSeparatorStyle.None
logSelectionTableView.backgroundColor = logSelectionViewContentScrollViewColor
self.view.addSubview(logSelectionTableView)
//self.logSelectionTableView.reloadData()
//self.addChildViewController(logSelectionTableViewController)
}
override func viewDidAppear(animated: Bool) {
// Create animation
let timeInterval : NSTimeInterval = 0.5
let delay : NSTimeInterval = 0
UIView.animateWithDuration(timeInterval, delay: delay, options: UIViewAnimationOptions.CurveEaseOut, animations: {
// Irrelevant code removed
// Set the size and position of the view and subviews after the animation is complete
self.view.frame = CGRect(x: self.frameXopen, y: self.frameYopen, width: self.frameWopen, height: self.frameHopen)
self.logSelectionTableView.frame = CGRect(x: self.subviewXopen, y: self.svYopen, width: self.subviewWopen, height: self.svHopen)
}, completion: { finished in
self.addChildViewController(self.logSelectionTableViewController)
})
}
}
class LogSelectionTableViewController : UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.registerClass(LogSelectionCell.self, forCellReuseIdentifier: "logCell")
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return pokerLibrary.logNames.count
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 20
}
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("Selected row: \(indexPath.row)")
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if let cell : LogSelectionCell = self.tableView.dequeueReusableCellWithIdentifier("logCell") as? LogSelectionCell {
cell.selectionStyle = UITableViewCellSelectionStyle.None
cell.textLabel!.text = pokerLibrary.logNames[indexPath.row].name
return cell
}
fatalError("Could not dequeue cell of type 'LogSelectionCell'")
}
}
Note: I can see the tableview after the animation is complete. The color is different than the view in the background view and the tableview does not disappear, just the cell. I expect there to be 1 cell, and I have printed out the number of rows in section 0 and it always returns 1.
Thanks for the help!
Edit:
Here is a screenshot of the view hierarchy before the cell disappears.
Here is a screenshot of the view hierarchy after I tap the cell and it disappears.
I overrode the touchesBegan method in my custom cell and did not call its superclass method. This stopped the cell from disappearing when I tap it, but it still disappears when I scroll the tableView.