I'm looking for a way to make UITableView editable like in phone app in iOS when I want to edit the contact data, user should view data and shall be able to press on edit in order to edit the data.
I've been searching for a way to do this for weeks on the web, but I didn't find anything
There are two methods that I use for editing table cells. The first method is tableView(_: canEditRowAtIndexPath indexPath:). In this method you specify the section and row of the table view that can be edited. For instance>
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
if indexPath.section == 0 {
if (indexPath.row == 0)||(indexPath.row == 2) {
return true
}
}
return false
}
In this example you specify that only the first and third row of the first section of the table can be edited.
The second method I use is the method tableView(_: didSelectRowAtIndexPath:). In this method you specify how to react when the user selects the cell (row). In this method you can specify anything you like to change the cell. You can for instance segue to another view and do whatever you want to do. I, however, usually use an alert or action sheet to change the cell. Here is an example that extends the previous example:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//First check whether the right cell is being selected.
let selectedIndexPath = self.trackDetailsTable.indexPathForSelectedRow
//If the selected row is not in the first section the method returns without doing anything.
guard selectedIndexPath?.section == 0 else {
return
}
if selectedIndexPath?.row == 0 {
//The first row is selected and here the user can change the string in an alert sheet.
let firstRowEditAction = UIAlertController(title: "Edit Title", message: "Please edit the title", preferredStyle: .Alert)
firstRowEditAction.addTextFieldWithConfigurationHandler({ (newTitle) -> Void in
newTitle.text = theOldTitle //Here you put the old string in the alert text field
})
//The cancel action will do nothing.
let cancelAction = UIAlertAction(title: "Cancel", style: .Cancel, handler: { (action) -> Void in
self.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
})
//The Okay action will change the title that is typed in.
let okayAction = UIAlertAction(title: "Ok", style: .Default, handler: { (action) -> Void in
yourObject.thatNeedsTheNewString = (firstRowEditAction.textFields?.first?.text)!
//Do some other stuff that you want to do
self.trackDetailsTable.reloadData() //Don’t forget to reload the table view to update the table content
self.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
})
trackTitleEditAction.addAction(okayAction)
trackTitleEditAction.addAction(cancelAction)
self.presentViewController(trackTitleEditAction, animated: true, completion: nil)
}
if selectedIndexPath?.row == 2 {
//The third row is selected and needs to be changed in predefined content using a standard selection action sheet.
let trackLevelAction = UIAlertController(title: "Select Predefined Content”, message: "Please, select the content”, preferredStyle: UIAlertControllerStyle.ActionSheet)
for content in arrayOfPredefinedContent {
predefinedContentAction.addAction(UIAlertAction(title: content, style: UIAlertActionStyle.Default, handler: { (UIAlertAction) -> Void in
yourObject.thatNeedsTheNewContent = content
//Do some other stuff that you want to do
self.trackDetailsTable.reloadData() //Don’t forget to reload the table view to update the table content
}))
}
trackLevelAction.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: { (UIAlertAction) -> Void in
}))
presentViewController(trackLevelAction, animated: true, completion: nil)
}
}
Hope this helps.
It's a general question but here's a general answer - you link your table cell with a segue to a new scene where you can display an editable form to the user. The user clicks the table cell which triggers the segue. You might find this Apple tutorial useful.
Related
I am having trouble formulating my question here, but I am doing an online course on Swift, it is kind of outdated and not many people reply.
So we are making an app with a todo list, simple todo list that has a list of things you need to do and you can add a checkmark to things you've done.
I decided to make it more channelling for me and decided that I want to be able to edit tasks that I already added. So now when I click on a task I can edit it, but I did not take into account that a regular click also set the checkmark for done.
My question is:
How can I make that the checkmark is set by regular touch, but the edit function is triggered by a long touch, or that is not possible?
Here is the part of the code that I have for the editing of the list, which is also when the checkmark was added, but I commented that out for now.
func editItems(tableView: UITableView, indexPath: IndexPath){
var textField = UITextField()
let alert = UIAlertController(title: "Edit Item", message: "", preferredStyle: .alert)
let action = UIAlertAction(title: "Save", style: .default) { [self] (action) in
self.itemArray[indexPath.row].title = textField.text
self.saveItems()
}
alert.addTextField { (alertTextField) in
alertTextField.text = self.itemArray[indexPath.row].title
textField = alertTextField
}
alert.addAction(action)
present(alert, animated: true, completion: nil)
tableView.reloadData()
}
I have a tableView with a prototype cell. Inside this prototype cell I have a button which I want to use to report a comment.
I tried to use an example UIAlertController but it gives me the following error.
Value of tableViewCell has no member present.
And also this error:
"nil" requires a contextual type.
I used the code below also somewhere else and it worked fine.
What exactly did I do wrong here? I already googled the error types but don't find anything that could help me.
func displayAlert() {
// Declare Alert message
let dialogMessage = UIAlertController(title: "Test", message: "Test", preferredStyle: .alert)
// Create OK button with action handler
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
print("Ok button tapped")
self.deleteRecord()
})
//Add OK and Cancel button to dialog message
dialogMessage.addAction(ok)
// Present dialog message to user
self.present(dialogMessage, animated: true, completion: nil)
}
func deleteRecord()
{
print("Delete record function called")
}
I have a UITableView with some swipe actions. One of them presents an action sheet via UIAlertController, and lets the user change category of a UITableViewCell, making the cell appear in another section.
What I try to achieve is to reload the tableView after the choice has been made, but it seems like the alert is called asynchronously. Can someone help me understand the call stack and where to put the tableview.reloadData() call?
let switchTypeAction = UIContextualAction(style: .normal, title: nil) { [weak self] (_,_,success) in
let ac = UIAlertController(title: "Change type", message: nil, preferredStyle: .actionSheet)
for type in orderItem.orderItemType.allValues where type != item.type {
ac.addAction(UIAlertAction(title: "\(type.prettyName)", style: .default, handler: self?.handleChangeType(item: item, type: type)))
}
ac.addAction(UIAlertAction(title: "Cancel", style: .cancel))
self?.present(ac, animated: true)
tableview.reloadData()
success(true)
}
func handleChangeType(item: orderItem, type: orderItem.orderItemType) -> (_ alertAction:UIAlertAction) -> (){
return { alertAction in
item.type = type
}
}
I would assume that the calls were being executed in this order, but when debugging I see that self?.present(ac, animated: true) are actually extecuted after the block, so the reload and the success response are executed first. Does this have anything to do with the closure being #escaping?
Yes, UIAlertController are "async", in that the VC that presents the alert will still run code, while the alert is presented. In fact this is generally true for any VC presenting another VC.
You should call reloadData in the handler closure that UIAlertAction.init accepts. That's the closure that will be called when the user selects the action. Here, you are passing the return value of handleChangeType, so you should write it here:
func handleChangeType(item: orderItem, type: orderItem.orderItemType) -> (_ alertAction:UIAlertAction) -> (){
return { alertAction in
item.type = type
self.tableView.reloadData()
}
}
If you know the index paths of to and from which the item is moved, you can use moveTow(at:to:) instead to only reload those two rows, instead of everything.
Could you try to help me to solve the problem of updating the UI of my program while showing the UIAlertView instance?
That's the situation:
I'm pressing the toolbar "hide-button" and the alertView is opening;
In the handler of UIAlertAction (OK button) i have a code, where i make several operations:
remove the toolbar "hide-button" pressed and set the button item with activity indicator instead;
making the indicator rolling;
THEN AND ONLY AFTER PREVIOUS STEPS next part of code should start and the data model is being updated, and because it's connected to the tableView by means of NSFetchedResultsControllerDelegate, the tableView's data is gonna be updated automatically. This step can take some time, so it's extremely needed to hold it asynchronously, and while it's being processed the activity indicator should roll;
after that the activity indicator rolling faults, the toolbar button item with it is being removed and the "hide-button" (removed at the 1st step) comes back.
FINISH.
The problem's with updating the UI, when i exchange "hide-button" and "activity-button".
private var hideDataBarButtonItem: UIBarButtonItem?
private var indicatorBarButtonItem = UIBarButtonItem(customView: UIActivityIndicatorView(activityIndicatorStyle: .Gray))
override func viewDidLoad() {
super.viewDidLoad()
...
hideDataBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Stop, target: self, action: #selector(hideAllLoginData))
toolbarItems?.insert(hideDataBarButtonItem!, atIndex: 2)
}
That's the action for hideDataBarButtonItem:
#IBAction func hideAllLoginData(sender: AnyObject) {
let confirmAlert = UIAlertController(title: "Hide all data?", message: "", preferredStyle: .Alert)
confirmAlert.addAction( UIAlertAction(title: "OK", style: .Default, handler: { action in
// remove the clear-button, set the indicator button instead and start indicator rolling
self.toolbarItems?.removeAtIndex(2)
self.toolbarItems?.insert(self.indicatorBarButtonItem, atIndex: 2)
(self.indicatorBarButtonItem.customView as! UIActivityIndicatorView).startAnimating()
print("button with indicator added")
sleep(5) // -> CHECK: this moment indicator should be visible and rolling!
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)) {
for section in self.resources {
for resource in section {
if resource.defRecord != nil {
resource.defRecord = nil
}
}
}
print("data cleared")
dispatch_async(dispatch_get_main_queue()) {
// remove indicator and set the clear-button back
print("button with indicator removed")
(self.indicatorBarButtonItem.customView as! UIActivityIndicatorView).stopAnimating()
self.toolbarItems?.removeAtIndex(2)
self.toolbarItems?.insert(self.hideDataBarButtonItem!, atIndex: 2)
}
}
}) )
confirmAlert.addAction( UIAlertAction(title: "Cancel", style: .Cancel, handler: nil ) )
self.presentViewController(confirmAlert, animated: true, completion: nil)
}
The result of the execution:
button with indicator added
// -> 5 sec of awaiting, but that's nothing in interface changed!
data cleared
button with indicator removed
If i don't remove the indicator button, i can see it after all, but it has to appear earlier. What do i make wrong?
Thank you!
The problem's solved.
It was because the NSFetchedResultsController updated the UI not in the main thread. I had to deactivate it's delegate while my model was updating. Then, in the main thread i has to activate it again and update the tableView data. The solution is here (look at the comments):
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)) {
// clear the delegate in order to not to allow UI update by fetchedResultsController
self.resourceModel.nullifyFetchedResultsControllerDelegate()
for section in self.resources {
for resource in section {
if resource.defRecord != nil {
resource.defRecord = nil
}
}
}
print("data cleared")
dispatch_async(dispatch_get_main_queue()) {
// set the delegate back and update the tableView
self.resourceModel.setFetchedResultsControllerDelegate(self)
self.reloadResources()
self.tableView.reloadData()
// remove indicator and set the clear-button back
print("button with indicator removed")
(self.indicatorBarButtonItem.customView as! UIActivityIndicatorView).stopAnimating()
self.toolbarItems?.removeAtIndex(2)
self.toolbarItems?.insert(self.hideDataBarButtonItem!, atIndex: 2)
}
}
PeterK, thank you for your advices!
I had created a normal segment controller which has two category, category 1 and 2 respectively. Now I have add button, which push me to the new view controller to add an item. When clicking on done button for adding an item I have an alert controller which show the category in which I have to save the item. But I don't know how to get that item in particular segment. If anyone can help.
Thanks
#IBAction func done(sender: AnyObject) {
if let item = itemToEdit {
item.text = textField.text!
item.dateTime = dateTime
textField.becomeFirstResponder()
//item.text = textAreaDescription.text!
//textAreaDescription.becomeFirstResponder()
delegate?.itemDetailViewController(self, didFinishEditingItem: item)
} else {
let alertController = UIAlertController(title: "Choose Category", message: "Choose Category To Save Your Item.", preferredStyle: .Alert)
let toDo = UIAlertAction(title: "Category 1", style: .Default) { (action) in
let item = NoToDoItem()
item.text = self.textField.text!
//item.text = textAreaDescription.text!
item.dateTime = self.dateTime
self.delegate?.itemDetailViewController(self, didFinishAddingItem: item)
}
alertController.addAction(toDo)
let notSure = UIAlertAction(title: "Category 2", style: .Default){ (action) in
let notSureItem = NotSureItem()
notSureItem.text = self.textField.text!
//item.text = textAreaDescription.text!
notSureItem.dateTime = self.dateTime
self.delegate?.itemDetailViewController(self, didFinishAddingNotSureItem: notSureItem)
}
alertController.addAction(notSure)
presentViewController(alertController, animated: true, completion: nil)
}
}
use commitEditingstyle, then your cells will still display the Delete button when you swipe. Use editingStyleForRowAtIndexPath: instead. Put an if statement in to test which segment is selected, and return UITableViewCellEditingStyle.None (to disable swipe to delete) or .Delete (to enable swipe to delete) accordingly. If you want to be able to delete the cells by putting the table view into editing mode, then also test tableView.editing to determine whether to use .Delete or .None.