Thomas Thomas - 6 months ago 139
iOS Question

Swift Custom UIAlertView

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

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
}
Comments