Creating a custom alert controller in UIKit
UIKit ships with UIAlertController, and for most confirmations it is still the right tool. A custom alert controller becomes useful when an app needs a distinct visual tone, a reusable action policy, or content that the stock alert cannot express cleanly.
As a developer working from Atyrau, I treat customisation as a focused layer over familiar system behaviour rather than a reason to rebuild every control. The goal is an alert that remains understandable to VoiceOver, adapts to Dynamic Type, and behaves consistently wherever it appears.
Why the stock alert reaches its limit
UIAlertController handles the essential work: modal presentation, a title and message, ordered actions, dismissal, and system accessibility. That reliability is why replacing it with a decorative screen is rarely a good default.
Product needs arrive when several screens repeat the same decision, a brand colour is missing, or a workflow needs an icon, status detail, or extra guidance. A reusable subclass can centralise those choices without changing the familiar interaction model.
Customisation still has limits. A destructive action should remain visibly destructive, a cancellation path must stay obvious, and a routine message may belong on a normal screen instead of a modal. If the content grows into a form, use a dedicated view controller rather than stretching an alert.
Defining behaviour before pixels
Start with a small contract: title, message, visual tone, and a list of actions. Model action roles such as confirmation, cancellation, and destruction instead of accepting arbitrary button order. This makes the component predictable and easy to review.
Decide who owns presentation as well. The caller should provide the action handlers, while the controller owns appearance and configuration. That separation prevents a view controller from reaching into private alert views and keeps presentation logic in one place.
Readable feedback is a principle shared by adjacent tools: a Python port scanner can use a custom banner to turn raw scan output into information people interpret quickly. An app alert should provide the same clarity, but through concise copy and clearly named actions.
Implementing the controller
A lightweight subclass is often enough. It preserves the native alert engine and public action API while allowing shared colours, corner radius, and tint behaviour.
enum AlertTone {
case information
case warning
}
final class AppAlertController: UIAlertController {
init(title: String, message: String, tone: AlertTone) {
super.init(title: title, message: message, preferredStyle: .alert)
configure(tone)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
private func configure(_ tone: AlertTone) {
let background: UIColor
let tint: UIColor
switch tone {
case .information:
background = .systemBackground
tint = .systemBlue
case .warning:
background = .secondarySystemBackground
tint = .systemOrange
}
view.backgroundColor = background
view.tintColor = tint
view.layer.cornerRadius = 20
view.layer.cornerCurve = .continuous
view.clipsToBounds = true
}
}
The subclass calls the designated initialiser before changing the view, so UIKit has already created its internal layout. Only public properties and standard Auto Layout or layer features are used. That restraint matters because UIKit can revise system presentation details between releases.
Styling actions and handling state
Actions should communicate intent before they share the brand palette. UIAlertAction already provides default, cancel, and destructive styles, plus an isEnabled state. Those semantics give assistive technology and users information that a coloured rectangle cannot.
let save = UIAlertAction(title: "Save draft", style: .default) { _ in
draft.save()
}
save.isEnabled = draft.isValid
let discard = UIAlertAction(title: "Discard draft", style: .destructive) { _ in
draft.delete()
}
let alert = AppAlertController(
title: "Keep your draft?",
message: "Your latest changes are stored on this device.",
tone: .warning
)
alert.addAction(save)
alert.addAction(discard)
alert.addAction(UIAlertAction(title: "Not now", style: .cancel))
alert.preferredAction = save
Avoid setting private keys such as an action’s internal title colour. Key-value coding tricks can produce an undefined key exception or silently stop working after an iOS update. If a design demands a different button layout, use a custom presented view controller and build that interface from public components.
Accessibility and Australian product details
Accessibility belongs in the component itself. Keep the message concise, avoid packing essential instructions into colour, and verify that every action has a useful VoiceOver label. The inherited system behaviour provides a strong baseline, while Dynamic Type and testing reveal whether the text fits on smaller screens.
Australian products should also use Australian English consistently: “cancelled”, “authorise”, and “organisation” are familiar choices. Consumer copy can be relaxed, such as “No worries, your draft is safe”, but security and financial messages should remain exact. For prices, A$12.99 removes ambiguity for travellers, and Australian App Store purchases should be presented as GST-inclusive.
Dates and schedules deserve local care too. Use “8 March” instead of an ambiguous numeric date, and remember that Sydney and Melbourne change time zone while Brisbane, Darwin, and Perth do not. A remote team or support message spanning Australian cities needs those differences handled explicitly.
Presenting and testing in real apps
Present the alert from the topmost visible view controller, not from a controller that may already be dismissed. Keep a weak reference to that presenter and present only once. If another alert must follow, dispatch its presentation after the first dismissal has completed.
Test more than the happy path. Run UI tests with VoiceOver enabled, the largest Dynamic Type sizes, increased contrast, and both light and dark appearance. On iPad, confirm placement and focus order; an action sheet needs a popover source, while a standard alert normally remains centred.
A useful test looks for roles and labels rather than pixel positions. Locate the destructive action, confirm it is disabled for invalid input, then trigger it and verify the resulting state. This catches accessibility regressions that screenshots miss and keeps the component reliable across screen sizes.
Comparing approaches and preparing to ship
Several approaches can deliver the right interaction, but they offer different levels of control and maintenance.
| Approach | Level of control | Best fit | Main trade-off |
|---|---|---|---|
System UIAlertController |
Native layout and behaviour | Simple confirmations | Limited visual customisation |
UIAlertController subclass |
Shared styling and action policy | Branded, reusable prompts | Internal label layout stays fixed |
Presented UIViewController |
Full layout and custom content | Rich cards or input | Requires manual focus, dismissal, and accessibility work |
| Custom action sheet | Purpose-built option list | Filters and related choices | More presentation setup than a system sheet |
Choose a subclass when the alert should feel familiar and the changes are modest. Choose a presented view controller when the layout, content, or interaction genuinely cannot fit the system design. This decision keeps customisation tied to user value rather than novelty.
- Define the alert’s roles and state before building its interface.
- Preserve native action semantics, especially for destructive choices.
- Support Dynamic Type, VoiceOver, contrast, and dark appearance.
- Test on iPhone and iPad, including presentation from busy screens.
- Avoid private view hierarchies and internal key-value coding.
- Keep presentation ownership explicit and dismiss alerts deterministically.
Your next concrete step is to add AppAlertController on a small branch with one real confirmation, then run its VoiceOver, Dynamic Type, and iPad checks before merging it.