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!
Related
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.
I want to display a UIAlertController on top of a UIViewController with a UICollectionView inside. The collection view needs to focus on startup, so I overrode the preferredFocusableView variable as follows:
override var preferredFocusedView: UIView? {
return self.collectionView
}
With tvOS 9 all worked fine: the alert controller opened properly and I was able to choose one of the UIAlertActions displayed.
On tvOS 10 Golden Master, after opening the alert controller and scrolling to another action, the focus disappears from the screen and I'm unable to scroll to other actions or tapping on the Menu button of the Siri Remote. The app remains stuck in the alert controller and I can hear the scrolling sound when I try to scroll to other actions but nothing happens on screen. I have to force quit the app and reopen it.
This is the code of the app. I tried to set the preferredFocusableView to alertController.preferredFocusedView or by removing the focus methods of the collection view but with no results.
var alertController : UIAlertController?
func showAlert() {
alertController = UIAlertController(title:"Example title", message: "Example description", preferredStyle: .Alert)
let action1 = UIAlertAction(title: "Option 1", style: .Default) { (action : UIAlertAction) -> Void in
//call to another method
}
// action2, action3, action4...
let action5 = UIAlertAction(title: "Option 5", style: .Default) { (action : UIAlertAction) -> Void in
//call to another method
}
let actionDismiss = UIAlertAction(title: "Dismiss", style: .Destructive) { (action : UIAlertAction) -> Void in
self.alertController!.dismissViewControllerAnimated(true, completion: nil)
}
alertController!.addAction(action1)
alertController!.addAction(action2)
alertController!.addAction(action3)
alertController!.addAction(action4)
alertController!.addAction(action5)
alertController!.addAction(actionDismiss)
alertController!.preferredAction = action1
self.presentViewController(alertController!, animated: true, completion: nil)
}
override var preferredFocusedView: UIView? {
if self.alertController != nil {
return self.alertController!.preferredFocusedView
} else {
return self.collectionView
}
}
Apple just replied to my radar:
Inside the attached application, you’re overwriting the functionality
of UIScrollView in an extension to return true for
canBecomeFocused(), which is the cause of these unexpected side
effects. The focus seems to be disappearing when moving to a second
UIAlertController option; however, it is actually transferring focus
to the scroll view wrapped around the various components of the
UIAlertController, since this is now allowed due to the extension
mentioned above.
To solve this, create a custom subclass of UIScrollView to be used
only in the instances where canBecomeFocused() must return true.
You're overriding the entire systems focus engine. Try this:
// MARK: - Update Focus Helper
var viewToFocus: UIView? = nil {
didSet {
if viewToFocus != nil {
self.setNeedsFocusUpdate()
self.updateFocusIfNeeded()
}
}
}
override weak var preferredFocusedView: UIView? {
if viewToFocus != nil {
return viewToFocus
} else {
return super.preferredFocusedView
}
}
Then set your alert as the view when you want the focus to change to it:
viewToFocus = someView
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 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.
The issue in question involves an AddItemView which is presented modally by its delegate and contains a tableView. When the user selects an item from the tableView, it triggers an action on the delegate. Depending on the response from the server, the delegate may present a either another modal view or a UIAlertView on top of the current modal.
Important Note: This UIAlertView needs to be presented while the modal is still on screen. The modally presented view containing the tableView cannot be dismissed after user selection because the user needs to be able to select multiple items from the table and, one by one, send them back to the delegate for processing.
Currently, the UIAlerView is not being displayed and I suspect it is because the already-presented modal is preventing that. Is there a workaround to present the UIAlertView from the delegate when the delegate is sitting underneath a modal and without dismissing that modal?
The UIAlertView is currently displayed like so by the delegate, while the delegate is sitting under a modal:
var alert = UIAlertController(title: "Error", message: "Error message from server", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "actionOne", style: .Default, handler: { action in
// perform some action
}))
alert.addAction(UIAlertAction(title: "actionTwo", style: .Destructive, handler: { action in
// perform some action
}))
self.presentViewController(alert, animated: true, completion: nil)
Here is the error that is returned when the UIAlertView is presented by the delegate:
Warning: Attempt to present <UIAlertController: 0x156da6300> on <productionLINK_Scanner.ContainerContents: 0x156e65b20> whose view is not in the window hierarchy!
If possible, please provide answer using Swift.
Solved:
Used the following extension, thanks to yonat on GitHub:
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.sharedApplication().keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
Within the delegate in question, it was implemented like so:
var alert = UIAlertController(title: "Alert Title", message: "Message Body", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: { action in
}))
if let topController = UIApplication.topViewController(base: self) {
topController.presentViewController(alert, animated: true, completion: nil)
} else {
// If all else fails, attempt to present the alert from this controller.
self.presentViewController(alert, animated: true, completion: nil)
}
This now allows the following process:
ContainerView is loaded as a delegate of ItemTableView
User clicks searchButton
ContainerView modally presents a list of items ItemTableView
Each time a use selects a row in ItemTableView, the didSelectItem function is called in the instance of ContainerView that presented the ItemTableView. The ItemTableView does NOT get dismissed - the user can continue selecting items.
ContainerView submits a request to the server
Depending on the response, the ContainerView may present a UIAlertView.
The alertView is properly displayed using the above-mention code on top of whatever view is top-most in the hierarchy.
"When the user selects an item, it triggers and action on the delegate"
set a break point at start of the delegate method which is triggered by selecting an item. check whether that delegate method is being called or not ?
and test this too. (ACTION :UIAlertAction!)in