Iterating through UITextViews within a tableview using Swift - ios

I'm currently learning how to build apps entirely programatically and have hit a bit of a wall in the process.
My app is going to be a fairly simple form that the user can fill in and it will generate a PDF at the end containing the information. For the basic information I have a tableview containing a set of UITextfields and have no problem iterating through them and extracting the data into an array of strings.
For the next section the user needed to input a lot more text so I have used UITextviews within the tableview. What I want to be able to do is extract the text inputted and save it to an array of strings that is used to create my customer object. The code that does this is as follows:
func createGoals() {
for section in 0..<secondTableView.numberOfSections {
for row in 0..<secondTableView.numberOfRows(inSection: section) {
let indexPath = NSIndexPath(row: row, section: section)
let thisCell = secondTableView.cellForRow(at: indexPath as IndexPath) as! mySecondCell
if thisCell.textview.text == "Enter Goals" {
print("NO CONTENT IN FIELD")
let ac = UIAlertController(title: "But...But...", message: "If you have no goals, how can you progress?!", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
present(ac, animated: true)
break
} else {
print("I MADE IT")
print("\(thisCell.textview.text!)")
newClient.goalArray.append(thisCell.textview.text)
print(newClient.goalArray)
}
}
}
}
But when this runs I get the error message "unexpectedly found nil while unwrapping an Optional value" and it particularly highlights this line:
let thisCell = secondTableView.cellForRow(at: indexPath as IndexPath) as! mySecondCell
This code is almost identical to how I iterate through the UITextfields successfully, all thats changed is the variables attached and as far as I can see there are no optionals.
Also if I don't fill in the UITextviews it will show the UIAlertController, it only seems to have a problem when I remove the placeholder and fill in the UITextviews.
Does anyone have any ideas as to why this is happening? I can provide more of my code if needed.

here you are loading cell in iteration. so issue is caused because you are going to load/access cell which is not visible.
here you should not load cell out side of cellForRowAtIndexPath.
Reason: UITableView is reusing cell with reusableCellIdentifier.
here you need to save values in array or dictionary when textfield-didEndEditing called (textfield delegate method).
and for validation check you can use saved value from array/dictionary.

Here is your optional
as! mySecondCell
Obviously, thisCell isn't a mySecondCell
Check if you did register the correct class
EDIT
You shouldn't call delegate method by yourself

Related

Gesture not recognized in all TableView Cells

Ok, this one may be a bit difficult to illustrate but here is the issue: I tried attaching a long press gesture recognizer on the tableView cell, and linked it in the Viewcontroller. But, the gesture does not work on every cell in the table - only 1. And the 1 cell it functions on changes (sometimes it is the first, sometimes the second, etc - depends on how many cells actually have data). If anyone can point me into the right direction it would be very appreciated.
Below is the code for handling the gesture. Thanks!
if recognizer.state == .changed
{
let alertController = UIAlertController(title: nil, message:
"Open Product in Safari", preferredStyle: .alert)
let indexPath = tableView.indexPathForSelectedRow
let itemSku = self.itemArray[indexPath?.row ?? 0].sku
alertController.addAction(UIAlertAction(title: "Go to Safari", style: .default,handler: { action in
UIApplication.shared.open(URL(string: "\(itemURL)\(itemSku ?? "")") ?? URL(string: "")!, options: [:]) { _ in
print("Link opened")
}
}))
present(alertController, animated: true, completion: nil)
}
}
Since you didn't post any code related to your issue, best guess is:
Gesture recognizers - such as UILongPressGestureRecognizer - are distinct instances. They can only be added to one object at a time. If you try to add it to multiple objects, it will only "exist" on the last one.
You probably created one gesture recognizer and then tried to add it to every cell.
Assuming you are using a custom cell class, you probably want to instantiate a UILongPressGestureRecognizer inside your cell init code and add it to self (or self.contentView or whatever view you want to respond to the gesture). Also set its target to a func within your cell class. When that is triggered, use either a "callback" closure or a protocol / delegate pattern to inform your controller that the gesture occurred.

How to save the text in textfield in swift?

I have a tableview consisting of 2 cells and each cell has a textfield. I'd like to hold the text in textfield in an another variable and append it into an array but it just save the second cell's textfield's text into the variable and doesn't append the array.
Here is cellForRowAt code :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView == queryTableView {
let cell = tableView.dequeueReusableCell(withIdentifier: queryCellReuseIdentifier,
for: indexPath) as! queryCell
cell.selectionStyle = UITableViewCell.SelectionStyle.gray
queryTextField = UITextField(frame: CGRect(x: 20, y: 0, width: 300, height: 20))
queryTextField.delegate = self
queryTextField.placeholder = "Soruyu buraya giriniz"
queryTextField.font = UIFont.systemFont(ofSize: 15)
cell.addSubview(queryTextField)
return cell
}
Here is my related function :
func textFieldDidEndEditing(_ textField: UITextField) {
var temp = queryTextField.text!
queryArray.append(temp)
print(temp)
}
if your project is non-storyboard approach, then I would say, it would be more easier and flexible to take control of each type and data and action since sometimes things not quite flexible with storyboard only.. . as you create, even a single UIElement, UI conforms to those properties, methods and actions which you define, means fully customized code - custom defined code base..
now to your question, according to what you did, probably, your textfield text is overwritten to blank, losing the previously entered value every time the cell is dequeued, cell is generated fresh again for use every time " dequeue cell " method is executed, if you want to persist your previous value, I would say, " remind the dequeued cell that it had some value before being re- dequeued again " means:
declare your global array or dict.
dequeue your cell and setup your cell properties
3 after cell is dequeued, do something like this, and your cells previous value is saved even after the cell is dequeued.
example, common approach:
struct DefaultInfo {
var string: String?
var type: SomeData? //optional
}
let array: [DefaultInfo] = []
var array = self.array[indexPath.row]
// after cell is dequeued:
switch array.type {
case .someCase:
let string = cell.textField.text
array.string = string
cell.textField.text = array.string
//print(array.string!)
// now you should be able to see your input after cell is
//renewed.. . try it.. .
default: break
}
It seems like you have an independant variable queryTextField which is overriden when you create the second cell.
Also, in textFieldDidEndEditing try accessing textField instead of queryTextField, like this:
func textFieldDidEndEditing(_ textField: UITextField) {
var temp = textField.text!
queryArray.append(temp)
print(temp)
}
Responding to this:
I need every cell has its textfield and I need to be able to save
their texts into an array. After that I am going to send them to web
service. To sum up, they will be my parameters.
You don't need to have global queryTextField just for that. You can remove this variable, if that's its only goal.
Do you need to send web request on some trigger? Like button tap. I assume, yes.
Since theoretically not all your cells are visible at the same time (e.g. when thay do not fit the screen), it's bad idea to try and track texts in text fields. Instead, you need some kind of model to store all texts (paired with indeces, maybe). The simplest would be dictionary where key is cell identifier (e.g. indexPath) and value is the text.
In textFieldDidEndEditing you can report the changes to the view controller. For this you need to assign the cell as the delegate for its text field. And view controller - as the delegate for the cell. In textFieldDidEndEditing cell would be calling view controller delegate method to report the text change, passing (for example) self and text as parameters. View controller would then be able to find its index path and store the text in the model (dictionary).
On trigger (button click?) view controller will be able to build parameters from the model (dictionary).
Consider this pseudocode as a direction:
cellForRow {
...
cell = ...
let textField = ...
textField.delegate = cell
cell.addSubview(textField)
cell.delegate = self
}
In cell:
textFieldDidEndEditing(textField: UITextField) {
delegate?.report(text: textField.text ?? "", in: self)
}
In view controller:
report(text: String, in cell: UITableViewCell) {
let indexPath = tableView.indexPath(of: cell)
model[indexPath] = text
}

UITableView Scrolls automatically while textfield begins editing

I am developing an iOS app which has different forms which is populated into a UITableview based on users selection. Each form has different fields like Textfield, DatePicker, PickerView. So I used a single TableViewCell (nib) to hold all these and show or hide the items based on question.
There is save function defined which will save values when user enters to an array.
My issue is, at times my tableview scrolls as if the index goes out of control. like when I select any textfield, Tableview scrolls to top. I have removed all keyboard observer methods and checked, still it is happening.
Below is my save function code:
func saveFormValue(mystr: String) {
//selectedIndex is a global variable (NSIndexPath)
let dict = sections[selectedIndex!.section].questions![selectedIndex!.row]
dict.answer = mystr
sections[selectedIndex!.section].questions![selectedIndex!.row] = dict
let answer = updated_answer.init(ID: ID, form_id: selected_form_id, form_name: formName, section_id: dict.section_id ?? "",question_id: dict.question_id ?? "", question_name: dict.question_name!,question_type:dict.question_type!)
updatedArray.append(answer)
self.tableView.reloadData()
}
This is the code in textfieldDidBeginEditing function (how selectedIndexPath is initialized):
guard let index = tableView.indexPath(for: cell) else {
return
}
selectedIndex = index as NSIndexPath
I have added delegate for cell, and one thing I noticed is, this issue is happening whenever I press pickerview or datepicker once. I couldn't see this issue If I only touch textField cells only.
Please let me know for any further details.
Try this code hope this helps to you.
if let thisIndexPath = tableView.indexPath(for: cell) {
tableView.scrollToRow(at: thisIndexPath, at: .top, animated: false)
}
On textfield delegate method textFieldDidBeginEditing use the following code:
func textFieldDidBeginEditing(_ textField: UITextField) {
let indexParh = NSIndexPath(row: textField.tag, section: 0)
self.constTBL_Bottom.constant = 260
self.tblViewObj.scrollToRow(at: indexParh as IndexPath, at: .middle, animated: false)
}
Also you need to manage the table bottom constant. When you resigning your keyboard set table view constant to 0
Hope this will work :)

Highlight UITableViewCell like iMessage when long pressed

I have simple chat created using UITableView. I want to add the ability to highlight a message after long press. In fact, I want to create the same feature like iMessage:
After a long press, we unhighlight background (more darker), highlight message, scroll to this message and show the actionSheet
For now I managed to add only longPress and actionSheet
Long press recongizer on viewDidLoad:
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(onCellLongPressed(gesture:)))
messagesTableView.addGestureRecognizer(longPressRecognizer)
onCellLongPressed function:
#objc func onCellLongPressed(gesture: UILongPressGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.began {
let touchPoint = gesture.location(in: self.messagesTableView)
if let indexPath = messagesTableView.indexPathForRow(at: touchPoint) {
self.messagesTableView.selectRow(at: indexPath, animated: true, scrollPosition: UITableViewScrollPosition.none)
shareWithFriend()
}
}
}
#objc func shareWithFriend() {
alert(style: .actionSheet, actions: [
UIAlertAction(title: "Share with friend", style: .default, handler: { [weak self] (_) in
print("SHARE HERE")
}),
UIAlertAction(title: "Cancel", style: .destructive),
])
}
func alert(_ title: String? = nil, message: String? = nil, style: UIAlertController.Style, actions: [UIAlertAction]) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: style)
actions.forEach(alertController.addAction)
present(alertController, animated: true)
}
As you can see, the background color is above the navigation bar so I guess there is a secondary view controller astutely presented above the collection view when the user selects a cell.
I think this is two different view hierarchies which look like one:
One view controller contains the balloon list
One view controller contains a copy of the selected balloon and the few actions associated to it
Here is the road map :
Detect a long press in the collection view cells
Copy the selected balloon and present it inside a secondary view controller (ActionVC)
Adjust the position of the selected balloon inside the ActionVC. If the selected balloon is under the future action button, it should be moved. If the selected balloon does not bother anyone, it should be presented without any change.
Adjust the content offset of the collection view to reflect 3. The modification should be done alongside 3 to look like if the cell was actually moved.
Detect any touch on the ActionVC
Dismiss the ActionVC
Here is an example project.
To copy the balloon, I actually create a view of the same class as the one used in the collection view cell. But you could use a snapshot.
To adjust the selected balloon position inside the ActionVC, I used constraints with priorities. One claims "don't be under the action button", another one claims "be exactly where the cell was". I used a simple coordinates conversion to compute the expected selected balloon position inside the ActionVC.
To perform 3 alongside 4, I used the transitionCoordinator of the ActionVC, but you could use a simple animation block and present the ActionVC without animation.
I'm sorry if this answer will not fulfill your request completely, but hope it will help you.
Your initial code was right, but you have not set Scrolling type. So I suggest you use this method selectRow(at:animated:scrollPosition:) Just set scroll position:
self.messagesTableView.selectRow(at: indexPath, animated: true, scrollPosition: UITableViewScrollPosition.top)
This also will select this row and hence call the following method tableView(_:didSelectRowAt:). So you will need to implement highlighting logic here:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// your logic of changing color
}
Then you should implement similar action, but to deselect the row using deselectRow(at:animated:)I believe this should be done when user finished his action. Also you need to implement similar function tableView(_:didDeselectRowAt:) to return color back:
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
// change color back
}
Update:
You may also use the setHighlighted(_:animated:) method to highlight a cell. In this way you may avoid using selectRowAt/didSelectRowAt/didDeselectRowAt and scroll tableView using
tableView?.scrollToRow(at: indexPath, at: UITableViewScrollPosition.top, animated: true).

Need help setting UISwitch in custom cell (XIB, Swift 4, Xcode 9)

Successes so far: I have a remote data source. Data gets pulled dynamically into a View Controller. The data is used to name a .title and .subtitle on each of the reusable custom cells. Also, each custom cell has a UISwitch, which I have been able to get functional for sending out both a “subscribe” signal for push notifications (for a given group identified by the cell’s title/subtitle) and an “unsubscribe” signal as well.
My one remaining issue: Whenever the user "revisits" the settings VC, while my code is "resetting" the UISwitches, it causes the following warnings in Xcode 9.2:
UISwitch.on must be used from main thread
UISwitch.setOn(_:animated:) must be used from main thread only
-[UISwitch setOn:animated:notifyingVisualElement:] must be used from main thread
The code below "works" -- however the desired result happens rather slowly (the UISwitches that are indeed supposed to be "on" take a good while to finally flip to "on").
More details:
What is needed: Whenever the VC is either shown or "re-shown," I need to "reset" the custom cell’s UISwitch to "on" if the user is subscribed to the given group, and to "off" if the user is not subscribed. Ideally, each time the VC is displayed, something should reach out and touch the OneSignal server and find out that user’s “subscribe state” for each group, using the OneSignal.getTags() function. I have that part working. This code is in the VC. But I need to do it the right way, to suit proper protocols regarding threading.
VC file, “ViewController_13_Settings.swift” holds a Table View with the reusable custom cell.
Table View file is named "CustomTableViewCell.swift"
The custom cell is called "customCell" (I know, my names are all really creative).
The custom cell (designed in XIB) has only three items inside it:
Title – A displayed “friendly name” of a “group” to be subscribed to or unsubscribed from. Set from the remote data source
Subtitle – A hidden “database name” of the aforementioned group. Hidden from the user. Set from the remote data source.
UISwitch - named "switchMinistryGroupList"
How do I properly set the UISwitch programmatically?
Here is the code in ViewController_13_Settings.swift that seems pertinent:
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! CustomTableViewCell
// set cell's title and subtitle
cell.textLabelMinistryGroupList?.text = MinistryGroupArray[indexPath.row]
cell.textHiddenUserTagName?.text = OneSignalUserTagArray[indexPath.row]
// set the custom cell's UISwitch.
OneSignal.getTags({ tags in
print("tags - \(tags!)")
self.OneSignalUserTags = String(describing: tags)
print("OneSignalUserTags, from within the OneSignal func, = \(self.OneSignalUserTags)")
if self.OneSignalUserTags.range(of: cell.textHiddenUserTagName.text!) != nil {
print("The \(cell.textHiddenUserTagName.text!) UserTag exists for this device.")
cell.switchMinistryGroupList.isOn = true
} else {
cell.switchMinistryGroupList.isOn = false
}
}, onFailure: { error in
print("Error getting tags - \(String(describing: error?.localizedDescription))")
// errorWithDomain - OneSignalError
// code - HTTP error code from the OneSignal server
// userInfo - JSON OneSignal responded with
})
viewWillAppear(true)
return cell
}
}
In the above portion of the VC code, this part (below) is what is functioning but apparently not in a way the uses threading properly:
if OneSignalUserTags.range(of: cell.textHiddenUserTagName.text!) != nil {
print("The \(cell.textHiddenUserTagName.text!) UserTag exists for this device.")
cell.switchMinistryGroupList.isOn = true
} else {
cell.switchMinistryGroupList.isOn = false
}
It's not entirely clear what your code is doing, but there seems to be a few things that need sorting out, that will help you solve your problem.
1) Improve the naming of your objects. This helps others see what's going on when asking questions.
Don't call your cell CustomTableViewCell - call it, say, MinistryCell or something that represents the data its displaying. Rather than textLabelMinistryGroupList and textHiddenUserTagName tree ministryGroup and userTagName etc.
2) Let the cell populate itself. Make your IBOutlets in your cell private so you can't assign to them directly in your view controller. This is a bad habit!
3) Create an object (Ministry, say) that corresponds to the data you're assigning to the cell. Assign this to the cell and let the cell assign to its Outlets.
4) Never call viewWillAppear, or anything like it! These are called by the system.
You'll end up with something like this:
In your view controller
struct Ministry {
let group: String
let userTag: String
var tagExists: Bool?
}
You should create an array var ministries: [Ministry] and populate it at the start, rather than dealing with MinistryGroupArray and OneSignalUserTagArray separately.
In your cell
class MinistryCell: UITableViewCell {
#IBOutlet private weak var ministryGroup: UILabel!
#IBOutlet private weak var userTagName: UILabel!
#IBOutlet private weak var switch: UISwitch!
var ministry: Ministry? {
didSet {
ministryGroup.text = ministry?.group
userTagName.text = ministry?.userTag
if let tagExists = ministry?.tagExists {
switch.isEnabled = false
switch.isOn = tagExists
} else {
// We don't know the current state - disable the switch?
switch.isEnabled = false
}
}
}
}
Then you dataSource method will look like…
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! MinistryCell
let ministry = ministries[indexPath.row]
cell.ministry = ministry
if ministry.tagExists == nil {
OneSignal.getTags { tags in
// Success - so update the corresponding ministry.tagExists
// then reload the cell at this indexPath
}, onFailure: { error in
print("Error")
})
}
return cell
}

Resources