56

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)

enter image description here

CalZone
  • 1,701
  • 1
  • 17
  • 29
  • I tried to work around the `UIAlertController` limitations, but no matter how I managed, it was never good enough. If you're still struggling with this, I have created [a library](https://github.com/danielsaidi/Sheeeeeeeeet) that may be of help. It lets you create custom sheets with a bunch of built-in types. It can be extended and restyled as well. – Daniel Saidi Mar 15 '18 at 22:52
  • @CalZone did you managed to do this? – Arshad Shaik Aug 17 '23 at 10:36

6 Answers6

92

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 enter image description here

iOS 9 enter image description here

iOS 10 enter image description here

Cesare
  • 9,139
  • 16
  • 78
  • 130
Keller
  • 17,051
  • 8
  • 55
  • 72
  • Thats awesome and really helpful I still get a line behind the green view in the margins. I guess I'll put a container view around the green view for that. with 0 margins. – CalZone Sep 29 '15 at 15:04
  • Thanks, mate. This was fantastic. Just made slight adjustment to the width to the custom view but all good! Cheers. – Felipe May 10 '16 at 11:23
  • My Objective C answer still works just fine. The Swift answer used old syntax but is now updated for Swift 3.0, thanks @palme. – Keller Nov 28 '16 at 21:42
  • 2
    That's why I mentioned in my answer that "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." The question was to add subviews to a UIAlertController, which the accepted answer does. – Keller Dec 07 '16 at 19:51
  • Thanks..It worked.. An amazing tweak using alert controller !! – Vinayak Hejib Mar 15 '17 at 13:00
  • 1
    I believe the initial frame size for the UIAlertController is the same as the UIView. On an iPhone the above code works fine because the alertController takes the full width of the device. On an iPad the alertController is resized. To make the subview automatically resize, set the resizing mask customView.autoresizingMask = UIViewAutoresizingFlexibleWidth; – Eric D'Souza Sep 10 '17 at 20:11
  • genius... nothing more to say!! Thank you! – Mattia Ducci Oct 24 '19 at 15:30
  • It fixed 120 height?. I change but not reflecting please help? – Yogesh Patel Mar 09 '21 at 15:35
  • this will throw a warning with constraints - "Unable to simultaneously satisfy constraints." – ShadeToD Mar 12 '21 at 08:47
  • This works but the drawback is an actionSheet with multiple actions. My actionSheet has 5 rations and the green view completely covers the top 2 and part of the one in the middle – Lance Samaria Nov 25 '21 at 11:07
44

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:

enter image description here

Cesare
  • 9,139
  • 16
  • 78
  • 130
  • this is the best answer - not the one that was "picked" I'm glad someone has done this properly :-) – GregJaskiewicz Sep 11 '20 at 15:52
  • The best answer regarding the topic. Wonder why this is not an accepted answer here. – Ankur Lahiry Feb 20 '21 at 18:52
  • It's going to break if dialog has a title under some conditions. Depending on title length & user font size preferences title could overlay with custom view. Top padding should be calculated. Don't have much ios experience otherwise would post solution. – IliaEremin Jul 21 '21 at 12:43
  • 1
    According to Apple [UIAlertController documentation](https://developer.apple.com/documentation/uikit/uialertcontroller): "Important: The view hierarchy for this class is private and must not be modified." – Jamie McDaniel Mar 06 '23 at 21:51
20

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: enter image description here

Sentry.co
  • 5,355
  • 43
  • 38
Zaggo
  • 786
  • 1
  • 6
  • 14
  • This works well. Just wondering if this will get rejected in the review process, any idea? – Knolraap Feb 22 '18 at 08:08
  • 2
    There's no private API involved, so I wouldn't know why Apple should reject it. – Zaggo Feb 23 '18 at 09:01
  • 3
    Facing only one problem, that `customView` showing in grey color instead of red color. Any idea?? – Bhavin_m Nov 26 '19 at 07:49
  • This looks like a nice, clean solution but I thought people highly advise against extending `UIAlertController` because Apple doesn't want you to do that and might break your "hack" with a future update. Might this still be the case with this solution too? – Neph Mar 24 '21 at 13:38
  • I tried to handle any failure as graceful as possible. In case of any structural changes by Apple, the worst case outcome should be that instead of the custom view, the alert just shows the fallback text message, which is part of the API exactly for that case. – Zaggo Mar 25 '21 at 14:34
  • Looks like the time has come eventually to forfeit those customizations to `UIAlertController`. For more look [here](https://stackoverflow.com/questions/56955800/ios13-uialertcontroller-with-custom-view-preferredstyle-as-actionsheet-graysca) – nayem Feb 16 '22 at 09:53
  • @Bhavin_m I found that if I change the preferredStyle of the UIAlertController to .alert, the red color is there instead of the gray. Not ideal if you need the actionSheet but there you go. – Troy Sartain Nov 27 '22 at 03:17
  • I like your idea to look the container view for message label but I don't like this strange bottom margin under the custom red view and I changed the code for myself and instead of finding this label I am searching the UIStackView that holds UIActionViews and then insert custom view and set constraints between [-customView-stackView] + that there is no need to manipulate with this marker string - Apple might stop in Future using stack view for UIActionViews (( – David Apr 28 '23 at 15:21
4

For the lazy people the Swift 3.0 and iOS >= 9 optimised version of @Keller's answer:

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:{})
palme
  • 2,499
  • 2
  • 21
  • 38
0

I liked @Zaggo's answer the most, but decided to leave my realization:

func showActionSheet(_ sender: UIViewController) { 

    let actionSheet = UIAlertController(title: "Debug menu", message: nil, preferredStyle: .actionSheet)
    
    let debugActions = [
        UIAlertAction(title: "Send mail", style: .default) { _ in
     
        },
        UIAlertAction(title: "Drop database", style: .default) { _ in

        },
        UIAlertAction(title: "Show logs console", style: .default) { _ in

        }
    ]
    
    let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { action in
        actionSheet.dismiss(animated: true)
    }
    
    debugActions.forEach(actionSheet.addAction)
    actionSheet.addAction(cancelAction)
    
    guard let actionSheetStackView = actionSheet.view.findStack() else { return }
    
    let customView = UIView()
    customView.translatesAutoresizingMaskIntoConstraints = false
    customView.backgroundColor = .red
    actionSheet.view.addSubview(customView)
    
    customView.translatesAutoresizingMaskIntoConstraints = false
    customView.topAnchor.constraint(equalTo: actionSheet.view.topAnchor, constant: 45).isActive = true
    customView.bottomAnchor.constraint(equalTo: actionSheetStackView.topAnchor).isActive = true
    customView.rightAnchor.constraint(equalTo: actionSheet.view.rightAnchor).isActive = true
    customView.leftAnchor.constraint(equalTo: actionSheet.view.leftAnchor).isActive = true
    customView.heightAnchor.constraint(equalToConstant: 50).isActive = true
    
    sender.present(actionSheet, animated: true)
}

func findStack() -> UIStackView? {
    if let stack = self as? UIStackView {
        return stack
    }
    
    for subview in self.subviews {
        if let found = subview.findStack() {
            return found
        }
    }
    
    return nil
}

result

David
  • 1,061
  • 11
  • 18
-1

Here is an Objective-C version of @Cesare's solution

- (void) showPickerController {
    UIAlertController * alertController = [UIAlertController alertControllerWithTitle:@"Translation Language" message:nil preferredStyle:UIAlertControllerStyleActionSheet];
    UIView *customView = [[UIView alloc] init];
    [alertController.view addSubview:customView];
    customView.translatesAutoresizingMaskIntoConstraints = NO;
    [customView.topAnchor constraintEqualToAnchor:alertController.view.topAnchor constant:45].active = YES;
    [customView.rightAnchor constraintEqualToAnchor:alertController.view.rightAnchor constant:-10].active = YES;
    [customView.leftAnchor constraintEqualToAnchor:alertController.view.leftAnchor constant:10].active = YES;
    [customView.heightAnchor constraintEqualToConstant:250].active = YES;

    alertController.view.translatesAutoresizingMaskIntoConstraints = NO;
    [alertController.view.heightAnchor constraintEqualToConstant:430].active = YES;
    
    customView.backgroundColor = [UIColor greenColor];
    
    UIAlertAction* selectAction = [UIAlertAction actionWithTitle:@"Select" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
    }];
    UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
    }];
    [alertController addAction:selectAction];
    [alertController addAction:cancelAction];
    
    [self presentViewController:alertController animated:YES completion:nil];
}
kanso
  • 750
  • 8
  • 17