I'm trying to create like button in tableview cell for my project. If like button clicked it must change tint color to red. I'm using Alamofire and if user liked it returns wich feed is liked and in cell:
let likerHash = data[indexPath.row]["liker_hash"] as? String
if(likerHash == ""){
cell.likeButton.tintColor = UIColor.blackColor()
}else{
cell.likeButton.tintColor = UIColor.redColor()
}
will set color of buttons for feeds. But if i click one button to like if it's not liked it changes color and iwhen i scroll down and come back again button color will change again to previous. (if on loading data it is not liked it will keep color when i scroll down.) I have tride to change value of liker_hash but it gives me an error: mutating method sent to immutable object. I am tying to change value like:
self.data[sender.tag]["liker_hash"] = ""
My data is from type [NSMutableDictionary](). Any idea how can i do it in swift language?
The main feature with using dequeueReusableCellWithIdentifier is that the cell is recreated every time you come back to it.
Based on the code that you've added, I don't believe you're editing your data source when the user hits the like button to save which cell has been changed the next time the tableView reloads.
What I'd suggest is holding all the cells in an array. Supposed you have 5 cells, create an array holding their current state:
var cellArr = ["Black", "Black", "Black", "Black", "Black"]
Then, if the user selects the like button, make sure to update this array with the right color as well. So if I select the second row, I update it like:
var cellArr = ["Black", "Red", "Black", "Black", "Black"]
And then in your cellForRowAtIndexPath function:
if cellArr[indexPath.row] == "Black"{
cell.likeButton.tintColor = UIColor.blackColor() }
else {
cell.likeButton.tintColor = UIColor.redColor()
}
Related
I'm building an app which which has built in with 2 different tabs. First tab is is "Home" which basically has a tableview with cells that configured from an api.(The api gets me country names for now)
Those cells also have a "Star" button which prints the data of the specific cell for now.
Second tab is "Saved" tab(SavedViewController), where I want to show the "starred" countries, using a tableview.
You can see the image below in order to get an idea for the app.
App simulation Image
The star button has a function in my CountriesTableViewCell. I'm using a saveButtonDelegate in order to let the SavedViewController know about an item is going to be saved. The code in CountriesTableViewCell for star button is as below.
#objc func buttonTapped() {
//If Button is selected fill the image. Else unfill it.
if !isSaveButtonSelected {
saveButton.setImage(UIImage(systemName: "star.fill"), for: .normal)
isSaveButtonSelected = true
saveButtonDelegate?.saveButtonClicked(with: countryData) //Checking if save button is clicked
}
}
countryData is the data that I get from the api, and this is the data I want to pass to SavedViewController.
struct CountryData: Codable {
let name : String
}
So on the SavedViewController, I'm handling the data using the SaveButtonProtocol conformance as below:
extension SavedViewController: SaveButtonProtocol {
func saveButtonClicked(with data: CountryData) {
countryDataArray.append(data)
print("saveButtonClicked")
print("countryData in savevc is \(countryDataArray)")
DispatchQueue.main.async {
self.countriesTableView.reloadData()
}
}
}
Whenever I click the star button on the first tab, this function is getting called on SavedViewController. So whenever I click to button, those print statements above work fine.
The problem is, whenever the star button is clicked, it should append the data of the current clicked cell to countryDataArray in SavedViewController. But the array is not populating as it should.
Let's say I pressed the first cell's star button, my print("countryData in savevc is (countryDataArray)") statement prints : ["Vatican City"], then I press the second cell's star button it only prints ["Ethiopia"] while it should print ["Vatican City", "Ethiopia"]
Why this issue is happening? My best guess is I'm delegating SavedViewController from the cell class so it behaves differently for every other cell. If that is the problem what should I do to solve it?
Many Thanks.
You should store your data in a shared (static array) object so you only have one source and add the saved indicator in your country struct so you do not rely on what is displayed in one view controller.
I've got a collection view and I'm using a custom class for the cells. Each cell has two text view, chapterTitleTextView and chapterBodyTextView. I've added placeholders to both text views like so:
class CustomWriterPageCell: UICollectionViewCell, UITextViewDelegate {
// When the user taps on a text view
func textViewDidBeginEditing(_ textView: UITextView) {
if textView.textColor == .gray {
textView.text = nil
textView.textColor = .black
}
}
// When the user taps out of a text view or taps on the done button in the toolbar
func textViewDidEndEditing(_ textView: UITextView) {
// If the chapter title text view is empty
if chapterTitleTextView.text.isEmpty {
chapterTitleTextView.text = "Chapter Title"
chapterTitleTextView.textColor = .gray
}
// If the chapter body text view is empty
if chapterBodyTextView.text.isEmpty {
chapterBodyTextView.text = "Chapter Body"
chapterBodyTextView.textColor = .gray
}
}
}
How this works is that, the text color is initially gray and there is some text, when the user taps on the text view, the color changes to black and the text in the text view is removed.
Now there is this problem with using dequeueReusableCell, it’s that it reuses the cells, this caused problem number 1: Whatever I type on the text view in the first cell appears on the 4th cell, to solve this problem I had to create 2 global lists to hold whatever I type and this gets displayed in the text views of the cells, here’s the code:
Global Lists:
var chapterTitleText = Array(repeating: "", count: 4) // To store the chapter title text
var chapterBodyText = Array(repeating: "", count: 4) // To store the chapter body text
The following code snippet is inside the textViewDidEndEditing from earlier
// Append the chapter body text to the chapterBodyText array
let titleText = chapterTitleTextView.text
let titleRow = textView.tag //This the indexPath.row
chapterTitleText[titleRow] = titleText!
// Append the chapter title text to the chapterTitleText array
let bodyText = chapterBodyTextView.text
let bodyRow = textView.tag
chapterBodyText[bodyRow] = bodyText!
And in cellForItemAt:
cell.chapterBodyTextView.tag = indexPath.row
cell.chapterTitleTextView.tag = indexPath.row
cell.chapterTitleTextView.text = chapterTitleText[indexPath.row]
cell.chapterBodyTextView.text = chapterBodyText[indexPath.row]
This got rid of problem number 1 (text in the text views duplicating). But then I got a new problem, remember the placeholder text I was talking about? When I type something in the one of the textviews of the first cell, the text color of the text view in the fourth cell changes.
Here is a GIF replicating the problem:
Following your chain of questions, I suggest you to do this every time you're dealing with UICollectionView or UITableView.
Define a method in your cell class and take whatever data it needs to display itself as arguments:
func configure(text : String?) { //text is optional here which means it can be nil.
//However, from my previous answer you can replace it with an empty string condition.
if let txt = text { //or if text != ""
self.chapterTitleTextView.text = txt
self.chapterTitleTextView.text.textColor = .black
}
else {// As others mentioned make sure to always handle else because cells are reusable.
self.chapterTitleTextView.text = "Chapter Title"
self.chapterTitleTextView.text.textColor = .gray
}
}
The intuition behind reusable cells are that since they are reusable, you should reconfigure them completely and not to expect that the configuration is saved or attached to the cell.
Now, in the cellForItemAt:
let cell = ...
cell.configure(text : chapterTitleText[indexPath.row])
And remember, in this way, you do not need to define a global array. As I've told you previously, this array need to be only defined in your Controller and your cell does not need to know about it. Your cell only needs to know about one index of that array which is passed through configure function.
Although that global array will work, I'm talking about propriety in coding.
Comment your problems with this approach(if any), I will try to answer with patience but don't expect a (copy-paste)able code.
This happens because of the reuse mechanism of the UICollectionView and UITableView. Since you only update the color in one way, collection "remembers" previous color and upon new cell recreates its previous state. You have two basic solutions here.
First is to update cell state for both cases like:
if myCondition == true {
color = UIColor.gray
} else {
color = UIColor.black
}
Second is to utilize prepareForReuse method of the UICollectionViewCell
func prepareForReuse() {
// code that resets state of your cell to default
}
Method description in Apple Documentation
I have four imageview contents in an XIB and a button that covers all my XIB. I want to make when the user tap the button, the first imageview is shown, the next tap is hidden and the second imageview is displayed and so on until all my imageview is shown / hidden. What would be the most efficient way to do it?
Save all your UIImageViews to an array, and current showing imageView to a variable, it may look like this:
var imageViews: [UIImageView] = []
var currentImageViewIndex = 0 {
didSet {
if currentImageViewIndex >= imageViews.count { currentImageViewIndex = 0 }
imageViews[oldValue].isHidden = true
imageViews[currentImageViewIndex].isHidden = false
}
}
func handleTap() {
currentImageViewIndex += 1
}
I suggest you use a state variable that contains an enum listing the various states (firstImageVisible, secondImage.... ) then you can have a function inside the enum that switches to the nextState (being the target of your button action) you can also easily iterate through states of an enum, check the documentation for the CaseIterable protocol. Often having a property observer (didSet) on the state is a handy place to update other parts of the UI which need to change every time the state changes.
Thanks for your attention.
I have an app that uses a UITableView as a timeline that shows certain events, The Cell prototype was a little complex because i use multiple labels, a button and some imageViews that automatically change in function of the content of other fields.
In that cell, I have a UILabel, this UILabel can have 140 characters or 4 line jumps, if the text inside the label have more line jumps (\n) or are longer that 140 chars, I take a fragment and only display that fragment and add the text "... READ MORE"; when the user taps on the text, the label change and shows all the text, and at the end, appends the label "READ LESS", If the user taps again the label, it return to the initial state showing the fragment and the label "READ MORE" and so.
When I test this, it works on a device with iOS9, but in devices with iOS 10 (including simulators) It stops to work; it appears that when I Tap the label, the label changes as usual, but immediately returns to their original form. I register only one tap.
Have an idea?, Here is my code that is called to update the cell when the user taps the text label:
func cellTextPressed(gesture: UITapGestureRecognizer){
let cell: TimeLineViewCell = gesture.view?.superview?.superview as! TimeLineViewCell
let tappedIndexPath: NSIndexPath = self.timelineTableView.indexPathForCell(cell)!
NSLog ("Text Tapped at: \(tappedIndexPath)")
if ((cell.isReasonExpanded) == true)
{
cell.isReasonExpanded = false
let attrs = [NSForegroundColorAttributeName: UIColor.magentaColor()]
let attributedReducedText = NSMutableAttributedString(string: cell.reducedReason)
attributedReducedText.appendAttributedString(NSAttributedString(string: "... "))
attributedReducedText.appendAttributedString(NSAttributedString(string: "READ MORE", attributes: attrs))
cell.labelReason.attributedText = attributedReducedText
}
else
{
cell.isReasonExpanded = true
let attrs = [NSForegroundColorAttributeName: UIColor.magentaColor()]
let attributedRealText = NSMutableAttributedString(string: cell.realReason)
attributedRealText.appendAttributedString(NSAttributedString(string: " "))
attributedRealText.appendAttributedString(NSAttributedString(string: "READ LESS", attributes: attrs))
cell.labelReason.attributedText = attributedRealText
}
let lastScrollOffset = self.timelineTableView.contentOffset
UIView.performWithoutAnimation
{
self.timelineTableView.beginUpdates()
self.timelineTableView.endUpdates()
self.timelineTableView.layer.removeAllAnimations()
self.timelineTableView.setContentOffset(lastScrollOffset, animated: false)
}
}
I already have a method to avoid this.In IOS 9 and older versions, there are no problem with that code; but in iOS 10 there is different.
In documentation, apple says that the delegate method tableView cellForRowAtIndexPath is called everytime when a cell is displayed on screen, for that cell. In the older versions of IOS, it appears to be a bug that make this situation don't work, let us modify the view outside of this method whitout callback cellForRowAtIndexPath after refresh the cell.
I Added an array of Bool values that represents the status of the labels (If it have extended or shrinked value of the label) and initialize it on the ItemsDownload Method of my JSON receiver method
I add a condition that evaluates the value in that stateArray in cellForRowAtIndexPath() and put the full or the fragment of the text as corresponds.
finally, in the method posted above, added the change of the value at index path to the status Array instead to change all in that method
I have a UITableView with a custom UITalbeCellView. Every cell has a title a body and a date. When I fetch the data some of the titles must be red so there is a Bool that is set to true for those cells which title has to be red.
The first time the data is fetched it looks fine, but as you scroll up and down a few times all of the titles end up being red.
Im using an array of structures to store the data and on the cellForRowAtIndexPath there is an if and if the boolean in that position of the array is true I change the color to red:
let cell = tableView.dequeueReusableCellWithIdentifier("CommentsRowTVC", forIndexPath: indexPath) as! CommentsRowTVC
let single_comment = self.AOS[indexPath.row]
cell.titulo_comentario?.text = single_comment.titulo_comment
if single_comment.flag == true {
cell.titulo_comentario.textColor = UIColor.redColor()
}
Im gessing it reuses Views or something like that. Does this have any easy fix? Or do I have to implement my own TableView to prevent this from happening?
Im also thinking the method dequeueReusableCellWithIdentifier might be the one causing my problem... but Im new on swift and cant think of any replacement for that.
It is happening similar thing on buttons there are on the cells, when you click a button from a cell it changes its color and disables it, but just the one on that cell. However when clicking on a cell, buttons from other cells are also suffering those changes.
Since cells get reused you end up with cells that were previously red because they hadsingle_comment.flag == true and are now used for another row where there actually is single_comment.flag == false. You have to reset the color in the else branch again:
if single_comment.flag == true {
cell.titulo_comentario.textColor = UIColor.redColor()
} else {
cell.titulo_comentario.textColor = UIColor.whiteColor()
}
UITableViews reuse their cells, meaning that you will get cells inside your cellForRowAtIndexPath which have previously been used before, maybe have have set the textColor to red. Your job is it now to revert the color of the text to its original state.
In general whatever change you make inside the if-branch of cell-customization has to be undone in the else-branch. If you remove a label in the if, add it in the else.