Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 2 years ago.
Improve this question
i have a UITableViewController configured with a static table made up of 5 custom cells (this is meant to be a form).
cell 0 captures a date
cell 1 is a UICollectionView filled with icons representing activities. depending on which activity is selected, one of many custom UITableViewCell are loaded on the fly in cell 2 .
cell 2 can be simple (just a slider), a textfield or more complex (a UITableView)
cell 3 has a textfield to capture a description.
cell 4 is for additional notes
so far everything is working as expected. I select an activity in cell 1, the matching XIB is loaded in cell 2, data is captured, comment added etc and form is saved. Where i am challenged is in the case where based on the the input in cell 2, i would like to update cell 3 automatically. i got it working but i feel it isn't elegant or clean so would appreciate others input. in
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// main table cells pseudo code
switch indexPath.row {
case 0:
<#code#>
case 1:
// dequeue collectionviewcell
case 2:
// dequeue the relevant uitableviewcell based on selected cell in the uicollectionview
switch selectedIndexPath.row {
case 2:
<#code#>
default:
<#code#>
}
case 3:
// handle the description field
default:
<#code#>
}
a specific activity will load "eventFlushMedTableViewCell" which is meant to display a list of meds in a uitableview. based on which meds I select, i would like to take the name of the meds and put them in cell 3.
case 2:
let cell = tableView.dequeueReusableCell(withIdentifier: "eventFlushMedTableViewCell", for: indexPath) as! eventFlushMedTableViewCell
// fill the meds table with the list of Rx meds
cell.medsArray = medListArray.filter { $0.medRx == 1}.map {return $0.medName}
// return the selected values from the table
cell.returnValue = { selectedIndexPaths in
let indexList = selectedIndexPaths.map { $0.row }
self.selectedMedsName = indexList.map {cell.medsArray[$0]}
// force a reload of cell 3
let myIndexPath = IndexPath.init(row: 3, section: 0)
tableView.reloadRows(at: [myIndexPath], with: .none)
}
return cell
and cell 3 in the main table is dequeued and configured
case 3:
let cell = tableView.dequeueReusableCell(withIdentifier: "eventDescriptionCell", for: indexPath) as! eventDescriptionCell
if !selectedMedsName.isEmpty {
cell.eventDescriptionTextField.text = selectedMedsName.joined(separator: ", ")
}
something tells me the closure around (cell.returnValue {...}) in case 2:, even though it is working, isn't the right way of doing this... any advice ?
thanks
I think a good way of approaching this would be to create a custom delegate.
Let's name it ActivityCellDelegate. You can create it like this:
protocol ActivityCellDelegate: class {
func activitySelected(_ activity: Activity)
}
We use 'class' so we can make the delegate 'weak' reference. Otherwise XCode will cause trouble :).
Using an enum to represent different activities is a nice touch. You could also use a String as an identifier or an Int as an id.
enum Activity {
case activity1
case activity2
...
}
Add a delegate property to the TableViewCell:
weak var cellDelegate: ActivityCellDelegate?
Then, when dequeuing a cell, you set it's delegate to self cell.cellDelegate = self, and make the controller that has tableView as a subview conform to the delegate protocol.
extension TableViewController: ActivityCellDelegate {
func activitySelected(_ activity: Activity) {
//...in here you would probably reload the third cell in the table view. You could add an additional property in the ViewController in order to know which activity is currently selected.
}
}
In the end, when user selects a CollectionViewCell to choose an activity in collectionView:didSelectItemAtIndexPath: you should call the delegate method:
delegate?.activitySelected(.activity1)
Related
How do I get the text of a selected cell in a UITableView? I know how to get the row (indexPath.row), for example, but I can't find how to get the contents of the selected cell.
The bigger picture:
Ultimately, I want to use the cell to select the appropriate segue. I am using a plist for the text of a UITableView's cells. When the user selects an item in the table, the segue is selected.
I set it up like this:
switch indexPath.row {
case 0: segueIdentifier = "segueToTopic0"
case 1: segueIdentifier = "segueToTopic1"
// etc.
default: segueIdentifier = "segueTest"
}
but ended up frequently adding and deleting items, or rearranging the plist, forcing me to rearrange this switch statement.
So, now I'm thinking I could do this by using the cell text in the switch statement:
switch indexPath.row {
case "Topic0": segueIdentifier = "segueToTopic0"
case "Topic1": segueIdentifier = "segueToTopic1"
// etc.
default: segueIdentifier = "segueTest"
}
(Of course, this means I would have to ensure the text in the plist and the text in the switch statement are identical.)
Or maybe there is a better way?
When asking a question here, it's a good idea to think through what you're asking as well as thinking through why you're asking.
Without additional context, your approach doesn't seem to make much sense.
"I am using a plist for the text of a UITableView's cells" ... Might you have 100 "topics" in your plist? Which would mean you have manually created and named 100 segue connections? Likely, there is a much better approach.
However, to answer your specific question...
We assume you have loaded your data from your plist into an array of strings, perhaps called arrayOfTopics ... after loading, it will be something like this:
arrayOfTopics [
"Topic0",
"Topic1",
"Topic2",
"Topic3",
"Topic4",
"Topic5",
]
So, your cellForRowAt implementation looks something like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = arrayOfTopics[indexPath.row]
return cell
}
Instead of thinking "how do I get the text from a row" just use the same data structure, so didSelectRowAt could look like this:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let segueID: String = "segueTo" + arrayOfTopics[indexPath.row]
performSegue(withIdentifier: segueID, sender: nil)
}
If you tap on the 3rd row (rows and arrays are Zero based), indexPath.row will be 2... arrayOfTopics[2] contains "Topic2"... appending it to "segueTo" will result in the string segueToTopic2, which you can then use as the segue identifier.
I am changing the numberOfLines attribute on a label that lives in a custom UITableViewCell when the cell is tapped. However, this is not reflected in the UI until the second tap. The cell is configured as a prototype cell in the table view to initially have 2 lines.
Interestingly enough, when I print out the numberOfLines value before and after my tapped() function runs, the values start off different, and then synchronize - after the first tap, I see 2 lines before the function runs, then 0 lines after the function runs. However, after subsequent taps, I see the same value before and after my function, which makes it seem like it's not doing anything, even though the UI does stretch and shrink the cell, and the numberOfLines value is changed for the next time the didSelectRowAtIndexPath function runs.
I'm only seeing this behavior with tableView.reloadRows(). If I do a full update with tableView.reloadData(), the cell appropriately grows and collapses the first time it is tapped. However, this feels a bit ham-fisted and doesn't animate nicely like reloadRows() does.
TableView Implementation
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ReviewTableViewCell
else { return }
let data = tableData[indexPath.row]
print("old number of lines: \(cell.detailLabel.numberOfLines)")
//data.isOpen is set to false initially
cell.tapped(data.isOpen)
tableData[indexPath.row].isOpen = !data.isOpen
tableView.reloadRows(at: [indexPath], with: .fade)
print("old number of lines: \(cell.detailLabel.numberOfLines)")
// tableView.reloadData()
}
Custom Table View Cell method
func tapped(_ isOpen: Bool) {
if !isOpen {
detailLabel.numberOfLines = 0 }
else {
detailLabel.numberOfLines = 2 }
}
I am expecting this code to expand the cell once it is reloaded with tableView.reloadRows() if the numberOfLines is set to 0 and collapse the cell when it is set to 2. This does work, but only after tapping the cell two+ times. This should work with the first tap as well.
Here is a link of a gif that shows the issue: https://imgur.com/a/qe2uAXj
Here is a sample project that is similar to what's going on in my app: https://github.com/imattice/CellLabelExample
Just to be clear, to get this trick work UILabel generally must be constrained on each side to it's superview, in this way when it changes its intrinsicContentSize is able to push each side to accomodate the text.
Saying that, try to wrap the tapped method with those two methods:
tableView.beginUpdates()
if !isOpen {
detailLabel.numberOfLines = 0
}
else {
detailLabel.numberOfLines = 2
}
tableView.endUpdates()
Of course tableview must be set to automatic size:
tableView.estimatedRowHeight = <#What you want#>
tableView.rowHeight = UITableView.automaticDimension
I was able to work out what was going on. The problem is in two parts.
The first part is calling reloadRows(). This method is swapping out the cells with a new cell rather than updating the cell that already exists. Therefore, I'm changing the number of lines on that hidden swap cell rather than the cell that is in view. This behavior is mentioned in the docs:
Reloading a row causes the table view to ask its data source for a new cell for that row. The table animates that new cell in as it animates the old row out.
Additionally, I'm using structs as the data model for tracking the open status of the cell. In Swift, structs are copy-on-write, which means that if a value is changed on that struct, a new struct is created rather than changing the value of that struct I'm pointing to. This means the line tableData[indexPath.row].isOpen = !data.isOpen doesn't do anything useful - we look at the tableData struct at the index path, get it's isOpen value, copy a new struct and change that new struct's isOpen value, and then throw it out because the new struct is not assigned anywhere.
The solution is to not use the reloadRows() method and to either use
A) a class for the data object
B) replace the data at indexPath.row to the copied struct
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? CustomCell else { return }
var data = tableData[indexPath.row]
tableView.beginUpdates()
cell.tapped(isOpen: data.isOpen)
data.isOpen = !data.isOpen
tableData[indexPath.row] = data
tableView.endUpdates()
}
I have a table view with custom cells. They are quite tall, so only one cell is completely visible on the screen and maybe, depending on the position of that cell, the top 25% of the second one. These cells represent dummy items, which have names. Inside of each cell there is a button. When tapped for the first time, it shows a small UIView inside the cell and adds the item to an array, and being tapped for the second time, hides it and removes the item. The part of adding and removing items works fine, however, there is a problem related to showing and hiding views because of the fact that cells are reused in a UITableView
When I add the view, for example, on the first cell, on the third or fourth cell (after the cell is reused) I can still see that view.
To prevent this I've tried to loop the array of items and check their names against each cell's name label's text. I know that this method is not very efficient (what if there are thousands of them?), but I've tried it anyway.
Here is the simple code for it (checkedItems is the array of items, for which the view should be visible):
if let cell = cell as? ItemTableViewCell {
if cell.itemNameLabel.text != nil {
for item in checkedItems {
if cell.itemNameLabel.text == item.name {
cell.checkedView.isHidden = false
} else {
cell.checkedView.isHidden = true
}
}
}
This code works fine at a first glance, but after digging a bit deeper some issues show up. When I tap on the first cell to show the view, and then I tap on the second one to show the view on it, too, it works fine. However, when I tap, for example, on the first one and the third one, the view on the first cell disappears, but the item is still in the array. I suspect, that the reason is still the fact of cells being reused because, again, cells are quite big in their height so the first cell is not visible when the third one is. I've tried to use the code above inside tableView(_:,cellForRow:) and tableView(_:,willDisplay:,forRowAt:) methods but the result is the same.
So, here is the problem: I need to find an EFFICIENT way to check cells and show the view ONLY inside of those which items are in the checkedItems array.
EDITED
Here is how the cell looks with and without the view (the purple circle is the button, and the view is the orange one)
And here is the code for the button:
protocol ItemTableViewCellDelegate: class {
func cellCheckButtonDidTapped(cell: ExampleTableViewCell)
}
Inside the cell:
#IBAction func checkButtonTapped(_ sender: UIButton) {
delegate?.cellCheckButtonDidTapped(cell: self)
}
Inside the view controller (NOTE: the code here just shows and hides the view. The purpose of the code is to show how the button interacts with the table view):
extension ItemCellsTableViewController: ItemTableViewCellDelegate {
func cellCheckButtonDidTapped(cell: ItemTableViewCell) {
UIView.transition(with: cell.checkedView, duration: 0.1, options: .transitionCrossDissolve, animations: {
cell.checkedView.isHidden = !cell.checkedView.isHidden
}, completion: nil)
}
EDITED 2
Here is the full code of tableView(_ cellForRowAt:) method (I've deleted the looping part from the question to make it clear what was the method initially doing). The item property on the cell just sets the name of the item (itemNameLabel's text).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier:
ItemTableViewCell.identifier, for: indexPath) as? ItemTableViewCell{
cell.item = items[indexPath.row]
cell.delegate = self
cell.selectionStyle = .none
return cell
}
return UITableViewCell()
}
I've tried the solution, suggested here, but this doesn't work for me.
If you have faced with such a problem and know how to solve it, I would appreciate your help and suggestions very much.
Try this.
Define Globally : var arrIndexPaths = NSMutableArray()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = self.tblVW.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.textLabel?.text = String.init(format: "Row %d", indexPath.row)
cell.btn.tag = indexPath.row
cell.btn.addTarget(self, action: #selector(btnTapped), for: .touchUpInside)
if arrIndexPaths.contains(indexPath) {
cell.backgroundColor = UIColor.red.withAlphaComponent(0.2)
}
else {
cell.backgroundColor = UIColor.white
}
return cell;
}
#IBAction func btnTapped(_ sender: UIButton) {
let selectedIndexPath = NSIndexPath.init(row: sender.tag, section: 0)
// IF YOU WANT TO SHOW SINGLE SELECTED VIEW AT A TIME THAN TRY THIS
arrIndexPaths.removeAllObjects()
arrIndexPaths.add(selectedIndexPath)
self.tblVW.reloadData()
}
I would keep the state of your individual cells as part of the modeldata that lies behind every cell.
I assume that you have an array of model objects that you use when populating you tableview in tableView(_:,cellForRow:). That model is populated from some backend service that gives you some JSON, which you then map to model objects once the view is loaded the first time.
If you add a property to your model objects indicating whether the cell has been pressed or not, you can use that when you populate your cell.
You should probably create a "wrapper object" containing your original JSON data and then a variable containing the state, lets call it isHidden. You can either use a Bool value or you can use an enum if you're up for it. Here is an example using just a Bool
struct MyWrappedModel {
var yourJSONDataHere: YourModelType
var isHidden = true
init(yourJSONModel: YourModelType) {
self.yourJSONDataHere = yourJSONModel
}
}
In any case, when your cell is tapped (in didSelectRow) you would:
find the right MyWrappedModel object in your array of wrapped modeldata objects based on the indexpath
toggle the isHidden value on that
reload your affected row in the table view with reloadRows(at:with:)
In tableView(_:,cellForRow:) you can now check if isHidden and do some rendering based on that:
...//fetch the modelObject for the current IndexPath
cell.checkedView.isHidden = modelObject.isHidden
Futhermore, know that the method prepareForReuse exists on a UITableViewCell. This method is called when ever a cell is just about to be recycled. That means that you can use that as a last resort to "initialize" your table view cells before they are rendered. So in your case you could hide the checkedView as a default.
If you do this, you no longer have to use an array to keep track of which cells have been tapped. The modeldata it self knows what state it holds and is completely independent of cell positions and recycling.
Hope this helps.
I have table contains custom cells contain uisegment
The problem is when select any item in uisegment and scroll down in the table view it change the selection in uisegment in the cells down on table
almost cell 1 like 11 and 2 like 12
It related to dequeueReusableCellWithIdentifier and my question is what is the best way to solve it ?
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let item = self.cells.items[indexPath.row]
if let cellHedaer: TcPationQuestionHeader = tableView.dequeueReusableCellWithIdentifier("HeaderItem") as? TcPationQuestionHeader {
cellHedaer.setCell(Results.Questions[indexPath.row/2 ])
if item as? SwiftyAccordionCells.HeaderItem != nil {
let cellItem: TcPationQuestionItem = tableView.dequeueReusableCellWithIdentifier("Item") as! TcPationQuestionItem
cellItem.setCell(Results.Questions[indexPath.row/2])
return cellItem
}
cellHedaer.selectionStyle = UITableViewCellSelectionStyle.None
return cellHedaer
}
I see 2 major problems in your code:
You shouldn't keep the state inside UI. Every accessing data from UI
is a big mistake. You should keep selection inside model. The easiest way is to keep array var inside controller.
In some cases you will call dequeueReusableCellWithIdentifier 2 times. That shouldn't happen too.
Don't forget the implementation of cellForRow is connected to numberOfRowsAtIndexPath and numberOfSections. If you want to more detailed help paste here these 2 functions.
I'm working on a "trading" application where I would like to have a static number of cells.
On load, users will see 5 cells, each displaying a label that says "Add."
When a "player" is added, that cell displays the players information, the other 4 cells still display the "Add" label. Another is added, 2 cells have player information, 3 have the "Add"
I'm having a hell of a time with this. Can anyone point my in the right direction? I have custom labels setup, I think my logic may just be off on how to perform this correctly.
You need to subclass the UICollectionViewDelegate and UICollectionViewDataSource protocols in your viewController, then you need to implement the numberOfItemsInSection and cellForItemAtIndexPath functions.
Additionally to that you need to create two type of cells in your storyboard and subclass them, in the following code i will suppose that you call AddedPlayerCell and DefaultCell your cells, i will suppose that each cell has a label called labelText too.
let players = ["Player1","Player2"] //players added till now
let numberOfCells = 5
//Here you set the number of cell in your collectionView
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return max(players.count,numberOfCells);
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if((indexPath.row + 1) < self.players.count){ //If index of cell is less than the number of players then display the player
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("yourIdentifierForAddedPlayerCell", forIndexPath: indexPath) as! AddedPlayerCell
cell.labelText.text = self.players[indexPath.row] //Display player
return cell;
}else{//Else display DefaultCell
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("yourIdentifierForDefaultCell", forIndexPath: indexPath) as! DefaultCell
cell.labelText.text = "Add"
return cell;
}
}
In order to manage two different cell types, you can:
Create 2 prototype cells for your collection view. Give one the identifier "Add" and the other "Info". The "Add" cell prototype will contain the label "Add", and the "Info" cell prototype will contain fields to display the player info.
Add an array property to your class which keeps track of which cells are displaying "Add".
var showingAdd = [true, true, true, true, true]
In cellForItemAtIndexPath, check the showingAdd array to determine which identifier to use when dequeueing the cell:
let identifier = showingAdd[indexPath.row] ? "Add" : "Info"
let cell = dequeueReusableCellWithIdentifer(identifier...)
if !showingAdd[indexPath.row] {
// configure the cell with the proper player info
// retrieve info from info property array item created in
// step 4.
let player = playerInfo[indexPath.row]
cell.playerName = player.name
...
}
When a cell is selected in didSelectItemAtIndexPath, check if it is showing add and then process it accordingly:
if showingAdd[indexPath.row] {
// query user to get player info
// store the info in a property array indexed by `indexPath.row`
playerInfo[indexPath.row] = PlayerInfo(name: name, ...)
showingAdd[indexPath.row] = false
// trigger a reload for this item
collectionView.reloadItemsAtIndexPaths([indexPath])
}