3
\$\begingroup\$

I'm in the process of converting a custom UIButton from Objective-C to Swift which has custom properties and default values for those properties:

MyButton.h

@interface MyButton : UIButton

@property (nonatomic, strong) IBInspectable UIColor *buttonColor;
@property (nonatomic, strong) IBInspectable UIColor *buttonSelectedColor;

// ...

@end

MyButton.m

@implementation MyButton

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {    
        [self adjustButtonColor];
    }

    return self;
}

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self adjustButtonColor];
    }

    return self;
}

- (void)setSelected:(BOOL)selected
{
    [super setSelected:selected];

    [self adjustButtonColor];
}

- (void)setHighlighted:(BOOL)highlighted
{
    [super setHighlighted:highlighted];

    [self adjustButtonColor];
}

- (void)adjustButtonColor
{
    if (self.selected || self.highlighted) {
        self.backgroundColor = [self buttonSelectedColor];
    } else {
        self.backgroundColor = [self buttonColor];
    }
}

#pragma mark - Default colors

- (UIColor *)buttonColor
{
    if (!_buttonColor) {
        _buttonColor = [UIColor grayColor];
    }
    return _buttonColor;
}

- (UIColor *)buttonSelectedColor
{
    if (!_buttonSelectedColor) {
        _buttonSelectedColor = [self.buttonColor colorWithAlphaComponent:.5];
    }
    return _buttonSelectedColor;
}

#pragma mark - Public

- (void)setButtonColor:(UIColor *)buttonColor
{
    _buttonColor = buttonColor;

    [self updateButton];
}

@end

I have attempted to replicate this pattern in Swift with public vars which return a default value, or access a private optional variable which is returned if it exists. I have also overridden the selected and highlighted properties, updating the button colors in didSet:

class MyButton: UIButton {

    private var _buttonColor: UIColor?
    var buttonColor: UIColor {
        get {
            if let color = _buttonColor { return color }
            return UIColor.grayColor()
        } set {
            _buttonColor = newValue
        }
    }

    private var _buttonSelectedColor: UIColor?
    var buttonSelectedColor: UIColor {
        get {
            if let selectedColor = _buttonSelectedColor { return selectedColor }
            return self.buttonColor.colorWithAlphaComponent(0.5)
        } set {
            _buttonSelectedColor = newValue
        }
    }

    override var selected: Bool {
        didSet {
            adjustButtonColor()
        }
    }

    override var highlighted: Bool {
        didSet {
            adjustButtonColor()
        }
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        adjustButtonColor()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        adjustButtonColor()
    }


    func adjustButtonColor() {
        if selected || highlighted {
            backgroundColor = buttonSelectedColor
        } else {
            backgroundColor = buttonColor
        }
    }
}

A few questions:

  1. Is this an appropriate use of private variables? Is there any way to accomplish the behavior I want with a single variable rather a second optional settable variable for each color?

  2. Is overriding UIButton properties to call adjustButtonColor in didSet abuse of the getter/setter?

  3. If initWithCoder is a failable initializer do I need to check if self is nil before calling adjustButtonColor()?

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

Is this an appropriate use of private variables? Is there any way to accomplish the behavior I want with a single variable rather a second optional settable variable for each color?

This can be much easier accomplished with a lazy stored property:

lazy var buttonColor: UIColor = UIColor.grayColor()
lazy var buttonSelectedColor: UIColor =  self.buttonColor.colorWithAlphaComponent(0.5)

The right-hand side is evaluated exactly once, when the property is accessed the first time. This makes the private _xxx variables obsolete. (I think that you seldom need such a "shadow copy" in Swift.)

But there seems to be no real need for buttonColor to be evaluated lazily. If you make it an ordinary stored property then you can add a property observer:

var buttonColor = UIColor.grayColor() {
    didSet {
        self.updateButton()
    }
}

If you want buttonSelectedColor to be computed from buttonColor then it needs to be a lazy property, otherwise you could make it a stored property as well:

var buttonSelectedColor = UIColor.grayColor().colorWithAlphaComponent(0.5)  

Is overriding UIButton properties to call adjustButtonColor in didSet abuse of the getter/setter?

That is fine. The Swift reference explicitly states: "You can also add property observers to any inherited property (whether stored or computed) by overriding the property within a subclass."

If initWithCoder is a failable initializer do I need to check if self is nil before calling adjustButtonColor()?

You don't need to (and you can't). If

 super.init(coder: aDecoder)

fails then your init?(coder:) method fails immediately, and the next statement is not reached.

\$\endgroup\$
6
  • \$\begingroup\$ Thanks Martin, great explanation. Quick follow-up: why is the type required on the second lazy var? If I try to just declare it like lazy var buttonSelectedColor = self.buttonColor.colorWithAlphaComponent(0.5) I get the error "Value of type 'NSObject -> () -> RoundedButton' has no member 'buttonColor'" Is it because of the lazy computation? \$\endgroup\$
    – JAL
    Commented Jun 9, 2016 at 19:36
  • 1
    \$\begingroup\$ @JAL: It seems that otherwise the compiler is not smart enough to figure out that for a lazy property, self on the RHS is evaluated later and refers to the instance. For a non-lazy property let foo = self, the self on the RHS is some curried function of the type MyButton -> () -> MyButton. \$\endgroup\$
    – Martin R
    Commented Jun 9, 2016 at 19:43
  • \$\begingroup\$ Care to comment on La citrouille's claim below? "I can't comment, I don't have enough gateau-juice for it, but that's obvious that a lazy instantiation on a de-referenced initializer would fail ; this is not about the compiler being smart or not." \$\endgroup\$
    – JAL
    Commented Jun 9, 2016 at 19:47
  • \$\begingroup\$ One other question: it looks like lazy vars can't have observers. How would I call updateButton like in the Objective-C setButtonColor: setter? \$\endgroup\$
    – JAL
    Commented Jun 9, 2016 at 20:10
  • \$\begingroup\$ I guess I could just keep everything as var and just override didSet to call that update method. \$\endgroup\$
    – JAL
    Commented Jun 9, 2016 at 20:19
3
\$\begingroup\$

This is a small point and not really relevant since you should absolutely go with Martin's suggestion. But if you stuck with your old way of doing the properties, you should rewrite your getters to use a guard instead of an if:

get {
    guard let color = _buttonColor else { return UIColor.grayColor() }
    return color
}

The reason guards are preferred over ifs in this case is that guards are more limited in their use, since there is only ever an else statement and you must exit the current function from that statement. Furthermore, guard let carries the exact semantics you need, unwrap an optional, do something with it after, do something else if it fails.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.