I have a tableView with section headers and I want to append a certain user input to multiple selected headers. I have created the section headers and am able to change the image on the section header to show that it is selected but I want to create an array of those selected.
Here is my code for the section header:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let userModel = Data.userModels[section]
let cell = tableView.dequeueReusableCell(withIdentifier: "nameCell") as! NameHeaderTableViewCell
cell.setup(model: userModel)
cell.checkMarkButton.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
cell.enable(on: false)
if isUserEditing == true {
cell.enable(on: true)
}
return cell.contentView
}
Here is where I change the section image when a user taps the section:
#objc func handleTap(sender: UIButton) {
sender.isSelected = !sender.isSelected
}
When the user clicks the save button, I want the user input to be appended to those cells where the section header is selected. Here is that code:
#IBAction func save(_ sender: Any) {
//VALIDATION
guard mealItemTextField.text != "", let item = mealItemTextField.text else {
mealItemTextField.placeholder = "please enter an item"
mealItemTextField.layer.borderWidth = 1
mealItemTextField.layer.borderColor = UIColor.red.cgColor
return
}
guard priceTextField.text != "", let price = Double(priceTextField.text!) else {
priceTextField.placeholder = "please enter a price"
priceTextField.layer.borderWidth = 1
priceTextField.layer.borderColor = UIColor.red.cgColor
return
}
tableView.reloadData()
}
Currently I am stuck on how to access all of the indexes of the sections that are selected(i.e. the ones that have a state of selected) Here are some screenshots to help visualize the program:
P.S. Not sure if this is helpful but I populate the data with an array of structs. Here is that code:
func numberOfSections(in tableView: UITableView) -> Int {
return Data.userModels.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if Data.userModels[section].isExpandable {
return Data.userModels[section].itemModels.count
} else {
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "cell")
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
}
cell?.textLabel?.text = Data.itemModels[indexPath.row].itemName
return cell!
}
Any help is appreciated, thanks!
The problem is that your checkmarks are merely a visual feature of the header. When the user taps on a checkmark, you're merely toggling its selected state:
#objc func handleTap(sender: UIButton) {
sender.isSelected = !sender.isSelected
}
That's not going to work. You need to be keeping track of this information at all times in a part of your data model that deals with the section headers. That way, when the Save button is clicked, the information is sitting there in the data model waiting for you. Your handleTap needs to work out what section this is the header of and reflect the info off into the model. The data model is the source of truth, not some view in the interface. (I am surprised that you are not already having trouble with this when you scroll your table view.)
Another problem with your code is this:
let cell = tableView.dequeueReusableCell(withIdentifier: "nameCell") as! NameHeaderTableViewCell
You can't use a UITableViewCell as a reusable section header view. You need to be using a UITableViewHeaderFooterView here.
Related
I have a weird situation where I swipe a cell to grey it out and it greys every 4th or 6th cell instead of only the single cell that was swiped.
The tableview is initialized as follows:
func setupView() {
view.backgroundColor = .white
tableView.register(EntityCell.self, forCellReuseIdentifier: "entityCell")
tableView.separatorStyle = .none
tableView.dataSource = self
tableView.delegate = self
}
Here is my query to get the data:
func getEntities(taxId : String) {
dispatchGroup.enter()
db.collection("Taxonomy").whereField("entityId", isEqualTo: entityId).whereField("status", isEqualTo: 401).getDocuments { (orderSnapshot, orderError) in
if orderError != nil {
self.showError(show: "Error", display: orderError!.localizedDescription)
} else {
self.entitiesArray.append(contentsOf: (orderSnapshot?.documents.compactMap({ (orderDocuments) -> Order in
Order(dictionary: orderDocuments.data(), invoiceId: orderDocuments.documentID, opened: false)!
}))!)
self.dispatchGroup.leave()
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
Here are the standard override functions to populate the tableview:
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return entitiesArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "entityCell", for: indexPath) as? EntityCell else { return UITableViewCell() }
let entityRow = entitiesArray[indexPath.row]
cell.selectionStyle = .none
cell.setTaxonomy(entity: entityRow) // Setting up the cell with the array values
return cell
}
Everything is working fine upto this point. And finally here is the override func for swipe action:
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let complete = UIContextualAction(style: .normal, title: "Verified") { (action, view, completionHandler) in
self.db.collection("Taxonomy").document(self.entitiesArray[indexPath.row].entityId).updateData(["status": 411]) { (error) in
if error == nil {
let cell = tableView.cellForRow(at: indexPath) as? EntityCell
cell?.changeStatus(currentEntity: self.entitiesArray[indexPath.row])
}
}
completionHandler(true)
}
complete.image = UIImage(named: "icon_approved")
complete.backgroundColor = UIColor(hex: Constants.Colors.secondary)
let swipe = UISwipeActionsConfiguration(actions: [complete])
return swipe
}
So I swipe right from the trailing edge of the cell and I see the underlying color and icon as expected. And the cell turns grey via this function via a protocol:
extension EntityCell : EntityStatusDelegate {
func changeStatus(currentEntity: EntityObject) {
entityCellBackground.backgroundColor = .systemGray4
}
}
The cell turns grey. And then I scroll down and I see every 4th or 6th cell is grey as well. Any idea what is going wrong? I am pretty flummoxed at this point.
Cells get recycled. You need either configure them completely or overwrite the prepareForReuse function of the cell or give each cell an unique reuseidentifyer so the tableview can recycle them.
(Last option is the worst as it cost a lot more memory)
Option 1:
Just set the backgroundcolor:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "entityCell", for: indexPath) as? EntityCell else { return UITableViewCell() }
let entityRow = entitiesArray[indexPath.row]
cell.selectionStyle = .none
cell.setTaxonomy(entity: entityRow) // Setting up the cell with the array values
cell.entityCellBackground.backgroundColor = (whatever the default color is)
return cell
}
im using two buttons in my controller
when i press button1 it should show tableview related to button 1
and when i press button 2 it shows tableview related to button 2
how can I achieve this?
First of all, create an enum that'll keep a track of what data and cell should be loaded in the tableView, i.e.
enum Source {
case button1
case button2
}
Now, you must have 2 arrays that'll use to load the tableView as per the source.
For example:
class VC: UIViewController, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var arr1 = ["one", "two"]
var arr2 = ["three", "four"]
var source = Source.button1
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch source {
case .button1:
return arr1.count
case .button2:
return arr2.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch source {
case .button1:
let cell = tableView.dequeueReusableCell(withIdentifier: "cell1", for: indexPath)
cell.textLabel?.text = arr1[indexPath.row]
return cell
case .button2:
let cell = tableView.dequeueReusableCell(withIdentifier: "cell2", for: indexPath)
cell.textLabel?.text = arr2[indexPath.row]
return cell
}
}
}
Now, create #IBAction for both the buttons, update the source and reload the tableView on button taps.
#IBAction func onTapButton1(_ sender: UIButton) {
self.source = .button1
self.tableView.reloadData()
}
#IBAction func onTapButton2(_ sender: UIButton) {
self.source = .button2
self.tableView.reloadData()
}
I think, what you're looking for, is SegmentedControl.
You should use ViewController as base for your page, add segmented control on top, add tableView after that. Then use segmented control to control the source of your table. Your code should look like that:
switch (segmentedControl.selectedSegmentIndex ) {
case 0:
yourDataCollection = someService.getData(); // depends on selected index
self.tableView.reloadData()
case 1:
yourDataCollection = someService.getSomeOtherData();
self.tableView.reloadData()
default:
return
}
Alternatively, if you want to use buttons instead of segmented control, you can create a bool/Int value and store it on button tap. The code will be almost the same.
I currently have a table view with cells that contain a label and a textfield, I also have a + bar button item that adds new cells.
What I hope to accomplish is when the user presses the + button the new cell is created and the text field of this cell would automatically become first responder.
Below is my current code for creating the new entry:
func newNoteline() {
let entityDescription = NSEntityDescription.entity(forEntityName: "NotebookContentEntity", in: context)
let item = NotebookContentEntity(entity: entityDescription!, insertInto: context)
item.notebookEntry = ""
item.timeOfEntry = timeOutlet.text
do {
try context.save()
} catch {
print(error)
return
}
loadNotelines()
}
I have thought of several ways of trying to solve this but without much luck of making them work including using a .tag on the text field as soon as it's made - using the text field delegate or using the tableView delegate method - indexPathForPreferredFocusedView.
I just can't figure out how to force the focus to a specific textfield within a cell without the user tapping the text field. Any thoughts?
Calling textField.becomeFirstResponder() method should do what you are looking for.
When to call this function is up to you. for e.g. in below code at cellForRowAt I checked if the value is empty then make the current textfield first responder.
class Test: UIViewController{
var myView: TestView{return view as! TestView}
unowned var tableView: UITableView {return myView.tableView}
unowned var button: UIButton {return myView.button}
var list = [String]()
override func loadView() {
view = TestView()
}
override func viewDidLoad() {
super.viewDidLoad()
for i in 1...10{
list.append("Test \(i)")
}
tableView.dataSource = self
button.addTarget(self, action: #selector(didSelect(_:)), for: .touchUpInside)
}
#objc func didSelect(_ sender: UIButton){
let indexPath = IndexPath(row: list.count, section: 0)
list.append("")
tableView.insertRows(at: [indexPath], with: .automatic)
tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
extension Test: UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return list.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TestViewCell", for: indexPath) as! TestViewCell
let value = list[indexPath.row]
cell.textField.text = value
if value.isEmpty{
cell.textField.becomeFirstResponder()
}
return cell
}
}
I have created prototype custom header cell for a tableView with a button on it. I am trying to get the indexPath of the cell when the button is tapped, but I don't receive it. Any idea what I am doing wrong here?
protocol MediaHeaderCellDelegate: class {
func editPost(cell: MediaHeaderCell)
}
class MediaHeaderCell: UITableViewCell {
weak var delegate: MediaHeaderCellDelegate?
#IBAction func moreOptionsAction(_ sender: UIButton) {
delegate?.editPost(cell: self)
}
}
class NewsfeedTableViewController:UITableViewController, MediaHeaderCellDelegate {
func editPost(cell: MediaHeaderCell) {
guard let indexPath = tableView.indexPath(for: cell) else {
print("indexpath could not be given")
return}
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
let cell = tableView.dequeueReusableCell(withIdentifier: Storyboard.mediaHeaderCell) as! MediaHeaderCell
cell.delegate = self
cell.media = media[section]
return cell
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: Storyboard.mediaCell, for: indexPath) as! MediaTableViewCell
cell.currentUser = currentUser
cell.media = media[indexPath.section]
cell.delegate = self
return cell
}
}
So this is actually all about learning what section a section header belongs to?? Here’s what I do. I have a header class:
class MyHeaderView : UITableViewHeaderFooterView {
var section = 0
}
I register it:
self.tableView.register(
MyHeaderView.self, forHeaderFooterViewReuseIdentifier: self.headerID)
I use and configure it:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let h = tableView
.dequeueReusableHeaderFooterView(withIdentifier: self.headerID) as! MyHeaderView
// other stuff
h.section = section // *
return h
}
Now if the header view is tappable or contains a button or whatever, learning what section this is the header of is trivial.
Your immediate issue is that you are using a table cell as a section header view. That should not be done. Once you resolve that, your next task is to determine the table section from the header view whose button was tapped.
First, change your MediaHeaderCell to be a header view that extends UITableViewHeaderFooterView and update your protocol accordingly:
protocol MediaHeaderViewDelegate: class {
func editPost(view: MediaHeaderView)
}
class MediaHeaderView: UITableViewHeaderFooterView {
weak var delegate: MediaHeaderViewDelegate?
#IBAction func moreOptionsAction(_ sender: UIButton) {
delegate?.editPost(cell: self)
}
}
Then you need to register the header view in your view controller.
Then update your viewForHeaderInSection:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: Storyboard.mediaHeaderView) as! MediaHeaderView
view.delegate = self
view.media = media[section]
view.tag = section
return view
}
And last, update your protocol method implementation:
func editPost(view: MediaHeaderView) {
let section = view.tag
// do something
}
There is one possible issue with this. If your table allows sections to be added or removed, then it is possible that a header view's tag could be wrong when the button is tapped.
I'm trying to create an application with sections in my TableView.
But actually, I don't know how to manage sections.
I create sections and it works fine but when I try to add a new row in my section I got a problem.
Example:
I Create a new Item in my first section.
The section name is "Aucun" and the rows label is going to set to "Test 1"
It works!
So, now I want to add something else
The section name is "Produits Laitiers" and the row label is "Test2"
FAIL :( The section is create but the row is not the good one
There is my code for the moment
func numberOfSections(in tableView: UITableView) -> Int {
return arraySection.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return arraySection[section]
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numberOfRowsInSection[section]
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell")
cell?.textLabel?.text = "\(String(describing: products[indexPath.row]["Name"]!))"
cell?.textLabel?.adjustsFontSizeToFitWidth = true
cell?.textLabel?.font = UIFont(name: "Arial", size: 14)
return cell!
}
numberOfRowsInSection is an array of int where I store the number of products which have to be in this section.
You are getting "Test 1" each time, because you have sections, but you are always trying to get a value by using an index of a row without checking an index of a section:
\(String(describing: products[indexPath.row]["Name"]!))
If all your values for cells are storing in a single array, then you should get a value from an array by using the section number:
\(String(describing: products[indexPath.section]["Name"]!))
But this will work only if each section will have only one row. Otherwise you will have to get the number of section first to detect the section where current row is allocated and then get the number of row.
If you have an array of sections with array of rows for each section, that will look like this:
let arraySection = [ [section #0 values], [section #1 values], ...
]
Then you can get the value for each row by using this:
let value = arraySection[indexPath.section][indexPath.row]
========EDIT===========
But there is a much better way - use objects or structs
struct Row {
var value: String = ""
}
struct Section {
var name: String = ""
var values: [Row] = []
}
So, using this structs, your code will be changed:
//your array of sections will contain objects
let arraySection: [Section] = []
func numberOfSections(in tableView: UITableView) -> Int {
return arraySection.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
//get the name of the section
return arraySection[section].name
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//check the count of rows for each section
return arraySection[section].values.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell")
// now we can get the value for current row by checking sections and then rows
cell?.textLabel?.text = arraySection[indexPath.section].values[indexPath.row].value
cell?.textLabel?.adjustsFontSizeToFitWidth = true
cell?.textLabel?.font = UIFont(name: "Arial", size: 14)
return cell!
}
I think the problem is with the products array in the UITableViewDataSource method cellForRowAt. You are accessing the same array element for the first object in each section with
products[indexPath.row]["Name"]!
Probably you need to adjust your model to handle the indexPath.section.
The first thing I going to creates a Section model to keep track of the sections, like the following:
/// Defines a section in data source
struct Section {
// MARK: - Properties
/// The title of the section
let title: String
/// The items in the section
var items: [String]
}
Then I going to use a UITableViewController with an Add right button to insert new section/rows to the UITableView, to add new sections/row I going to use a UIAlertController for the sake of brevity with two UITextField's inside where you need to put the name of the section and the name of the row. At the end should look like the following image:
In case the section already exist is going to add the new row to the section, otherwise, is going to create the new section and row dynamically.
DynamicTableViewController
class DynamicTableViewController: UITableViewController {
// MARK: - Properties
/// The data source for the table view
var dataSource = [Section(title: "Section 1", items: ["Row 1"])]
// MARK: - UIViewController
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return dataSource.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource[section].items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = dataSource[indexPath.section].items[indexPath.row]
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return dataSource[section].title
}
#IBAction func didTapAddButton(_ sender: Any) {
presentAlerController()
}
}
extension DynamicTableViewController {
func add(_ sectionName: String?, _ row: String?) {
guard let name = sectionName, let rowName = row,
!name.isEmpty, !rowName.isEmpty else {
return
}
if let index = dataSource.index(where: { $0.title == name }) {
dataSource[index].items.append(rowName)
} else {
dataSource.append(Section(title: name, items: [rowName]))
}
tableView.reloadData()
}
func presentAlerController() {
let alertController = UIAlertController(title: "Add", message: "Add new Section/Row", preferredStyle: .alert)
let addAction = UIAlertAction(title: "Add", style: .default) { [weak self] _ in
let sectionTextField = alertController.textFields![0] as UITextField
let rowTextField = alertController.textFields![1] as UITextField
self?.add(sectionTextField.text, rowTextField.text)
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in }
alertController.addTextField { textField in
textField.placeholder = "Add a new section name"
}
alertController.addTextField { textField in
textField.placeholder = "Add a new row"
}
alertController.addAction(addAction)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
}
In the above example, I'm not checking for the existence of duplicate row, you can do it is very easy. Also, I'm using the reloadData(), but you can use too if you want :
// insert the new section with row of the row in the existent section
tableView.beginUpdates()
// insert new section in case of any
insertSections(_ sections: IndexSet, with animation: UITableViewRowAnimation)
// insert new row in section using:
// insertRows(at: [IndexPath], with: UITableViewRowAnimation)
tableView.endUpdates()
Also you need to create a new UIBarButtonItem and connect it to the #IBAction I create it to be able to present the UIAlertController and add new section/row to the UITableView.
I hope this helps you.