TextView custom subclass - textViewDidChange does not trigger - swift

I'm trying to add an variable to my TextView subclass but I can't get textViewDidChange to work properly once I do that and I think it's because the delegate.
It works without the subclass and prints the itemOrderID. How can I get this to work with my custom subclass?
class textViewSub: UITextView {
var itemOrderID: Double = 0.0
}
class TextviewCell: UITableViewCell, UITextViewDelegate {
#IBOutlet weak var textView: textViewSub!
#IBOutlet weak var textViewLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
textView.delegate = self
}
func textViewDidChange(_ textView: textViewSub) {
let id = textView.tag
let text = textView.text
let itemOrderID = textView.itemOrderID
print(itemOrderID)
}
}
cellForRowAt code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TextviewCell", for: indexPath) as! TextviewCell
cell.tag = indexPath.row
cell.textView.tag = indexPath.row
cell.textView.itemOrderID = checklistData[indexPath.section].checklistItems?[indexPath.row].itemOrderID ?? 0.0
cell.textView.delegate = cell
return cell
}

Your textViewDidChange function does not match the UITextViewDelegate protocol.
It should be:
func textViewDidChange(_ textView: UITextView) {
guard let sub = textView as? textViewSub else { return }
let id = sub.tag
let text = sub.text
let itemOrderID = sub.itemOrderID
print(itemOrderID)
}

Related

Problem using an extension to load image into a custom cell

My last question for today. I'm too newby with swift to know what's happening but my problem is as follows:
I've use the next extension to load an image from a URL and it works when I use it in a imageView in a normal view, but when the imageView is inside of a custom cell It doesn't recognize de method so I can`t use the extension. What am I doing wrong? Thank you all in advances.
My code for the extesion is:
import UIKit
extension UIImageView {
func loadFrom(URLAddress: String) {
guard let url = URL(string: URLAddress) else {
return
}
DispatchQueue.main.async { [weak self] in
if let imageData = try? Data(contentsOf: url) {
if let loadedImage = UIImage(data: imageData) {
self?.image = loadedImage
}
}
}
}
}
And the code for the table where I try to use it into a custom cell:
import UIKit
class EventosCustomCellController: UITableViewCell {
#IBOutlet weak var imEvento: UIView!
#IBOutlet weak var txtNombreEvento: UILabel!
#IBOutlet weak var txtFechaEvento: UILabel!
#IBOutlet weak var txtEstadoEvento: UILabel!
}
class ListaEventosTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Eventos"
}
// MARK: - Table view data source
override func viewWillAppear(_ animated: Bool) {
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return eventos.contarEventos()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "prototipoCeldaEvento", for: indexPath) as! EventosCustomCellController
let evento = eventos.buscarEventoPorID(id: indexPath.row)
cell.txtNombreEvento?.text = evento?.nombre
cell.txtFechaEvento?.text = evento?.fecha
cell.txtEstadoEvento?.text = evento?.tipo
if evento?.tipo == "deportivo"{
cell.backgroundColor = .blue}
else if evento?.tipo == "cultural"{
cell.aplicarFondoDegradado()
cell.backgroundColor = .green}
else{
cell.backgroundColor = .red}
cell.layer.masksToBounds = true
cell.layer.cornerRadius = 10
//There is no method loadFrom when I try to use as follows
cell.imEvento?.loadFrom(URLAddress: (evento?.imagenes![0])!)
return cell
}
// Override to support conditional editing of the table view.
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
}
After updating code my problem is that the cell isn`t growthn appropiately
I think, you better use UIImageView instead of UIView because your extension for UIImageView and you can access to this.
#IBOutlet weak var imEvento: UIImageView!
and then it is good to override the prepareForReuse method like this:
override func prepareForReuse() {
super.prepareForReuse()
imEvento.image = nil
}
you can read more about the prepareForReuse method from apple documentation

TableViewController issue: won't save user input from textView

Update:
so goal of this to save the text that user enters in a UITextView in cell, so that the text is saved for that particular cell number and does not duplicate, move or remove the text.
as suggested I am trying to handle the textViewdidChange func inside of the custom cell, by doing the following:
var onTextEntered: ((String?) -> ())!
func setup() {
notesTextView.delegate = self
}
func textViewDidChange(_ textView: UITextView) {
onTextEntered(notesTextView.text)
}
making a string that holds the text and then adding the text to the String every time textViewDidChange is called (trying to explain this to myself as we go along, so please correct me if my explanation needs it).
next in CellForRowAt
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "TableViewNotesCell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! TableViewNotesCell
cell.setup()
cell.onTextEntered = {input in
if let text = input {
self.cellText[indexPath.row] = text // here is the error (index out of range)
}
if indexPath.row < self.cellText.count {
cell.notesTextView.text = self.cellText[indexPath.row] ?? "placeholder"
}
}
return cell
}
when I do the above code, as soon as textViewDidChange is called (when i type a single letter or digit in textView) the I get the error : "Fatal error: Index out of range" on the line which I use the array of cellText[indexPath.row] = text. please help or let me know if my understanding of the process is wrong would love to learn!
You can try to save for every single edit
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// inside cellForRowAt
cell.textView.delegate = self
cell.textView.tag = indexPath.row
return cell
}
#objc func textViewDidChange(_ tex: UITextView) {
cellEndEdit[tex.tag] = tex.text!
}
class VC:UIViewController,UITextViewDelegate {
give default values for the array
var cellEndEdit = [String](repeating: "", count: numberOfRows)
Assuming, that you tableView has a variable number of cells, and all of them have a UITextView, whose content should be recorded and indexed, I would recommend to create a custom UITableViewCell, that handles the textView itself.
class CustomTableViewCell: UITableViewCell, UITextViewDelegate {
#IBOutlet weak var textView: UITextView()
var onTextEntered: ((String?) -> ()) //that's a callback
func setup() {
textView.delegate = self
}
override func textViewDidChange(textView: UITextView) {
onTextEntered(textView.text)
}
}
Since you're working with a sorted list of user inputs, you should have your array at hands, in which you can store and from which you can retrieve data. So if some data already exists, grep through your array and fill the cells that deserve it. Also define the onTextEntered callback here to tell the cell what to do, if it gets called (in your case, store the text of the UITextView in your array).
//your carrier, if you store the already existing user inputs some where, map them in here
//depending on how you calculate the number of cells, this array has to have the same size so use the same logic for this
var yourStringCarrierArray: [String?] = []
override func tableView(_ tableview: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "YourTableViewCell", for: indexPath) as! CustomTableViewCell
cell.setup()
cell.onTextEntered = { input in
if let text = input {
self.yourStringCarrierArray[indexPath.row] = text
}
if indexPath.row < yourStringCarrierArray.count {
cell.textView.text = yourStringCarrierArray[indexPath.row] ?? "placeholder string, because there's no input here so far"
}
}
I hope this will help or at least give you a new perspective, it has been a while, i coded in Swift. Feel free to ask me, if something is unclear.
Use an object to save the string value, since String in swift is value type. Here is an example:
class TestViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var dataArray: [CellData] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
tableView.delegate = self
tableView.dataSource = self
for _ in 0..<20 {
dataArray.append(CellData())
}
}
}
extension TestViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TestTableViewCell", for: indexPath) as! TestTableViewCell
cell.setData(data: dataArray[indexPath.row])
return cell
}
}
class TestTableViewCell: UITableViewCell {
#IBOutlet weak var textView: UITextView!
var data: CellData!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
textView.text = ""
textView.delegate = self
}
func setData(data: CellData) -> Void {
self.data = data
self.textView.text = data.stringValue
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
extension TestTableViewCell: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
self.data.stringValue = textView.text
}
}
class CellData {
var stringValue: String = ""
}

Protocol UITableViewDataSource is not called from xib - Swift

I don't know why my protocol UITableViewDataSource is not execute when i run the APP.
Here my xib design:
My init of class from xib is: CustomSearchDestination
#IBOutlet weak var searchText: UITextField!
#IBOutlet weak var tableView: UITableView!
let xibName = "CustomSearchDestination"
var destinationListLocal: [Destinos] = []
var SearchViewControllerView: SearchViewController?
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
xibSetup(xibName)
SearchViewControllerView = SearchViewController()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
searchText.delegate = self
tableView.delegate = self
tableView.dataSource = self
}
override init(frame: CGRect) {
super.init(frame : frame)
xibSetup(xibName)
}
Inside of this class have implemented the protocol UITableViewDelegate and UITableViewDataSource
extension CustomSearchDestination: UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("El count es: \(destinationListLocal.count)")
return destinationListLocal.count
}
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedCell: UITableViewCell = tableView.cellForRow(at: indexPath)!
searchText.text = selectedCell.textLabel!.text
}}
extension CustomSearchDestination: UITableViewDataSource {
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as UITableViewCell
let index = indexPath.row as Int
if destinationListLocal.count == 0 {
if let str = destinationListLocal[index].desDestino {
cell.textLabel?.text = str
}
}
return cell
}}
Can someone point me my mistake?
EDIT: My hierarchy is:
I have a xib where i put other xib like this:
this is my class CustomViewHandler. So.. my class CustomSearchDestination is a class of the superClass CustomViewHandler.
At the same time is a class of CustomSearch
class CustomViewHandler : UIView {
var view: UIView!
var controllerView : UIView!
// Keyboard
var activeField : UITextField?
var isKeyboardVisible = false
var fixValue : CGFloat?
var keyboardSize : CGFloat = 280
fileprivate func loadViewFromNib(_ nibName: String) -> UIView {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: nibName, bundle: bundle)
// Assumes UIView is top level and only object in CustomView.xib file
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
return view
}
func xibSetup(_ nibName: String) {
view = loadViewFromNib(nibName)
// use bounds not frame or it'll be offset
view.frame = bounds
// Makes the view stretch with containing view
view.autoresizingMask = [UIViewAutoresizing.flexibleWidth, UIViewAutoresizing.flexibleHeight]
addSubview(view)
}}

Retrieve information from Table Cell?

I have a custom UITableViewCell with 2 labels and a button. The cell has it's own class:
class personTableCell: UITableViewCell {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var emailLabel: UILabel!
#IBAction func inviteButtonPressed(_ sender: Any) {
self.accessoryType = .checkmark
}
}
Inside the view controller that contains the table view, I add the cells to the table in this method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "person", for: indexPath) as? personTableCell
cell?.nameLabel.text = results[indexPath.row].name
cell?.emailLabel.text = results[indexPath.row].email
return cell!
}
When a user presses the button inside the cell that calls the #IBAction func inviteButtonPressed, I want to add the cell's labels' text into an array that's initialized in the same view controller as the table.
How can I achieve such a thing if the #IBAction func inviteButtonPressed is in a seperate file as the table's view controller?
I think using delegate is one of solutions.
In TableViewCell class
#objc protocol PersonTableViewCellDelegate {
func personTableViewCellInviteButtonPressed(cell: PersonTableViewCell)
}
class PersonTableViewCell: UITableViewCell {
weak var delegate: PersonTableViewCellDelegate?
#IBAction func inviteButtonPressed(_ sender: Any) {
delegate?.personTableViewCellInviteButtonPressed(cell: self)
}
}
In ViewController class
class TableViewController: UITableViewController, PersonTableViewCellDelegate {
var results: [Person] = []
var invited: [Person] = []
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "person", for: indexPath) as! PersonTableViewCell
cell.nameLabel.text = results[indexPath.row].name
cell.emailLabel.text = results[indexPath.row].email
cell.delegate = self
return cell
}
func personTableViewCellInviteButtonPressed(cell: PersonTableViewCell) {
guard let indexPath = tableView.indexPath(for: cell) else {
return
}
let person = results[indexPath.row]
invited.append(person)
}
}

How can I get indexPath.row in cell.swift

I have 2 files.
myTableViewController.swift
myTableCell.swift
Can I get the indexPath.row in myTabelCell.swift function?
Here is myTableCell.swift
import UIKit
import Parse
import ActiveLabel
class myTableCell : UITableViewCell {
//Button
#IBOutlet weak var commentBtn: UIButton!
#IBOutlet weak var likeBtn: UIButton!
#IBOutlet weak var moreBtn: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
}
#IBAction func likeBtnTapped(_ sender: AnyObject) {
//declare title of button
let title = sender.title(for: UIControlState())
//I want get indexPath.row in here!
}
Here is myTableViewController.swift
class myTableViewController: UITableViewController {
//Default func
override func viewDidLoad() {
super.viewDidLoad()
//automatic row height
tableView.estimatedRowHeight = 450
tableView.rowHeight = UITableViewAutomaticDimension
}
// cell config
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//define cell
let cell = tableView.dequeueReusableCell(withIdentifier: "myTableCell", for: indexPath) as! myTableCell
}
As you can see... I'm trying to get indexPath.row in myTableCell, liktBtnTapped function.
Could you let me know how can I access or get IndexPath.row?
I have created a UIResponder extension with a recursive method that you can use in any UIView (which inherits from UIResponder) to find a parent view of a specific type.
import UIKit
extension UIResponder {
/**
* Returns the next responder in the responder chain cast to the given type, or
* if nil, recurses the chain until the next responder is nil or castable.
*/
func next<U: UIResponder>(of type: U.Type = U.self) -> U? {
return self.next.flatMap({ $0 as? U ?? $0.next() })
}
}
Using this, we can extend UITableViewCell with some convenient read-only computed properties for the table view and index path of the cell.
extension UITableViewCell {
var tableView: UITableView? {
return self.next(of: UITableView.self)
}
var indexPath: IndexPath? {
return self.tableView?.indexPath(for: self)
}
}
Here is how you could use it in your example:
#IBAction func likeBtnTapped(_ sender: AnyObject) {
//declare title of button
let title = sender.title(for: UIControlState())
//I want get indexPath.row in here!
self.indexPath.flatMap { print($0) }
}
Swift 4+
Try this inside your cell.
func getIndexPath() -> IndexPath? {
guard let superView = self.superview as? UITableView else {
print("superview is not a UITableView - getIndexPath")
return nil
}
indexPath = superView.indexPath(for: self)
return indexPath
}
Easy.. You can do like this inside button action:
let section = 0
let row = sender.tag
let indexPath = IndexPath(row: row, section: section)
let cell: myTableCell = self.feedTableView.cellForRow(at: indexPath) as! myTableCell
And afterwards in cellForRowAtIndexPath:
// add the row as the tag
cell.button.tag = indexPath.row
Another Approach for Swift 4.2 and not assuming Superview will be always a tableview
extension UITableViewCell{
var tableView:UITableView?{
return superview as? UITableView
}
var indexPath:IndexPath?{
return tableView?.indexPath(for: self)
}
}
Usage example
#IBAction func checkBoxAction(_ sender: UIButton) {
guard let indexPath = indexPath else { return }
sender.isSelected = !sender.isSelected
myCustomCellDelegate?.checkBoxTableViewCell(didSelectCheckBox: sender.isSelected, for: indexPath)
}
Swift 4.1. Here I created function to get IndexPath. Just pass your UIView(UIButton,UITextField etc) and UITableView object to get IndexPath.
func getIndexPathFor(view: UIView, tableView: UITableView) -> IndexPath? {
let point = tableView.convert(view.bounds.origin, from: view)
let indexPath = tableView.indexPathForRow(at: point)
return indexPath
}
Create a property indexPath in the cell class and set it in cellForRowAtIndexPath when the cell is reused.
But there is a caveat: Some table view methods to rearrange the cells don't call cellForRowAtIndexPath. You have to consider this case.
But if you use always only reloadData() it's safe and pretty easy.
Another way is to put the code regarding controlling things back in the controller class and run it via callback closures capturing the index path.
Heres another way of doing it
import UIKit
import Parse
import ActiveLabel
class myTableCell : UITableViewCell {
//Button
#IBOutlet weak var commentBtn: UIButton!
#IBOutlet weak var likeBtn: UIButton!
#IBOutlet weak var moreBtn: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
}
}
class myTableViewController: UITableViewController {
//Default func
//assuming you have an array for your table data source
var arrayOfTitles = [String]()
override func viewDidLoad() {
super.viewDidLoad()
//automatic row height
tableView.estimatedRowHeight = 450
tableView.rowHeight = UITableViewAutomaticDimension
}
// cell config
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//define cell
let cell = tableView.dequeueReusableCell(withIdentifier: "myTableCell", for: indexPath) as! myTableCell
cell.commentBtn.tag = indexPath.row
cell.commentBtn.addTarget(self, action: #selector(likeBtnTapped(_:), forControlEvents:.TouchUpInside)
//cell config end
#IBAction func likeBtnTapped(sender: UIButton) {
let btn = sender
let indexP = NSIndexPath(forItem: btn.tag, inSection: 0)
let cell = tableView.dequeueReusableCell(withIdentifier: "myTableCell", for: indexP) as! myTableCell
//I want get indexPath.row in here!
let title = arrayOfTitles[indexP.row]
//declare title of button
cell.commentBtn.setTitle(title, forState: UIControlState.Normal)
}
}
My solution was subclassing UITableViewCell, so can add IndexPath property. assign custom class for table view cell in storyboard. assign IndexPath value when rowAtIndexPath called.
class MyTableViewCell: UITableViewCell {
var indexPath: IndexPath?
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "cellid1", for: indexPath)
(cell as? MyTableViewCell)?.indexPath = indexPath
return cell
}
Swift 5:
if
let collectionView = superview as? UICollectionView,
let index = collectionView.indexPath(for: self)
{
// stuff
}