Swift自定义UIAlertController方案:如何实现含进度条、列表的弹窗?
Great question! Apple’s documentation makes it crystal clear that subclassing UIAlertController is not recommended—its view hierarchy is private, so tinkering with it will almost certainly lead to breakages in future iOS updates. Instead, here are two solid, future-proof approaches tailored to your two use cases:
1. ProgressBar Popup
Option 1: iOS 16+ (Official Recommended Approach)
Apple added the contentViewController property to UIAlertController in iOS 16, which lets you inject a custom view controller directly into the alert. This is the cleanest, most supported way:
// Custom view controller holding the progress bar class ProgressAlertContentVC: UIViewController { private let progressView = UIProgressView(progressViewStyle: .default) override func viewDidLoad() { super.viewDidLoad() setupProgressViewLayout() } private func setupProgressViewLayout() { view.addSubview(progressView) progressView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), progressView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), progressView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8) ]) } func updateProgress(_ progress: Float) { progressView.setProgress(progress, animated: true) } } // How to use it let alert = UIAlertController(title: "Loading", message: "Please wait...", preferredStyle: .alert) let progressContentVC = ProgressAlertContentVC() alert.contentViewController = progressContentVC // Add a cancel button if needed let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in // Handle cancellation logic here } alert.addAction(cancelAction) present(alert, animated: true) // Later, update the progress as needed progressContentVC.updateProgress(0.6)
Option 2: Compatible with iOS 15 and Earlier
If you need to support older iOS versions, build a fully custom UIViewController and present it as a popup. This gives you full control over the UI without relying on private APIs:
class CustomProgressAlertVC: UIViewController { private let progressView = UIProgressView(progressViewStyle: .default) private let titleLabel = UILabel() private let messageLabel = UILabel() init(title: String, message: String) { super.init(nibName: nil, bundle: nil) self.titleLabel.text = title self.messageLabel.text = message } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() setupFullUI() } private func setupFullUI() { view.backgroundColor = .systemBackground view.layer.cornerRadius = 12 view.clipsToBounds = true // Build vertical stack for title, message, progress bar let stackView = UIStackView(arrangedSubviews: [titleLabel, messageLabel, progressView]) stackView.axis = .vertical stackView.spacing = 12 stackView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stackView) NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20), stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20), progressView.heightAnchor.constraint(equalToConstant: 4) ]) // Style text titleLabel.font = .systemFont(ofSize: 17, weight: .semibold) messageLabel.font = .systemFont(ofSize: 14) messageLabel.textColor = .secondaryLabel messageLabel.numberOfLines = 0 } func updateProgress(_ progress: Float) { progressView.setProgress(progress, animated: true) } } // How to use it let progressAlert = CustomProgressAlertVC(title: "Loading", message: "Please wait...") progressAlert.modalPresentationStyle = .overFullScreen progressAlert.modalTransitionStyle = .crossDissolve // Add a semi-transparent backdrop let backdropView = UIView(frame: progressAlert.view.bounds) backdropView.backgroundColor = UIColor.black.withAlphaComponent(0.5) progressAlert.view.insertSubview(backdropView, at: 0) present(progressAlert, animated: true) // Update progress later progressAlert.updateProgress(0.8)
2. Error Message List Popup
Option 1: iOS 16+ (Official Approach)
Again, leverage the contentViewController property to inject a table view or stack view displaying your errors:
class ErrorListContentVC: UIViewController { private let errorMessages: [String] private let tableView = UITableView() init(errorMessages: [String]) { self.errorMessages = errorMessages super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() setupTableView() } private func setupTableView() { view.addSubview(tableView) tableView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ tableView.edgesAnchor.constraint(equalTo: view.edgesAnchor) ]) tableView.dataSource = self tableView.register(UITableViewCell.self, forCellReuseIdentifier: "ErrorCell") tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 44 tableView.separatorStyle = .none } } extension ErrorListContentVC: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { errorMessages.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ErrorCell", for: indexPath) cell.textLabel?.text = "• \(errorMessages[indexPath.row])" cell.textLabel?.font = .systemFont(ofSize: 14) cell.textLabel?.textColor = .systemRed cell.selectionStyle = .none return cell } } // How to use it let errors = [ "Invalid email format", "Password must be at least 8 characters", "Username already taken" ] let alert = UIAlertController(title: "Validation Errors", message: "Please fix these issues:", preferredStyle: .alert) let errorContentVC = ErrorListContentVC(errorMessages: errors) alert.contentViewController = errorContentVC let okAction = UIAlertAction(title: "OK", style: .default) alert.addAction(okAction) present(alert, animated: true)
Option 2: Compatible with Older iOS Versions
For broader compatibility, use a UIStackView to display error messages without a table view:
class ErrorListAlertVC: UIViewController { init(title: String, errorMessages: [String]) { super.init(nibName: nil, bundle: nil) setupErrorUI(title: title, errors: errorMessages) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupErrorUI(title: String, errors: [String]) { view.backgroundColor = .systemBackground view.layer.cornerRadius = 12 view.clipsToBounds = true let titleLabel = UILabel() titleLabel.text = title titleLabel.font = .systemFont(ofSize: 17, weight: .semibold) // Stack for error messages let errorStack = UIStackView() errorStack.axis = .vertical errorStack.spacing = 8 for error in errors { let errorLabel = UILabel() errorLabel.text = "• \(error)" errorLabel.font = .systemFont(ofSize: 14) errorLabel.textColor = .systemRed errorLabel.numberOfLines = 0 errorStack.addArrangedSubview(errorLabel) } // OK button let okButton = UIButton(type: .system) okButton.setTitle("OK", for: .normal) okButton.addTarget(self, action: #selector(dismissAlert), for: .touchUpInside) okButton.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) // Main stack to hold all elements let mainStack = UIStackView(arrangedSubviews: [titleLabel, errorStack, okButton]) mainStack.axis = .vertical mainStack.spacing = 16 mainStack.translatesAutoresizingMaskIntoConstraints = false view.addSubview(mainStack) NSLayoutConstraint.activate([ mainStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), mainStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), mainStack.topAnchor.constraint(equalTo: view.topAnchor, constant: 20), mainStack.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20), okButton.heightAnchor.constraint(equalToConstant: 44) ]) } @objc private func dismissAlert() { dismiss(animated: true) } } // How to use it let errors = [ "Invalid email format", "Password must be at least 8 characters", "Username already taken" ] let errorAlert = ErrorListAlertVC(title: "Validation Errors", errorMessages: errors) errorAlert.modalPresentationStyle = .overFullScreen errorAlert.modalTransitionStyle = .crossDissolve // Add backdrop let backdropView = UIView(frame: errorAlert.view.bounds) backdropView.backgroundColor = UIColor.black.withAlphaComponent(0.5) errorAlert.view.insertSubview(backdropView, at: 0) present(errorAlert, animated: true)
Key Takeaway
The core principle here is to avoid subclassing UIAlertController entirely. Either use the official iOS 16+ API to extend its content, or build a custom popup controller from scratch. Both approaches keep your code stable across iOS updates while giving you the flexibility to add custom elements like progress bars or error lists.
内容的提问来源于stack exchange,提问作者Simon Hessner

