My CustomTableViewCell's read more/less function crashes - ios

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

Related

How do you access a textfield which does not have a label or static text in XCTest?

so I've recently started testing an iOS app with xctest. I'm on a time model view where I would like to change the number in a cell. The number in this cell is the number of days after which the selected time model repeats itself. But I'm unable to access this textfield and change the number as it does not have a label / name / static text. When I record a tap on this field, Xcode gives me a strange element hierarchy which I've defined as the parameter 'onDay' below
func testRepetitionTypeMonthsOnDay() throws {
let app = XCUIApplication()
let tablesQuery = app.tables
let cellsQuery = tablesQuery.cells
XCTAssertTrue(app.navigationBars["Zeitmodelle"].waitForExistence(timeout: standardTimeout)) //wait for the time model view to open
app.staticTexts["Wiederholend"].firstMatch.tap() //tapping on a cell
XCTAssertTrue(app.navigationBars["Wiederholend"].waitForExistence(timeout: standardTimeout)) // wait for the cell to open
let repetitionType = app.tables.cells["Am, Am Tag, Expand"] //cell 1 from screenshot
let onDay = tablesQuery.children(matching: .cell).element(boundBy: 7).children(matching: .other).element(boundBy: 1).children(matching: .other).element.children(matching: .textField).element //cell 2
let endDate = app.tables.cells["Endet, Endet nicht, Expand"] // cell 3 from screenshot
onDay.tap()
onDay.clearAndEnterText("5")
}
However, Xcode cannot find the parameter onDay that itself has generated in the previous step, tap on it and enter a new text. I've attached a screenshot of the app with this cell here. The cells above and below the marked cell can be identified easily and the assertions for their existence work. The marked cell, however, is a different matter as it does not have a label / static text. Do you have an idea how I can get around this? Thanks a lot!
So I've finally found a way to access this element. Instead of going into the cell view I've accessed the textfields array from tables view.
func testRepetitionTypeMonthsOnDay() throws {
let app = XCUIApplication()
let tablesQuery = app.tables
XCTAssertTrue(app.navigationBars["Zeitmodelle"].waitForExistence(timeout: standardTimeout)) //wait for the time model view to open
app.staticTexts["Wiederholend"].firstMatch.tap() //tapping on a cell
XCTAssertTrue(app.navigationBars["Wiederholend"].waitForExistence(timeout: standardTimeout)) // wait for the cell to open
let onDay = tablesQuery.textFields.element(boundBy: 3)
onDay.tap()
onDay.clearAndEnterText("5")
}
Because the app had a mixture of picker wheels and textfields in the same view, it was a bit problematic for Xcode to find the element I wanted from a general element index. But the use of the textfields array filtered the index list down to only textfields. Xcode was then able to identify the element without any conflict and perform the test. I hope this helps anyone having the same problem.

dequeueReusableCell causes text to change color

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

Printing random string from an array on textview

I can't seem to get my array printed onto my textview. I have an array and have set my textview (texthold) to the randomIndex of the array.. Anything you guys see I'm doing wrong?
This is my button that when clicked is supposed to get a random string from the devices array and print it into the textview field. I am getting no errors just nothing displays onto the textview when the button is clicked
#IBAction func Generate(_ sender: Any) {
let devices = ["Apple Watch", "iPhone", "iPad", "Mac", "Apple TV"]
// Generate a random index
let randomIndex = Int(arc4random_uniform(UInt32(devices.count)))
// Get a random item
let randomItem = devices[randomIndex]
texthold.text = devices[randomIndex]
}
This code works in my program. Check your layout using "Debug view hierarchy tool", maybe it will help.
And:
texthold.text = randomItem
Name methods with lowercase (Generate -> generate)
Check if your button touch calls this method
Check if your UI items have connected outlets.
There may be problem with changing text from background thread, but if you really have this method connected from storyboard/xib as method for outlet's action - this situation impossible.
And check that your code is being called on .touchUpInside action of your button (second screenshot in storyboard)!

Changing button color in cell if clicked

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()
}

Swift 2: Call a method after pressing Return in UITextField

I have a Text Field in a Tip Calculator. I want the Tip Calculator to update all the fields of my calculator.
With the current code that I have now, for whatever reason. It doesn't call calcTip() (or atleast it doesn't seem to) until I press return once, enter another (or the same) value and press return again.
I'm really close to having this app work exactly how I want it to, and I feel like I'm just 1 or 2 lines away.
My ViewController conforms to UITextFieldDelegate, and I declared amountBeforeTaxTextField.delegate = self in viewDidLoad.
TextShouldReturn:
func textFieldShouldReturn(textField: UITextField) -> Bool {
amountBeforeTaxTextField.clearsOnBeginEditing = true
amountBeforeTaxTextField.becomeFirstResponder()
calcTip()
amountBeforeTaxTextField.resignFirstResponder()
return true
}
calcTip():
func calcTip() {
let tip = tipCalc.calculateTip()
let total = tipCalc.calculateTotal()
let tax = tipCalc.calculateTax()
let perPerson = total / Float(Int(partySizeSlider.value))
tipCalc.tipPercentage = (tipPercentageSlider.value)
tipCalc.amountBeforeTax = (amountBeforeTaxTextField.text! as
NSString).floatValue
tipCalc.taxPercentage = (taxPercentageSlider.value)
resultLabel.text = String(format: "Tip $%.2f, Tax: $%.2f Total: $%.2f", tip, tax, total)
perPersonLabel.text = String(format: "Per-Person Total: $%.2f", perPerson)
}
Note: I have the same outcome with & without .becomeFirstResponder()
Thanks for reading.
So your textFieldShouldReturn function is called when you press the bottom right button on the keyboard, which can have various titles; such as "Go", "Return", "Done", or "Send".
Your first line, only accomplishes a UI effect; meaning when the user taps on the textfield to input text, there will be a little box that appears on the far right handside of the textfield that allows the user to clear the inputted text, if there is any. Sort of like a reset.
When you use becomeFirstResponder, you are pretty much calling another keyboard to pop up, so you aren't accomplishing anything by doing that.
When you use resignFirstResponder, you are hiding the keyboard.
What you want to do is remove "becomeFirstResponder" as well as the first line, because those don't do anything to help your problem. That should be the solution to the problem you are facing.

Resources