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
Related
New swift user here. I would like to open a pop up (controlled by a separate view controller) over the normal view controller as part of a function call (i.e. there is no button that is pressed or similar). How do I write this? I would also like to send information to that influences the images that are shown in the pop up.
I have previously managed to open a modal pop-up via a button press. I don't really know if there is something peculiar about these kinds of popups but if there are I'd like to have the pop up above look the same.
I hope this is clear enough for someone to understand what I need.
You can add a method in super view controller like this:
class MainViewController: UIViewController {
// MARK: - View methods
override func viewDidLoad() {
super.viewDidLoad()
}
/// This Method is used for Alert With Action Method
func showAlertViewWithPopAction(message: String, title: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.alert)
let okAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in
///add you logic here
}
alert.addAction(okAction)
self.present(alert, animated: true, completion: nil)
}
}
And Inherit this controller in every controller like this and call this method from the controller like this:
class FirstViewController: MainViewController {
// MARK: - View methods
override func viewDidLoad() {
super.viewDidLoad()
///Call alert method according to your use
Self.showAlertViewWithPopAction(message: "This is demo alert", title: "Alert")
}
}
I have a tableview definition in which I am attempting to invoke an UIAlertController popup. I installed a button in the prototype tableView cell, when the button is touched, an IBAction handles the event. The problem is that the compiler won't let me.
present(alertController, animated: true, completion: nil)
Generates compiler error: "Use of unresolved identifier 'present'
Here is the code:
class allListsCell: UITableViewCell {
#IBOutlet var cellLable: UIView!
#IBOutlet var cellSelected: UILabel!
var colorIndex = Int()
#IBAction func cellMarkButton(_ sender: UIButton, forEvent event: UIEvent) {
if colors[self.colorIndex].selected == false {
colors[self.colorIndex].selected = true
cellSelected.text = "•"
let alertController = UIAlertController(title: "???", message: "alertA", preferredStyle: .alert)
let OKAction = UIAlertAction(title: "dismiss", style: .default) { (action:UIAlertAction!) in
print("Sand: you have pressed the Dismiss button");
}
alertController.addAction(OKAction)
present(alertController, animated: true, completion: nil) // ERROR
} else {
colors[self.colorIndex].selected = false
cellSelected.text = ""
}
}
If I comment that one line, the app runs correctly for each cell...
You can't call present AlertController inside a tableView cell , it needs a subclass of UIViewController or other equivalent one , you should use a delegate or some sort of notification to handle that , see my answer here for the same problem AlertControllerCallInsideCell
Edit : Form Docs , it's an instance method inside UIViewController . so it can't be called inside any other class of other type (UITableViewCell) in your case
It is not possible to call the "present" method from a TableViewCell, I recommend having a function in the main controller to show your UIAlertController.
Using this code you can instantiate the parent driver and execute any available function:
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
//UITableViewCell
if let controller = self.parentViewController as? YourController
{
controller.showAlert()
}
Here is an example of its use with a CollectionViewCell:
https://github.com/AngelH2/CollectionViewCell-Comunication/tree/master/CollectionCellAction
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.
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