I have a UITextView embedded in a UITableView Cell, CustomCell. If I tap the return key when my curser is at the end of the text, I want to create a new cell below that cell, but if I tap return when I am in the middle of the textView, I want to get rest of the words after the curser and create a cell with that text in the textView.
To do that I need to know curser position in textView which I am getting with the help of the UITextViewDelegate method textViewDidChangeSelection(_:).
It works only when I tap on different location on the same cell but if I move to a different cell and tap again on a previously tapped cell(in the textview)at the same position, the delegate method doesn't get triggered.
Let me show the case with an exapmle-
I type some texts for testing. Here, after the 5th line, I decide to tap on the 2nd line just before the word "juice" like -
The delegate method, textViewDidChangeSelection(_:) gets triggered.
Now, I click on the 3rd line after the whole text "Skim Milk" like-
and again the delegate method, textViewDidChangeSelection(_:) gets called.
But now when I move back to the 2nd line at the same position, meaning before the word “juice”, the delegate method doesn’t get triggered. Again if I tap on the 3rd line at the same position meaning after the word “milk”, the delegate method doesn't get called.
I think, the reason is in those cells, I tapped on the sane position, and as the selection didn’t get changed, the delegate method didn’t get called.
Following is the code for the delegate method which is in the CustomCell(UITableViewCell) class-
func textViewDidChangeSelection(_ textView: UITextView){
if let range = textView.selectedTextRange, let indexPath = currentIndexPath{
let curserStartPosition = textView.offset(from: textView.beginningOfDocument, to: range.start)
setActiveCell?(indexPath, curserStartPosition)
}
}
The following is the code for the Controller where new cells are created when tapped the return key-
class MyViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
var cellCount = 1
var initialCellText: String?
var activeCell: CustomCell?
var cursorPosition: Int?
#IBOutlet weak var myTableView: UITableView!{
didSet{
myTableView.delegate = self
myTableView.dataSource = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
func goToNextCell(_ indexPath:IndexPath){
let nextCelIndexPath = IndexPath(row: indexPath.row + 1, section: 0)
if let text = activeCell?.myTextView.text, let cursorPos = cursorPosition, cursorPos < text.count-1 {
let fromIndex = text.index(text.startIndex, offsetBy: cursorPos)
let toIndex = text.index(text.endIndex, offsetBy: -1)
if fromIndex < toIndex {
let truncatedText = text[fromIndex...toIndex]
self.initialCellText = String(truncatedText)
}
}
cellCount += 1
myTableView.beginUpdates()
myTableView.insertRows(at: [nextCelIndexPath], with: .none)
myTableView.endUpdates()
initialCellText = nil
cursorPosition = nil
if let cell = self.myTableView.cellForRow(at: nextCelIndexPath) as? CustomCell{
self.activeCell = cell
self.activeCell?.myTextView.becomeFirstResponder()
}
}
func setActiveCell(on indexPath: IndexPath, at curserPos: Int){
if let tappedOnCell = myTableView.cellForRow(at: indexPath) as? CustomCell{
self.activeCell = tappedOnCell
self.cursorPosition = curserPos
}
}
}
// MARK: - UITableViewDelegate and UITableViewDataSource
extension MyViewController{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "customCell") as? CustomCell{
cell.myTextField.becomeFirstResponder()
cell.returnKeyTapped = goToNextCell(_:)
cell.setActiveCell = setActiveCell(on:at:)
return cell
}
return UITableViewCell()
}
}
To create the following cell, I need to know where the cursor position is on a particular cell everytime. So, I need the delegate method to be triggered everytime.
Can anyone have any suggestions as to how can I solve my problem.
If I understand you right, you basically want to know the curser position after you have typed into a UITextView.
Now, if you type into a UITextView, that text view becomes the first responder, and begins an editing session. This is announced by a UITextView.textDidBeginEditingNotification for which you can add an observer, see here.
In the function called by the notification, you could compute the cursor position as you do in textViewDidChangeSelection
Related
I have a viewControl called PostViewController which has a UITableView of posts. I also have a class called PostCell which defines the UITableViewCell. I made a button function in PostCell called likeButtonClicked to favour a post similar to twitter.
#IBAction func likesButtonClicked(_ sender: Any) { NotificationCenter.default.post(name: NSNotification.Name(rawValue: "likeButtonClicked"), object: nil, userInfo: ["cell":self, "likesButton":likesButton!]) }
This is to pass the cell indexPath and the button name to PostViewController. I need indexPath to increase the likes by 1 and the button name to change its image to pink when post is favoured.
I then subscribed to the notification in viewDidLoad of PostViewController.
NotificationCenter.default.addObserver(self, selector: #selector(postLiked), name: NSNotification.Name(rawValue: "likeButtonClicked"), object: nil)
I then wrote this function in the same viewController
#objc func postLiked(notification: Notification){
if let cell = notification.userInfo?["cell"] as? UITableViewCell{
let likesButton = notification.userInfo?["likesButton"] as? SpringButton
if let indexPath = postsTableView.indexPath(for: cell){
let post = posts[indexPath.row]
postId = post.id
PostAPI.getPostById(postId: postId) { post in
//Check if the same post were already favoured.
if !self.liked || self.oldPostId != self.postId{
self.newLikes = post.likes + 1
self.liked = true
self.oldPostId = self.postId
}else{
self.newLikes = self.newLikes - 1
self.liked = false
}
PostAPI.favourPost(postId: self.postId, likes: self.newLikes) {
PostAPI.getPostById(postId: self.postId) { postResponse in
let post = postResponse
self.posts[indexPath.row] = post
let cellNumber = IndexPath(row: indexPath.row, section: indexPath.section)
self.reloadRowData(cellNumber: cellNumber){
if !self.liked{
likesButton?.tintColor = .systemPink
}else{
likesButton?.tintColor = .darkGray
}
}
}
}
}
}
}
}
func reloadRowData(cellNumber: IndexPath, completion: #escaping () -> ()) {
self.postsTableView.reloadRows(at: [cellNumber], with: .none)
completion()
}
Please tell me why the last 4 lines of postLiked function is executed before reloadRowData function, which causes the button to change its color to pink then returns immediately to gray when it should stay pink.
Any help will be most appreciated.
Thank you.
I expect the specific problem is your call to reloadRows. As the docs note:
Reloading a row causes the table view to ask its data source for a new cell for that row. The table animates that new cell in as it animates the old row out. Call this method if you want to alert the user that the value of a cell is changing. If, however, notifying the user is not important—that is, you just want to change the value that a cell is displaying—you can get the cell for a particular row and set its new value.
So this is likely creating an entirely new cell, and then the later code modifies the old cell that is being removed from the table. (So you see the change, and then the cell is removed and replaced.)
I would start by getting rid of the entire reloadRowData call.
Generally, though, this code is fragile, and I'd redesign it. The cell should take care of setting the tint colors itself based on the data. You generally shouldn't be reaching into a cell and manipulating its subviews. This will cause you a problem when cells are recycled (for example, when this scrolls off screen). All the configuration should be done in cellForRow(at:), and the cell should observe its Post and update itself when there are changes.
Data should live in the Model. The View should observe the Model and react. The Model should not reach into the View and manipulate anything.
As a side: your reloadRowData looks async, but it's not. There's no reason for a completion handler. It could just call return.
A table view's .reloadRows(at:...) (and .reloadData()) functions are async processes.
So your reloadRowData() func is returning before the table view actually reloads the row(s).
This is a rather unusual approach - both in using NotificationCenter for your cells to communicate with the controller, and in trying to change the button's tint color by holding a reference to the button.
The tint color really should be set in cellForRowAt, based on your data source.
Edit
My description of table view data reloading being asynchronous was misleading.
The point I wanted to make was this...
If I change my data, call .reloadData(), and then change my data again:
// set myData values
myData = ["You", "Say", "Hello"]
// call .reloadData here
tableView.reloadData()
// change myData values
myData = ["I", "Say", "Goodbye"]
the table view will not display "You Say Hello" even though we call .reloadData() when those are the values of myData.
Here's a complete, simple example to demonstrate:
class TestTableViewController: UITableViewController {
var myData: [String] = ["Some", "Song", "Lyrics"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "c")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath)
c.textLabel?.text = myData[indexPath.row]
return c
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// call my func when a row is selected
self.myReload()
}
func myReload() {
// set myData values
myData = ["You", "Say", "Hello"]
// call .reloadData here
tableView.reloadData()
// change myData values
myData = ["I", "Say", "Goodbye"]
// table view doesn't reload itself we exit this func
// so we'll get
// I
// Say
// Goodbye
}
}
So, among the other issues described by Rob Napier in his answer, your original code was trying to change the tint color of an object in a cell before the table reloaded its data.
I currently have a table view with cells that contain a label and a textfield, I also have a + bar button item that adds new cells.
What I hope to accomplish is when the user presses the + button the new cell is created and the text field of this cell would automatically become first responder.
Below is my current code for creating the new entry:
func newNoteline() {
let entityDescription = NSEntityDescription.entity(forEntityName: "NotebookContentEntity", in: context)
let item = NotebookContentEntity(entity: entityDescription!, insertInto: context)
item.notebookEntry = ""
item.timeOfEntry = timeOutlet.text
do {
try context.save()
} catch {
print(error)
return
}
loadNotelines()
}
I have thought of several ways of trying to solve this but without much luck of making them work including using a .tag on the text field as soon as it's made - using the text field delegate or using the tableView delegate method - indexPathForPreferredFocusedView.
I just can't figure out how to force the focus to a specific textfield within a cell without the user tapping the text field. Any thoughts?
Calling textField.becomeFirstResponder() method should do what you are looking for.
When to call this function is up to you. for e.g. in below code at cellForRowAt I checked if the value is empty then make the current textfield first responder.
class Test: UIViewController{
var myView: TestView{return view as! TestView}
unowned var tableView: UITableView {return myView.tableView}
unowned var button: UIButton {return myView.button}
var list = [String]()
override func loadView() {
view = TestView()
}
override func viewDidLoad() {
super.viewDidLoad()
for i in 1...10{
list.append("Test \(i)")
}
tableView.dataSource = self
button.addTarget(self, action: #selector(didSelect(_:)), for: .touchUpInside)
}
#objc func didSelect(_ sender: UIButton){
let indexPath = IndexPath(row: list.count, section: 0)
list.append("")
tableView.insertRows(at: [indexPath], with: .automatic)
tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
extension Test: UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return list.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TestViewCell", for: indexPath) as! TestViewCell
let value = list[indexPath.row]
cell.textField.text = value
if value.isEmpty{
cell.textField.becomeFirstResponder()
}
return cell
}
}
I have this chunk of code in my ViewController named PlayViewController:
words = [String]()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "PlayTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? PlayTableViewCell else {
fatalError("The dequeued cell is not an instance of PlayTableViewCell.")
}
// Configure the cell...
cell.wordLabel.text = words[indexPath.row]
if (indexPath.row == 0) {
cell.wordLabel.isHidden = false
}
return cell
}
And this is my code for TableViewCell named PlayTableViewCell:
import UIKit
class PlayTableViewCell: UITableViewCell {
//MARK: Properties
#IBOutlet weak var wordLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
wordLabel.lineBreakMode = .byWordWrapping;
wordLabel.numberOfLines = 0;
wordLabel.isHidden = true
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
As expected, only my first wordLabel appears in my tableView.
My goal is to reveal the second wordLabel when the user swipes right or left anywhere on the screen and continue this way until the user reveal the last wordLabel.
I've found how to set the swipe part (only the swipe right, behaves weirdly when the left is added) but I can't figure out how to toggle .isHidden property when I detect the gesture.
I'm not sure to be on the right path with the cell configuration but because of the placing of the wordLabel inside PlayTableViewCell, it's hard to reach it outside the function tableView.
I can index neither cell nor wordLabel and I can't figure out how could I reach the right wordLabel to toggle its visibility.
Somewhere store property for sum of revealed labels
var numberOfRevealedLabels = 1
then every time user swipes somewhere, increase this value and then reload data of your table view (you can reload it in didSet of your variable or after you increase this value in action which is called by swiping)
numberOfRevealedLabels += 1
Now, since cells are reusable, set visibility depending on if indexPath.row is less then or equal to numberOfRevealedLabels - 1
cell.wordLabel.isHidden = !(indexPath.row <= numberOfRevealedLabels - 1)
... this also covers the case that indexPath.row is greater
I have used a tableview with 16 cells on a view controller. Each cell has a textfield and a picker view as a inputview for textfield. The odd thing is that When I choose the value for the first cell, it's fine. When I scrolled down to the last cell, the value is same as the first one. But I have never touched the last cell. Why would this happened?
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int)
{
// selected value in Uipickerview in Swift
answerText.text = pickerDataSource[row]
answerText.tag = row
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = myTable.dequeueReusableCellWithIdentifier("addFollowCell", forIndexPath: indexPath) as! AddFollowTableViewCell
cell.questionView.text = listQuestion1[indexPath.row]
cell.pickerDataSource = dictPicker[indexPath.row]!
cell.answerText.addTarget(self, action: #selector(AddFollowUpViewController.textFieldDidChange(_:)), forControlEvents: UIControlEvents.EditingDidEnd)
return cell
}
func textFieldDidChange(sender: UITextField){
let rowIndex: Int!
let selectValue = sender.tag
if let txtf = sender as? UITextField {
if let superview = txtf.superview {
if let cell = superview.superview as? AddFollowTableViewCell {
rowIndex = myTable.indexPathForCell(cell)?.row
dictAnswer[rowIndex] = selectValue - 1
}
}
}
}
After two days, it solved by thousands of trials:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
var cell = myTable.dequeueReusableCellWithIdentifier("addFollowCell") as! AddFollowTableViewCell
if(cell.identifier == true){
cell.answerText.text = selectedAnswerForRow[indexPath.row]
}
cell.questionView.text = listQuestion1[indexPath.row]
cell.pickerDataSource = dictPicker[indexPath.row]!
dictAnswer[indexPath.row] = cell.pickerValue
cell.answerText.addTarget(self, action: #selector(AddFollowUpViewController.textFieldDidChange(_:)), forControlEvents: UIControlEvents.EditingDidEnd)
cell.identifier = true
return cell
}
func textFieldDidChange(sender: UITextField){
let rowIndex: Int!
let cell = sender.superview?.superview as! AddFollowTableViewCell
rowIndex = myTable.indexPathForCell(cell)?.row
selectedAnswerForRow[rowIndex] = cell.answerValue
print(selectedAnswerForRow[rowIndex])
cell.answerText.text = sender.text
cell.identifier = true
}
It might have some performance issue need to be optimised , but it shows exactly what i want. LOL
You're basically recycling your views and not clearing them. That's the whole point of -dequeueReusableCellWithIdentifier:indexPath:.
Allocating and deallocating memory is very power consuming, so the system recycles every cell that goes out of viewport bounds.
You don't set the text inside answerText (I assume it's the text field that causes trouble) so its content will be kept when recycled.
Assuming you'll store user selection inside a dictionary var selectedAnswerForRow: [IndexPath:String]:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = myTable.dequeueReusableCellWithIdentifier("addFollowCell", forIndexPath: indexPath) as! AddFollowTableViewCell
cell.questionView.text = listQuestion1[indexPath.row]
cell.pickerDataSource = dictPicker[indexPath.row]!
cell.answerText.addTarget(self, action: "textFieldDidChange:"), forControlEvents: UIControlEvents.EditingDidEnd)
cell.answerText.text = self.selectedAnswerForRow[indexPath] ?? "" // add this
return cell
}
self.selectedAnswerForRow[indexPath] ?? "" returns the result or an empty string if it's not present in the dictionary.
Also, you're adding several times the action for edition control event. You have to check first if it isn't already bound.
Because the cell is reused. So you have to implement prepareForReuse() in your custom cell class and reset all the changing variables
UPDATE
See :
class MyCell : UITableViewCell {
#IBOutlet weak var myTextField : UITextField!
//Add the following
override func prepareForReuse() {
myTextField.text = nil
myTextField.inputView = myPickerView
super.prepareForReuse()
}
}
I have an app that has a custom button in a custom cell. If you select the cell it segues to the a detail view, which is perfect. If I select a button in a cell, the code below prints the cell index into the console.
I need to access the contents of the selected cell (Using the button) and add them to an array or dictionary. I am new to this so struggling to find out how to access the contents of the cell. I tried using didselectrowatindexpath, but I don't know how to force the index to be that of the tag...
So basically, if there are 3 cells with 'Dog', 'Cat', 'Bird' as the cell.repeatLabel.text in each cell and I select the buttons in the rows 1 and 3 (Index 0 and 2), it should add 'Dog' and 'Bird' to the array/dictionary.
// MARK: - Table View
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postsCollection.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: CustomCell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! CustomCell
// Configure the cell...
var currentRepeat = postsCollection[indexPath.row]
cell.repeatLabel?.text = currentRepeat.product
cell.repeatCount?.text = "Repeat: " + String(currentRepeat.currentrepeat) + " of " + String(currentRepeat.totalrepeat)
cell.accessoryType = UITableViewCellAccessoryType.DetailDisclosureButton
cell.checkButton.tag = indexPath.row;
cell.checkButton.addTarget(self, action: Selector("selectItem:"), forControlEvents: UIControlEvents.TouchUpInside)
return cell
}
func selectItem(sender:UIButton){
println("Selected item in row \(sender.tag)")
}
OPTION 1. Handling it with delegation
The right way of handling events fired from your cell's subviews is to use delegation.
So you can follow the steps:
1. Above your class definition write a protocol with a single instance method inside your custom cell:
protocol CustomCellDelegate {
func cellButtonTapped(cell: CustomCell)
}
2. Inside your class definition declare a delegate variable and call the protocol method on the delegate:
var delegate: CustomCellDelegate?
#IBAction func buttonTapped(sender: AnyObject) {
delegate?.cellButtonTapped(self)
}
3. Conform to the CustomCellDelegate in the class where your table view is:
class ViewController: CustomCellDelegate
4. Set your cell's delegate
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as CustomCell
cell.delegate = self
return cell
}
5. Implement the required method in your view controller class.
EDIT: First define an empty array and then modify it like this:
private var selectedItems = [String]()
func cellButtonTapped(cell: CustomCell) {
let indexPath = self.tableView.indexPathForRowAtPoint(cell.center)!
let selectedItem = items[indexPath.row]
if let selectedItemIndex = find(selectedItems, selectedItem) {
selectedItems.removeAtIndex(selectedItemIndex)
} else {
selectedItems.append(selectedItem)
}
}
where items is an array defined in my view controller:
private let items = ["Dog", "Cat", "Elephant", "Fox", "Ant", "Dolphin", "Donkey", "Horse", "Frog", "Cow", "Goose", "Turtle", "Sheep"]
OPTION 2. Handling it using closures
I've decided to come back and show you another way of handling these type of situations. Using a closure in this case will result in less code and you'll achieve your goal.
1. Declare a closure variable inside your cell class:
var tapped: ((CustomCell) -> Void)?
2. Invoke the closure inside your button handler.
#IBAction func buttonTapped(sender: AnyObject) {
tapped?(self)
}
3. In tableView(_:cellForRowAtIndexPath:) in the containing view controller class :
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! CustomCell
cell.tapped = { [unowned self] (selectedCell) -> Void in
let path = tableView.indexPathForRowAtPoint(selectedCell.center)!
let selectedItem = self.items[path.row]
println("the selected item is \(selectedItem)")
}
Since you have 1 section in the table view you can get the cell object as below.
let indexPath = NSIndexPath(forRow: tag, inSection: 0)
let cell = tableView.cellForRowAtIndexPath(indexPath) as! CustomCell!
where tag you will get from button tag.
Swift 3
I just get solution for access cell in #IBAction function using superview of button tag.
let cell = sender.superview?.superview as! ProductCell
var intQty = Int(cell.txtQty.text!);
intQty = intQty! + 1
let strQty = String(describing: intQty!);
cell.txtQty.text = strQty
#IBAction func buttonTap(sender: UIButton) {
let button = sender as UIButton
let indexPath = self.tableView.indexPathForRowAtPoint(sender.center)!
}
I updated option 1 of the answer from Vasil Garov for Swift 3
1. Create a protocol for your CustomCell:
protocol CustomCellDelegate {
func cellButtonTapped(cell: CustomCell)
}
2. For your TableViewCell declare a delegate variable and call the protocol method on it:
var delegate: CustomCellDelegate?
#IBAction func buttonTapped(sender: AnyObject) {
delegate?.cellButtonTapped(self)
}
3. Conform to the CustomCellDelegate in the class where your tableView is:
class ViewController: CustomCellDelegate
4. Set your cell's delegate
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as CustomCell
cell.delegate = self
return cell
}
5. Implement the required method in your ViewController.
Based on Cao's answer, here is a solution to handle buttons in a collection view cell.
#IBAction func actionButton(sender: UIButton) {
let point = collectionView.convertPoint(sender.center, fromView: sender.superview)
let indexPath = collectionView.indexPathForItemAtPoint(point)
}
Be aware that the convertPoint() function call will translate the button point coordinates in the collection view space. Without, indexPath will always refer to the same cell number 0
XCODE 8: Important Note
Do not forget to set the tags to a different value than 0.
If you attempt to cast an object with tag = 0 it might work but in some weird cases it doesn't.
The fix is to set the tags to different values.
Hope it helps someone.