How to toggle visibility with the UILabel in UITableViewCell? - swift

I have this chunk of code in my ViewController named PlayViewController:
words = [String]()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "PlayTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? PlayTableViewCell else {
fatalError("The dequeued cell is not an instance of PlayTableViewCell.")
}
// Configure the cell...
cell.wordLabel.text = words[indexPath.row]
if (indexPath.row == 0) {
cell.wordLabel.isHidden = false
}
return cell
}
And this is my code for TableViewCell named PlayTableViewCell:
import UIKit
class PlayTableViewCell: UITableViewCell {
//MARK: Properties
#IBOutlet weak var wordLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
wordLabel.lineBreakMode = .byWordWrapping;
wordLabel.numberOfLines = 0;
wordLabel.isHidden = true
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
As expected, only my first wordLabel appears in my tableView.
My goal is to reveal the second wordLabel when the user swipes right or left anywhere on the screen and continue this way until the user reveal the last wordLabel.
I've found how to set the swipe part (only the swipe right, behaves weirdly when the left is added) but I can't figure out how to toggle .isHidden property when I detect the gesture.
I'm not sure to be on the right path with the cell configuration but because of the placing of the wordLabel inside PlayTableViewCell, it's hard to reach it outside the function tableView.
I can index neither cell nor wordLabel and I can't figure out how could I reach the right wordLabel to toggle its visibility.

Somewhere store property for sum of revealed labels
var numberOfRevealedLabels = 1
then every time user swipes somewhere, increase this value and then reload data of your table view (you can reload it in didSet of your variable or after you increase this value in action which is called by swiping)
numberOfRevealedLabels += 1
Now, since cells are reusable, set visibility depending on if indexPath.row is less then or equal to numberOfRevealedLabels - 1
cell.wordLabel.isHidden = !(indexPath.row <= numberOfRevealedLabels - 1)
... this also covers the case that indexPath.row is greater

Related

Creating UITableView Cells ONCE Before They are Visible | Swift

Each of my EntryCell: UITableViewCells has an id number that get initialized and saved when the cell is created. My problem is that the cells can get initialized more than once, resulting in different cells being created with different cell ids and different details.
Here is my minimal, reproducible code for the cell initialization:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myCellType", for: indexPath) as! MyCellType
cell.textLabel.text = myCellType.id
return cell
}
Here is my minimal, reproducible code for the MyCellType.swift file:
class EntryCell: UITableViewCell {
#IBOutlet weak var textLabel: UILabel!
var id = 0
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
id = UserDefaults.standard.integer(forKey: "nextID")
UserDefaults.standard.set(UserDefaults.standard.integer(forKey: "nextID") + 1, forKey: "nextID")
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
When you scroll down far enough that some cells aren't visible, it essentially deletes them ( I think from my research so far ), so when you scroll back up it creates a new, higher id for the new cell.
My question is: is there a way to not have the cells initialize more than once because they are becoming visible multiple times? Or can I have them all initialize at the beginning and never get deleted after that?

Delegate method, textViewDidChangeSelection not triggered everytime

I have a UITextView embedded in a UITableView Cell, CustomCell. If I tap the return key when my curser is at the end of the text, I want to create a new cell below that cell, but if I tap return when I am in the middle of the textView, I want to get rest of the words after the curser and create a cell with that text in the textView.
To do that I need to know curser position in textView which I am getting with the help of the UITextViewDelegate method textViewDidChangeSelection(_:).
It works only when I tap on different location on the same cell but if I move to a different cell and tap again on a previously tapped cell(in the textview)at the same position, the delegate method doesn't get triggered.
Let me show the case with an exapmle-
I type some texts for testing. Here, after the 5th line, I decide to tap on the 2nd line just before the word "juice" like -
The delegate method, textViewDidChangeSelection(_:) gets triggered.
Now, I click on the 3rd line after the whole text "Skim Milk" like-
and again the delegate method, textViewDidChangeSelection(_:) gets called.
But now when I move back to the 2nd line at the same position, meaning before the word “juice”, the delegate method doesn’t get triggered. Again if I tap on the 3rd line at the same position meaning after the word “milk”, the delegate method doesn't get called.
I think, the reason is in those cells, I tapped on the sane position, and as the selection didn’t get changed, the delegate method didn’t get called.
Following is the code for the delegate method which is in the CustomCell(UITableViewCell) class-
func textViewDidChangeSelection(_ textView: UITextView){
if let range = textView.selectedTextRange, let indexPath = currentIndexPath{
let curserStartPosition = textView.offset(from: textView.beginningOfDocument, to: range.start)
setActiveCell?(indexPath, curserStartPosition)
}
}
The following is the code for the Controller where new cells are created when tapped the return key-
class MyViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
var cellCount = 1
var initialCellText: String?
var activeCell: CustomCell?
var cursorPosition: Int?
#IBOutlet weak var myTableView: UITableView!{
didSet{
myTableView.delegate = self
myTableView.dataSource = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
func goToNextCell(_ indexPath:IndexPath){
let nextCelIndexPath = IndexPath(row: indexPath.row + 1, section: 0)
if let text = activeCell?.myTextView.text, let cursorPos = cursorPosition, cursorPos < text.count-1 {
let fromIndex = text.index(text.startIndex, offsetBy: cursorPos)
let toIndex = text.index(text.endIndex, offsetBy: -1)
if fromIndex < toIndex {
let truncatedText = text[fromIndex...toIndex]
self.initialCellText = String(truncatedText)
}
}
cellCount += 1
myTableView.beginUpdates()
myTableView.insertRows(at: [nextCelIndexPath], with: .none)
myTableView.endUpdates()
initialCellText = nil
cursorPosition = nil
if let cell = self.myTableView.cellForRow(at: nextCelIndexPath) as? CustomCell{
self.activeCell = cell
self.activeCell?.myTextView.becomeFirstResponder()
}
}
func setActiveCell(on indexPath: IndexPath, at curserPos: Int){
if let tappedOnCell = myTableView.cellForRow(at: indexPath) as? CustomCell{
self.activeCell = tappedOnCell
self.cursorPosition = curserPos
}
}
}
// MARK: - UITableViewDelegate and UITableViewDataSource
extension MyViewController{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "customCell") as? CustomCell{
cell.myTextField.becomeFirstResponder()
cell.returnKeyTapped = goToNextCell(_:)
cell.setActiveCell = setActiveCell(on:at:)
return cell
}
return UITableViewCell()
}
}
To create the following cell, I need to know where the cursor position is on a particular cell everytime. So, I need the delegate method to be triggered everytime.
Can anyone have any suggestions as to how can I solve my problem.
If I understand you right, you basically want to know the curser position after you have typed into a UITextView.
Now, if you type into a UITextView, that text view becomes the first responder, and begins an editing session. This is announced by a UITextView.textDidBeginEditingNotification for which you can add an observer, see here.
In the function called by the notification, you could compute the cursor position as you do in textViewDidChangeSelection

How to change certain cell's view across different view controllers swift

I am building a recorder app that users can save their recordings.
How can I change a view of a certain cell in a UITableView to show users that this cell is being played and the cell with the same content in other ViewControllers can be changed simultaneously?
Furthermore, when the audio player finished. The view can be changed back to default.
I have tried override setSelected, but the cell cannot stay selected when I reload the table view and the cell cannot be deselected by itself when audio has finished. I also cannot change the cell with the same content (same audio)' view simultaneously.
Is there a way to overcome this problem?
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
if selected {
print("task has began")
} else if !selected {
print("task has finished")
}
}
The result will be like apple music or podcast that the cell with the same shows across the app will show whether or not it is being played.
First make IBOutlet of View Inside xib
class TableViewCell: UITableViewCell {
#IBOutlet weak var generaV: UIView!
}
set Identifier = TableViewCell
and call the Datasource Method in as many file you want and change the color like .red/.blue in in different VC
Call the Cell in ViewController
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
if (audioEnded){
cell.generaV.backgroundColor = UIColor.green
}
else {
cell.generaV.backgroundColor = UIColor.red
}
return cell
}

adding Button in last tableView Cell shown twice when scrolling

Using Swift4, iOS11.2.1, Xcode9.2,
I successfully added a custom button to the last cell of a tableView. (the button is used to add cells in the tableView - this also works fine...) - see Screenshot-1.
But now the issue: When adding more cells (i.e. more than fit in the tableView-height), and if you scroll to the top, then there is a second cell that shows this button - see Screenshot-2.
(see code below....)
Here are the two screenshots:
How can I get rid of the extra button ????
(it seems that a cell is reused and the button is partly drawn. The cut-off can be explained since the height of the last cell is made bigger than the other cells. But still, there shouldn't be parts of buttons in any other cell than the very last one...even when scrolling up).
Any help appreciated !
Here is my code (or excerts of it...):
override func viewDidLoad() {
super.viewDidLoad()
// define delegates
self.fromToTableView.dataSource = self
self.fromToTableView.delegate = self
// define cell look and feel
self.addButton.setImage(UIImage(named: "addButton"), for: .normal)
self.addButton.addTarget(self, action: #selector(plusButtonAction), for: .touchUpInside)
//...
self.fromToTableView.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let resItems = resItems else {
self.rowCount = 0
return 0
}
self.rowCount = resItems.count - 1
return resItems.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if(indexPath.row == (self.rowCount)) {
return 160
} else {
return 120
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = fromToTableView.dequeueReusableCell(withIdentifier: "SettingsCell") as? SettingsCustomTableViewCell
// configure the cell
cell?.configureCell(tag: indexPath.row)
// fill cell-data
if let resItem = self.resItems?[indexPath.row] {
cell?.connectionItem = resItem
}
// add addButton to last cell
if(indexPath.row == (self.rowCount)) {
// add addButton
cell?.contentView.addSubview(self.addButton)
// scroll to last cell (that was just added)
// First figure out how many sections there are
let lastSectionIndex = self.fromToTableView.numberOfSections - 1
// Then grab the number of rows in the last section
let lastRowIndex = self.fromToTableView.numberOfRows(inSection: lastSectionIndex) - 1
// Now just construct the index path
let pathToLastRow = IndexPath(row: lastRowIndex, section: lastSectionIndex)
// Scroll to last cell
self.fromToTableView.scrollToRow(at: pathToLastRow as IndexPath, at: UITableViewScrollPosition.none, animated: true)
}
return cell!
}
You already figured out what the problem is :)
As you say here:
it seems that a cell is reused and the button is partly drawn
And that is exactly what happens here. You have a pool of UITableViewCell elements, or in your case SettingsCustomTableViewCell. So whenever you say:
let cell = fromToTableView.dequeueReusableCell(withIdentifier: "SettingsCell") as? SettingsCustomTableViewCell
Then, as the name implies, you dequeue a reusable cell from your UITableView.
That also means that whatever you may have set on the cell previously (like your button for instance) stays there whenever you reuse that specific instance of a cell.
You can fix this in (at least) three ways.
The Hack
In your tableView(_:cellForRowAt:indexPath:) you have this:
if(indexPath.row == (self.rowCount)) {
//last row, add plus button
}
You can add an else part and remove the plus button again:
if(indexPath.row == (self.rowCount)) {
//last row, add plus button
} else {
//not last row, remove button
}
The Right Way
UITableViewCell has the function prepareForReuse and as the documentation says:
If a UITableViewCell object is reusable—that is, it has a reuse identifier—this method is invoked just before the object is returned from the UITableView method dequeueReusableCell(withIdentifier:)
So, this is where you "reset" your custom table view cell. In your case you remove the plus button.
You already have a custom UITableViewCell it seems, so this should be a small change.
The Footer Way
As #paragon suggests in his comment, you can create a UIView and add that as a tableFooterView to your UITableView.
let footerView = YourFooterViewHere()
tableView.tableFooterView = footerView
Hope that helps

How to keep an object's reference when using UITableView?

The problem I'm facing is probably some lack of understanding on the concept of reusable cells. I have, let's say, 30 rows to be created and each of them has a UISwitch.
When I toggle one of the switches, it's behavior should affect the other 29. The point is: as far as I know, iOS doesn't create all of them at once, but rather wait to reuse the cells when the TableView is scrolled up and down.
How can I keep a copy of those reused objects and tell iOS to set the proper value to the switches?
I've thought on having the cells appended to a [UISwitch] but I can't manage to have all the 30 cells in there, look:
...
var switches = [UISwitch]()
...
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Field10Cell", for: indexPath) as! Field10TableViewCell
...
//cell.value is a UISwitch
if !switches.contains(cell.value) {
switches.append(cell.value)
}
return cell
}
You could create a set which stores the indexes of the cells whose switches have been pressed.
var activeSwitches = Set<IndexPath>()
Whenever a user presses the switch on a cell, you store it on the set like this:
activeSwitches.insert(indexPath)
If you need to check if a switch was activated, just check if its container cell's indexPath is in active switches like so:
if activeSwitches.contains(indexPath) {
// do something
}
In order to know when a user pressed a specific switch I recommend the folliwing:
In cellForRowAtIndexPath save the current indexPath into your Field10TableViewCell.
Create a protocol on Field10TableViewCell and add a delegate.
protocol Field10Delegate {
func didChangeSwitch(value: Bool, indexPath: IndexPath)
}
class Field10TableViewCell {
var delegate: Field10Delegate?
var indexPath: IndexPath?
#IBOutlet weak var fieldSwitch: UISwitch! // Can't use 'switch' as a variable name
#IBAction func switchValueChanged(_ sender: UISwitch) {
if let indexPath = indexPath {
delegate?.didChangeSwitch(value: sender.isOn, indexPath: indexPath)
}
}
When you create a cell, set the view controller as a delegate
let cell = tableView.dequeueReusableCell(withIdentifier: "Field10Cell", for: indexPath) as! Field10TableViewCell
cell.delegate = self
cell.indexPath = indexPath
Make your view controller comply with the protocol:
extension ViewController: Field10Delegate {
/* Whenever a switch is pressed on any cell, this delegate will
be called. This is a good place also to trigger a update to
your UI if it has to respond to switch changes.
*/
func didChangeSwitch(value: Bool, indexPath: IndexPath) {
if value {
activeSwitches.insert(indexPath)
} else {
activeSwitches.remove(indexPath)
}
updateUI()
}
}
With the above, at any point you will know which switches are active or not and you can process the dequeued cells with this information.