Swift 4: Button triggers the same action in another UITableViewCell - uitableview

I have a cell with 2 buttons (for listening and speech recognition), label, and textfield. What I am trying to achieve is when the speech recognition button is selected the user speaks the content that is displayed in a label.
My issue with this is that listening button works fine according to the indexPath.row, but the speaking button doesn't. As when it is active, the button in another cell is becoming active too. And it records the same in those cells.
You can see the picture of what I am talking about here
The methods for listening (which is audio synthesizer) and speech recognition are in the UITableViewCell. I have tried all the solutions I could find online, none of them did the trick. Have tried
protocol RepeatCellDelegate: class {
func buttonTapped(cell: RepeatCell)
}
but the problem remains the same. Also, have created another project and instead of using the button to do speech recognition I just used direct textField input, still the same problem occurs.
Button in TableViewCell class:
#IBAction func speakButtonPressed(_ sender: Any) {
self.delegate?.buttonTapped(cell: self)
}
My cellForRowAt indexPath:
let cell = tableView.dequeueReusableCell(withIdentifier: "RepeatCell") as! RepeatCell
cell.delegate = self
cell.conditionlabel.text = repeatTask[indexPath.row].conditionLabel
return cell
The buttonTapped function which detects the cell index and record speech input. It prints right cell index after the button is tapped, but the action gets triggered in another cell too.
func buttonTapped(cell: RepeatCell) {
guard let indexPath = self.repeatTV.indexPath(for: cell) else {
return
}
cell.speakButton.isSelected = !cell.speakButton.isSelected
if (cell.speakButton.isSelected){
self.recordAndRecognizeSpeech()
} else {
audioEngine.inputNode.removeTap(onBus: 0)
recognitionTask?.cancel()
}
print("Button tapped on row \(indexPath.row)")
}
// the speech input recognizer function:
// variables for speech recognizer
let audioEngine = AVAudioEngine()
let speechRecognizer: SFSpeechRecognizer? = SFSpeechRecognizer(locale: Locale.init(identifier: "en-US"))
let request = SFSpeechAudioBufferRecognitionRequest()
var recognitionTask: SFSpeechRecognitionTask?
// speech function
func recordAndRecognizeSpeech(){
let node = audioEngine.inputNode
let recordingFormat = node.outputFormat(forBus: 0)
node.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, _ in
self.request.append(buffer)
}
audioEngine.prepare()
do {
try audioEngine.start()
} catch {
return print(error)
}
guard let myRecognizer = SFSpeechRecognizer() else {
return
}
if !myRecognizer.isAvailable {
return
}
recognitionTask = speechRecognizer?.recognitionTask(with: request, resultHandler: { (result, error) in
if result != nil { //
if let result = result{
let cell = self.repeatTV.dequeueReusableCell(withIdentifier: "RepeatCell") as! RepeatCell
let bestString = result.bestTranscription.formattedString
if cell.speakButton.isSelected == true {
cell.userInput.text = bestString
}
}else if let error = error{
print(error)
}
}
})
}
I get data from a local JSON file and this is a model:
struct RepeatTask: Codable {
let name: String
let label: String
let conditionWord: String
}
Perhaps someone could help me with this?

There isn't enough code here to re-create your issue, in the future please provide a Minimal, Complete, and Verifiable example. Unfortunately, no one can offer you a effective solution to help you resolve the issue if they cannot re-create the problem.
However I believe I understand what you are trying to accomplish:
A Model Object i.e. a struct.
A Protocol with a default implementation that is the same for all cells.
A TableViewCell class conforming to the protocol that calls the protocol's methods.
A TableViewDelegate and Datasource to manage the objects from 1.
Consider the following:
import UIKit
/// 1.
/// Data model for "Repeat Cell Objects"
struct RepeaterModel {
var outputText:String?
var inputAudio:Data?
}
/// 2.
/// Allows a cell to delegate listening and repeating (speaking)
protocol RepeatableCell {
func listen()
func speak()
}
// Extend your protocol to add a default implementation,
// that way you can just confrom to the protocol
// without implementing it every time, in every cell class.
extension RepeatableCell {
func listen() {
print("default implementation for listen")
}
func speak(){
print("default implementation for speak")
}
}
/// 3.
final class RepeatCell: UITableViewCell, RepeatableCell {
// MARK: - Properties
var model:RepeaterModel? {
didSet {
DispatchQueue.main.async {
self.titleLabel.text = self.model?.outputText
}
}
}
// MARK: - Views
lazy var listenButton: UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("Listen", for: .normal)
btn.addTarget(self, action: #selector(activateListen), for: .touchUpInside)
btn.setTitleColor(.white, for: .normal)
btn.backgroundColor = .blue
btn.translatesAutoresizingMaskIntoConstraints = false
return btn
}()
lazy var speakButton: UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("Speak", for: .normal)
btn.addTarget(self, action: #selector(activateSpeak), for: .touchUpInside)
btn.setTitleColor(.white, for: .normal)
btn.backgroundColor = .green
btn.translatesAutoresizingMaskIntoConstraints = false
return btn
}()
let titleLabel: UILabel = {
let l = UILabel()
l.translatesAutoresizingMaskIntoConstraints = false
l.textColor = .black
l.textAlignment = .center
l.text = "No Text"
return l
}()
//MARK: - Initializers
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Class Methods
func setup() {
self.contentView.addSubview(listenButton)
self.contentView.addSubview(speakButton)
self.contentView.addSubview(titleLabel)
let spacing: CGFloat = 25.0
//Listen top left
listenButton.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: spacing).isActive = true
listenButton.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: spacing).isActive = true
listenButton.widthAnchor.constraint(equalToConstant: 100).isActive = true
listenButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
// title label, center top.
titleLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: spacing).isActive = true
titleLabel.leadingAnchor.constraint(equalTo: self.listenButton.trailingAnchor, constant: spacing).isActive = true
titleLabel.trailingAnchor.constraint(equalTo: self.speakButton.leadingAnchor, constant: -spacing).isActive = true
titleLabel.heightAnchor.constraint(equalToConstant: 50).isActive = true
//Speak top right
speakButton.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: spacing).isActive = true
speakButton.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -spacing).isActive = true
speakButton.widthAnchor.constraint(equalToConstant: 100).isActive = true
speakButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
#objc func activateListen() {
print("listen was pressed! on cell \(self.model?.outputText ?? "No Text")")
/// The user wants to listen
// call the delegate method..
listen()
// use self.model?.outputText
}
#objc func activateSpeak() {
print("Speak was pressed! on cell \(self.model?.outputText ?? "No Text")")
/// The user is speaking, record audio
// call the delegate method..
speak()
//self.model?.inputAudio = somedata
}
}
/// 4.
class ViewController: UITableViewController {
// Array of your model objects
var objects:[RepeaterModel] = []
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(RepeatCell.self, forCellReuseIdentifier: "Repeat")
// create or fetch model objects
let items = [
RepeaterModel(outputText: "1st Cell", inputAudio: nil),
RepeaterModel(outputText: "2nd Cell", inputAudio: nil),
RepeaterModel(outputText: "3rd Cell", inputAudio: nil),
RepeaterModel(outputText: "4th Cell", inputAudio: nil),
RepeaterModel(outputText: "5th Cell", inputAudio: nil),
RepeaterModel(outputText: "6th Cell", inputAudio: nil),
RepeaterModel(outputText: "8th Cell", inputAudio: nil),
RepeaterModel(outputText: "9th Cell", inputAudio: nil),
RepeaterModel(outputText: "10th Cell", inputAudio: nil),
RepeaterModel(outputText: "11th Cell", inputAudio: nil),
RepeaterModel(outputText: "12th Cell", inputAudio: nil)
]
self.objects += items
}
//MARK: - TableView Methods
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// 25 top spacing + 50 view element width + 25 bottom spacing
return 100.0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = self.tableView.dequeueReusableCell(withIdentifier: "Repeat") as? RepeatCell {
cell.model = objects[indexPath.row]
// other cell stuff
return cell
}
return UITableViewCell()
}
}
Pressing "Listen" then "Speak" on each cell going downward yields this output:

Related

How could UILabel always be nil -- Unexpectedly found nil while implicitly unwrapping an Optional value

As many people encountered, I tried to build tableView. I found many similar questions but it seems answers are not helping. I would be very grateful if anyone could help me. The problem I encountered:
Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
This is a description Xcode gives me
Here's what I did:
(1) I connected Labels in the storyboard to the class it related to, which should be right as it's not hollow.
(2) I used tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath), and I tried to print cell I got, all cells aren't nil and belongs to CollegeTableViewCell, which is correct.
(3) I changed the identifier of tableViewCell to Cell which matches, and I changed it's class to CollegeTableViewCell too.
My program crashed directly when it executes following code. I only works when I make labels optional. So the problem is what did I do wrong so that labels in cell are always nil?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CollegeTableViewCell
let college = colleges[indexPath.row]
cell.collegeName.text = college.name // <-CRASH
cell.collegeGeo.text = college.city + ", " + college.state
return cell
}
Following is my CollegeTableViewCell class:
class CollegeTableViewCell: UITableViewCell {
#IBOutlet weak var collegeName: UILabel!
#IBOutlet weak var collegeGeo: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
EDIT: more codes related to this problem.
class CollegeChooseViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var tableView: UITableView!
var colleges = [CollegeInfo]()
let searchController = UISearchController(searchResultsController: nil)
let collegeApiUrl = "https://api.collegeai.com/v1/api/autocomplete/colleges?api_key=b47484dd6e228ea2cc5e1bf6ca&query="
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.register(CollegeTableViewCell.self, forCellReuseIdentifier: "Cell")
getColleges(contentInSearch: "MIT")
}
func getColleges(contentInSearch: String) {
guard let url = URL(string: (collegeApiUrl + contentInSearch)) else { return }
URLSession.shared.fetchData(for: url) {(result: Result<Initial, Error>) in
switch result {
case .success(let initial):
self.colleges = initial.collegeList
DispatchQueue.main.async {
self.tableView.reloadData()
}
case .failure(let error):
print("failed fetching college list from API: \(error)")
}
}
}
}
extension URLSession {
func fetchData<T: Decodable>(for url: URL, completion: #escaping (Result<T, Error>) -> Void) {
self.dataTask(with: url) { (data, response, error) in
if let error = error {
completion(.failure(error))
}
if let data = data {
do {
let object = try JSONDecoder().decode(T.self, from: data)
completion(.success(object))
} catch let decoderError {
completion(.failure(decoderError))
}
}
}.resume()
}
}
extension CollegeChooseViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CollegeTableViewCell
let college = colleges[indexPath.row]
cell.collegeName.text = college.name // <-CRASH
cell.collegeGeo.text = college.city + ", " + college.state
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(colleges.count)
return colleges.count
}
}
class CollegeTableViewCell: UITableViewCell {
#IBOutlet weak var collegeName: UILabel!
#IBOutlet weak var collegeGeo: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(true, animated: true)
}
}
This is a sample of your tableview programmatically way... If I don't know where your data comes from, I used a simulation of your arrays... conform your controller to UITableViewDelegate and Datasource:
class YourController: UIViewController, UITableViewDelegate, UITableViewDataSource
Now set tableView and constraints
var name = ["Mike", "Jhon", "Carl", "Steve", "Elon", "Bill", "Bruce"] // simulation of your array
var city = ["Milano", "New Yor", "Paris", "Los Angeles", "Madrid", "Amsterdam", "Tokyo"] // simulation of your array
var state = ["Italia", "USA", "France", "USA", "Spain", "Holland", "Japan"] // simulation of your array
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkBlue
tableView.backgroundColor = .white
tableView.register(CollegeTableViewCell.self, forCellReuseIdentifier: "cellId") // register cell
tableView.delegate = self
tableView.dataSource = self
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.separatorColor = .lightGray
view.addSubview(tableView)
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
// this is my extension to configure navigation bar, you can configure it as you want
configureNavigationBar(largeTitleColor: .red, backgoundColor: .black, tintColor: .red, title: "Sample", preferredLargeTitle: true)
}
After that set your tableView Delegate and DataSource:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
// Mark: - set number of rows with your array.count
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return name.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let name = name[indexPath.row]
let city = city[indexPath.row]
let state = state[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! CollegeTableViewCell
cell.collegeName.text = name
cell.collegeGeo.text = "\(city), \(state)"
return cell
}
This is how your cell look like:
class CollegeTableViewCell: UITableViewCell {
let collegeName: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = .systemFont(ofSize: 16, weight: .semibold)
label.backgroundColor = .clear
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let collegeGeo: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = .systemFont(ofSize: 14, weight: .semibold)
label.backgroundColor = .clear
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = .ultraDark
let stackView = UIStackView(arrangedSubviews: [collegeName, collegeGeo]) // use stack view for automatic table view dimension
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.spacing = 2
stackView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stackView)
stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20).isActive = true
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20).isActive = true
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20).isActive = true
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
this is the result:
EDIT based on new information full code and Json decoder:
struct CollegeInfo: Decodable {
let collegeList: [MyDataResults]
}
struct MyDataResults: Decodable {
let id: String
let name: String
let city: String
let state: String
}
class tableController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var myData = [MyDataResults]() // simulation of your array
let urlString = "https://api.collegeai.com/v1/api/autocomplete/colleges?api_key=b47484dd6e228ea2cc5e1bf6ca&query="
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkBlue
tableView.backgroundColor = .white
tableView.register(CollegeTableViewCell.self, forCellReuseIdentifier: "cellId") // register cell
tableView.delegate = self
tableView.dataSource = self
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.separatorColor = .lightGray
view.addSubview(tableView)
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
// this is my extension to configure navigation bar, you can configure it as you want
configureNavigationBar(largeTitleColor: .red, backgoundColor: .black, tintColor: .red, title: "Sample", preferredLargeTitle: true)
fetchJson { [weak self] (res) in
switch res {
case .success(let dataResults):
dataResults.forEach { (dataresult) in
self?.myData.removeAll()
DispatchQueue.main.asyncAfter(deadline: .now() + 0) {
self?.myData = dataresult.collegeList
self?.tableView.reloadData()
}
}
case .failure(let err):
print("Failed to fetch json", err)
}
}
}
fileprivate func fetchJson(completion: #escaping (Result<[CollegeInfo], Error >) -> ()) {
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { data, resp, err in
if let err = err {
completion(.failure(err))
return
}
do {
guard let data = data else { return }
let results = try JSONDecoder().decode(CollegeInfo.self, from: data)
//succesful
completion(.success([results]))
} catch let jsonErr {
completion(.failure(jsonErr))
}
}.resume()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
// Mark: - set number of rows with your array.count
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let myResults = myData[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! CollegeTableViewCell
cell.collegeName.text = myResults.name
cell.collegeGeo.text = "\(myResults.city), \(myResults.state)"
return cell
}
The cell:
class CollegeTableViewCell: UITableViewCell {
let collegeName: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = .systemFont(ofSize: 16, weight: .semibold)
label.backgroundColor = .clear
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let collegeGeo: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = .systemFont(ofSize: 14, weight: .semibold)
label.backgroundColor = .clear
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = .ultraDark
let stackView = UIStackView(arrangedSubviews: [collegeName, collegeGeo]) // use stack view for automatic table view dimension
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.spacing = 2
stackView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stackView)
stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20).isActive = true
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20).isActive = true
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20).isActive = true
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The result:
you're using the wrong bundle name for dequeueReusableCell.
instead of cell use CollegeTableViewCell
it should be :
let cell = tableView.dequeueReusableCell(withIdentifier: "CollegeTableViewCell", for: indexPath) as! CollegeTableViewCell

UITableView CustomCell Reuse (ImageView in CustomCell)

I'm pretty new to iOS dev and I have an issue with UITableViewCell.
I guess it is related to dequeuing reusable cell.
I added an UIImageView to my custom table view cell and also added a tap gesture to make like/unlike function (image changes from an empty heart(unlike) to a filled heart(like) as tapped and reverse). The problem is when I scroll down, some of the cells are automatically tapped. I found out why this is happening, but still don't know how to fix it appropriately.
Below are my codes,
ViewController
import UIKit
struct CellData {
var title: String
var done: Bool
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var models = [CellData]()
private let tableView: UITableView = {
let table = UITableView()
table.register(TableViewCell.self, forCellReuseIdentifier: TableViewCell.identifier)
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.frame = view.bounds
tableView.delegate = self
tableView.dataSource = self
configure()
}
private func configure() {
self.models = Array(0...50).compactMap({
CellData(title: "\($0)", done: false)
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = models[indexPath.row]
guard let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.identifier, for: indexPath) as? TableViewCell else {
return UITableViewCell()
}
cell.textLabel?.text = model.title
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
tableView.reloadData()
}
}
TableViewCell
import UIKit
class TableViewCell: UITableViewCell {
let mainVC = ViewController()
static let identifier = "TableViewCell"
let likeImage: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(systemName: "heart")
imageView.tintColor = .darkGray
imageView.isUserInteractionEnabled = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(likeImage)
layout()
//Tap Gesture Recognizer 실행하기
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapImageView(_:)))
likeImage.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
}
override func prepareForReuse() {
super.prepareForReuse()
}
private func layout() {
likeImage.widthAnchor.constraint(equalToConstant: 30).isActive = true
likeImage.heightAnchor.constraint(equalToConstant: 30).isActive = true
likeImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
likeImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20).isActive = true
}
#objc func didTapImageView(_ sender: UITapGestureRecognizer) {
if likeImage.image == UIImage(systemName: "heart.fill"){
likeImage.image = UIImage(systemName: "heart")
likeImage.tintColor = .darkGray
} else {
likeImage.image = UIImage(systemName: "heart.fill")
likeImage.tintColor = .systemRed
}
}
}
This gif shows how it works now.
enter image description here
I've tried to use "done" property in CellData structure to capture the status of the uiimageview but failed (didn't know how to use that in the correct way).
I would be so happy if anyone can help this!
You've already figured out that the problem is cell reuse.
When you dequeue a cell to be shown, you are already setting the cell label's text based on your data:
cell.textLabel?.text = model.title
you also need to tell the cell whether to show the empty or filled heart image.
And, when the user taps that image, your cell needs to tell the controller to update the .done property of your data model.
That can be done with either a protocol/delegate pattern or, more commonly (particularly with Swift), using a closure.
Here's a quick modification of the code you posted... the comments should give you a good idea of what's going on:
struct CellData {
var title: String
var done: Bool
}
class ShinViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var models = [CellData]()
private let tableView: UITableView = {
let table = UITableView()
table.register(ShinTableViewCell.self, forCellReuseIdentifier: ShinTableViewCell.identifier)
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.frame = view.bounds
tableView.delegate = self
tableView.dataSource = self
configure()
}
private func configure() {
self.models = Array(0...50).compactMap({
CellData(title: "\($0)", done: false)
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ShinTableViewCell.identifier, for: indexPath) as! ShinTableViewCell
let model = models[indexPath.row]
cell.myLabel.text = model.title
// set the "heart" to true/false
cell.isLiked = model.done
// closure
cell.callback = { [weak self] theCell, isLiked in
guard let self = self,
let pth = self.tableView.indexPath(for: theCell)
else { return }
// update our data
self.models[pth.row].done = isLiked
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
class ShinTableViewCell: UITableViewCell {
// we'll use this closure to communicate with the controller
var callback: ((UITableViewCell, Bool) -> ())?
static let identifier = "TableViewCell"
let likeImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(systemName: "heart")
imageView.tintColor = .darkGray
imageView.isUserInteractionEnabled = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
let myLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
// we'll load the heart images once in init
// instead of loading them every time they change
var likeIMG: UIImage!
var unlikeIMG: UIImage!
var isLiked: Bool = false {
didSet {
// update the image in the image view
likeImageView.image = isLiked ? likeIMG : unlikeIMG
// update the tint
likeImageView.tintColor = isLiked ? .systemRed : .darkGray
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// make sure we load the heart images
guard let img1 = UIImage(systemName: "heart"),
let img2 = UIImage(systemName: "heart.fill")
else {
fatalError("Could not load the heart images!!!")
}
unlikeIMG = img1
likeIMG = img2
// add label and image view
contentView.addSubview(myLabel)
contentView.addSubview(likeImageView)
layout()
//Tap Gesture Recognizer 실행하기
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapImageView(_:)))
likeImageView.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
}
override func prepareForReuse() {
super.prepareForReuse()
}
private func layout() {
// let's use the "built-in" margins guide
let g = contentView.layoutMarginsGuide
// image view bottom constraint
let bottomConstraint = likeImageView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
// this will avoid auto-layout complaints
bottomConstraint.priority = .required - 1
NSLayoutConstraint.activate([
// constrain label leading
myLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
// center the label vertically
myLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// constrain image view trailing
likeImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// constrain image view to 30 x 30
likeImageView.widthAnchor.constraint(equalToConstant: 30),
likeImageView.heightAnchor.constraint(equalTo: likeImageView.widthAnchor),
// constrain image view top
likeImageView.topAnchor.constraint(equalTo: g.topAnchor),
// activate image view bottom constraint
bottomConstraint,
])
}
#objc func didTapImageView(_ sender: UITapGestureRecognizer) {
// toggle isLiked (true/false)
isLiked.toggle()
// inform the controller, so it can update the data
callback?(self, isLiked)
}
}

Can not validate if textfield is empty

I am posting this after having tried all the .isEmpty solutions i found.
I am unable to detect a value in textField. I have cells which are set to cell.selectionStyle = .none. These cells have a label and textFields.
I have given the cells identifiers:
let cell = addRestaurant.dequeueReusableCell(withIdentifier: String(describing: RestaurantAddViewCells.self), for: indexPath) as! RestaurantAddViewCells
My goal is to have a button which checks if any field is empty onclick.
let saveButton = UIBarButtonItem(image: UIImage(named: "save"), style: .plain, target: self, action: #selector(saveRestaurant))
Here is how i try to check if the textFields are empty or not:
#objc func saveRestaurant() {
if let indexPath = addRestaurant.indexPathForSelectedRow {
let cell = addRestaurant.cellForRow(at: indexPath) as! RestaurantAddViewCells
if (cell.nameTextField.text == "" || cell.typeTextField.text == "" || cell.locationTextField.text == "" || cell.hotelPhoneText.text == "") {
let saveAlertController = UIAlertController(title: "Fields Empty", message: "fill all fields", preferredStyle: .alert)
let saveAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
saveAlertController.addAction(saveAction)
self.present(saveAlertController, animated: false, completion: nil)
}
else { }
}
}
However nothing happens
I have tried and comment cell.selectionStyle = .none, still no effect
Here addRestaurant is tableView in same file while RestaurantAddViewCells is a class containing properties for labels , textFields
UPDATE - here is what i do in my cellForRowAt, sample for first two cells
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.row {
case 0:
let cell = addRestaurant.dequeueReusableCell(withIdentifier: String(describing: RestaurantAddViewCells.self), for: indexPath) as! RestaurantAddViewCells
view.addSubview(cell.contentView)
view.addSubview(cell.hotelImage)
view.addSubview(cell.imageButton)
view.addSubview(cell)
cell.imageButton.translatesAutoresizingMaskIntoConstraints = false
cell.hotelImage.translatesAutoresizingMaskIntoConstraints = false
//set the cell height
cell.heightAnchor.constraint(greaterThanOrEqualToConstant: 200).isActive = true
//set the hotelImage
cell.hotelImage.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
cell.hotelImage.heightAnchor.constraint(equalToConstant: 200).isActive = true
cell.hotelImage.leadingAnchor.constraint(equalTo: cell.leadingAnchor).isActive = true
cell.hotelImage.topAnchor.constraint(equalTo: cell.topAnchor).isActive = true
// pin it
//No need to pin it as width is already pinned to to lead and trail of screen
cell.imageButton.heightAnchor.constraint(equalToConstant: 30).isActive = true
cell.imageButton.widthAnchor.constraint(equalToConstant: 30).isActive = true
cell.imageButton.centerXAnchor.constraint(equalTo: cell.centerXAnchor).isActive = true
cell.imageButton.centerYAnchor.constraint(equalTo: cell.centerYAnchor).isActive = true
cell.imageButton.image = UIImage(named: "photo")
return cell
case 1:
let cell = addRestaurant.dequeueReusableCell(withIdentifier: String(describing: RestaurantAddViewCells.self), for: indexPath) as! RestaurantAddViewCells
cell.nameTextFiled.tag = 1
cell.nameTextFiled.delegate = self
cell.nameTextFiled.becomeFirstResponder()
cell.selectionStyle = .none
cell.heightAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive = true
view.addSubview(cell.nameLabel)
view.addSubview(cell.nameTextFiled)
view.addSubview(cell)
view.addSubview(cell.contentView)
cell.nameLabel.translatesAutoresizingMaskIntoConstraints = false
cell.nameTextFiled.translatesAutoresizingMaskIntoConstraints = false
cell.nameTextFiled.heightAnchor.constraint(equalToConstant: 50).isActive = true
cell.nameLabel.text = "Name:"
//Define custom fonts
let font = UIFont(name: "Rubik-Medium", size: 18)
let dynamicFonts = UIFontMetrics(forTextStyle: .body)
cell.nameLabel.font = dynamicFonts.scaledFont(for: font!)
cell.nameTextFiled.font = dynamicFonts.scaledFont(for: font!)
cell.nameTextFiled.borderStyle = .roundedRect
cell.nameTextFiled.placeholder = "Enter Your Name"
let stackName = UIStackView()
view.addSubview(stackName)
stackName.alignment = .top
stackName.axis = .vertical
stackName.spacing = 5.0
stackName.distribution = .fill
stackName.translatesAutoresizingMaskIntoConstraints = false
stackName.addArrangedSubview(cell.nameLabel)
stackName.addArrangedSubview(cell.nameTextFiled)
stackName.topAnchor.constraint(equalTo: cell.topAnchor, constant: 10).isActive = true
stackName.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 10).isActive = true
stackName.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -10).isActive = true
cell.nameTextFiled.trailingAnchor.constraint(equalTo: stackName.trailingAnchor).isActive = true
return cell
This is what my updated RestaurantAddViewCells lookslike with input from one member here
class RestaurantAddViewCells: UITableViewCell, UITextFieldDelegate, UITextViewDelegate {
var nameLabel: UILabel = UILabel()
var nameTextFiled: RoundedTextFields = RoundedTextFields()
var typeLabel: UILabel = UILabel()
var typeTextField: RoundedTextFields = RoundedTextFields()
var locationLabel: UILabel = UILabel()
var locationTextField: RoundedTextFields = RoundedTextFields()
var imageButton: UIImageView = UIImageView()
var hotelImage: UIImageView = UIImageView()
var hotelDescriptionLabel: UILabel = UILabel()
var hotelTextDescription: UITextView = UITextView()
var hotelPhonelabel: UILabel = UILabel()
var hotelPhoneText: RoundedTextFields = RoundedTextFields()
var isEmptyTextFields: Bool {
return nameTextFiled.text!.isEmpty || typeTextField.text!.isEmpty || locationTextField.text!.isEmpty || hotelTextDescription.text!.isEmpty || hotelPhoneText.text!.isEmpty
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
UPDATE 2 - so here is my update while trying to ask the question again, i have a tableview where each cell has some textfields, when i press a button before i do any thing with the data, i want to check if the fields are filled or not, how can i do this, now as per my design i use a class to define the view properties and call them and give then values where i use the tableView, i use cellForRowAt to define constraints and provide values to labels etc, but how can i successfully access them outside of cellForRowAt to check current state , with out totally changing the design of project
UPDATE 3 - it seems if i were to do the same thing in story board and add the outlets to same function it is capable of detecting a change in state of textbox, if it is empty or not, in case of trying to do it programatically, its not able to detect a change in state of textbox from empty to not empty, but i still have no way to check like in javascript if the textboxes are empty or not on button click
Update 4 - I am now using below code on button tap, but for some strange reason , i am not able to detect the text entered, it always keeps returning empty even if there is text there
#objc func saveRestaurant(sender: AnyObject) {
let cell = addRestaurant.dequeueReusableCell(withIdentifier: String(describing: RestaurantAddViewCells.self)) as! RestaurantAddViewCells
if cell.nameTextFiled.text == "" || cell.typeTextField.text == "" || cell.locationTextField.text == "" || cell.hotelPhoneText.text == "" || cell.hotelTextDescription.text == "" {
let alertController = UIAlertController(title: "Oops", message: "We can't proceed because one of the fields is blank. Please note that all fields are required.", preferredStyle: .alert)
let alertAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(alertAction)
present(alertController, animated: true, completion: nil)
// addRestaurant.reloadData()
return
}
else {
print("Name: \(cell.nameTextFiled.text ?? "")")
print("Type: \(cell.typeTextField.text ?? "")")
print("Location: \(cell.locationTextField.text ?? "")")
print("Phone: \(cell.hotelPhoneText.text ?? "")")
print("Description: \(cell.hotelTextDescription.text ?? "")")
dismiss(animated: true, completion: nil)
}
}
Update 5 - this is what has finally worked , but is very long and not so good, can any one suggest a loop for going through all the indexPath
#objc func saveRestaurant(sender: AnyObject) {
let index = IndexPath(row: 1, section: 0)
let cell: RestaurantAddViewCells = self.addRestaurant.cellForRow(at: index) as! RestaurantAddViewCells
let nameVal = cell.nameTextFiled.text!
let index1 = IndexPath(row: 2, section: 0)
let cell2: RestaurantAddViewCells = self.addRestaurant.cellForRow(at: index1) as! RestaurantAddViewCells
let typeVal = cell2.typeTextField.text!
let index2 = IndexPath(row: 3, section: 0)
let cell3: RestaurantAddViewCells = self.addRestaurant.cellForRow(at: index2) as! RestaurantAddViewCells
let locationVal = cell3.locationTextField.text!
let index3 = IndexPath(row: 4, section: 0)
let cell4: RestaurantAddViewCells = self.addRestaurant.cellForRow(at: index3) as! RestaurantAddViewCells
let phoneVal = cell4.hotelPhoneText.text!
let index4 = IndexPath(row: 5, section: 0)
let cell5: RestaurantAddViewCells = self.addRestaurant.cellForRow(at: index4) as! RestaurantAddViewCells
let descVal = cell5.hotelTextDescription.text!
if(nameVal == "" || typeVal == "" || locationVal == "" || phoneVal == "" || descVal == "") {
let saveAlertController = UIAlertController(title: "Fields Empty", message: "fill all fields", preferredStyle: .alert)
let saveAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
saveAlertController.addAction(saveAction)
self.present(saveAlertController, animated: false, completion: nil)
// return
}
else {
print("Name: \(nameVal)")
print("Type: \(typeVal)")
print("Location: \(locationVal)")
print("Phone: \(phoneVal)")
print("Description: \(descVal)")
self.navigationController?.popViewController(animated: false)
}
}
First of all, add a computed property in RestaurantAddViewCells that'll return if any of the textFields in the cell is empty, i.e.
class RestaurantAddViewCells: UITableViewCell {
#IBOutlet weak var nameTextField: UITextField!
#IBOutlet weak var typeTextField: UITextField!
var isEmptyTextFields: Bool {
return nameTextField.text!.isEmpty || typeTextField.text!.isEmpty
}
}
Now, your saveRestaurant() method will loop through the numberOfCells. Get the cell for each row and check if isEmptyTextFields returns true. In that case you need to show the alert.
#objc func saveRestaurant() {
let numberOfCells = 6
var isAnyTextFieldEmpty = false
(0..<numberOfCells).forEach {
let cell = tableView.cellForRow(at: IndexPath(row: $0, section: 0)) as! RestaurantAddViewCells
isAnyTextFieldEmpty = isAnyTextFieldEmpty || cell.isEmptyTextFields
}
if isAnyTextFieldEmpty {
//Show alert...
} else {
//....
}
}
I'll try to give you an answer as easy as I can make possible.
Problem
You're using a table view and it's cell which contains a textfield where user may enter some input, also the cell may get reused when scrolled and its possible we may lose input from user.
Solution
We have a textfield in our cells, and we need all the inputs from user to be stored somewhere lets say a dictionary of IndexPath as key and String as value. Something like var inputs:[IndexPath: String] = [:]. Whenever user enters something and as soon as leaves the textfield we'll store that input in our dictionary against it's cell indexPath. when user clicks on button, we'll loop through and check which textfield is empty
A very simple example is here
import UIKit
class TextFieldTableViewCell: UITableViewCell {
lazy var textField: UITextField = {
let textField = UITextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.placeholder = "Enter your text here"
return textField
} ()
lazy var label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.setContentHuggingPriority(.required, for: .horizontal)
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension TextFieldTableViewCell {
//MARK: Private
private func setupView() {
selectionStyle = .none
contentView.addSubview(label)
contentView.addSubview(textField)
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8).isActive = true
label.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 8).isActive = true
label.bottomAnchor.constraint(greaterThanOrEqualTo: contentView.bottomAnchor, constant: -8).isActive = true
label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
textField.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 8).isActive = true
textField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8).isActive = true
textField.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 8).isActive = true
textField.bottomAnchor.constraint(greaterThanOrEqualTo: contentView.bottomAnchor, constant: -8).isActive = true
textField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
}
}
class ViewController: UIViewController {
private lazy var tableView: UITableView = {
let tableView = UITableView.init(frame: .zero, style: .grouped)
tableView.translatesAutoresizingMaskIntoConstraints = false
return tableView
}()
private var inputs: [IndexPath: String] = [:]
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.dataSource = self
tableView.register(TextFieldTableViewCell.self, forCellReuseIdentifier: "TextFieldTableViewCell")
title = "Some random title"
let barbutton = UIBarButtonItem.init(barButtonSystemItem: .done, target: self, action: #selector(saveAction(_:)))
navigationItem.rightBarButtonItem = barbutton
}
#objc
func saveAction(_ sender: UIBarButtonItem) {
view.endEditing(true)
for i in 0 ..< tableView.numberOfSections {
for j in 0 ..< tableView.numberOfRows(inSection: i) {
let indexPath = IndexPath.init(row: j, section: i)
print("Input at indexPath: Row: \(indexPath.row), Section: \(indexPath.section)")
if let input = inputs[indexPath], input.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
print(input)
}
else {
print("user has not input any value or kept it empty")
}
print("__________")
}
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TextFieldTableViewCell", for: indexPath) as? TextFieldTableViewCell ?? TextFieldTableViewCell.init(style: .default, reuseIdentifier: "TextFieldTableViewCell")
cell.label.text = "Row: \(indexPath.row)"
cell.textField.delegate = self
cell.textField.text = inputs[indexPath]
return cell
}
}
extension ViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
if let cell = textField.superview?.superview as? TextFieldTableViewCell, let indexPath = tableView.indexPath(for: cell) {
inputs[indexPath] = textField.text
}
}
}
EDIT
After OPs comments, here's the solution that will work for OP
Solution:
As OP has only 6 cells, we can cache then in a dictionary and return from the dictionary whenever needed. (We're doing this only because of small number of table cells and OP's structure. I do not recommend this solution)
Add a new dictionary to your viewcontroller
private var cachedCell: [IndexPath: TextFieldTableViewCell] = [:]
Change Cell for row to this (Note that you should not reuse tableview cell as we're caching them in our cachedCell dictionary otherwise it may result in unknown. It is never recommended to cache cells by me as well as other developers)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = cachedCell[indexPath] {
return cell
}
let cell = TextFieldTableViewCell.init(style: .default, reuseIdentifier: "TextFieldTableViewCell")
cell.label.text = "Row: \(indexPath.row)"
cell.textField.delegate = self
cell.textField.text = inputs[indexPath]
cachedCell[indexPath] = cell
return cell
}
Change Save Action to
#objc
func saveAction(_ sender: UIBarButtonItem) {
view.endEditing(true)
for (indexPath, cell) in cachedCell {
print("Input at indexPath: Row: \(indexPath.row), Section: \(indexPath.section)")
if let input = cell.textField.text, input.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
print(input)
}
else {
print("user has not input any value or kept it empty")
}
print("__________")
}
}
If you have any other questions, you can ask it in comment section.
Happy Coding

UItableview Cell changes data and attributes randomly or while scrolling

I've been trying to tackle this issue for hours, I know my code isn't the fanciest but i just can't seem to pin point the problem. I have an array of NSmanagedobject that has attributes in it. if the attribute of "isComplete" is true i want the background color of my cell to be green. I have a custom view that creates a new nsmanaged object and adds it to the tableview. where by default it should add a white background cell. I know there's a lot of code to explain but it's been hours and i just can't figure out why my tableview is loading the cells data and configuration incorrectly. this is my view controller with the tableview inside.
import CoreData
import UIKit
var coreTasks: [NSManagedObject] = []
var taskAdditionView:TaskAdditionView!
let appDel : AppDelegate = UIApplication.shared.delegate as! AppDelegate
let context: NSManagedObjectContext = appDel.persistentContainer.viewContext
class HomeViewController: UIViewController {
#IBOutlet var addTaskFunction: UIBarButtonItem!
#IBOutlet var homeTableView: UITableView!
var todaysdeadlineLabel: UILabel!
#IBAction func addTaskFunctions(_ sender: UIBarButtonItem) {
animateIn()
addTaskFunction.isEnabled = false
}
func animateIn(){
taskAdditionView = TaskAdditionView() // the view where i create a new nsmanaged object
view.addSubview(taskAdditionView)
view.bringSubviewToFront(taskAdditionView)
}
func tableviewsConstraints(){
homeTableView.translatesAutoresizingMaskIntoConstraints = false
homeTableView.layer.cornerRadius = 4
homeTableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20).isActive = true
homeTableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20).isActive = true
homeTableView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 120).isActive = true
homeTableView.heightAnchor.constraint(equalToConstant: homeTableView.rowHeight * 3).isActive = true
homeTableView.layer.borderWidth = 0.5
homeTableView.layer.borderColor = UIColor
.black.cgColor
}
override func viewDidLoad() {
super.viewDidLoad()
loadTasks()
tableviewsConstraints()
NotificationCenter.default.addObserver(self, selector: #selector(reloadTable), name: NSNotification.Name(rawValue: "reloadTableNotification"), object: nil)
homeTableView.delegate = self
homeTableView.dataSource = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound,.badge]) { (didallow, error) in
}
}
#objc func reloadTable(notification:Notification){
animateOut()
DispatchQueue.main.async {
self.homeTableView.reloadData()
self.addTaskFunction.isEnabled = true
print("reloadTable() fired!")
}
}
}
extension HomeViewController: UITableViewDelegate,UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return coreTasks.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let task = coreTasks[indexPath.row] // creating a new task from the already stored task depending on the indexpath.row if indexPath.row is 3 then the task is tasks[3]
let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell") as! CustomCell // setting the identifier ( we have already set in the storyboard, the class of our cells to be our custom cell)
cell.setTask(task: task) // this changes the label and date text since an instance of the task contains both the task and the date
print("CellData Task :", task.value(forKey: "isComplete") as! Bool, task.value(forKey: "name") as! String)
if (task.value(forKey: "isComplete") as! Bool == true){
cell.labelsToYellow()
cell.backgroundColor = Colors.greencomplete
cell.selectionStyle = .none
}
return cell
}
// Leading Swipe Action
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let task = coreTasks[indexPath.row]
let complete = markComplete(at: indexPath)
if !(task.value(forKey: "isComplete") as! Bool){
return UISwipeActionsConfiguration(actions: [complete])
} else {
return UISwipeActionsConfiguration(actions: [])
}
}
// Mark as Completed Task
func markComplete(at: IndexPath) -> UIContextualAction {
let df = DateFormatter()
df.dateFormat = "dd-MM-yyyy" // assigning the date format
let now = df.string(from: Date()) // extracting the date with the given format
let cell = homeTableView.cellForRow(at: at) as! CustomCell
let task = coreTasks[at.row]
let completeActionImage = UIImage(named: "AddTask")?.withTintColor(.white)
let action = UIContextualAction(style: .normal, title: "Complete") { (action, view, completion) in
task.setValue(!(task.value(forKey: "isComplete") as! Bool), forKey: "isComplete")
self.homeTableView.cellForRow(at: at)?.backgroundColor = task.value(forKey: "isComplete") as! Bool ? Colors.greencomplete : .white
cell.backgroundColor = Colors.greencomplete
cell.labelsToYellow()
task.setValue("Finished " + now, forKey: "date")
do {
try
context.save()
self.homeTableView.reloadData()
} catch {
print("Markcomplete save error")
}
// cell.displayIcons(task: task)
completion(true)
}
//action.image = #imageLiteral(resourceName: "AddTask")
action.image = completeActionImage
action.backgroundColor = Colors.greencomplete
return action
}
// Trailing Swipe Actions
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let task = coreTasks[indexPath.row]
let important = importantAction(at: indexPath)
let delete = deleteAction(at: indexPath)
if task.value(forKey: "isComplete") as? Bool == true {
return UISwipeActionsConfiguration(actions: [delete])
} else {
return UISwipeActionsConfiguration(actions: [delete,important])
}
}
// Delete Action
func deleteAction(at: IndexPath) -> UIContextualAction {
// remove !!!! from coredata memory as well not just array
let deleteActionImage = UIImage(named: "Delete")?.withTintColor(.white)
let action = UIContextualAction(style: .destructive , title: "Delete") { (action, view, completion) in
let objectToDelete = coreTasks.remove(at: at.row)
context.delete(objectToDelete)
self.homeTableView.deleteRows(at: [at], with: .automatic)
do {
try
context.save()
} catch {
print("Problem while saving")
}
completion(true)
}
action.image = deleteActionImage
action.backgroundColor = Colors.reddelete
return action
}
func loadTasks(){
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Tasks")
do {
coreTasks = try context.fetch(request) as! [NSManagedObject]
print("loadTasks() fired!")
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
}
and this is my taskAddition view
import UserNotifications
import UIKit
import CoreData
class TaskAdditionView: UIView {
var importanceSegmentControl: CustomSegmentControl!
var headerLabel:UILabel!
var taskTextField: CustomTextField!
var submitButton:CustomButton!
var reminderSwitch: UISwitch!
var datePicker: UIDatePicker!
var dateSelected: Date?
var importanceValue: Int16 = 0
override init(frame: CGRect) {
super.init(frame: frame)
} // initiliaze the view like this TaskAdditionView()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setupSwitchPicker(){
reminderSwitch = UISwitch()
reminderSwitch.transform = CGAffineTransform(rotationAngle: CGFloat.pi/2)
datePicker = UIDatePicker()
datePicker.minimumDate = Date()
datePicker.addTarget(self, action: #selector(pickingDate(sender:)), for: .valueChanged)
}
#objc func pickingDate(sender: UIDatePicker){
self.dateSelected = sender.date
print("Date selected: \(dateSelected)")
}
func setupLabel(){
headerLabel = UILabel()
headerLabel?.text = "Add Task"
headerLabel.textAlignment = .center
headerLabel.textColor = .black
headerLabel?.font = UIFont(name: "AvenirNext-Bold", size: 30.0)
headerLabel?.backgroundColor = UIColor.clear
}
#objc func indexChanged(control : CustomSegmentControl) {
// This all works fine and it prints out the value of 3 on any click
switch control.selectedIndex {
case 0:
importanceValue = 0
print(importanceValue)
case 1:
importanceValue = 1
print(importanceValue)
case 2:
importanceValue = 2
print(importanceValue)
default:
break;
} //Switch
} // indexChanged for the Segmented Control
func setupSegmentControl(){
importanceSegmentControl = CustomSegmentControl()
importanceSegmentControl.addTarget(self, action: #selector(indexChanged(control:)),for: UIControl.Event.valueChanged)
}
func setupButton(){
let myAttributes = [ NSAttributedString.Key.font: UIFont(name: "AvenirNext-DemiBold", size: 18.0)! , NSAttributedString.Key.foregroundColor: UIColor.white ]
let myTitle = "Add"
let myAttributedTitle = NSAttributedString(string: myTitle, attributes: myAttributes)
submitButton = CustomButton()
submitButton.setAttributedTitle(myAttributedTitle, for: .normal)
submitButton.addTarget(self, action: #selector(submitFunction(sender:)), for: .touchUpInside)
}
// Submit Function
#objc func submitFunction(sender: CustomButton){
print("Worked")
submitButton.shake()
NotificationCenter.default.post(name: NSNotification.Name("reloadTableNotification") , object: nil)
addTask()
if (reminderSwitch.isOn){
setupNotification()
print(dateSelected)
}
NSLayoutConstraint.deactivate(self.constraints)
removeFromSuperview()
}
func setupTextField(){
taskTextField = CustomTextField()
}
func setupConstraints(){
setupLabel()
setupTextField()
setupSegmentControl()
setupButton()
setupSwitchPicker()
addSubview(headerLabel!)
addSubview(importanceSegmentControl!)
addSubview(taskTextField)
addSubview(submitButton)
reminderSwitch.transform = reminderSwitch.transform.rotated(by: -(.pi/2))
addSubview(reminderSwitch)
addSubview(datePicker)
}
override func didMoveToSuperview() {
setupConstraints()
}
override func removeFromSuperview() {
for view in self.subviews{
view.removeFromSuperview()
}
NSLayoutConstraint.deactivate(self.constraints)
removeAllConstraintsFromView(view: self)
}
func addTask(){
let df = DateFormatter()
df.dateFormat = "dd-MM-yyyy" // assigning the date format
let now = reminderSwitch.isOn ? "Deadline " + df.string(from: dateSelected!) : df.string(from: Date()) // extracting the date with the given format
print("Reminder Switch is ON: ", reminderSwitch.isOn)
// Adding a task to the array
let entity =
NSEntityDescription.entity(forEntityName: "Tasks",
in: context)!
let newTask = NSManagedObject(entity: entity, insertInto: context)
newTask.setValue(taskTextField.text!, forKey: "name")
newTask.setValue(false, forKey: "isComplete")
newTask.setValue(now, forKey: "date")
newTask.setValue(importanceValue, forKey: "importValue")
do {
try
context.save()
coreTasks.append(newTask)
print("addTask() fired!")
} catch {
print("Problem while saving")
}
}
func setupNotification(){
let currentDate = Date()
let interval = dateSelected?.timeIntervalSince(currentDate)
print(interval)
let notifcation = UNMutableNotificationContent()
notifcation.title = "Task Reminder"
notifcation.subtitle = taskTextField.text ?? "Empty"
notifcation.badge = 1
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: interval!, repeats: false)
let request = UNNotificationRequest(identifier: "taskReminder", content: notifcation, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
taskTextField.endEditing(true)
}
}
func removeAllConstraintsFromView(view: UIView) { for c in view.constraints { view.removeConstraint(c) } }
extension UIView {
func removeAllConstraints() {
let superViewConstraints = superview?.constraints.filter{ $0.firstItem === self || $0.secondItem === self } ?? []
superview?.removeConstraints(superViewConstraints + constraints)
}
}
and this is my custom tableview cell
import UIKit
import CoreData
class CustomCell: UITableViewCell {
#IBOutlet var taskLabel: UILabel!
#IBOutlet var dateLabel: UILabel!
func setTask(task: NSManagedObject ){
taskLabel.text = task.value(forKey: "name") as? String
dateLabel.text = task.value(forKey: "date") as? String
}
func labelsToYellow() {
taskLabel.textColor = .white
dateLabel.textColor = .white
}
func labelsToBlack() {
taskLabel.textColor = .black
dateLabel.textColor = .black
}
Ideally When i create a new task of type nsmanaged object via the task addition. my tableview that is populate by an array of nsmanagedobject should add the task in a cell with a background color white and the task labels accordingly. I have a contextual action that marks a task complete and makes the cell background green. weirdly enough it was working at some point. Now randomly sometimes the task cell is created with a green background and sometime the labels are blank or when i scroll down or up all the labels turn green. I'd really appreciate some help.
I've had this issue before, because TableViewCells are re-used you need to ensure you set the background regardless of if it is default or not.
So when you are adding in the code to set the background to green, add an else statement or before the query set the cell background to white/your default color Issue with UITableViewCells Repeating Content

When I change the text color of UITextField inside a UITableView, the color has applied to another UITextfield when I scroll

I really don't understand why this happen. I'm changing the text color of a UITextField that is inside a UITableView if the text has changed but when I scroll down or up the tableView other UITextFields change the text color.
How can solve this problem?
Here is my code and some screenshot of the problem:
import Foundation
import UIKit
private let reuseIdentifier = "ApcGenericCell"
private let portIdentifier = "ApcPortCell"
private let addressIdentifier = "ApcAddressCell"
class MyViewController: UIViewController {
private var tableView: UITableView!
private var bottomConstraint: NSLayoutConstraint!
private var newBottomConstraint: NSLayoutConstraint!
var apc = [Apc]()
override func viewDidLoad() {
super.viewDidLoad()
// configure the table view for the left menu
configureTableView()
}
private func configureTableView() {
tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = 60
tableView.register(UINib(nibName: "ApcGeneric", bundle: nil), forCellReuseIdentifier: reuseIdentifier)
tableView.register(UINib(nibName: "ApcPortAt", bundle: nil), forCellReuseIdentifier: portIdentifier)
tableView.register(UINib(nibName: "ApcIpAddress", bundle: nil), forCellReuseIdentifier: addressIdentifier)
self.view.addSubview(tableView)
tableView.backgroundColor = .clear
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: view.frame.height / 8).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: view.frame.width / 8).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -(view.frame.width / 8)).isActive = true
bottomConstraint = tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -(view.frame.height / 8))
bottomConstraint.isActive = true
tableView.alwaysBounceVertical = false
tableView.tableFooterView = UIView(frame: .zero)
// register for notifications when the keyboard appears and disappears:
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(note:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(note:)), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(receivedMessage), name: Notification.Name("ReceivedMessage"), object: nil)
apc = ApcSection.emptySection()
self.tableView.reloadData()
}
#objc func receivedMessage() {
DispatchQueue.global(qos: .default).async {
ApcSection.fetchData(map: self.apcConfig, { (apcResult) in
self.apc = apcResult
DispatchQueue.main.async {
self.tableView.reloadData()
}
})
}
}
// Handle keyboard frame changes here.
// Use the CGRect stored in the notification to determine what part of the screen the keyboard will cover.
// Adjust our table view's bottomAnchor so that the table view content avoids the part of the screen covered by the keyboard
#objc func keyboardWillShow(note: NSNotification) {
// read the CGRect from the notification (if any)
if let newFrame = (note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
if bottomConstraint.isActive {
bottomConstraint.isActive = false
newBottomConstraint = tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -newFrame.height)
newBottomConstraint.isActive = true
tableView.updateConstraints()
}
}
}
// User dismiss the keyboard
#objc func keyboardWillHide(note: NSNotification) {
newBottomConstraint.isActive = false
bottomConstraint.isActive = true
tableView.updateConstraints()
}
#objc func textHasChanged(sender: UITextField) {
let cell = sender.superview?.superview as! UITableViewCell
let indexPath = tableView.indexPath(for: cell)
if let index = indexPath?.row {
if let _ = apc[index] {
// change textColor if the value has been changed
if sender.text != apc[index]!) {
sender.textColor = .systemRed
} else {
sender.textColor = .myBlue
}
}
}
}
}
extension MyViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return apc.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: portIdentifier, for: indexPath) as! ApcPortAt
cell.selectionStyle = .none
return cell
} else if indexPath.row == 7 || indexPath.row == 9 {
let cell = tableView.dequeueReusableCell(withIdentifier: addressIdentifier, for: indexPath) as! ApcIpAddress
cell.selectionStyle = .none
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! ApcGeneric
cell.value.text = apc[indexPath.row]
cell.value.delegate = self
cell.value.addTarget(self, action: #selector(textHasChanged(sender:)), for: .editingChanged)
cell.selectionStyle = .none
return cell
}
}
}
Normal View
Editing
Scroll down after editing
Return to the top
You have 2 possible solutions
1. Apply the changes in cellForRowAt of UITableViewDataSource delegate.
2. Subclass UITableViewCell and override prepareForReuse() which within it you can make your updates. And don't forget to register the cell subclass with your table view.
I consider solution #1 much easier
In UITableView dequeueReusableCell- Each UITableViewCell will be reused several times with different data(image).
In your case, When you scrolled, cell at IndexPath(row: x, section: 0) was reused by another cell that is displaying at the top. Ex: cell with red text -> this cell x have red text, because you did not reset the text color to it
Solution:
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! ApcGeneric
cell.value.text = apc[indexPath.row]
if index == selectedIndex { // condition to red text
cell.value.textColor = .systemRed
} else {
cell.value.textColor = .myBlue
}
cell.value.delegate = self
cell.value.addTarget(self, action: #selector(textHasChanged(sender:)), for: .editingChanged)
cell.selectionStyle = .none
return cell

Resources