I haven’t been blogging for some time, but on this gray Sunday it seems like the right time to pick it up again to talk to you about how to create a custom alert. I’ve been working in Aviero Portugal for a while. Aveiro is a quiet, though somewhat touristy city in the North of Portugal. Contrary to the south, the North is very green, mountainous and less dry. I’ve been staying at the fantastic Airbnb apartment of Didi, a nice Portuguese lady who lives in the Philippines most of the year running a software company. Portugal is a great country to work. The people are so nice and easy-going, though older people don’t speak a word of English. The weather is fine and, not unimportant, it’s cheap to live (esp. when used to the prices in Amsterdam). Anyway, enjoyed it very much out there! On the picture above you see Didi and her mother on their family domain where I had a great afternoon with freaking delicious Port-wine!
Custom alert
At the moment I’m working on an exciting new app for a psychologist. In the app on numerous occasions we have to show an alert. Since the standard UIAlert Apple provide is umhh, rather standard.. I wanted to create a custom alert with adaptable title, message and number of buttons (0, 1 or 2 buttons). In this blog I show you what I did.
The view
I choose to make the custom alert programmatically using anchors because it’s easier to vary the amount of buttons that way. I use 2 classes; a CustomAlertview (a UIView-class) and a CustomAlertController (UIViewController) in which I animate the view in and out.
The CustomAlertView class sets up the subviews programmatically. It uses the presentCustomAlert function to set the constraints of the subviews in its superview. This function in turn, calls setButtons(..) which places no, one or two buttons, set the title and the message text. It’s important to notice that the messageLabel does not have a height constraint and has it’s numberOfLines property set to 0, which means the label’s height increases if it’s message text is longer. When scaling a view like this dynamically, it’s important to set the buttomAnchor of the bottom subview (in this case backgroundView) to the bottomAnchor of the superview, otherwise it will not know it’s own height.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
class InsetLabel: UILabel { let topInset = CGFloat(0) let bottomInset = CGFloat(0) let leftInset = CGFloat(10) let rightInset = CGFloat(10) override func drawText(in rect: CGRect) { let insets: UIEdgeInsets = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset) super.drawText(in: UIEdgeInsetsInsetRect(rect, insets)) } override public var intrinsicContentSize: CGSize { var intrinsicSuperViewContentSize = super.intrinsicContentSize intrinsicSuperViewContentSize.height += topInset + bottomInset intrinsicSuperViewContentSize.width += leftInset + rightInset return intrinsicSuperViewContentSize } } class CustomAlertView: UIView { let titleHeader: UILabel = { let th = UILabel() th.textAlignment = .center th.backgroundColor = .black th.font = UIFont.boldSystemFont(ofSize: 17) th.textColor = .white th.text = "Alert" th.translatesAutoresizingMaskIntoConstraints = false return th }() let logoImageView: UIImageView = { let imv = UIImageView() imv.translatesAutoresizingMaskIntoConstraints = false imv.layer.cornerRadius = 15 imv.clipsToBounds = true imv.backgroundColor = .white imv.image = #imageLiteral(resourceName: "logoImage") return imv }() let messageLabel: InsetLabel = { let ml = InsetLabel() ml.translatesAutoresizingMaskIntoConstraints = false ml.textAlignment = .center ml.numberOfLines = 0 ml.backgroundColor = .white return ml }() let backgroundView: UIView = { let bv = UIView() bv.backgroundColor = .white bv.translatesAutoresizingMaskIntoConstraints = false return bv }() let topBackgroundView: UIView = { let bv = UIView() bv.backgroundColor = .white bv.translatesAutoresizingMaskIntoConstraints = false return bv }() let leftActionButton: UIButton = { let btn = UIButton(type: UIButtonType.system) btn.titleLabel?.font = UIFont.boldSystemFont(ofSize: 11) btn.setTitleColor(UIColor.buttonTextColor(), for: .normal) btn.backgroundColor = UIColor.buttonBackgroundColor() btn.titleLabel?.textColor = .black btn.setTitle("Left", for: .normal) btn.layer.cornerRadius = CGFloat.buttonCornerRadius() btn.translatesAutoresizingMaskIntoConstraints = false return btn }() let rightActionButton: UIButton = { let btn = UIButton(type: UIButtonType.system) btn.titleLabel?.font = UIFont.boldSystemFont(ofSize: 11) btn.setTitleColor(UIColor.buttonTextColor(), for: .normal) btn.backgroundColor = UIColor.buttonBackgroundColor() btn.titleLabel?.textColor = .black btn.setTitle("Right", for: .normal) btn.layer.cornerRadius = CGFloat.buttonCornerRadius() btn.translatesAutoresizingMaskIntoConstraints = false return btn }() let cancelActionButton: UIButton = { let btn = UIButton(type: UIButtonType.system) btn.setImage(#imageLiteral(resourceName: "cancelImage").withRenderingMode(.alwaysTemplate), for: .normal) btn.tintColor = .white btn.translatesAutoresizingMaskIntoConstraints = false return btn }() override init(frame: CGRect) { super.init(frame: frame) } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } func setButtons(amount: Int, alertTitle: String, firstBtnTitle: String?, secondBtnTitle: String?, message: String) { switch amount { case 0: addSubview(messageLabel) messageLabel.topAnchor.constraint(equalTo: topBackgroundView.bottomAnchor).isActive = true messageLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true messageLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true case 1: addSubview(messageLabel) messageLabel.topAnchor.constraint(equalTo: topBackgroundView.bottomAnchor).isActive = true messageLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true messageLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true addSubview(backgroundView) backgroundView.topAnchor.constraint(equalTo: messageLabel.bottomAnchor).isActive = true backgroundView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true backgroundView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true backgroundView.heightAnchor.constraint(equalToConstant: CGFloat.buttonHeight() + 50).isActive = true backgroundView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true addSubview(leftActionButton) leftActionButton.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true leftActionButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true leftActionButton.widthAnchor.constraint(equalToConstant: CGFloat.buttonWidth()).isActive = true leftActionButton.heightAnchor.constraint(equalToConstant: CGFloat.buttonHeight()).isActive = true leftActionButton.setTitle(firstBtnTitle, for: .normal) case 2: addSubview(messageLabel) messageLabel.topAnchor.constraint(equalTo: topBackgroundView.bottomAnchor).isActive = true messageLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true messageLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true addSubview(backgroundView) backgroundView.topAnchor.constraint(equalTo: messageLabel.bottomAnchor).isActive = true backgroundView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true backgroundView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true backgroundView.heightAnchor.constraint(equalToConstant: CGFloat.buttonHeight() + 50).isActive = true backgroundView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true addSubview(leftActionButton) leftActionButton.centerXAnchor.constraint(equalTo: self.centerXAnchor, constant: -70).isActive = true leftActionButton.widthAnchor.constraint(equalToConstant: CGFloat.buttonWidth()).isActive = true leftActionButton.heightAnchor.constraint(equalToConstant: CGFloat.buttonHeight()).isActive = true leftActionButton.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: 25).isActive = true addSubview(rightActionButton) rightActionButton.centerXAnchor.constraint(equalTo: self.centerXAnchor, constant: 70).isActive = true rightActionButton.widthAnchor.constraint(equalToConstant: CGFloat.buttonWidth()).isActive = true rightActionButton.heightAnchor.constraint(equalToConstant: CGFloat.buttonHeight()).isActive = true rightActionButton.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: 25).isActive = true leftActionButton.setTitle(firstBtnTitle, for: .normal) rightActionButton.setTitle(secondBtnTitle, for: .normal) default: break } } func presentCustomAlert(amountOfBtns: Int, alertTitle: String, firstBtnTitle: String?, secondBtnTitle: String?, message: String) { self.backgroundColor = .white addSubview(titleHeader) titleHeader.topAnchor.constraint(equalTo: self.topAnchor).isActive = true titleHeader.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true titleHeader.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true titleHeader.heightAnchor.constraint(equalToConstant: 66).isActive = true titleHeader.addSubview(logoImageView) logoImageView.centerXAnchor.constraint(equalTo: titleHeader.centerXAnchor, constant: -100).isActive = true logoImageView.centerYAnchor.constraint(equalTo: titleHeader.centerYAnchor).isActive = true logoImageView.widthAnchor.constraint(equalToConstant: 30).isActive = true logoImageView.heightAnchor.constraint(equalToConstant: 30).isActive = true addSubview(cancelActionButton) cancelActionButton.centerYAnchor.constraint(equalTo: titleHeader.centerYAnchor).isActive = true cancelActionButton.trailingAnchor.constraint(equalTo: titleHeader.trailingAnchor, constant: -20).isActive = true cancelActionButton.widthAnchor.constraint(equalToConstant: 20).isActive = true cancelActionButton.heightAnchor.constraint(equalToConstant: 20).isActive = true addSubview(topBackgroundView) topBackgroundView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true topBackgroundView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true topBackgroundView.topAnchor.constraint(equalTo: titleHeader.bottomAnchor).isActive = true topBackgroundView.heightAnchor.constraint(equalToConstant: 20).isActive = true titleHeader.text = alertTitle messageLabel.text = message setButtons(amount: amountOfBtns, alertTitle: alertTitle, firstBtnTitle: firstBtnTitle, secondBtnTitle: secondBtnTitle, message: message) } } |
The controller
In the controller we place the alertView in its original position with setUpAlertView() in viewDidLoad . This is a position of-screen, because we want to animate it in. We only add a width-constraint and no height-constraint because in that way, the view will scale its height to its intrinsic size based on the length of the message its text.
In ViewDidAppear we gently animate in the alertview. There are loads of ways to work out the animation, but I choose a simple CGAffine transform on the alertView to animate it in from the top. The click on the dismissButton can be handled in the controller, because the action always will be the same: animate out the alert and dismiss it form the stack.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
class AlertViewController: UIViewController { let alertView: CustomAlertView = { let av = CustomAlertView() av.layer.cornerRadius = 12 av.layer.masksToBounds = true av.translatesAutoresizingMaskIntoConstraints = false return av }() override func viewDidLoad() { super.viewDidLoad() setUpAlertView() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 6, options: .curveEaseIn, animations: { self.alertView.transform = CGAffineTransform(translationX: 0, y: self.view.frame.height) self.view.backgroundColor = UIColor(white: 0, alpha: 0.3) }, completion: nil) } func setUpAlertView() { view.addSubview(alertView) alertView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true alertView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -view.frame.height).isActive = true alertView.widthAnchor.constraint(equalToConstant: view.bounds.width*0.8).isActive = true alertView.cancelActionButton.addTarget(self, action: #selector(dismissAlert), for: .touchUpInside) } func dismissAlert() { UIView.animate(withDuration: 1.0, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 4, options: .curveEaseIn, animations: { self.alertView.transform = CGAffineTransform.identity self.view.backgroundColor = UIColor(white: 0, alpha: 0.0) }) { (completed) in self.dismiss(animated: false, completion: nil) } } } |
Presenting the alert
The custom alert can be presented now from anywhere in the app. The target-actions are added at the place the alert is used, because the action it triggers, will differ on every use of the alert. We use the modalPresentStyle .overCurrentContext because we want to be able to see the underlying view the alert is presented over. This view is visible because we set the view of the viewcontroller to .clear.
1 2 3 4 5 6 7 8 9 |
func presentAlert() { let alertVC = AlertViewController() alertVC.alertView.presentCustomAlert(amountOfBtns: 2, alertTitle: "Alert", firstBtnTitle: "Zelf", secondBtnTitle: "Met begeleiding", message: "Wil je zelf een sessie doen, of wil je liever samen met begeleiding?") alertVC.alertView.leftActionButton.addTarget(self, action: #selector(presentZelf), for: .touchUpInside) alertVC.alertView.rightActionButton.addTarget(self, action: #selector(presentBegeleidng), for: .touchUpInside) alertVC.modalPresentationStyle = .overCurrentContext present(alertVC, animated: false, completion: nil) |
Hope you enjoyed it, happy coding!