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.
Related
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")
}
Trying to figure out why I am getting this error. I have a single view application with two viewcontrollers. I have a segue to the second vc on a button touch:
#IBAction func showAccount(_ sender: Any) {
performSegue(withIdentifier: "toAccountFromRoot", sender: self)
}
I have a facade pattern set up with a Controller file and several helper files. I set the Controller up on the AccountViewController file:
let myController = Controller.sharedInstance
One of the helper files is to set up a central AlertController for simplified alerts (no extra buttons). It has one method, presentAlert that takes a few arguments, alert title, messsage, presenter.
On my AccountViewController I am attempting to present an alert using this code:
myController.alert.presentAlert("No Agent", "You have not registered an agent yet.", self)
and in the AlertManager file, presentAlert looks like this:
func presentAlert(_ title:String, _ message:String, _ presenter:UIViewController) {
let myAlert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
myAlert.addAction(okAction)
presenter.present(myAlert, animated:true, completion:nil)
}
I've done this in quite a few apps without an issue so I am confused as to why the compiler thinks the AccountViewController view is not in the window hierarchy.
Dang it, typed all that and then I solved it 5 seconds later. Just in case someone else runs into the same problem, I was trying to present the alert in viewDidLoad, should of been doing it in viewDidAppear.
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 have a setting where I used UIAlertController to show progress of a task till the task has some status to return.
The code looks like this
class AlertVCDemo: UIViewController {
let alertVC = UIAlertController(title: "Search",
message: "Searching....", preferredStyle:
UIAlertControllerStyle.Alert)
override func viewDidLoad() {
super.viewDidLoad()
alertVC.addAction(UIAlertAction(title: "Ok", style: .Default, handler: { action in
switch action.style{
case .Default:
print("default")
case .Cancel:
print("cancel")
case .Destructive:
print("destructive")
}
}))
}
func showAlertVC() {
dispatch_async(dispatch_get_main_queue(), {
self.presentViewController(self.alertVC, animated: true, completion: nil)
})
}
#IBAction func searchButtonClicked(sender: AnyObject) {
showAlertVC()
// Now do the real search that will take a while,
// depending on the result change the message of the alert VC
}
}
Somehow I see that the alert view controller is not shown in the main thread. The search logic completes eventually. I am using iOS 9.3. I did thorough research before asking this question and none of the solutions suggested on similar threads helped. Not sure why dispatch_async doesn't present the alert VC when search is still happening.
Even if I have the dispatch_async not in a method, things don't change.
#IBAction func searchButtonClicked(sender: AnyObject) {
showAlertVC()
// Now do the real search that will take a while,
// depending on the result change the message of the alert VC
}
is called in the main thread. So i guess your "search code" is blocking the UI thread.
Try something like this
#IBAction func searchButtonClicked(sender: AnyObject) {
showAlertVC()
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
// put your search code here
}
}
Check for UIViewController's presentedViewController property. It should be nil. If it is not nil, presentViewController method wont work.
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.