How to add animation to UIViews inside a UITableViewCell? - ios

I'm currently developing an iOS app using swift and I get some problem trying to add animations to UIViews inside a cell.
It's a menu using UITableView with sections.
Some of the cells can be expanded, by press a button in the cell.
The button is an arrow like ">" but pointing to the bottom , when pressed it should become up side down and the cell expand. Press it again the arrow point to bottom again and the cell shrink.I want to add an animation to it .
Animation
extension UIView{
func addAnimation_rotation_x (from : Double , to : Double){
let rotationAnimation = CABasicAnimation()
rotationAnimation.keyPath = "transform.rotation.x"
rotationAnimation.duration = 0.3
rotationAnimation.removedOnCompletion = false
rotationAnimation.fillMode = kCAFillModeForwards
rotationAnimation.fromValue = from
rotationAnimation.toValue = to
self.layer.addAnimation(rotationAnimation, forKey: nil)
}
}
ViewController
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if index_of_expandedRow.contains(indexPath){
let height = CGFloat(self.total_dishArray[indexPath.section][indexPath.row].expanded_rows.count) * 60 + 150
return height
}else{
return 150
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
let cell = tableView.dequeueReusableCellWithIdentifier("DishTableViewCell") as! DishTableViewCell
if isFinishedLoadingDishes{
//setting cells
cell.btn_expand.addTarget(self, action: #selector(OrderViewController.expand(_:)), forControlEvents: UIControlEvents.TouchUpInside)
if index_of_expandedRow.contains(indexPath){
cell.btn_expand.addAnimation_rotation_y(0, to: M_PI)
}else{
cell.btn_expand.addAnimation_rotation_y(M_PI, to: 0)
}
}
return cell
}
func expand (sender :UIButton){
let cell = sender.superview!.superview as! UITableViewCell
let indexpath = tableView_right.indexPathForCell(cell)!
if self.index_of_expandedRow.contains(indexpath){
let index = self.index_of_expandedRow.indexOf(indexpath)!
self.index_of_expandedRow.removeAtIndex(index)
}else{
self.index_of_expandedRow.append(indexpath)
}
tableView_right.reloadRowsAtIndexPaths([indexpath], withRowAnimation: UITableViewRowAnimation.None)
//if the expanded content is not visible then make it visible
let frame = self.tableView_right.cellForRowAtIndexPath(indexpath)!.frame
if frame.origin.y + frame.height > tableView_right.frame.height + tableView_right.contentOffset.y {
tableView_right.setContentOffset(CGPointMake(0, tableView_right.contentOffset.y + ((frame.origin.y + frame.height) - (tableView_right.frame.height + tableView_right.contentOffset.y))) ,animated: true)
}
}
I expand the rows by changing their height.
The animation worked well when hitting the button the first time , but failed after.
Any suggestion would be appreciated.

Related

PrepareForReuse() to reset UIView width not working

I am stuck at this for a while now, and while I think I know what is the issue I am still not able to solve it. I am working with tableView with each cell having countdown bar animation. Each cell has a custom time duration set by the user. Then a bar animation slides as the time elapses.
I am using CADisplayLink to get this animation running, this code is written in controller file and I am just changing the width of a UIView as the time elapses:
#objc func handleProgressAnimation() {
let currenttime = Date()
let elapsed = currenttime.timeIntervalSince(animationStartDate)
let totalWidth: Double = 400.0
print(elapsed)
let arrayLength = durationBar.count
var i: Int = 0
for _ in 0..<arrayLength {
let indexPath = IndexPath(row: i, section: 0)
let percentage = (elapsed / Double(durationBar[i].durationTime))
let newWidth = Double(totalWidth - (totalWidth * percentage))
durationBar[i].width = newWidth
if (percentage < 1)
{
if let cell = listTableView.cellForRow(at: indexPath) as! listTableViewCell? {
cell.timeRemainingView.frame.size.width = CGFloat(durationBar[indexPath.row].width)
}
}
else {
if let cell = listTableView.cellForRow(at: indexPath) as! listTableViewCell? {
cell.timeRemainingView.frame.size.width = 400
cell.timeRemainingView.isHidden = true
}
}
i = i + 1
}
}
When all the bars are decreasing, everything goes fine but as one of the cells time is completed then the UIView width becomes zero and then as the user scrolls that cell is reused and this vanishes the progress bar from other cells where there is still time.
I am using prepareforsegue() method to reset the width of the bar to 400(default value) but that doesn't seem to work.
This is cellforRow code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellid, for: indexPath) as! listTableViewCell
cell.testlabel.text = "\(duration[indexPath.row].durationTime)"
cell.timeRemainingView.frame.size.width = CGFloat(durationBar[indexPath.row].width)
return cell
}
And this is PrepareForReuse()
override func prepareForReuse() {
super.prepareForReuse()
self.timeRemainingView.frame.size.width = 400
self.timeRemainingView.isHidden = false
}
Here is a screenshot. You can see in some cells the time is up, but in others there is still time remaining but the progress bar has disappeared.
You have to set the frame again. width is a get-only property.

Guidance needed on using CADisplayLink with tableViewCells

I am creating an app that contains lists which will be valid for a certain amount of time. For that I am creating a countdown bar animation on each cell. Each cell has it's own custom duration after which the cell displays "Time's Up". The cell even shows the time in seconds as it ticks down to 0 and then displays "Time's Up" message.
Right above this, there is a countdown bar animation. I am controlling all these animations from the viewController and not doing these in the customView cell as cell reusability makes the timers go haywire.
I am using two timers:
Timer() : that invokes every 1 second. This is used for the simple seconds countdown. I am using an array of type struct that houses two variables durationTime & width. The width is set to 400 by default. The durationTime is set by the user.
#objc func handleCountdown() {
let arrayLength = duration.count
var i: Int = 0
var timeCount: Int = 0
for _ in 0..<arrayLength {
let value = duration[i].durationTime - 1 //Decreasing the timer every second
let indexPath = IndexPath(row: i, section: 0)
if (value > 0)
{
duration[i].durationTime = value
if let cell = listTableView.cellForRow(at: indexPath) as! listTableViewCell? {
cell.testlabel.text = "\(value)"
}
//listTableView.reloadRows(at: [indexPath], with: .automatic)
i = i + 1
} else {
if let cell = listTableView.cellForRow(at: indexPath) as! listTableViewCell? {
cell.testlabel.text = "Time's Up"
}
i = i + 1
timeCount = timeCount + 1
}
}
if (timeCount == (arrayLength - 1)) {
self.timer?.invalidate()
}
}
The above code is invoked every second. It decrements the value of the time, and then displays it in the tableview cell. The above code works fine as it should.
CADisplayLink: this timer is used to run the progress bar animation. In this case however, I am calculating the elapsed time and dividing it by the durationTime of each element and calculating a percentage. This percentage is used to update the value of width of each progress bar. (Keep in mind that durationBar is simply duplicate of duration array. In this array however, the durationTime is not being decremented -> Just for testing purposes) Here is the code:
#objc func handleProgressAnimation() {
let currenttime = Date()
let elapsed = currenttime.timeIntervalSince(animationStartDate)
let totalWidth: Double = 400.0
print(elapsed)
let arrayLength = duration.count
var i: Int = 0
for _ in 0..<arrayLength {
let indexPath = IndexPath(row: i, section: 0)
let percentage = (elapsed / Double(durationBar[i].durationTime))
let newWidth = Double(totalWidth - (totalWidth * percentage))
durationBar[i].width = newWidth
if let cell = listTableView.cellForRow(at: indexPath) as! listTableViewCell? {
cell.timeRemainingView.frame.size.width = CGFloat(durationBar[indexPath.row].width)
}
i = i + 1
}
}
Question: After some time, some progress bars just disappear from the cells even though the time has not been completed. It occurs most often once I have done some scrolling on the tableview.
This is willDisplayCell & cellForRowIndexPath:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellid, for: indexPath) as! listTableViewCell
return cell
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let cell:listTableViewCell = cell as! listTableViewCell
cell.testlabel.text = "\(duration[indexPath.row].durationTime)"
cell.timeRemainingView.frame.size.width = CGFloat(duration[indexPath.row].width)
}
As you can in pictures below, after sometime, some progress bars vanished even though there is still some time left:
What is the issue here, I am controlling all the animations and timers from the viewController and not the cell in order to prevent cell reuseability to become a problem. Help is needed!
The thing is that you update cell.timeRemainingView.frame.size.width with
durationBar[indexPath.row].width for handleProgressAnimation and
duration[indexPath.row].width for willDisplayCell.
And also I would switch to tableView.indexPathsForVisibleRows for updating UI only for visible cells so it wouldn't call .cellForRow for every cell in if-let.

IOS Swift how can I highlight a cell on tap with TableView recycling

I have a TableView and a label inside of it called Post. I have a Gesture Tap function and when a user taps that label then the TableView label that was tapped changes color. The issue is with the tableView recycling if I go down the tableView then other cells are also highlighted that were not clicked . How can I fix it so that only the clicked cell is highlighted ? This is my code
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UserProfileTVC", for: indexPath) as! UserProfileTVC
cell.post.text = Posts[indexPath.row]
let post_tap = UITapGestureRecognizer(target: self, action: #selector(UserProfileC.post_tapped(sender:)))
cell.post.addGestureRecognizer(post_tap)
return cell
}
func post_tapped(sender:UITapGestureRecognizer) {
let point = CGPoint(x: 0, y: 0)
let position: CGPoint = sender.view!.convert(point, to: self.TableSource)
if self.TableSource.indexPathForRow(at: position) != nil {
sender.view?.backgroundColor = UIColor.blue
}
}
Again the code works and it highlights the correct TableCell label the issue is when scrolling down the tableView other tableCells label also get highlighted without being clicked .
I have updated the code with the sample given below but it is still giving the same results
if let existingRecognizerView = cell.viewWithTag(101) as UIView? {
existingRecognizerView.backgroundColor = UIColor.white
} else {
let post_tap = UITapGestureRecognizer(target: self, action: #selector(UserProfileC.post_tapped(sender:)))
post_tap.view?.tag = 101
cell.post.addGestureRecognizer(post_tap)
}
In your cellForRowAtIndexPath function you are dequeuing a cell that already has a UITapGestureRecognizer on it with a blue background. You will need to add a tag to its view so that you can get access to it when dequeuing and remove the background color.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UserProfileTVC", for: indexPath) as! UserProfileTVC
cell.post.text = Posts[indexPath.row]
// Check if we already have a GestureRecognizer view
if let existingRecognizerView = cell.post.viewWithTag(101) {
existingRecognizerView.backgroundColor = UIColor.white
} else {
let post_tap = UITapGestureRecognizer(target: self, action: #selector(UserProfileC.post_tapped(sender:)))
cell.post.addGestureRecognizer(post_tap)
post_tap.view.tag = 101
}
return cell
}
func post_tapped(sender:UITapGestureRecognizer) {
let point = CGPoint(x: 0, y: 0)
let position: CGPoint = sender.view!.convert(point, to: self.TableSource)
if self.TableSource.indexPathForRow(at: position) != nil {
sender.view?.backgroundColor = UIColor.blue
}
}
*Note: coded from a mobile device... not tested for syntactical or functional errors.

updating tableViewCell's height and moving tableView at the same time

i'm moving up my tableView (via setting contentOffset) and changing a specific (row no. 1 here ) cell's height at the same time , thats why i'm getting a shaky animation and also tableView restores itself to the default position (which is not desired behavior )
my code :
func keyboardWillShow(notification:NSNotification) {
let userInfo:NSDictionary = notification.userInfo!
let keyboardFrame:NSValue = userInfo.valueForKey(UIKeyboardFrameEndUserInfoKey) as! NSValue
let keyboardRectangle = keyboardFrame.CGRectValue()
let duration = (notification.userInfo![UIKeyboardAnimationDurationUserInfoKey] as! Double)
keyboardHeight = keyboardRectangle.height
accurateheight = accurateheight - (keyboardHeight + inputToolbar.frame.size.height) // accurateHeight is the desired CGFloat value that i want
self.tableView.beginUpdates()
self.tableView.endUpdates()
let frame = tableView.rectForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0))
defaultRowHeight = frame.height
let cgpoint = CGPointMake(0, (CGFloat(-64 + defaultRowHeight)))
self.tableView.setContentOffset(cgpoint, animated: true) // here i'm moving up my tableView
}
in my heightForRowAtIndexPath:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.row == 0 {
return 44
}else {
return accurate height // cell's height which i'm changing
}
}
i also tried to update only specific row (the ones that i'm changing height of) like this :
var indexPath = NSIndexPath(forRow: 1, inSection: 0)
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Top)
but the above didnt worked also sometimes my keyboard was not popping up.
how can i move (scroll up) my tableView and change a cell's height at the same time ?? any clue ?

Fade in UITableViewCell row by row in Swift

I am new to swift, I am trying to have a UITableView and the cells will be animated to appear one by one. How can I do that? Also, if the newly appeared row of cell not on the screen (hiding below the table). How can I move the table up when each cell appear?
var tableData1: [String] = ["1", "2", "3", "4", "5", "6", "7"]
override func viewWillAppear(animated: Bool) {
tableView.scrollEnabled=false
tableView.alpha=0.0
NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(3), target: self, selector: "animateTable", userInfo: nil, repeats: false)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableData1.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:TblCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! TblCell
cell.lblCarName.textAlignment = NSTextAlignment.Justified;
return cell
}
func animateTable() {
//what should be the code?//
}
Step-1
In your cellForRowAtIndexPath method where initialize your cell, hide it like that;
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TblCell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TblCell
cell.continueLabel.textAlignment = .justified
cell.contentView.alpha = 0
return cell
}
Step-2
Let's make fade animation. UITableViewDelegate has willDisplayCell method which is able to detect that when you scroll to top or bottom, first cell will display on the window.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
UIView.animate(withDuration: 0.8, animations: {
cell.contentView.alpha = 1
})
}
Your fade animation is on the progress. The most important thing is that you can not setup your cell's alpha directly in runtime because iOS is doing some special internal handling with the cell as part of your UITableView and ignores your setup. So if you setup your cell's contentView, everything's gonna be fine.
In the UITableView, the rows are prepared automatically for you when they get to be in your "range vision". I'm assuming you are, at least not initially, being able to scroll the tableView, so we would scroll it programmatically making the rows appear as it goes. How are we doing that? Actually UITableView has a method that let us scroll it to a specific row:
scrollToRowAtIndexPath(indexPath : NSIndexPath, atScrollPosition : UITableViewScrollPosition, animated : Bool);
I'm too tired to write the code, so I'm going to try to explain. First, for every cell you set alpha to 0, as soon as they get loaded (cellForRowAtIndexPath).
Then let's suppose our screen fits the 5 first rows. You are going to animate their alphas sequentially (using UIView.animateWithDuration ) until the fifth one (index 4), then you are going to scrollToRowAtIndexPath passing NSIndexPath using 5, then 6,... until the rest of them (using scrollPosition = .Bottom). And for each of them, you would animate as soon as they get loaded. Just remember to put some time between this interactions. (animate first, run NSTimer to the second, and it goes on). And the boolean animated should be true of course.
To appear each visible cell one by one, you can do it by playing with alpha and duration value.
extension UITableView {
func fadeVisibleCells() {
var delayDuration: TimeInterval = 0.0
for cell in visibleCells {
cell.alpha = 0.0
UIView.animate(withDuration: delayDuration) {
cell.alpha = 1.0
}
delayCounter += 0.30
}
}
}
Here is some code which can get you started. In my COBezierTableView
I subclassed UITableView and override the layoutSubviewsmethod. In there you can manipulate the cells according to their relative position in the view. In this example I fade them out in the bottom.
import UIKit
public class MyCustomTableView: UITableView {
// MARK: - Layout
public override func layoutSubviews() {
super.layoutSubviews()
let indexpaths = indexPathsForVisibleRows!
let totalVisibleCells = indexpaths.count - 1
if totalVisibleCells <= 0 { return }
for index in 0...totalVisibleCells {
let indexPath = indexpaths[index]
if let cell = cellForRowAtIndexPath(indexPath) {
if let superView = superview {
let point = convertPoint(cell.frame.origin, toView:superView)
let pointScale = point.y / CGFloat(superView.bounds.size.height)
cell.contentView.alpha = 1 - pointScale
}
}
}
}
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell .alpha = 1.0
let transform = CATransform3DTranslate(CATransform3DIdentity, 0, 3000, 1200)
//let transform = CATransform3DTranslate(CATransform3DIdentity, 250, 0, 1250)
//let transform = CATransform3DTranslate(CATransform3DIdentity, 250, 1250, 0)
// let transform = CATransform3DTranslate(CATransform3DIdentity, -250, 300, 120)
cell.layer.transform = transform
UIView.animate(withDuration: 1.0) {
cell.alpha = 1.0
cell.layer.transform = CATransform3DIdentity
cell.layer.transform = CATransform3DIdentity
}
}
I think this method will be a great solution.
Set all cell alpha to 0
animate them in tableView(_:,willDisplay:,forRowAt:)
set a delay for every indexPath.row
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0.05 * Double(indexPath.row), animations: {
cell.alpha = 1
})
}

Resources