How to detect dismissed TextView in TableView? - swift

I have a tableView with dynamic cells with multiple TextViews. I am dismissing a keyboard with a "Cancel" and trying to determine which TextView is being dismissed to "undo" the changes made by user.
Based on this similar question: How to determine which textfield is active swift I have adapted one of the answers for the following extension:
extension UIView {
var textViewsInView: [UITextView] {
return subviews
.filter ({ !($0 is UITextView) })
.reduce (( subviews.compactMap { $0 as? UITextView }), { summ, current in
return summ + current.textViewsInView
})
}
var selectedTextView: UITextView? {
return textViewsInView.filter { $0.isFirstResponder }.first
}
}
This is working and I am presently testing in the following code:
#objc func cancelButtonAction() {
if let test = tableView.selectedTextView {
print("View Found")
}
tableView.beginUpdates()
tableView.endUpdates()
}
Doing a break at print("View Found") I can inspect "test". The following is the result.
This appears to only identify the view Test by a memory address. My question is how do I interpret this to identify the view that was being edited?
Update: There seems to be some issue in understanding. Assume I have a table with two cells and in each cell two textViews (dynamic cells). Assume the table loads by saying in the 4 textViews. "Hi John", "Hi Sam", "Bye John", "Bye Sam". Suppose the user starts editing a cell and changes one cell to read "Nachos". Then the user decides to cancel. I want to then replace with the value that was there before (from my model). I can find the textView but it now reads "Nachos". Therefore I do not know which textView to reload with the appropriate Hi and Bye.

Implement a placeholder for your textviews so that when their text is empty, it will have a default value. Therefore, when a user presses cancel while in focus of a textview, we can set the textview's text to its default value. See link to implement a textview placeholder.. Text View Placeholder Swift

I did solve this problem by adding the .tag property to the textView objects. I also dropped the extension approach and used textView delegate. The solution required me to first assign the tag and delegate = self for each textView in the tableView: cellForRowAt. The following shows one TextView of many. Notice the tag is setup so I may determine the section and the row it came from and the specific item.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
...
cell.directionsTextView.delegate = self
cell.directionsTextView.tag = indexPath.section*1000 + indexPath.row+1
return cell
}
Two global variables are defined in my tableView class:
var activeTextView = UITextView()
var activeTextViewPresentText = String()
The textViewDidBeginEditing captures the original state of the textView text before the user starts editing.
// Assign the newly active textview to store original value and original text
func textViewDidBeginEditing(_ textView: UITextView) {
print("textView.tag: \(textView.tag)")
self.activeTextView = textView
self.activeTextViewPresentText = textView.text
}
Lastly, if the user cancels the editing, the original text is reloaded.
#objc func cancelButtonAction() {
if activeTextView.text != nil {
activeTextView.text = activeTextViewPresentText
}
self.view.endEditing(true)
tableUpdate()
}

Related

NSTextField keep focus/first responder after NSPopover

The object of this app is to ensure the user has entered a certain text in an NSTextField. If that text is not in the field, they should not be allowed to leave the field.
Given a macOS app with a subclass text field, a button and another generic NSTextField. When the button is clicked, an NSPopover is shown which is 'attached' to the field which is controlled by an NSViewController called myPopoverVC.
For example, the user enters 3 in the top field and then clicks the Show Popover button which displays the popover and provides a hint: 'What does 1 + 1 equal'.
Note this popover has a field labelled 1st resp so when the popover shows, that field becomes the first responder. Nothing will be entered at this time - it's just for this question.
The user would click the Close button, which closes the popover. At that point what should happen if the user clicks or tabs away from the field with the '3' in it, the app should not permit that movement - perhaps emitting a Beep or some other message. But what happens when the popover closes and the user presses Tab
Even though that field with the '3' in it had a focus ring, which should indicate the first responder again in that window, the user can click or tab away from it as the textShouldEndEditing function is not called. In this case, I clicked the close button in the popover, the '3' field had a focus ring and I hit tab, which then went to the next field.
This is the function in the subclassed text field that works correctly after the text has been entered into the field. In this case, if the user types a 3 and then hits Tab, the cursor stays in that field.
override func textShouldEndEditing(_ textObject: NSText) -> Bool {
if self.aboutToShowPopover == true {
return true
}
if let editor = self.currentEditor() { //or use the textObject
let s = editor.string
if s == "2" {
return true
}
return false
}
The showPopover button code sets the aboutToShowPopover flag to true which will allow the subclass to show the popover. (set to false when the popover closes)
So the question is when the popover closes how to return the firstResponder status to the original text field? It appears to have first responder status, and it thinks it has that status although textShouldEndEditing is not called. If you type another char into the field, then everything works as it should. It's as if the window's field editor and the field with the '3' in it are disconnected so the field editor is not passing calls up to that field.
The button calls a function which contains this:
let contentSize = myPopoverVC.view.frame
theTextField.aboutToShowPopover = true
parentVC.present(myPopoverVC, asPopoverRelativeTo: contentSize, of: theTextField, preferredEdge: NSRectEdge.maxY, behavior: NSPopover.Behavior.applicationDefined)
NSApplication.shared.activate(ignoringOtherApps: true)
the NSPopover close is
parentVC.dismiss(myPopoverVC)
One other piece of information. I added this bit of code to the subclassed NSTextField control.
override func becomeFirstResponder() -> Bool {
let e = self.currentEditor()
print(e)
return super.becomeFirstResponder()
}
When the popover closes and the textField becomes the windows first responder, that code executes but prints nil. Which indicates that while it is the first responder it has no connection to the window fieldEditor and will not receive events. Why?
If anything is unclear, please ask.
Here's my attempt with help from How can one programatically begin a text editing session in a NSTextField? and How can I make my NSTextField NOT highlight its text when the application starts?:
The selected range is saved in textShouldEndEditing and restored in becomeFirstResponder. insertText(_:replacementRange:) starts an editing session.
var savedSelectedRanges: [NSValue]?
override func becomeFirstResponder() -> Bool {
if super.becomeFirstResponder() {
if self.aboutToShowPopover {
if let ranges = self.savedSelectedRanges {
if let fieldEditor = self.currentEditor() as? NSTextView {
fieldEditor.insertText("", replacementRange: NSRange(location: 0, length:0))
fieldEditor.selectedRanges = ranges
}
}
}
return true
}
return false
}
override func textShouldEndEditing(_ textObject: NSText) -> Bool {
if super.textShouldEndEditing(textObject) {
if self.aboutToShowPopover {
let fieldEditor = textObject as! NSTextView
self.savedSelectedRanges = fieldEditor.selectedRanges
return true
}
let s = textObject.string
if s == "2" {
return true
}
}
return false
}
Maybe rename aboutToShowPopover.
If you subclass each of your NSTextField, you could override the method becomeFirstResponder and make it send self to a delegate class you will create, that will keep a reference of the current first responder:
NSTextField superclass:
override func becomeFirstResponder() -> Bool {
self.myRespondersDelegate.setCurrentResponder(self)
return super.becomeFirstResponder()
}
(myRespondersDelegate: would optionally be your NSViewController)
Note: do not use the same superclass for your alerts TextFields and ViewController TextFields. Use this superclass with added functionality only for TextFields you would want to return to firstResponder after an alert is closed.
NSTextField delegate:
class MyViewController: NSViewController, MyFirstResponderDelegate {
var currentFirstResponderTextField: NSTextField?
func setCurrentResponder(textField: NSTextField) {
self.currentFirstResponderTextField = textField
}
}
Now, after your pop is dismissed, you could in viewWillAppear or create a delegate function that will be called on a pop up dismiss didDismisss (Depends how your pop up is implemented, I will show the delegate option)
Check If a TextField has existed, and re-make it, the firstResponder.
Pop up delegate:
class MyViewController: NSViewController, MyFirstResponderDelegate, MyPopUpDismissDelegate {
var currentFirstResponderTextField: NSTextField?
func setCurrentResponder(textField: NSTextField) {
self.currentFirstResponderTextField = textField
}
func didDismisssPopUp() {
guard let isLastTextField = self.currentFirstResponderTextField else {
return
}
self.isLastTextField?.window?.makeFirstResponder(self.isLastTextField)
}
}
Hope it works.
Huge thanks to Willeke for the help and an answer that lead to a pretty simple solution.
The big picture issue here was that when the popover closed, the 'focused' field was the original field. However, it appears (for some reason) that the windows field editor delegate disconnected from that field so functions such as control:textShouldEndEditing were not being passed to the subclassed field in the question.
Executing this line when the field becomes the first reponder seems to re-connect the windows field editor with this field so it will receive delegate messages
fieldEditor.insertText("", replacementRange: range)
So the final solution was a combination of the following two functions.
override func textShouldEndEditing(_ textObject: NSText) -> Bool {
if self.aboutToShowPopover == true {
return true
}
let s = textObject.string
if s == "2" {
return true
}
return false
}
override func becomeFirstResponder() -> Bool {
if super.becomeFirstResponder() == true {
if let myEditor = self.currentEditor() as? NSTextView {
let range = NSMakeRange(0, 0)
myEditor.insertText("", replacementRange: range)
}
return true
}
return false
}

edit a TextView in a TableView cell in swift

I'm stuck: I have a TableView populated by .xib cells that I made. Each of these cells contains an editable TextView.
I'm trying to save on my Firebase database the text that the user input in those TextViews. I don't want to implement any button, the text should be saved as soon as the TextView editing end.
I tried to connect the TextView from the .xib file to the UITableViewCell class but it doesn't allow me to connect it as an IBAction but only as outlet or outlet connection.
Please Help me, thanks!
screenshot
You need to implement the UITextFieldDelegate in your
UITableViewCell class.
Connect the delegate of the UITextView to the cell class.
Do whatever you want in the func textViewDidEndEditing(UITextView) which you need to implement in your cell class.
Here you can read more: https://developer.apple.com/documentation/uikit/uitextviewdelegate/1618603-textviewshouldendediting?changes=_2
I solved replacing the TextViews with TextFields that could look the same but could be linked to the UITableViewCell.swift as IBAction.
Thus I wrote the code to update the "comments" section of my database inside the IBAction:
#IBAction func commentTextFieldToggle(_ sender: UITextField) {
if commentTextField.text != "" {
let comment = commentTextField.text
// I declared the next 7 constants to retreive the exact position of the string "comment" that I want to change
let date = dateLabel.text!
let time = timeLabel.text!
let year = date.suffix(4)
let day = date.prefix(2)
let partialMonth = date.prefix(5)
let month = partialMonth.suffix(2)
//I use this "chosenDate" constant to retreive the database query that I previously saved using the date in the below format as index:
let chosenDate = "\(year)-\(month)-\(day) at: \(time)"
let commentsDB = Database.database().reference().child("BSL Checks")
commentsDB.child((Auth.auth().currentUser?.uid)!).child(String(chosenDate)).child("Comments").setValue(comment) {
(error, reference) in
if error != nil {
print(error!)
} else {
print("User Data saved successfully")
}
}
}
}

Using swift to populate NSTableView rows with a NSPopupButtonCell

I have been trying to change one of the cells in an NSTableView to a pull-down menu, but have been unsuccessful. I read the Apple developer documentation, but it doesn't give an example of how to use NSPopupButtonCell in a NSTableView. I searched forums, including here, and only found one somewhat relevant example, except that it was in objective-c, so it doesn't work for my swift app. Code for the table is here:
extension DeviceListViewController:NSTableViewDataSource, NSTableViewDelegate{
// get the number of rows for the table
func numberOfRows(in tableView: NSTableView) -> Int {
return homedevices.count
}
// use the data in the homedevices array to populate the table cells
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?{
let result = tableView.make(withIdentifier: (tableColumn?.identifier)!, owner: self) as! NSTableCellView
if tableColumn?.identifier == "ID" {
result.textField?.stringValue = homedevices[row].id
} else if tableColumn?.identifier == "Name" {
result.textField?.stringValue = homedevices[row].name
result.imageView?.image = homedevices[row].image
} else if tableColumn?.identifier == "Type" {
result.textField?.stringValue = homedevices[row].type
} else if tableColumn?.identifier == "Button" {
result.textField?.integerValue = homedevices[row].button
}
return result
}
// facilitates data sorting for the table columns
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
let dataArrayMutable = NSMutableArray(array: homedevices)
dataArrayMutable.sort(using: tableView.sortDescriptors)
homedevices = dataArrayMutable as! [HomeDevice]
tableView.reloadData()
}
}
I really just want to be able to allow pull-down selection to change the button assigned to a particular homedevice (a simple integer), instead of having to type a number into the textfield to edit this value. Unfortuantely, when I add the popupbuttoncell to my table in IB, all of the views for my table cells are removed. So I may need to create the table differently. But most of the things I have read about and tried have caused runtime errors or display an empty table.
EDIT:
Day 3:
Today I have been reading here: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TableView/PopulatingViewTablesWithBindings/PopulatingView-TablesWithBindings.html
and many other places too, but I don't have rep to post any more links.
I have added a NSPopupButton in IB, but am not sure how to set the value. I tried result.objectValue = homedevices[row].button, but that does not work. I suppose that I need an array controller object. So then I tried creating an outlet for the object in my DeviceListViewController like #IBOutlet var buttonArrayController: NSArrayController! I guess that I now need to somehow find a way to connect the array controller to my homedevices array.
so I looked at example code here:
https://github.com/blishen/TableViewPopup
This is in objective-C, which is not a language I am using, but maybe if I keep looking at it at various times over the course of the week, I might figure out how to make a pull-down menu.
So I am continuing to work at this, with no solution currently.
This issue is solved, thanks to #vadian.
The button is inserted as NSPopUpButton object, rather than a NSPopUpButtonCell.
Then the cell gets its own custom class, which I called ButtonCellView as a subclass of NSTableCellView.
Then the created subclass can receive an outlet from the NSPopUpButton to the custom subclass. I can give this a selectedItem variable and create the menu here.
Then in the table view delegate, when making the table, I can just set the selectedItem of my ButtonCellView object to the value from my data array.
It works great!

textDidChange points to the wrong NSCollectionViewItem

I am trying to build an NSCollectionView filled with multiple editable TextViews. (OS X app in Swift.) My subclass of NSCollectionViewItem is called NoteViewItem. I am trying to have the program detect when one of the TextView has changed. I tried using both controlTextDidChange and textDidChange in the NoteViewItem's delegate with test print statement to see which would work. ControlTextDidChange did nothing; textDidChange recognized a change happened, so I went with that.
The problem is that textDidChange appears to point to a different NoteViewItem than the one that was shown on screen in the first place. It wasn't able to recognize the variable (called theNote) set in the original NoteViewItem; when I ask NoteViewItem to print String(self), I get two different results, one while setting the initial text and one in textDidChange. I'm wondering if I've set up my delegates and outlets wrongly. Any thoughts on why my references are off here?
Here's my code for NoteViewItem:
import Cocoa
class NoteViewItem: NSCollectionViewItem, NSTextViewDelegate
{
// MARK: Variables
#IBOutlet weak var theLabel: NSTextField!
#IBOutlet var theTextView: NSTextView!
var theNote: Note?
{
didSet
{
// Pre: The NoteViewItem's theNote property is set.
// Post: This observer has set the content of the *item's text view*, and label if it has one.
guard viewLoaded else { return }
if let theNote = theNote
{
// textField?.stringValue = theNote.noteText
theLabel.stringValue = theNote.filename
theTextView.string = theNote.noteText
theTextView.display()
print("theTextView.string set to "+theTextView.string!+" in NoteViewItem "+String(self))
}
else
{
theLabel.stringValue = "Empty note?"
}
}
}
// MARK: Functions
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
// Hopefully this will set the note's background to white.
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.whiteColor().CGColor
}
// MARK: - NSTextViewDelegate
/*
override func controlTextDidChange(notification: NSNotification)
{
print("Control text changed.")
}
*/
func textDidChange(notification: NSNotification)
{
if let noteyMcNoteface = theNote
{
print("On edit, we have a note: "+String(noteyMcNoteface))
}
else
{
print("On edit, we have no note. I am NoteViewItem "+String(self))
}
}
}
I figured it out. My delegate, in the TextView, was connected to the wrong object in the Interface Builder for NoteViewItem.xib. I had connected it to the object labelled Note View Item, under objects in the outline. It should have been connected to File's Owner instead, since File's Owner stands for the NoteViewItem.swift class associated with the xib.
You'd think that if you want to connect the delegate to the NoteViewItem class and there is exactly one Note View Item listed in the outline, then that Note View Item is the thing you want to connect it to. Nope, you connect it to something entirely different that isn't called the Note View Item but is the Note View Item. I'm glad Interface Builder makes things so simple.

How to filter table view cells with a UISegmentedControl in Swift?

I've already searched this before asking the question but I didn't find what I need.
I'm building this app where the user puts a task (not going to the app store, just for me and some friends), and the task has a category. For example: school, home, friends, etc. When the user is going to add a new task, there are 2 text fields, the description text field and the category text field. I'm using a UIPickerView so the user picks a category, then, after creating the new task, it will add the category to an array I've created called "categories".
I want to put an UISegmentedControl on top of the table view with the sections:
All - School - Home - Friends
If all is selected, it will show all the cells with no filtering. If not, it will show the cell(s) with the corresponding categories.
I've read that I need to create table view sections to each category, but this would change my code a lot, and I don't even have an idea of how to work with multiple table view sections, I've tried once but it kept repeating the cells of one section in the second.
So how can I filter the cells per category?
Can I just put for example this? :
if //code to check in which section the picker is here {
if let schoolCell = cell.categories[indexPath.row] == "School" {
schoolCell.hidden = true
}
}
Please help me!!!
EDIT:
I have this code by now:
if filterSegmentedControl.selectedSegmentIndex == 1 {
if categories[indexPath.row] == "School" {
}
}
I just don't know where to go from here. How do I recognize and hide the cells?
It seems to me that you may want to take a simpler approach first and get something working. Set up your ViewController and add a tableView and two(2) arrays for your table data. One would be for home and the other for work. Yes, I know this is simple but if you get it working, then you can build on it.
Add a variable to track which data you are displaying.
#IBOutlet var segmentedControl: UISegmentedControl!
#IBOutlet var tableView: UITableView!
// You would set this to 0, 1 or 2 for home, work and all.
var dataFilter = 0
// Data for work tasks
var tableDataWork : [String] = ["Proposal", "Send mail", "Fix printer", "Send payroll", "Pay rent"]
// Data for home tasks
var tableDataHome : [String] = ["Car payment", "Mow lawn", "Carpet clean"]
Add these functions for the segmented control.
#IBAction func segmentedControlAction(sender: AnyObject) {
switch segmentedControl.selectedSegmentIndex {
case 0:
print("Home")
dataFilter = 0
case 1:
print("Work")
dataFilter = 1
case 2:
print("All")
dataFilter = 2
default:
print("All")
dataFilter = 2
}
reload()
}
func reload() {
dispatch_async( dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("task-cell")
var title: String?
switch dataFilter {
case 0:
title = tableDataHome[indexPath.row]
case 1:
title = tableDataWork[indexPath.row]
case 2:
if indexPath.row < tableDataWork.count {
title = tableDataWork[indexPath.row]
} else {
title = tableDataHome[indexPath.row - tableDataWork.count]
}
default:
if indexPath.row < tableDataWork.count {
title = tableDataWork[indexPath.row]
} else {
title = tableDataHome[indexPath.row + tableDataWork.count]
}
}
cell?.textLabel?.text = title
if cell != nil {
return cell!
}
return UITableViewCell()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// If
switch dataFilter {
case 0: return tableDataHome.count
case 1: return tableDataWork.count
default: return tableDataHome.count + tableDataWork.count
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1;
}
You can find the entire project here: https://github.com/ryantxr/segmented-control-app
It depends on your tableview.
If you use NSFetchedResultsController then you need to modify your fetch request. If you use an array directly, just use the filter function in Swift, passing in the condition, e.g. filteredArray = array.filter{$0.isAudioFile} Then, after setting your datasource array to the filtered one, call reloadData on your tableview.
You will need to keep a reference to the full array, and use the filtered one as your datasource in cellForRow...