PrepareForReuse() to reset UIView width not working - ios

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.

Related

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.

How to add animation to UIViews inside a UITableViewCell?

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.

Prevent tableview cell recycle label value

I have a tableview with custom cell that contain 1 label and 1 button. The button is use for everytime I tap it, increase the number inside label, example if my label value is 0 then everytime I tap the button it is increase by 1
The problem is I have a lot of data, example I have 20 and everytime I set a value for the label when I scroll all the value is changed
this is my code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("testCell", forIndexPath: indexPath)
let pos = cell.viewWithTag(6) as! UIButton
pos.addTarget(self, action: #selector(testTableViewController.pos(_:)), forControlEvents: .TouchUpInside)
pos.tag = indexPath.row
return cell
}
func pos(sender: UIButton) {
let position: CGPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(position)
let cell: UITableViewCell = self.tableView.cellForRowAtIndexPath(indexPath!)! as UITableViewCell
let countNo = cell.viewWithTag(7) as! UILabel
var counter:Int = Int(countNo.text!)!
counter = counter + 1
countNo.text = String(counter)
}
please help me how to solve this problem. I have search a lot of web to find the answer, example like make dequeueReusableCellWithIdentifier nil but in Swift 2 dequeueReusableCellWithIdentifier never nil and a lot more with no one is work
I need some hint how to solve this problem
Here is your answer.
Initialise one array having number of objects same as your tableview's number of row. Initially insert value "0" or blank string (if do not want to display any number in label initially) for each object.
ex,
var arrValues:NSMutableArray = NSMutableArray()
for _ in 1...numberOfrow
{
arrValues.addObject("0")
}
Now in cellForRowAtIndexPath method , display text of label from this array
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("testCell", forIndexPath: indexPath)
let pos = cell.viewWithTag(6) as! UIButton
**let countNo = cell.viewWithTag(7) as! UILabel
countNo.text = String(arrValues.objectAtIndex(indexPath.row))**
pos.addTarget(self, action: #selector(testTableViewController.pos(_:)), forControlEvents: .TouchUpInside)
pos.tag = indexPath.row
return cell
}
On button click method replace object at particular index with incremented number. See your updated method as below.
func pos(sender: UIButton) {
let position: CGPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(position)
let cell: UITableViewCell = self.tableView.cellForRowAtIndexPath(indexPath!)! as UITableViewCell
let countNo = cell.viewWithTag(7) as! UILabel
var counter:Int = Int(countNo.text!)!
counter = counter + 1
countNo.text = String(counter)
**arrValues.replaceObjectAtIndex(indexPath.row, withObject: String(counter))**
}
So in cellForRowAtIndexPath , it will display updated number in label text

PrepareForReuse in CollectionViewCell

I want to hide the label in a cell that was tapped and instead show an image. But I want to do this only if a cell with a certain index has already been set to the imageView.
What is the best way to address the cells and store if they are set to imageView or not? How do I use the prepareForReuse method?
This is the way I do it until now, but as the cells are reused. The image is shown in other cells at scrolling.
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
println("user tapped on door number \(indexPath.row)")
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! MyCollectionViewCell
if (cell.myLabel.text == "1") {
one = true
if(seven = true) {
if (cell.myLabel.hidden) {
cell.myLabel.hidden = false
cell.MyImageView.image = nil
}
else {
cell.myLabel.hidden = true
cell.MyImageView.image = UIImage(named:"1")!
}
}
}
You didn't say if your collection view has exactly 7 cells or if it can have "N" (e.g. 100) cells in the collection, so if this were my problem and I had to solve it, I would make the state of your "seven" cell a property of the class (e.g. "var sevenState : Bool") and then I could display the button or image of other cells depending on what sevenState is.
In my app I have to configure a UICollectionReusableView based on the index path, if the indexPath has a particular value then I send an array which is used to set labels and images.
I use a function in the custom UICollectionReusableView, if I call it with an array it populates the labels and images and if I call it with nil it resets these.
func collectionView(collectionView: UICollectionView!, viewForSupplementaryElementOfKind kind: String!, atIndexPath indexPath: NSIndexPath!) -> UICollectionReusableView! {
.... [logic around selecting index path based on data returned]
....
if filteredEvents != nil{
reusableView.populateCalendarDayDates(sortedEvents)
}else{
reusableView.populateCalendarDayDates(nil)
}
In the function in the custom UICollectionReusableView I reset labels back to default values before possibly updating them :
func populateCalendarDayDates(arrayEvents: NSArray?){
let firstDayTag = tagStartDay()
var dayDate = 1
for var y = 1; y < 43; y++ {
let label = self.viewWithTag(y) as! BGSCalendarMonthLabel
label.delegate = callingCVC
label.backgroundColor = UIColor.clearColor()
label.textColor = UIColor.whiteColor()
label.text = ""
You can get the same effect, and it is probably a bit more readable, by moving this code to prepareForReuse in the custom UICollectionReusableView :
override func prepareForReuse() {
super.prepareForReuse()
for var y = 1; y < 43; y++ {
let label = self.viewWithTag(y) as! BGSCalendarMonthLabel
label.backgroundColor = UIColor.clearColor()
label.textColor = UIColor.whiteColor()
label.text = ""
}
}
Hope that helps.

Retaining button content in UICollectionView after scrolling with swift

I'm trying to make a UICollectionView that has infinite scrolling of buttons and the button's background is populated base on the result of http request to a server.
let reuseIdentifier = "Cell"
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
let categoryApiUrl = "url"
let categoryImageField = "field"
class BrowseViewController: UICollectionViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var categoryImgUrl:[String] = []
var buttonList:[UIButton] = []
func setupView(){
self.title = "Browse"
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)
layout.itemSize = CGSize(width: screenWidth/2-15, height: screenHeight/3.5)
collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
collectionView!.dataSource = self
collectionView!.delegate = self
collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
collectionView!.backgroundColor = UIColor.whiteColor()
self.view.addSubview(collectionView!)
}
func setupButton(cell: UICollectionViewCell, cellNumber: Int){
var button = UIButton.buttonWithType(UIButtonType.System) as UIButton
button.frame = CGRectMake(0, 0, screenWidth/2-15, screenHeight/3.5)
button.backgroundColor = UIColor.orangeColor()
button.setTitle("Category", forState: UIControlState.Normal)
button.addTarget(self, action: "btnClicked:", forControlEvents: UIControlEvents.TouchUpInside)
buttonList.append(button)
cell.addSubview(button)
}
override func viewDidLoad(){
super.viewDidLoad()
let url = NSURL(string: categoryApiUrl)
let request = NSURLRequest(URL: url!)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {(response, dataValue, error) in
let json = JSON(data: dataValue)
for(var i = 0; i < json.count; i++){
self.categoryImgUrl.append(json[i]["CATEGORY_IMAGE"].stringValue)
let imageUrl = self.categoryImgUrl[i]
let url = NSURL(string: imageUrl)
let data = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
self.buttonList[i].setBackgroundImage(image, forState: .Normal)
}
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
//#warning Incomplete method implementation -- Return the number of sections
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//#warning Incomplete method implementation -- Return the number of items in the section
return 10;
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as UICollectionViewCell
let cellNumber = indexPath.row as Int
setupButton(cell, cellNumber: cellNumber)
// Configure the cell
return cell
}
override func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.size.height {
numberOfItemsPerSection += 6
self.collectionView!.reloadData()
}
}
}
Currently, the code is able to pull the image from the server and populate it as the button's background image.
However, since I made this collection view scrollable. When I scroll the view down and then back up, the background image of the previous buttons disappear.
I did some research but couldn't find a solution to it. The reason that the button disappears is because IOS only loads the cell that is visible on screen. So when I scroll down and then scroll back up, the previous cells are consider as "New Cells". Therefore the background image that was in it are now gone.
Questions:
Does anyone have an idea on how to retain the previous buttons even if we scroll down and then scroll back up? In addition, with my current code, I added the image onto the button inside the http request because the http request is always the last execution that finishes. Is there anyway to change the code so then the http request will be finish before the cells get loaded?
I would suggest to create uicollectionview in interface builder, subclass uicollectionviewcell, add one dynamic cell to collectionview, change its class to collectionviewcell you subclassed, drop uibutton on it, and everytime cell is being created, you would download the image.
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as myCollectionViewCell
let cellNumber = indexPath.row as Int
//downloadimghere
cell.myButton.setBackgroundImage(downloadedImg, forState: .Normal)
return cell
}
This would download image everytime cell is being created. For more info you should checkout "lazy image loading". I think this is a better approach to your problem.
Now to your code, first of all you are not using your buttonList array, everytime cell is being created you create a new button and place it there, so you are not reusing already created buttons. If you fixed this, it might work like you wanted.
Here is another problem, since collectionview is reusing cells, everytime you create a button and place it on cell, it stays there, so basically now you are creating button on button. So if you want this to work correctly and have only one button on your cell, you need to remove previous button from the cell before you create it, you can do this in cellForItemAtIndexPath.
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as myCollectionViewCell
//something like this
for view in cell.subviews(){
if view == <UIButton>{
view.removeFromSuperview()
}
}
return cell
}
There might be some syntax errors in my code, I didnt test it, but you get the idea how to do it.

Resources