I have following code to display data in table view.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? ArticleTalbeViewCell else {
fatalError("ArticleTableViewCell not found")
}
let articleVM = self.articleListVM.articleAtIndex(indexPath.row)
// cell.viewModel = articleVM
cell.titlelabel?.text = articleVM.title
cell.descriptionLabel?.text = articleVM.description
return cell
}
}
Now, my code with
cell.titlelabel?.text = articleVM.title
cell.descriptionLabel?.text = articleVM.description
work well.
Is cell.viewModel = articleVM a good practice?
Imagine I must set the date to the cell many times? This approach cell.viewModel = articleVM will save several lines.
The UITableViewCell's code is below:
class ArticleTalbeViewCell: UITableViewCell {
var titlelabel:UILabel?
var descriptionLabel:UILabel?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setupUI()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("init(coder:) has not been implemented")
}
private func setupUI() -> () {
let label1 = UILabel()
label1.numberOfLines = 0
let label2 = UILabel()
label2.numberOfLines = 0
label2.textColor = UIColor.lightGray
label2.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.vertical)
titlelabel = label1
descriptionLabel = label2
let staview = UIStackView()
staview.axis = .vertical
staview.addArrangedSubview(label1)
staview.addArrangedSubview(label2)
staview.spacing = 8
staview.translatesAutoresizingMaskIntoConstraints = false // !important
self.contentView.addSubview(staview)
staview.topAnchor.constraint(equalTo: self.contentView.topAnchor,constant: 5).isActive = true
staview.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -5).isActive = true
staview.leftAnchor.constraint(equalTo: self.contentView.leftAnchor, constant: 5).isActive = true
staview.rightAnchor.constraint(equalTo: self.contentView.rightAnchor, constant: -5).isActive = true
}
}
The ArticleListViewModel code is below:
struct ArticleListViewModel {
let articles:[Article]
}
extension ArticleListViewModel {
var numbeOfSections: Int {
return 1
}
func numOfRowsInSection(_ section: Int) -> Int {
return self.articles.count
}
func articleAtIndex(_ index: Int) -> ArticleViewModel {
let article = self.articles[index]
return ArticleViewModel(article)
}
}
struct ArticleViewModel {
private let article: Article
}
extension ArticleViewModel {
init(_ article: Article) {
self.article = article
}
}
extension ArticleViewModel {
var title: String {
return self.article.title ?? "null"
}
var description: String {
return self.article.description ?? "null"
}
}
The ArticleList code is below:
import Foundation
struct ArticleList: Decodable {
let articles: [Article]
}
struct Article: Decodable {
let title: String?
let description: String?
let chines: String?
}
How to edit the "cell" code to implement the cell.viewModel = articleVM?
First of all, I do not think giving cell.viewModel = articleVM does not change too much thing. Because of you have created ArticleListViewModel and ArticleViewModel it would be same if you create these vm's for your table view cell. However, I can give you some advices that is used by me while using MVVM approach.
You can add new property inside table view cell then using property observer give a value to it.
cell.article = articleVM
Therefore, either your cellForRowAt method won't be too long and it is almost same like giving your view model to your cell.
private var titlelabel:UILabel?
private var descriptionLabel:UILabel?
var article: ArticleViewModel? {
didSet {
guard let article = article else { return }
titlelabel?.text = article.title
descriptionLabel?.text = article.description
}
}
As far as I understand, your second question is if you have date variable and formatting inside cellForRowAt method takes too many spaces and looks ugly. That is why you need to do your business logic inside view model. Your table view cell is only responsible for showing it not to format it. I hope, it is clear for you. If you have any question you can ask.
Instead of configuring the cell in tableView(_:cellForRowAt:) method, you must configure your cell in the ArticleTalbeViewCell itself using the dataSource model, i.e.
class ArticleTalbeViewCell: UITableViewCell {
private var titlelabel: UILabel?
private var descriptionLabel: UILabel?
//rest of the code...
func configure(with article: Article) {
self.titlelabel?.text = article.title
self.descriptionLabel?.text = article.description
}
}
In the above code,
1. There is no need to expose titlelabel and descriptionLabel outside the class ArticleTalbeViewCell. So, mark both of them private.
2. You just need to call configure(with:) method along with the Article instance.
Why method and not a property of type Article?
There is no need to save the model object inside the ArticleTalbeViewCell until and unless it is required somewhere else after the cell is configured once.
In tableView(_:cellForRowAt:) method, you need to simply call configure(with:) method on your cell along with the article instance at that particular indexPath.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? ArticleTalbeViewCell else {
fatalError("ArticleTableViewCell not found")
}
let article = self.articleListVM.articleAtIndex(indexPath.row)
cell.configure(with: article)
return cell
}
Also, I don't think there is any need to create a separate ArticleViewModel. It is not doing anything special. It's just an extra wrapper on Article.
You can simply return Article type from articleAtIndex(_:) method in extension ArticleListViewModel, i.e
func articleAtIndex(_ index: Int) -> Article {
let article = self.articles[index]
return article
}
Why configuration in custom UITableViewCell?
This is because your tableView might contain multiple custom UITableViewCells. Adding the configuration of all the cell in tableView(_:cellForRowAt:) will make the code hefty and unable to read. Its better that the cell specific configuration is handled by the cell itself instead of the ViewController.
Related
I am making a currency converter that updates currencies simultaneously as you type. The currencies are held in a tableView and the cell is a custom cell.
I want to be able to type into one cell, and see all the other cells update with the value from the conversion calculation. The calculation works, but I am not sure how to get the data back into the correct cells, as essentially there is only one as it is a reusable cell.
Here is the cell class, (I am just showing the input to keep things clear.):
class PageCell: UITableViewCell {
let cellCalcInput = UITextField()
func configureCustomCell() {
contentView.addSubview(cellCalcInput)
cellCalcInput.translatesAutoresizingMaskIntoConstraints = false
cellCalcInput.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -10).isActive = true
cellCalcInput.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
cellCalcInput.font = secondFont?.withSize(18)
cellCalcInput.textColor = .white
cellCalcInput.placeholder = "Enter an amount"
cellCalcInput.keyboardType = .decimalPad
cellCalcInput.borderStyle = .roundedRect
cellCalcInput.backgroundColor = .clear
cellCalcInput.isHidden = true
self.backgroundColor = .darkGray
contentView.contentMode = .scaleAspectFit
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String!) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Next I create the cells, I am showing more so that you get the idea of how I am setting the data for each cell to the selected currency.
Then I add a textFieldDidChange listener:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var coinName = String()
tableView.separatorStyle = .none
let cell:PageCell = tableView.dequeueReusableCell(withIdentifier: "PageCell") as! PageCell
cell.configureCustomCell()
let index = indexPath.row
let coins = Manager.shared.coins
let coin = coins[index]
var coinIndex = Int()
coinIndex = CGPrices.shared.coinData.index(where: { $0.id == coin })!
let unit = CGExchange.shared.exchangeData[0].rates[defaultCurrency]!.unit
coinIndexes.append(coinIndex)
//Prices from btc Exchange rate.
let btcPrice = CGPrices.shared.coinData[coinIndex].current_price!
let dcExchangeRate = CGExchange.shared.exchangeData[0].rates[defaultCurrency]!.value
let realPrice = (btcPrice*dcExchangeRate)
setBackground(dataIndex: coinIndex, contentView: cell.contentView)
coinName = CGPrices.shared.coinData[coinIndex].name
let imageString = CGPrices.shared.coinData[coinIndex].image
cell.theImageView.sd_setImage(with: URL(string: imageString), placeholderImage: UIImage(named: "CryptiXJustX"))
cell.cellTextLabel.text = coinName
cell.cellDetailLabel.text = "\(unit)\((round(1000*realPrice)/1000))"
cell.cellCalcInput.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
// here is the text listener
return cell
}
#objc func textFieldDidChange(_ textField: UITextField) {
var index = Int()
index = textField.tag
if textField.text != "" {
calculations(dataIndex: index, calcInput: textField)
} else {
print("no text")
}
}
and here is where I do the calculation when it is typed, and get the results, it is not complete but I need to now somehow get these results shown inside the UITextfield for each cell, relating to the correct currency.
var coinIndexes = [Int]()
var answers = [Double]()
//
var comparitorIndex = 0
func calculations(dataIndex: Int, calcInput: UITextField) {
let exchangeRate = CGExchange.shared.exchangeData[0].rates[defaultCurrency]!.value
//
let btcPrice = CGPrices.shared.coinData[dataIndex].current_price!
//
if answers.count < coinIndexes.count {
var calculation = ""
if CGPrices.shared.coinData[dataIndex].id == "bitcoin" {
calculation = String(Double(calcInput.text!)! / btcPrice)
} else {
calculation = String(Double(calcInput.text!)! * btcPrice)
}
let calcAsDouble = Double(calculation)
let calcFinal = Double(round(1000*calcAsDouble!)/1000)
var comparitor = coinIndexes[comparitorIndex]
var comparitorPrice = CGPrices.shared.coinData[comparitor].current_price!
var comparitorAnswer = (calcFinal/comparitorPrice)
answers.append(comparitorAnswer)
comparitorIndex += 1
print(comparitorAnswer)
calculations(dataIndex: dataIndex, calcInput: calcInput)
}
comparitorIndex = 0
}
Here basically I do the calculations based on which cell is being typed and I can find out which currency it is from the tag, and then using the index of the currency I can check my API and get its name and values, then I do the calculation to compare the other currencies to the value that the user entered. The calculations work and give the correct results, I just don't know how to send the results back into the correct cells. Thank you.
Sorry if it is very sloppy work I am still very new to coding.
You can put your results in a dictionary type [Int : Double] where Int is the coinIndex, and Double part is the answer from the conversion. Then, after your calculations finish, you can call tableView.reloadData.().
You also need to make modifications to your cellForRowAt to show the conversion.
Try this:
In your UIViewController, declare
var conversions: [Int : Double] = [:]
Then in cellForRowAt:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// your code here
let conversion = conversions[index]
// the ?? operator will pass "No Value" string if conversion is nil.
cell.cellCalcInput.text = String(conversion ?? "No Value")
}
You need to update the conversions in your calculation function
func calculations(dataIndex: Int, calcInput: UITextField) {
// your code here
conversions[comparatorIndex] = comparatorAnswer
// once all conversions are entered
tableView.reloadData()
}
I think you can improve your calculations function. You can iterate on the coin index instead of updating the answers recursively. Good luck!
I've got a tableViewCell that I need to have an array passed into the tableViewCell but not just passed into a text label or something like that. I'll let my code show.
My TableViewController:
let subjectsDict = ["Spanish": ["Lesson 1", "Lesson 2"], "Math":["Problem set 1", "Problem set 2"], "Science": ["Lab"]]
let subjectArray = ["Spanish", "Math", "Science"]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "subjectCell", for: indexPath) as? SubjectTableViewCell else {
return UITableViewCell()
}
cell.subjectList = subjectsDict[subjectArray[indexPath.row]]
return cell
}
And my tableViewCell looks like this.
class subjectTableViewCell: UITableViewCell {
var subjectList: [String] = []
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style , reuseIdentifier: reuseIdentifier)
setUpTable()
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
}
override func awakeFromNib() {
super.awakeFromNib()
setUpTable()
}
func setUpTable() {
print(subjectList)
}
//other code for creating the cell
}
But when I print the subjectList from the subjectTableViewCell it prints none
Your code makes no attempt to update the cell's content with the value of subjectList. All you show is a print.
Also note that your print is called before any attempt to set subjectList is made. And remember that cells get reused. setUpTable will only be called once but subjectList will be set over and over as the cell gets used.
The simplest solution is to update the cell when subjectList is set.
var subjectList: [String] = [] {
didSet {
textLabel?.text = subjectList.joined(separator: ", ")
}
}
I'm assuming you are using the standard textLabel property. If you have your own label then update accordingly.
If you just want to invoke setUpTable() when your subjectList in the cell gets updated, try using:
var subjectList: [String] = [] {
didSet {
setUpTable()
}
}
You're trying to print subjectList in the moment you initialise your table view cell so in this moment you haven't set subjectList yet. If you want to print subjectList you can do it after you set it.
After this line gets executed:
cell.subjectList = subjectsDict[subjectArray[indexPath.row]]
I have a cell class 'NewsCell' (subclass of UITableViewCell) that I use for two different kinds of news: OrganizationNews and ProjectNews. These news has common things, but some of elements are different. Namely, when my cell is used for ProjectNews I want to hide Organization's logo, when it is for OrganizationNews I want to hide Project's name button.
I have 'configureCell(_, forNews, ofProject)' method. I call it in 'NewsViewController'. I used 'removeFromSuperview' method, because I need to rearrange my elements in 'NewsCell'. Changing 'isHidden' value won't give me that effect.
So, that is the issue. I have 'Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value' exception in the lines projectNameButton.removeFromSuperview() or logoImageView.removeFromSuperview().
What should I do?
// NewsViewController.swift
func configureCell(_ cell: NewsCell, forNews news: News, ofProject project: Project? = nil) {
//...
if news is OrganizationNews {
cell.projectNameButton.removeFromSuperview()
} else if news is ProjectNews {
cell.logoImageView.removeFromSuperview()
}
// ...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let news = newsCollection[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellIdentifiers.newsCell, for: indexPath) as! NewsCell
configureCell(cell, forNews: news)
cell.delegate = self
return cell
}
A UITableView or UICollectionView are built on the reuse concept, where the cells are reused and repopulated when you work on it.
When you try to call dequeReusableCell(withIdentifier:), it sometimes returns something that is created before. So, suppose you dequed before something which had all controls, then removed one (removeFromSuperview), then tried to deque again, the new dequed one may NOT have the subview.
I think the best solution for you is making two different cells.
Example:
class BaseNewsCell: UITableViewCell {
// Put the common views here
}
class OrganizationNewsCell: BaseNewsCell {
// Put here things that are ONLY for OrganizationNewsCell
}
class ProjectNewsCell: BaseNewsCell {
// Put here things that are ONLY for ProjectNewsCell
}
Then deque them from 2 different identifier by two different storyboard cells, xibs.
Or
class BaseNewsCell: UITableViewCell {
// Put the common views here
}
class OrganizationNewsCell: BaseNewsCell {
// This happens when this kind of cell is created for the first time
override func awakeFromNib() {
super.awakeFromNib()
someNonCommon.removeFromSuperview()
}
}
class ProjectNewsCell: BaseNewsCell {
override func awakeFromNib() {
super.awakeFromNib()
someOtherNonCommon.removeFromSuperview()
}
}
Note: This violates Liskov's principle (one of the SOLID principles), because you remove functionality from superclass in the subclass.
Change the removing lines as below,
if news is OrganizationNews {
cell.projectNameButton?.removeFromSuperview()
} else if news is ProjectNews {
cell.logoImageView?.removeFromSuperview()
}
This will fix the issue. But a good approach would be to create separate classes for each cell. You can create a base class to keep common logic there.
You shouldn't remove the subview from the outside of the cell. Let's refactor your code.
NewsCell.swift
final class NewsCell: UITableViewCell {
enum Kind {
case organization
case project
}
var logoImageView: UIImageView?
let nameLabel = UILabel()
var kind: NewsCell.Kind {
didSet {
if kind != oldValue {
setupLogoImageView()
self.setNeedsLayout()
}
}
}
init(kind: NewsCell.Kind, reuseIdentifier: String?) {
self.kind = kind
super.init(style: .default, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Positioning
extension NewsCell {
override func layoutSubviews() {
super.layoutSubviews()
// Your layouting
switch kind {
case .organization:
// Setup frame for organization typed NewsCell
case .project:
// Setup frame for project typed NewsCell
}
}
}
// MARK: - Setup
extension NewsCell {
private func setupLogoImageView() {
logoImageView = kind == .organization ? UIImageView() : nil
}
}
How to use:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let news = newsCollection[indexPath.row]
var cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellIdentifiers.newsCell) as? NewsCell
if cell == nil {
cell = NewsCell(kind: .organization, reuseIdentifier: TableViewCellIdentifiers.newsCell)
}
cell!.kind = news is Organization ? .organization: .project
return cell!
}
I have question about the tableView.
Here is my tableView code
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tierCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "InterestRateTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? InterestRateTableViewCell else {
fatalError("The dequed cell is not an instance of InterestRateTableViewCell.")
}
cell.interestRateTextField.delegate = self
cell.rowLabel.text = "\(indexPath.row + 1)."
if let interestText = cell.interestRateTextField.text {
if let interest = Double(interestText){
interestRateArray[indexPath.row] = interest
} else {
interestRateArray[indexPath.row] = nil
}
} else {
interestRateArray[indexPath.row] = nil
}
return cell
}
As you can see, I have the cellForRowAt method to get the value from the textfields in the cell, and assign to my arrays. (I actually have 2 textfields per cell.)
Basically, I let the users input and edit the textfield until they are happy then click this calculate button, which will call the calculation method. In the calculation method I call the "tableView.reloadData()" first to gather data from the textfields before proceed with the actual calculation.
The problem was when I ran the app. I typed values in all the textfields then clicked "calculate", but it showed error like the textfields were still empty. I clicked again, and it worked. It's like I had to reload twice to get things going.
Can anyone help me out?
By the way, please excuse my English. I'm not from the country that speak English.
edited: It may be useful to post the calculate button code here as someone suggested. So, here is the code of calculate button
#IBAction func calculateRepayment(_ sender: UIButton) {
//Reload data to get the lastest interest rate and duration values
DispatchQueue.main.async {
self.interestRateTableView.reloadData()
}
//Get the loan value from the text field
if let loanText = loanTextField.text {
if let loanValue = Double(loanText) {
loan = loanValue
} else {
print("Can not convert loan value to type Double.")
return
}
} else {
print("Loan value is nil")
return
}
tiers = []
var index = 0
var tier: Tier
for _ in 0..<tierCount {
if let interestRateValue = interestRateArray[index] {
if let durationValue = durationArrayInMonth[index] {
tier = Tier(interestRateInYear: interestRateValue, tierInMonth: durationValue)
tiers.append(tier)
index += 1
} else {
print("Duration array contain nil")
return
}
} else {
print("Interest rate array contain nil")
return
}
}
let calculator = Calculator()
repayment = calculator.calculateRepayment(tiers: tiers, loan: loan!)
if let repaymentValue = repayment {
repaymentLabel.text = "\(repaymentValue)"
totalRepaymentLabel.text = "\(repaymentValue * Double(termInYear!) * 12)"
} else {
repaymentLabel.text = "Error Calculating"
totalRepaymentLabel.text = ""
}
}
cellForRowAt is used for initially creating and configuring each cell, so the textfields are empty when this method is called.
UITableView.reloadData() documentation:
// Reloads everything from scratch. Redisplays visible rows. Note that this will cause any existing drop placeholder rows to be removed.
open func reloadData()
As it says in Apple's comment above, UITableView.reloadData() will reload everything from scratch. That includes your text fields.
There are a number of ways to fix your issue, but it's hard to say the best way without more context. Here's an example that would fit the current context of your code fairly closely:
class MyCustomTableViewCell: UITableViewCell {
#IBOutlet weak var interestRateTextField: UITextField!
var interestRateChangedHandler: (() -> ()) = nil
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
interestRateTextField.addTarget(self, action: #selector(interestRateChanged), for: UIControlEvents.editingChanged)
}
#objc
func interestRateChanged() {
interestRateChangedHandler?()
}
}
and cellForRowAtIndex:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "InterestRateTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? InterestRateTableViewCell else {
fatalError("The dequed cell is not an instance of InterestRateTableViewCell.")
}
cell.rowLabel.text = "\(indexPath.row + 1)."
cell.interestRateChangedHandler = { [weak self] in
if let interestText = cell.interestRateTextField.text {
if let interest = Double(interestText){
self?.interestRateArray[indexPath.row] = interest
} else {
self?.interestRateArray[indexPath.row] = nil
}
} else {
self?.interestRateArray[indexPath.row] = nil
}
}
return cell
}
Still very much a Swift noob, I have been looking around for a proper way/best practice to manage row deletions in my UITableView (which uses custom UserCells) based on tapping a UIButton inside the UserCell using delegation which seems to be the cleanest way to do it.
I followed this example: UITableViewCell Buttons with action
What I have
UserCell class
protocol UserCellDelegate {
func didPressButton(_ tag: Int)
}
class UserCell: UITableViewCell {
var delegate: UserCellDelegate?
let addButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Add +", for: .normal)
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
addSubview(addButton)
addButton.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -6).isActive = true
addButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
addButton.heightAnchor.constraint(equalToConstant: self.frame.height / 2).isActive = true
addButton.widthAnchor.constraint(equalToConstant: self.frame.width / 6).isActive = true
}
func buttonPressed(_ sender: UIButton) {
delegate?.didPressButton(sender.tag)
}
}
TableViewController class:
class AddFriendsScreenController: UITableViewController, UserCellDelegate {
let cellId = "cellId"
var users = [User]()
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! UserCell
cell.delegate = self
cell.tag = indexPath.row
return cell
}
func didPressButton(_ tag: Int) {
let indexPath = IndexPath(row: tag, section: 0)
users.remove(at: tag)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
where the Users in users are appended with a call to the database in the view controller.
My issues
The button in each row of the Table View is clickable but does not do anything
The button seems to be clickable only when doing a "long press", i.e. finger stays on it for a ~0.5s time
Will this method guarantee that the indexPath is updated and will not fall out of scope ? I.e. if a row is deleted at index 0, will deleting the "new" row at index 0 work correctly or will this delete the row at index 1 ?
What I want
Being able to click the button in each row of the table, which would remove it from the tableview.
I must be getting something rather basic wrong and would really appreciate if a Swift knight could enlighten me.
Many thanks in advance.
There are at least 3 issues in your code:
In UserCell you should call:
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
once your cell has been instantiated (say, from your implementation of init(style:reuseIdentifier:)) so that self refers to an actual instance of UserCell.
In AddFriendsScreenController's tableView(_:cellForRowAt:) you are setting the tag of the cell itself (cell.tag = indexPath.row) but in your UserCell's buttonPressed(_:) you are using the tag of the button. You should modify that function to be:
func buttonPressed(_ sender: UIButton) {
//delegate?.didPressButton(sender.tag)
delegate?.didPressButton(self.tag)
}
As you guessed and as per Prema Janoti's answer you ought to reload you table view once you deleted a row as your cells' tags will be out of sync with their referring indexPaths. Ideally you should avoid relying on index paths to identify cells but that's another subject.
EDIT:
A simple solution to avoid tags being out of sync with index paths is to associate each cell with the User object they are supposed to represent:
First add a user property to your UserCell class:
class UserCell: UITableViewCell {
var user = User() // default with a dummy user
/* (...) */
}
Set this property to the correct User object from within tableView(_:cellForRowAt:):
//cell.tag = indexPath.row
cell.user = self.users[indexPath.row]
Modify the signature of your UserCellDelegate protocol method to pass the user property stored against the cell instead of its tag:
protocol UserCellDelegate {
//func didPressButton(_ tag: Int)
func didPressButtonFor(_ user: User)
}
Amend UserCell's buttonPressed(_:) action accordingly:
func buttonPressed(_ sender: UIButton) {
//delegate?.didPressButton(sender.tag)
//delegate?.didPressButton(self.tag)
delegate?.didPressButtonFor(self.user)
}
Finally, in your AddFriendsScreenController, identify the right row to delete based on the User position in the data source:
//func didPressButton(_ tag: Int) { /* (...) */ } // Scrap this.
func didPressButtonFor(_ user: User) {
if let index = users.index(where: { $0 === user }) {
let indexPath = IndexPath(row: index, section: 0)
users.remove(at: index)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
Note the if let index = ... construct (optional binding) and the triple === (identity operator).
This downside of this approach is that it will create tight coupling between your User and UserCell classes. Best practice would dictate using a more complex MVVM pattern for example, but that really is another subject...
There is a lot of bad/old code on the web, even on SO. What you posted has "bad practice" written all over it. So first a few pointers:
Avoid an UITableViewController at all cost. Have a normal view controller with a table view on it
Delegates should always be weak unless you are 100% sure what you are doing
Be more specific when naming protocols and protocol methods
Keep everything private if possible, if not then use fileprivate. Only use the rest if you are 100% sure it is a value you want to expose.
Avoid using tags at all cost
The following is an example of responsible table view with a single cell type which has a button that removes the current cell when pressed. The whole code can be pasted into your initial ViewController file when creating a new project. In storyboard a table view is added constraint left, right, top, bottom and an outlet to the view controller. Also a cell is added in the table view with a button in it that has an outlet to the cell MyTableViewCell and its identifier is set to "MyTableViewCell".
The rest should be explained in the comments.
class ViewController: UIViewController {
#IBOutlet private weak var tableView: UITableView? // By default use private and optional. Always. For all outlets. Only expose it if you really need it outside
fileprivate var myItems: [String]? // Use any objects you need.
override func viewDidLoad() {
super.viewDidLoad()
// Attach table viw to self
tableView?.delegate = self
tableView?.dataSource = self
// First refresh and reload the data
refreshFromData() // This is to ensure no defaults are visible in the beginning
reloadData()
}
private func reloadData() {
myItems = nil
// Simulate a data fetch
let queue = DispatchQueue(label: "test") // Just for the async example
queue.async {
let items: [String] = (1...100).flatMap { "Item: \($0)" } // Just generate some string
Thread.sleep(forTimeInterval: 3.0) // Wait 3 seconds
DispatchQueue.main.async { // Go back to main thread
self.myItems = items // Assign data source to self
self.refreshFromData() // Now refresh the table view
}
}
}
private func refreshFromData() {
tableView?.reloadData()
tableView?.isHidden = myItems == nil
// Add other stuff that need updating here if needed
}
/// Will remove an item from the data source and update the array
///
/// - Parameter item: The item to remove
fileprivate func removeItem(item: String) {
if let index = myItems?.index(of: item) { // Get the index of the object
tableView?.beginUpdates() // Begin updates so the table view saves the current state
myItems = myItems?.filter { $0 != item } // Update our data source first
tableView?.deleteRows(at: [IndexPath(row: index, section: 0)], with: .fade) // Do the table view cell modifications
tableView?.endUpdates() // Commit the modifications
}
}
}
// MARK: - UITableViewDelegate, UITableViewDataSource
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myItems?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCell", for: indexPath) as? MyTableViewCell {
cell.item = myItems?[indexPath.row]
cell.delegate = self
return cell
} else {
return UITableViewCell()
}
}
}
// MARK: - MyTableViewCellDelegate
extension ViewController: MyTableViewCellDelegate {
func myTableViewCell(pressedMainButton sender: MyTableViewCell) {
guard let item = sender.item else {
return
}
// Delete the item if main button is pressed
removeItem(item: item)
}
}
protocol MyTableViewCellDelegate: class { // We need ": class" so the delegate can be marked as weak
/// Called on main button pressed
///
/// - Parameter sender: The sender cell
func myTableViewCell(pressedMainButton sender: MyTableViewCell)
}
class MyTableViewCell: UITableViewCell {
#IBOutlet private weak var button: UIButton?
weak var delegate: MyTableViewCellDelegate? // Must be weak or we can have a retain cycle and create a memory leak
var item: String? {
didSet {
button?.setTitle(item, for: .normal)
}
}
#IBAction private func buttonPressed(_ sender: Any) {
delegate?.myTableViewCell(pressedMainButton: self)
}
}
In your case the String should be replaced by the User. Next to that you will have a few changes such as the didSet in the cell (button?.setTitle(item.name, for: .normal) for instance) and the filter method should use === or compare some id or something.
try this -
update didPressButton method like below -
func didPressButton(_ tag: Int) {
let indexPath = IndexPath(row: tag, section: 0)
users.remove(at: tag)
tableView.reloadData()
}