UIAlertController - add custom views to actionsheet
I'm trying to make the actionsheet as it shows in the messages app on iOS when we try to attach an image as in the screenshot.
I realized in new UIAlertController, we can't fit any custom views. Any way I can make exactly this?
My code looks pretty standard.
let alertController = UIAlertController(title: "My AlertController", message: "tryna show some images here man", preferredStyle: UIAlertControllerStyle.ActionSheet)
let okAction = UIAlertAction(title: "oks", style: .Default) { (action: UIAlertAction) -> Void in
alertController.dismissViewControllerAnimated(true, completion: nil)
}
let cancelAction = UIAlertAction(title: "Screw it!", style: .Cancel) { (action: UIAlertAction) -> Void in
alertController.dismissViewControllerAnimated(true, completion: nil)
}
alertController.addAction(okAction)
alertController.addAction(cancelAction)
self.presentViewController(alertController, animated: true, completion: nil)
Solution 1:
UIAlertController extends UIViewController, which has a view property. You can add subviews to that view to your heart's desire. The only trouble is sizing the alert controller properly. You could do something like this, but this could easily break the next time Apple adjusts the design of UIAlertController.
Swift 3
let alertController = UIAlertController(title: "\n\n\n\n\n\n", message: nil, preferredStyle: UIAlertControllerStyle.actionSheet)
let margin:CGFloat = 10.0
let rect = CGRect(x: margin, y: margin, width: alertController.view.bounds.size.width - margin * 4.0, height: 120)
let customView = UIView(frame: rect)
customView.backgroundColor = .green
alertController.view.addSubview(customView)
let somethingAction = UIAlertAction(title: "Something", style: .default, handler: {(alert: UIAlertAction!) in print("something")})
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: {(alert: UIAlertAction!) in print("cancel")})
alertController.addAction(somethingAction)
alertController.addAction(cancelAction)
DispatchQueue.main.async {
self.present(alertController, animated: true, completion:{})
}
Swift
let alertController = UIAlertController(title: "\n\n\n\n\n\n", message: nil, preferredStyle: UIAlertControllerStyle.actionSheet)
let margin:CGFloat = 10.0
let rect = CGRect(x: margin, y: margin, width: alertController.view.bounds.size.width - margin * 4.0, height: 120)
let customView = UIView(frame: rect)
customView.backgroundColor = .green
alertController.view.addSubview(customView)
let somethingAction = UIAlertAction(title: "Something", style: .default, handler: {(alert: UIAlertAction!) in print("something")})
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: {(alert: UIAlertAction!) in print("cancel")})
alertController.addAction(somethingAction)
alertController.addAction(cancelAction)
self.present(alertController, animated: true, completion:{})
Objective-C
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"\n\n\n\n\n\n" message:nil preferredStyle:UIAlertControllerStyleActionSheet];
CGFloat margin = 8.0F;
UIView *customView = [[UIView alloc] initWithFrame:CGRectMake(margin, margin, alertController.view.bounds.size.width - margin * 4.0F, 100.0F)];
customView.backgroundColor = [UIColor greenColor];
[alertController.view addSubview:customView];
UIAlertAction *somethingAction = [UIAlertAction actionWithTitle:@"Something" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {}];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {}];
[alertController addAction:somethingAction];
[alertController addAction:cancelAction];
[self presentViewController:alertController animated:YES completion:^{}];
That being said, a much less hacky approach would be to make your own view subclass that works similarly to UIAlertController's UIAlertActionStyle layout. In fact, the same code looks slightly different in iOS 8 and iOS 9.
iOS 8
iOS 9
iOS 10
Solution 2:
Cleanest solution I've found so far using AutoLayout constraints:
func showPickerController() {
let alertController = UIAlertController(title: "Translation Language", message: nil, preferredStyle: .actionSheet)
let customView = UIView()
alertController.view.addSubview(customView)
customView.translatesAutoresizingMaskIntoConstraints = false
customView.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 45).isActive = true
customView.rightAnchor.constraint(equalTo: alertController.view.rightAnchor, constant: -10).isActive = true
customView.leftAnchor.constraint(equalTo: alertController.view.leftAnchor, constant: 10).isActive = true
customView.heightAnchor.constraint(equalToConstant: 250).isActive = true
alertController.view.translatesAutoresizingMaskIntoConstraints = false
alertController.view.heightAnchor.constraint(equalToConstant: 430).isActive = true
customView.backgroundColor = .green
let selectAction = UIAlertAction(title: "Select", style: .default) { (action) in
print("selection")
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(selectAction)
alertController.addAction(cancelAction)
self.present(alertController, animated: true, completion: nil)
}
Output:
Solution 3:
I wrote an extension for UIAlertController (in Swift 4), which solves the layout issues with autolayout. There's even a fallback message string in case something doesn't work (due to future changes in the UIAlertController layout).
import Foundation
extension UIAlertController {
/// Creates a `UIAlertController` with a custom `UIView` instead the message text.
/// - Note: In case anything goes wrong during replacing the message string with the custom view, a fallback message will
/// be used as normal message string.
///
/// - Parameters:
/// - title: The title text of the alert controller
/// - customView: A `UIView` which will be displayed in place of the message string.
/// - fallbackMessage: An optional fallback message string, which will be displayed in case something went wrong with inserting the custom view.
/// - preferredStyle: The preferred style of the `UIAlertController`.
convenience init(title: String?, customView: UIView, fallbackMessage: String?, preferredStyle: UIAlertController.Style) {
let marker = "__CUSTOM_CONTENT_MARKER__"
self.init(title: title, message: marker, preferredStyle: preferredStyle)
// Try to find the message label in the alert controller's view hierarchie
if let customContentPlaceholder = self.view.findLabel(withText: marker),
let customContainer = customContentPlaceholder.superview {
// The message label was found. Add the custom view over it and fix the autolayout...
customContainer.addSubview(customView)
customView.translatesAutoresizingMaskIntoConstraints = false
customContainer.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[customView]-|", options: [], metrics: nil, views: ["customView": customView]))
customContainer.addConstraint(NSLayoutConstraint(item: customContentPlaceholder,
attribute: .top,
relatedBy: .equal,
toItem: customView,
attribute: .top,
multiplier: 1,
constant: 0))
customContainer.addConstraint(NSLayoutConstraint(item: customContentPlaceholder,
attribute: .height,
relatedBy: .equal,
toItem: customView,
attribute: .height,
multiplier: 1,
constant: 0))
customContentPlaceholder.text = ""
} else { // In case something fishy is going on, fall back to the standard behaviour and display a fallback message string
self.message = fallbackMessage
}
}
}
private extension UIView {
/// Searches a `UILabel` with the given text in the view's subviews hierarchy.
///
/// - Parameter text: The label text to search
/// - Returns: A `UILabel` in the view's subview hierarchy, containing the searched text or `nil` if no `UILabel` was found.
func findLabel(withText text: String) -> UILabel? {
if let label = self as? UILabel, label.text == text {
return label
}
for subview in self.subviews {
if let found = subview.findLabel(withText: text) {
return found
}
}
return nil
}
}
And here's a usage sample:
// Create a custom view for testing...
let customView = UIView()
customView.translatesAutoresizingMaskIntoConstraints = false
customView.backgroundColor = .red
// Set the custom view to a fixed height. In a real world application, you could use autolayouted content for height constraints
customView.addConstraint(NSLayoutConstraint(item: customView,
attribute: .height,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1,
constant: 100))
// Create the alert and show it
let alert = UIAlertController(title: "Alert Title",
customView: customView,
fallbackMessage: "This should be a red rectangle",
preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: "Yay!", style: .default, handler: nil))
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
self.present(alert, animated: true, completion: nil)
Which will show something like this: