iosuikituialertcontrollersubclassing

Is an empty subclass of UIAlertController safe for use with `appearance(whenContainedInInstancesOf:)`


Somebody here on SO wanted to alter the behavior of UIAlertAction buttons in a specific UIAlertController, but not others. (They wanted multi-line button labels for one alert but normal behavior for all other alerts.) (Here is a link to the other question.)

If you read the docs for UIAlertController it says that

The UIAlertController class is intended to be used as-is and doesn’t support subclassing. The view hierarchy for this class is private and must not be modified.

As an experiment, I decided to try creating a dummy, empty subclass of UIAlertController, purely so that I had a class name to give to the UIAppearance method appearance(whenContainedInInstancesOf:)

The definition of the dummy subclass is just this:

class FooController: UIAlertController {
}

That then lets me use the statement

        UILabel.appearance(whenContainedInInstancesOf: [FooController.self]).numberOfLines = 2

and override the appearance of UILabels specifically in instances ofFooController

It works, seemingly flawlessly.

The FooController creates an alert with 2-line button labels like this:

enter image description here

And the Vanilla UIAlertController creates an alert with 1-line button titles even though it gets passed 2 line titles:

enter image description here

You can download the sample project from Github here.

When you create a vanilla UIAlertController, its UIAlertAction buttons have single-line labels as normal. When you create a FooController, its UIAlertAction buttons have multi-line labels.

While it seems to work perfectly, I am leery of going against an explicit statement in Apple's docs not to subclass UIAlertController.

What are the risks of ignoring that admonition and using an empty subclass?

Here is the code from my sample project for reference:

import UIKit

class FooController: UIAlertController {
}

class ViewController: UIViewController {
    
    let buttonLabels = [
    """
    Button1
    line2
    """,
    """
    Button2
    line2
    """,
    """
    Button3
    line2
    """
    ]

    @IBAction func handleAlertButton(_ sender: Any) {
        presentAlert(type: UIAlertController.self)
    }
    
    @IBAction func handleFooButton(_ sender: Any) {
        presentAlert(type: FooController.self)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        UILabel.appearance(whenContainedInInstancesOf: [FooController.self]).numberOfLines = 2
    }
    
    func presentAlert(type: UIAlertController.Type) {
        let sheet = type.init(title: type.description(), message: nil, preferredStyle: .actionSheet)
        for buttonTitle in buttonLabels {
            let item = UIAlertAction(title: buttonTitle, style: .default) { (action) in
                print("Button \(buttonTitle) tapped")
            }
            sheet.addAction(item)
        }
        present(sheet, animated: true, completion: nil)
    }
}

Solution

  • What are the risks of ignoring that admonition and using an empty subclass?

    1. Apple might reject the app.
    2. The UIAlertController might not function correctly in a future update of iOS.
    3. The specific reason (using UILabel.appearance) for the subclass might not function correctly in a future update of iOS.

    Those are the possible risks.

    While Apple always has the final say, it's highly unlikely that Apple would reject an app simply because you subclassed UIAlertController in such a manner. I've personally subclassed several UIKit classes that Apple says should not be subclassed. I've done this in an app that has been in the app store for many years and has had many updates. You are not using any private APIs. And Apple does not say that you can't subclass UIAlertController. It says that the class isn't intended to be subclassed.

    This leads to risk #2. Apple states that UIAlertController isn't intended to be subclassed and it doesn't support subclassing. This means that it does not provide any API that is meant to be overridden or modified. But it does not mean that you can't subclass it to add helper methods, for example. Or simply to give the class a new name such that you can do things like your UILabel.appearance. Your subclass is benign and makes no attempt to modify the functionality or dig into the private subview structure. It's a "safe" subclass that won't break any existing functionality.

    Lastly, risk #3. While minor, this is probably the "biggest" risk of the 3. Apple could make any number of changes to UIAlertController in a future update to iOS that might hinder or break the desired result of using UILabel.appearance on your subclass. Testing the code on every release of iOS would be prudent. Likely, the worst case will be that the "hack" will stop working.