iosuiviewuiappearance

UIView: how does the appearance() proxy work?


I have created a simple custom UIView:

final class TestView: UIView {
    var testColor: UIColor = .white {
        didSet {
            backgroundColor = testColor
        }
    }
}

Then I wrote this in my view controller:

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var testView: TestView!
    @IBOutlet weak var testView2: TestView!        
    
    override func viewDidLoad() {
        super.viewDidLoad()
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
            TestView.appearance().testColor = .red
        }
    }
}

By doing this, I get an error:

enter image description here

Could you help me understanding what's wrong here and how to implement the UIAppearance proxy for any custom UIView?

Thank you for your help


Solution

  • You need to make the property @objc and dynamic:

    final class TestView: UIView {
        @objc dynamic var testColor: UIColor? = .white {
            didSet {
                backgroundColor = testColor
            }
        }
    }
    

    Worth noting: the UIAppearance proxy does NOT affect views which are already part of the view hierarchy.

    So, in your example, adding @objc dynamic to your property will get rid of the crash, but you will not see any change to the two @IBOutlet views.

    If you call it as part of viewDidLoad() (instead of DispatchQueue.main.asyncAfter):

    override func viewDidLoad() {
        super.viewDidLoad()
        TestView.appearance().testColor = .red
    }
    

    The two @IBOutlet views will get the red background.

    Or, if you add a new view to the hierarchy, it will get the red background:

    class AppearanceTestViewController: UIViewController {
        @IBOutlet weak var testView: TestView!
        @IBOutlet weak var testView2: TestView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
                TestView.appearance().testColor = .red
                self.addAnotherTestView()
            }
        }
        func addAnotherTestView() -> Void {
            let v = TestView()
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
            NSLayoutConstraint.activate([
                v.widthAnchor.constraint(equalToConstant: 240.0),
                v.heightAnchor.constraint(equalTo: v.widthAnchor),
                v.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                v.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            ])
    
            // this newly added view WILL have a red background
        }
    }