Search code examples
iosuiviewuitraitcollection

How to assign a view's UIUserInterfaceLevel?


The documentation for UITraitCollection's userInterfaceLevel states:

When you want parts of your UI to stand out from the underlying background, assign the UIUserInterfaceLevel.elevated level to them. For example, the system assigns the UIUserInterfaceLevel.elevated level to alerts and popovers.

How do you do this? If you try myOverlayView.traitCollection.userInterfaceLevel = .elevated you get an error "Cannot assign to property: 'userInterfaceLevel' is a get-only property."

I wondered if there's a property overrideUserInterfaceLevel similar to overrideUserInterfaceStyle but alas there is not. 🤔


Solution

  • iOS 17+

    iOS 17 introduced an API to make it super easy to override any trait collection value, including the user interface level.

    myOverlayView.traitOverrides.userInterfaceLevel = .elevated

    iOS 16-

    It's not possible to override the userInterfaceLevel on a UITraitCollection. Attempting to override the traitCollection variable in a UIView subclass is not supported. Nor is there a way to override the interface level on a UIView like you can override the interface style, unfortunately.

    There are two APIs to override the trait collection at the view controller level, which UIView subviews inherit:

    This must be what the documentation is referring to as alerts and popovers are presentation controllers.

    If you're presenting a view controller in a way that looks elevated but the system does not automatically elevate it, you can manually elevate it via:

    let myViewController = MyViewController()
    myViewController.modalPresentationStyle = .overFullScreen
    myViewController.modalTransitionStyle = .crossDissolve
    myViewController.presentationController?.overrideTraitCollection = UITraitCollection(userInterfaceLevel: .elevated)
    present(myViewController, animated: true, completion: nil)
    

    If you want a view elevated that needs to be a subview in a non-elevated view controller, what you can seemingly do is create a view controller for your desired elevated view, add it as a child, insert its view as a subview, and override the trait collection for the child. This isn't always possible though, such as if you're using an input accessory view you'd like to be elevated.

    If you can't actually elevate your view, you could manually color your views (and its subviews) as if they were elevated. To do this, you can resolve the color for a specific trait collection, which should be the view's trait collection with the elevated user interface level trait applied. I created this UIColor extension:

    extension UIColor {
        func elevated(for view: UIView) -> UIColor {
            let elevatedTraitCollection = UITraitCollection(userInterfaceLevel: .elevated)
            return resolvedColor(with: UITraitCollection(traitsFrom: [view.traitCollection, elevatedTraitCollection]))
        }
    }
    

    To use it:

    overlayView.backgroundColor = .systemBackground.elevated(for: overlayView)
    

    Note this returns a static UIColor that will no longer dynamically respond to color appearance changes - for example if they change the user interface style from light to dark mode, the color will not automatically update. You'd need to manually resolve the color again with the changed trait collection like so:

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
            //update colors
        }
    }