Swift Custom UIAlertView

Thomas picture Thomas · May 26, 2016 · Viewed 10.4k times · Source

I'm trying to make a confirm deletion popup view. Because the design I want is very different from the style of the typical UIAlertView popup, I decided to create a custom ConfirmationViewController that I would trigger to popup.

Here is what the typical UIAlertView looks like:

enter image description here

And here's what I want mine to look like:

enter image description here

Here's how I'm currently making my custom ConfirmationViewController popup:

let confirmationViewController = ConfirmationViewController()
confirmationViewController.delegate = self
confirmationViewController.setTitleLabel("Are you sure you want to remove \(firstName)?")
confirmationViewController.modalPresentationStyle = UIModalPresentationStyle.Popover
confirmationViewController.preferredContentSize = CGSizeMake(230, 130)

let popoverConfirmationViewController = confirmationViewController.popoverPresentationController
popoverConfirmationViewController?.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
popoverConfirmationViewController?.delegate = self
popoverConfirmationViewController?.sourceView = self.view
popoverConfirmationViewController?.sourceRect = CGRectMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds),0,0)
presentViewController(
    confirmationViewController,
    animated: true,
    completion: nil)

And here's how I'm getting the notification when the CANCEL or REMOVE button is pressed:

extension UserProfileTableViewController: ConfirmationViewControllerDelegate {
    func cancelButtonPressed() {
        print("Cancel button pressed")
    }

    func confirmationButtonPressed(objectToDelete: AnyObject?) {
        print("Delete button pressed")
    }
}

However, what I like about using a UIAlertView is that I can hardcode in the action I want performed when a particular button is pressed, like this:

let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .Alert)

let cancelAction = UIAlertAction(title: "Cancel", style: .Default, handler: {(ACTION) in
    print("Perform cancel action")
})

let deleteAction = UIAlertAction(title: "Remove", style: .Destructive, handler: {(ACTION) in
    print("Perform delete action")
})

alertController.addAction(cancelAction)
alertController.addAction(deleteAction)

presentViewController(alertController, animated: true, completion: nil)

So my question is, how can I create a completion handler (inline) in such a way that when the CANCEL or REMOVE button is pressed with my custom ConfirmationViewController I can trigger the action, just as I've shown how it's done with the UIAlertController, instead of the current way I'm doing it with delegation?

Is the answer to just create the custom popup I'm looking for with a UIAlertController? And if so, how can I customize it to the degree I'm looking for?

Thanks in advance and sorry for the long post :)

P.S. Here's what my ConfirmationViewController and ConfirmationViewControllerDelegate look like:

protocol ConfirmationViewControllerDelegate {
    func cancelButtonPressed()
    func confirmationButtonPressed(objectToDelete: AnyObject?)
}

class ConfirmationViewController: UIViewController {
    var didSetupConstraints = false

    let titleLabel = UILabel.newAutoLayoutView()
    let buttonContainer = UIView.newAutoLayoutView()
    let cancelButton = ButtonWithPressingEffect.newAutoLayoutView()
    let confirmationButton = ButtonWithPressingEffect.newAutoLayoutView()

    var delegate: ConfirmationViewControllerDelegate?

    var objectToDelete: AnyObject?

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor.whiteColor()

        titleLabel.numberOfLines = 0

        cancelButton.backgroundColor = UIColor.colorFromCode(0x7f7f7f)
        cancelButton.layer.cornerRadius = 5
        cancelButton.setAttributedTitle(NSMutableAttributedString(
            string: "CANCEL",
            attributes: [
                NSFontAttributeName: UIFont(name: "AvenirNextLTPro-Demi", size: 12)!,
                NSForegroundColorAttributeName: UIColor.whiteColor(),
                NSKernAttributeName: 0.2
            ]
        ), forState: UIControlState.Normal)
        cancelButton.addTarget(self, action: #selector(cancelButtonPressed), forControlEvents: .TouchUpInside)

        confirmationButton.backgroundColor = Application.redColor
        confirmationButton.layer.cornerRadius = 5
        confirmationButton.setAttributedTitle(NSMutableAttributedString(
            string: "REMOVE",
            attributes: [
                NSFontAttributeName: UIFont(name: "AvenirNextLTPro-Demi", size: 12)!,
                NSForegroundColorAttributeName: UIColor.whiteColor(),
                NSKernAttributeName: 0.2
            ]
        ), forState: UIControlState.Normal)
        confirmationButton.addTarget(self, action: #selector(confirmationButtonPresssed), forControlEvents: .TouchUpInside)

        view.addSubview(titleLabel)
        view.addSubview(buttonContainer)
        buttonContainer.addSubview(cancelButton)
        buttonContainer.addSubview(confirmationButton)
        updateViewConstraints()
    }

    func cancelButtonPressed() {
        delegate?.cancelButtonPressed()
        dismissViewControllerAnimated(false, completion: nil)
    }

    func confirmationButtonPresssed() {
        delegate?.confirmationButtonPressed(objectToDelete)
        dismissViewControllerAnimated(false, completion: nil)
    }

    func setTitleLabel(text: String) {
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.alignment = NSTextAlignment.Center
        paragraphStyle.lineSpacing = 4.5
        titleLabel.attributedText = NSMutableAttributedString(
            string: text,
            attributes: [
                NSFontAttributeName: UIFont(name: "AvenirNextLTPro-Regular", size: 14)!,
                NSForegroundColorAttributeName: UIColor.colorFromCode(0x151515),
                NSKernAttributeName: 0.5,
                NSParagraphStyleAttributeName: paragraphStyle
            ]
        )
    }

    override func updateViewConstraints() {
        if !didSetupConstraints {
            titleLabel.autoPinEdgesToSuperviewEdgesWithInsets(UIEdgeInsets(top: 10, left: 10, bottom: 0, right: 10), excludingEdge: .Bottom)
            titleLabel.autoAlignAxisToSuperviewAxis(.Vertical)

            buttonContainer.autoPinEdge(.Top, toEdge: .Bottom, ofView: titleLabel, withOffset: 3)
            buttonContainer.autoAlignAxisToSuperviewAxis(.Vertical)
            buttonContainer.autoPinEdgeToSuperviewEdge(.Bottom, withInset: 10)

            let contactViews: NSArray = [cancelButton, confirmationButton]
            contactViews.autoDistributeViewsAlongAxis(.Horizontal, alignedTo: .Horizontal, withFixedSpacing: 7, insetSpacing: true, matchedSizes: false)

            cancelButton.autoPinEdgeToSuperviewEdge(.Top)
            cancelButton.autoPinEdgeToSuperviewEdge(.Bottom)
            cancelButton.autoSetDimensionsToSize(CGSize(width: 100, height: 50))

            confirmationButton.autoPinEdgeToSuperviewEdge(.Top)
            confirmationButton.autoPinEdgeToSuperviewEdge(.Bottom)
            confirmationButton.autoSetDimensionsToSize(CGSize(width: 100, height: 50))

            didSetupConstraints = true
        }

        super.updateViewConstraints()
    }
}

Answer

SeanCAtkinson picture SeanCAtkinson · May 26, 2016

Something like the following should allow it. Note there quite a few improvements that could be made. For example you could use a generic for the object being deleted instead of AnyObject. You also don't necessarily need to pass it in if you pass the closure inline anyway so you could probably just remove it.

You could also make your buttons more reusable rather than hard-coding to cancel and remove but now we're getting off topic :)

class ConfirmViewController : UIViewController {
    var onCancel : (() -> Void)?
    var onConfirm : ((AnyObject?) -> Void)?

    var objectToDelete : AnyObject?

    func cancelButtonPressed() {
        // defered to ensure it is performed no matter what code path is taken
        defer {
            dismissViewControllerAnimated(false, completion: nil)
        }

        let onCancel = self.onCancel
        // deliberately set to nil just in case there is a self reference
        self.onCancel = nil
        guard let block = onCancel else { return }
        block()
    }

    func confirmationButtonPresssed() {
        // defered to ensure it is performed no matter what code path is taken
        defer {
            dismissViewControllerAnimated(false, completion: nil)
        }
        let onConfirm = self.onConfirm
        // deliberately set to nil just in case there is a self reference
        self.onConfirm = nil
        guard let block = onConfirm else { return }
        block(self.objectToDelete)
    }
}

let confirm = ConfirmViewController()
confirm.objectToDelete = NSObject()
confirm.onCancel = {
    // perform some action here
}
confirm.onConfirm = { objectToDelete in
    // delete your object here
}