Contact us

BOOK A PRESENTATION

Creating a Custom UIAlertController in iOS: A Step-by-Step Guide

May 24, 2023
NO NAME
We are all familiar with Apple's alert view that sends us important messages when using apps. But sometimes, our companies require custom solutions that follow the company’s branding. Developing a customized UIAlertController may seem daunting, especially for Swift beginners. In this blog post, we will walk you through the process of constructing and configuring a UIAlertController using RxSwift.

With this guide, you'll be able to effortlessly create your own personalized alerts. We'll cover all the necessary steps to set up an alert view from scratch, explore its features in detail, and provide you with the essential knowledge to get started on crafting your own UIAlertController. As a result, you can expect something like this:

Custom Alert View

Step 1: Project Setup

To follow along with this tutorial, it's assumed that you have already created a custom onboarding screen as described in our previous blog post. If you haven't, make sure to incorporate the following frameworks into your project:

  • PureLayout
  • RxSwift

Step 2: Creating the Custom UIAlertController

To begin creating a custom UIAlertController, we'll inherit from UIViewController. This approach gives us greater control over the overall design compared to using the default UIAlertController which only has few methods publicly available.

class CustomAlertViewController: UIViewController {
    // Class implementation
}

Step 3: Utilizing Closures

To achieve similar behavior for buttons as UIAlertController has, we’ll use closures as completion handlers.

In Swift, closures are self-contained blocks of functionality that can be passed around and used in code. This behavior allows closures to access and manipulate the values of captured variables even after they have gone out of scope.

convenience init(alertTitle: String, alertMessage: String, alertImage: String, okButton: String, cancelButton: String? = nil, completionOk: (() -> Void)? = nil, completionCancel: (() -> Void)? = nil) {
    // Code omitted for brevity
    self.completionOk = completionOk
    self.completionCancel = completionCancel
}

// ...

func okClicked() {
    completionOk?()
}

func cancelClicked() {
    completionCancel?()
}

Here, the `convenience init` method of `CustomAlertViewController` accepts two optional closure parameters: `completionOk` and `completionCancel`. These closures define the actions to be executed when the user taps the OK button or the cancel button, respectively.

Step 4: Designing and Configuring the UI

In our example, we'll create an alert view consisting of a title, message, and two action buttons. These elements will be configured within a UIView called containerView, which dynamically adjusts its size based on other content.

Inside the `setupInitialUI()` method, we dynamically calculate the dimensions and position of the container view based on the text sizes of the header label and description label.

func setupInitialUI() {
        
        view.backgroundColor = .BLACK.withAlphaComponent(0.7)
        
        view.addSubview(containerView)
        containerView.addSubview(alertImage)
        containerView.addSubview(headerLabel)
        containerView.addSubview(descriptionLabel)
        containerView.addSubview(okButton)
        containerView.addSubview(cancelButton)
        
        let shouldHideCancelButton = cancelButtonTitle == nil
        let shouldHideAlertImage = image == nil
        
        let minimumTitleLabelHeight: CGFloat = UIScreen.main.bounds.height * 0.035
        var titleLabelHeight = headerLabel.text?.height(withConstrainedWidth: containerWidth - 40, font: .systemFont(ofSize: 24, weight: .medium)) ?? minimumTitleLabelHeight
        titleLabelHeight = max(titleLabelHeight, minimumTitleLabelHeight)
        
        var descriptionLabelHeight: CGFloat = descriptionLabel.text?.height(withConstrainedWidth: containerWidth - 40, font: .systemFont(ofSize: 16)) ?? 60
        
        descriptionLabelHeight = min(descriptionLabelHeight, UIScreen.main.bounds.height * 0.60)
        
        let height: CGFloat = titleLabelHeight + descriptionLabelHeight + (shouldHideCancelButton ? UIScreen.main.bounds.height * 0.21 : UIScreen.main.bounds.height * 0.27) + (shouldHideAlertImage ? 0 : imageHeight)
        
        let containerHeight = min(height, view.frame.height - 2 * 40)
        
        containerView.autoCenterInSuperview()
        containerView.autoSetDimensions(to: CGSize(width: containerWidth, height: containerHeight))
        
        alertImage.autoPinEdge(.top, to: .top, of: containerView, withOffset: UIScreen.main.bounds.height * 0.05)
        alertImage.autoSetDimensions(to: CGSize(width: imageHeight, height: imageHeight))
        alertImage.autoAlignAxis(toSuperviewAxis: .vertical)
        
        if shouldHideAlertImage {
            
            headerLabel.autoPinEdge(.top, to: .top, of: containerView, withOffset: UIScreen.main.bounds.height * 0.05)
            
        } else {
            
            headerLabel.autoPinEdge(.top, to: .bottom, of: alertImage, withOffset: 20.0)
            
        }
        
        headerLabel.autoPinEdge(toSuperviewEdge: .left, withInset: 20)
        headerLabel.autoPinEdge(toSuperviewEdge: .right, withInset: 20)
        headerLabel.autoSetDimension(.height, toSize: titleLabelHeight)
        
        descriptionLabel.autoPinEdge(.top, to: .bottom, of: headerLabel, withOffset: 16.0)
        descriptionLabel.autoPinEdge(toSuperviewEdge: .left, withInset: 20)
        descriptionLabel.autoPinEdge(toSuperviewEdge: .right, withInset: 20)
        descriptionLabel.autoSetDimension(.height, toSize: descriptionLabelHeight)
        descriptionLabel.autoPinEdge(.bottom, to: .top, of: okButton, withOffset: -20)
        
        okButton.autoSetDimension(.height, toSize: UIScreen.main.bounds.height * 0.064)
        okButton.autoSetDimension(.width, toSize: containerWidth - 2 * 32)
        
        okButton.autoPinEdge(toSuperviewEdge: .left, withInset: 32)
        okButton.autoPinEdge(toSuperviewEdge: .right, withInset: 32)
        
        if shouldHideCancelButton {
            okButton.autoPinEdge(toSuperviewEdge: .bottom, withInset: UIScreen.main.bounds.height * 0.04)
        } else {
            
            okButton.autoPinEdge(.bottom, to: .top, of: cancelButton, withOffset: -20)
            
            cancelButton.autoPinEdge(toSuperviewEdge: .right, withInset: 32)
            cancelButton.autoPinEdge(toSuperviewEdge: .left, withInset: 32)
            cancelButton.autoPinEdge(toSuperviewEdge: .bottom, withInset: UIScreen.main.bounds.height * 0.04)
            
            cancelButton.autoSetDimension(.height, toSize: 24)
        }
        
        cancelButton.isHidden = shouldHideCancelButton
        
    }

The `titleLabelHeight` is determined using the `height(withConstrainedWidth:font:)` method applied to the header label's text. this function calculates the height required to display a given string within a specified width and font. It leverages the boundingRect(with:options:attributes:context:) method to obtain the bounding box of the rendered text and then returns the height of the bounding box, rounded up to the nearest whole number:

func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
        
        return ceil(boundingBox.height)
    }

The same calculation is performed for `descriptionLabelHeight`.

When developing your own custom UIAlertController feel free to add as many or different UI elements, just be sure to calculate size appropriately 😉

The overall height of the container view is determined by summing the calculated heights of the header label and description label, as well as additional heights for the buttons, image, and spacing.

By dynamically calculating the container view's size based on the text content, the `CustomAlertViewController` ensures that the alert view adjusts appropriately to display the text without truncation or unnecessary empty space.  

Be aware that this is only for smaller messages. You can also use UITextView to display even more text!

To prevent the container view's height from exceeding the available vertical space within the view, we use the `min(_:_)` function to select the smaller value between the calculated `contentHeight` and the available vertical space.

Step 5: Incorporating the Custom UIAlertController into Your Project

To simplify the transition and presentation of our CustomAlertViewController, we'll create a helper class called MessagePresenterHelper.

 This class provides two methods:

  • showAlertPresenter(title:message:image:from:btnTitle:completion:), which creates an alert view with a single button, and,
  • showAlertPresenter(title:message:image:from:okBtnTitle:cancelBtnTitle:completionOk:completionCancel:), which creates an alert view with two buttons.

We'll use the `.overFullScreen` modal presentation style and the `.crossDissolve` transition style. They closely resemble the native alert view's transition. However, feel free to adjust these settings to suit your preferences.

import UIKit

class MessagePresenterHelper {
    
    static let presentationStyle: UIModalPresentationStyle = .overFullScreen
    static let transitionStyle: UIModalTransitionStyle = .crossDissolve
    
    static func showAlertPresenter(title: String, message: String, image: String?, from: UIViewController?, btnTitle: String, completion: (() -> Void)? = nil) {
        
        let alertView = CustomAlertViewController(alertTitle: title, alertMessage: message, alertImage: image, okButton: btnTitle, cancelButton: nil) {
            from?.dismiss(animated: true)
            completion?()
        } completionCancel: {
            return
        }

        alertView.modalPresentationStyle = presentationStyle
        alertView.modalTransitionStyle = transitionStyle
        from?.present(alertView, animated: true)
        
    }
    
    static func showAlertPresenter(title: String, message: String, image: String?, from: UIViewController?, okBtnTitle: String, cancelBtnTitle: String, completionOk: (() -> Void)? = nil, completionCancel: (() -> Void)? = nil) {
        
        let alertView = CustomAlertViewController(alertTitle: title, alertMessage: message, alertImage: image, okButton: okBtnTitle, cancelButton: cancelBtnTitle) {
            from?.dismiss(animated: true)
            completionOk?()
        } completionCancel: {
            from?.dismiss(animated: true)
            completionCancel?()
        }

        alertView.modalPresentationStyle = presentationStyle
        alertView.modalTransitionStyle = transitionStyle
        from?.present(alertView, animated: true)
        
    }
}

To use it simply call it from anywhere and define your desired actions:

createAccountButton
            .rx
            .tap
            .bind{ [weak self] in
                
                guard let `self` = self else { return }
                
                MessagePresenterHelper.showAlertPresenter(title: "Unable to process request",
                                                          message: "Before continuing please check your internet connection. \n\n Proin molestie varius dui, in consectetur magna vulputate ac. In viverra vestibulum imperdiet. Vivamus faucibus finibus tempus. Nam egestas lobortis erat. Maecenas quam mi, vestibulum ac commodo sed, accumsan a ligula. ",
                                                          image: "errorIcon",
                                                          from: self,
                                                          okBtnTitle: "OK",
                                                          cancelBtnTitle: "Cancel") {
                    print("OK button")
                } completionCancel: {
                    print("Cancel button")
                }
            }
            .disposed(by: disposeBag)

Conclusion

In summary, the provided code leverages closures to enable customizable button actions within the custom alert view controller. By allowing external code to define the behavior to be executed when the user interacts with the OK and cancel buttons, closures provide a flexible and reusable solution for handling user actions in custom alert view controllers.

Experiment with the design and happy coding 🙂

Karolina Škunca

Karolina is an iOS Software Developer. She works on preventing security attacks on iOS phones and frequently tests ASEE’s applications. Her greatest passions are designing and developing new applications.

Want to learn more about cybersecurity trends and industry news?

SUBSCRIBE TO OUR NEWSLETTER

CyberSecurityhub

chevron-down linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram