manonthemoon
manonthemoon

Reputation: 2691

UIAlertController Action Sheet without Blurry View Effect

I'm using UIAlertController for some actions.

But I'm not a big fan of the Blurry View Effect in the actions group view (see screenshot below).

Action Sheet with Blurry View

I'm trying to remove this blurry effect. I made some research online, and I couldn't find any API in UIAlertController that allows to remove this blurry effect. Also, according to their apple doc here :

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

I see that Instagram also removes this blurry view effect :

Instagram's fixed version

The only way I could find to remove it is to update the view hierarchy myself via an extension of UIAlertController.

extension UIAlertController {
    @discardableResult private func findAndRemoveBlurEffect(currentView: UIView) -> Bool {
        for childView in currentView.subviews {
            if childView is UIVisualEffectView {
                childView.removeFromSuperview()
                return true
            } else if String(describing: type(of: childView.self)) == "_UIInterfaceActionGroupHeaderScrollView" {
                // One background view is broken, we need to make sure it's white.
                if let brokenBackgroundView = childView.superview {
                    // Set broken brackground view to a darker white
                    brokenBackgroundView.backgroundColor = UIColor.colorRGB(red: 235, green: 235, blue: 235, alpha: 1)
                }
            }
            findAndRemoveBlurEffect(currentView: childView)
        }
        return false
    }
}

let actionSheetController = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
actionSheetController.view.tintColor = .lightBlue
actionSheetController.removeBlurryView()

This worked fine, it removed my blurry view effect:

Fixed action sheet

What I'm wondering... Is my solution the only way to accomplish that? Or there is something that I'm missing about the Alert Controller appearance? Maybe there is a cleaner way to accomplish exactly that result? Any other ideas?

Upvotes: 5

Views: 1554

Answers (2)

SwiftStudier
SwiftStudier

Reputation: 2324

Answer by Vadim works really well.

What I missed in it (testing on iOS 14.5) is lack of separators and invisible title and message values.

So I added setting correct textColor for labels and skipping separator visual effect views in order to get correct appearance. Also remember to override traitCollectionDidChange method if your app supports dark mode to update controls backgroundColor accordingly

override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        for subview in view.allViews {
            if let label = subview as? UILabel, label.textColor == .white {
                label.textColor = .secondaryLabel
            }
            if let color = subview.backgroundColor, color != .clear {
                subview.backgroundColor = buttonBackgroundColor
            }
            if let visualEffectView = subview as? UIVisualEffectView,
               String(describing: subview).contains("Separator") == false {
                visualEffectView.effect = nil
                visualEffectView.contentView.backgroundColor = buttonBackgroundColor
            }
        }

        popoverPresentationController?.backgroundColor = buttonBackgroundColor
    }

Upvotes: 1

Vadim Ahmerov
Vadim Ahmerov

Reputation: 777

It is easier to subclass UIAlertController.

The idea is to traverse through view hierarchy each time viewDidLayoutSubviews gets called, remove effect for UIVisualEffectView's and update their backgroundColor:

class AlertController: UIAlertController {

    /// Buttons background color.
    var buttonBackgroundColor: UIColor = .darkGray {
        didSet {
            // Invalidate current colors on change.
            view.setNeedsLayout()
        }
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        // Traverse view hierarchy.
        view.allViews.forEach {
            // If there was any non-clear background color, update to custom background.
            if let color = $0.backgroundColor, color != .clear {
                $0.backgroundColor = buttonBackgroundColor
            }
            // If view is UIVisualEffectView, remove it's effect and customise color.
            if let visualEffectView = $0 as? UIVisualEffectView {
                visualEffectView.effect = nil
                visualEffectView.backgroundColor = buttonBackgroundColor
            }
        }

        // Update background color of popoverPresentationController (for iPads).
        popoverPresentationController?.backgroundColor = buttonBackgroundColor
    }

}


extension UIView {

    /// All child subviews in view hierarchy plus self.
    fileprivate var allViews: [UIView] {
        var views = [self]
        subviews.forEach {
            views.append(contentsOf: $0.allViews)
        }

        return views
    }

}

Usage:

  1. Create alert controller.
  2. Set buttons background color: alertController.buttonBackgroundColor = .darkGray
  3. Customise and present controller.

Result:

Result screenshot

Upvotes: 3

Related Questions