Programming
How to present UIAlertController when not in a view controller
Presenting a UIAlertController when you’re not directly within a view controller can be a tricky situation that many iOS developers encounter. It’s a common scenario, particularly when working with app extensions, background tasks, or certain architectural patterns. This seemingly simple task can become a source of frustration if you’re not familiar with the correct approach. In this article, we’ll dive deep into the intricacies of presenting alerts outside of a view controller context, exploring various solutions, best practices, and potential pitfalls.
Understanding the Challenge
UIAlertController, the standard way to present alerts and action sheets in iOS, is designed to be presented by a view controller. Its presentation relies on the view controller’s presentation hierarchy. When you’re outside this hierarchy, such as in an app extension or a background task, attempting to present an alert directly can lead to crashes or unexpected behavior.
The core issue stems from the fact that present(_:animated:completion:), the method used to display a UIAlertController, requires a presenting view controller. Without one, the system doesn’t know where to anchor the alert in the UI.
The key is to find the topmost presented view controller and use that as the presenter for your alert.
Finding the Topmost View Controller
The solution involves traversing the application’s window hierarchy to locate the topmost presented view controller. This is typically the view controller currently visible to the user.
Here’s a Swift function to achieve this:
func topMostViewController() -> UIViewController? { guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let window = windowScene.windows.first(where: { $0.isKeyWindow }) else { return nil } var topController = window.rootViewController while let presentedViewController = topController?.presentedViewController { topController = presentedViewController } return topController }
This function iterates through the presented view controllers until it finds the one at the top of the hierarchy. This ensures that your alert is presented on the currently active screen.
Presenting the Alert
Once you’ve obtained the topmost view controller, presenting the UIAlertController becomes straightforward:
if let topVC = topMostViewController() { let alert = UIAlertController(title: "Alert Title", message: "Alert Message", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) topVC.present(alert, animated: true, completion: nil) }
This code snippet demonstrates how to present a simple alert. Remember to adapt the title, message, and actions to your specific needs. This method ensures your alert displays correctly, even outside a typical view controller context.
Handling Edge Cases
While the above approach works in most cases, certain situations require additional consideration.
App Extensions
In app extensions, accessing the application’s main window directly is not permitted. Instead, you need to use the extension context’s present(_:animated:completion:) method, providing a view controller within the extension as the presenter. Often, this means creating a temporary or dedicated view controller within the extension solely for presenting alerts.
Background Tasks
Presenting UI elements from background tasks is generally discouraged. If absolutely necessary, ensure you dispatch the alert presentation back to the main thread using DispatchQueue.main.async.
- Always dispatch UI updates to the main thread.
- Avoid presenting UI from background tasks if possible.
Best Practices
Following these best practices ensures a smooth and reliable alert presentation experience.
- Encapsulate the logic: Create a reusable utility function to handle finding the topmost view controller and presenting the alert. This promotes code cleanliness and reduces redundancy.
- Handle nil topmost view controller: The
topMostViewController()function might returnnilunder specific circumstances. Implement appropriate error handling to prevent crashes. Consider logging the event or providing fallback behavior. - Contextualize your alerts: Craft clear and concise alert messages that provide context to the user. Avoid generic messages that offer little value.
For further information, refer to Apple’s official documentation on UIAlertController.
Infographic Placeholder: [Insert infographic illustrating the view controller hierarchy and the process of finding the topmost view controller]
Alternatives to UIAlertController
While UIAlertController is the standard approach, other methods exist for displaying information to the user, especially in constrained environments like app extensions. Consider using local notifications or, if appropriate, updating the UI of your containing app directly (if applicable) as alternatives.
Explore third-party libraries for customized alert presentations that offer more flexibility in terms of appearance and behavior. However, carefully evaluate any third-party library to ensure it aligns with your project’s requirements and maintains the integrity of your app’s performance.
See also this helpful resource on Stack Overflow.
- Consider user experience when choosing alternative presentation methods.
- Thoroughly test any alternative solutions to ensure compatibility and functionality.
This article provides you with the tools and techniques to effectively present UIAlertController instances when you aren’t working directly within a view controller. Remember to handle edge cases and follow best practices to ensure a polished and user-friendly experience. By understanding the underlying principles and applying the provided solutions, you can overcome this common challenge and deliver a seamless user experience. Learn more about advanced iOS development techniques here. Dive into the code examples and adapt them to your specific use cases. You can find more useful tips and tricks on iOS development and Swift programming.
FAQ
Q: What if the topMostViewController() function still returns nil?
A: This could indicate an issue with your app’s window setup. Double-check that the keyWindow property is correctly set. Alternatively, if working in a context where a window isn’t readily available (like some background tasks), you might need to re-evaluate the necessity of presenting a UI element and explore alternative communication methods.
Question & Answer :
Scenario: The user taps on a button on a view controller. The view controller is the topmost (obviously) in the navigation stack. The tap invokes a utility class method called on another class. A bad thing happens there and I want to display an alert right there before control returns to the view controller.
+ (void)myUtilityMethod { // do stuff // something bad happened, display an alert. }
This was possible with UIAlertView (but perhaps not quite proper).
In this case, how do you present a UIAlertController, right there in myUtilityMethod?
At WWDC, I stopped in at one of the labs and asked an Apple Engineer this same question: “What was the best practice for displaying a UIAlertController?” And he said they had been getting this question a lot and we joked that they should have had a session on it. He said that internally Apple is creating a UIWindow with a transparent UIViewController and then presenting the UIAlertController on it. Basically what is in Dylan Betterman’s answer.
But I didn’t want to use a subclass of UIAlertController because that would require me changing my code throughout my app. So with the help of an associated object, I made a category on UIAlertController that provides a show method in Objective-C.
Here is the relevant code:
#import "UIAlertController+Window.h" #import <objc/runtime.h> @interface UIAlertController (Window) - (void)show; - (void)show:(BOOL)animated; @end @interface UIAlertController (Private) @property (nonatomic, strong) UIWindow *alertWindow; @end @implementation UIAlertController (Private) @dynamic alertWindow; - (void)setAlertWindow:(UIWindow *)alertWindow { objc_setAssociatedObject(self, @selector(alertWindow), alertWindow, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (UIWindow *)alertWindow { return objc_getAssociatedObject(self, @selector(alertWindow)); } @end @implementation UIAlertController (Window) - (void)show { [self show:YES]; } - (void)show:(BOOL)animated { self.alertWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; self.alertWindow.rootViewController = [[UIViewController alloc] init]; id<UIApplicationDelegate> delegate = [UIApplication sharedApplication].delegate; // Applications that does not load with UIMainStoryboardFile might not have a window property: if ([delegate respondsToSelector:@selector(window)]) { // we inherit the main window's tintColor self.alertWindow.tintColor = delegate.window.tintColor; } // window level is above the top window (this makes the alert, if it's a sheet, show over the keyboard) UIWindow *topWindow = [UIApplication sharedApplication].windows.lastObject; self.alertWindow.windowLevel = topWindow.windowLevel + 1; [self.alertWindow makeKeyAndVisible]; [self.alertWindow.rootViewController presentViewController:self animated:animated completion:nil]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; // precaution to ensure window gets destroyed self.alertWindow.hidden = YES; self.alertWindow = nil; } @end
Here is a sample usage:
// need local variable for TextField to prevent retain cycle of Alert otherwise UIWindow // would not disappear after the Alert was dismissed __block UITextField *localTextField; UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Global Alert" message:@"Enter some text" preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { NSLog(@"do something with text:%@", localTextField.text); // do NOT use alert.textfields or otherwise reference the alert in the block. Will cause retain cycle }]]; [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { localTextField = textField; }]; [alert show];
The UIWindow that is created will be destroyed when the UIAlertController is dealloced, since it is the only object that is retaining the UIWindow. But if you assign the UIAlertController to a property or cause its retain count to increase by accessing the alert in one of the action blocks, the UIWindow will stay on screen, locking up your UI. See the sample usage code above to avoid in the case of needing to access UITextField.
I made a GitHub repo with a test project: FFGlobalAlertController