Subclassing Views - swift

I want to subclass UIView to support gradients (border / fill). I have subclass with #IBInspectable vars, so I'm able to setup this behavior in IB.
I also need to subclass also UIButton with the same methods. Is there any way I can do it without copying all the methods and instance variables to that subclass of UIButton?

Multiple class inheritance is not allowed in Swift (only multiple protocol inheritance), therefore what you are trying to achieve is not straight-forwardly possible.
One of the possible workarounds, however, is to use extension for UIView. Provided that both UIView (itself) and UIButton are variants of UIView, the following code would apply to them all.
Example with corner radius:
extension UIView {
#IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
}
Now, even non-subclassed UIButton will acquire this property, which will be reflected in Interface Builder.
You can try employing the power of extensions to implement unified IBInspectables. One obstacle you will inevitably stumble across, however, is that you won't be able to have any storage in the extension. But for several cases this can serve as a solution.
P.S. Few other examples of use (added to UIView extension):
#IBInspectable var borderColor: UIColor? {
get { return layer.borderColor.map(UIColor.init) }
set { layer.borderColor = newValue?.cgColor }
}
#IBInspectable var borderWidth: CGFloat {
get { return layer.borderWidth }
set { layer.borderWidth = newValue }
}

Related

Overriding UIStackView background color variable

I was shocked to learn that ability to set a background color to a UIStackView come only starting from iOS14. In the older versions such attempts are just ignored.
I have to support older versions as well, so I wrote this code to fix the issue:
public extension UIStackView {
private var helperSubview: UIView {
subviews.first(where: { $0.id == "helperSubview" }) ?? {
let hsv = UIView()
hsv.id = "helperSubview"
insertSubview(hsv, at: 0)
hsv.fillSuperview()
return hsv
}()
}
override var backgroundColor: UIColor? {
didSet {
if #available(iOS 14.0, *) {
return
} else {
helperSubview.backgroundColor = backgroundColor
}
}
}
}
The code works just fine.
But there is one strange moment: in iOS14 didSet doesn't fire at all (like we can even don't check #availability). That suits me, this behaviour doesn't cause any problems. But I don't understand why does it behave like that?
You can't use extension to override a class property. You need to subclass UIStackView if you would like to override any property or method.
From the docs:
Extensions can add new functionality to a type, but they can’t override existing functionality

How to resolve ambiguity between property in extension and property in class?

I made an extension to UIView to add the basic shadow properties so that I could set them using #IBInspectable.
extension UIView {
var shadowRadius: CGFloat {
get {
return layer.shadowRadius
}
set {
layer.shadowRadius = newValue
}
}
}
However, I now want to use a library that has redefined the property:
class ClassA: UIView {
#objc public dynamic var shadowRadius = DPDConstant.UI.Shadow.Radius {
willSet { tableViewContainer.layer.shadowRadius = newValue }
didSet { reloadAllComponents() }
}
}
When I try to call the property I get an "ambiguous use of shadowRadius" message.
I am considering just removing the extension and manually calling layer.shadowRadius since I am no longer using IBInspectable on shadows (seemed to be running slow loading all the IBInspectable properties, so I opted to just set stuff in code).
However, I feel like there ought to be another way to handle this situation, so I thought I would ask here.
Your extension provides this var for all instances of UIView and subclasses. The subclass in the library defines a subclass-specific property of the same name and type. I don’t have the reference but the Swift language documentation says you can’t override an extension’s members with a subclass.

Use #IBInspectable with any UIView subclass within storyboards

I have a class to use with #IBInspectable, to get the properties within my storyboard. Here is a small chunk of it:
/// UIView subclass to allow creating corners, shadows, and borders in storyboards.
final class GEView: UIView {
// MARK: - Rounded corners
#IBInspectable
var cornerRadius: CGFloat = 0 {
didSet {
layer.cornerRadius = self.cornerRadius
layer.masksToBounds = true
}
}
/* ... */
}
This works completely fine within UIViews in my storyboard. However, I want this to also work with UIImageViews and other subclasses of UIView. Is this possible without subclassing my GEView, by somehow making this a generic?
Move your code to UIView's #IBDesignable extension like below:
#IBDesignable extension UIView {
#IBInspectable var cornerRadius: CGFloat {
set {
layer.cornerRadius = newValue
layer.masksToBounds = true
}
get {
return self.cornerRadius
}
}
}
Clear unwanted values/types of previously deifned vars in your GEView. Here are values in inspector.
Properties available in inspector of any UIView, included itself.

How to create a global function that affects all or specific UIButton instance types?

I want to create a global function that affects either all UIButton instances or only those of a certain UIButton type which would update the corner radius or border property. I'm familiar with UIAppearances however my client would like to have a global file where they could update changes on the fly as if it were a CSS stylesheet. So far I've been able to make extensions of UIColor and UIFont which returns specific colors and fonts however I can't figure out how this would work for UIButton instances. Here is what I've thought of so far however I don't think this would work:
#objc extension UIButton {
func changeUIButtonBorder() -> UIButton {
self.layer.borderWidth = 3
self.layer.borderColor = UIColor.white.cgColor
return self
}
}
What you're doing is great, and it does work for UIButton instances. But there is no need to return anything. In the extension, self is the button. So it can just change itself.
#objc extension UIButton {
func changeUIButtonBorder() {
self.layer.borderWidth = 3
self.layer.borderColor = UIColor.white.cgColor
}
}
You can now call changeUIButtonBorder on any UIButton instance.
#IBOutlet var myButton : UIButton!
override func viewDidLoad() {
super.viewDidLoad()
myButton.changeUIButtonBorder()
}
However, there is no magical way to shoutcast to all UIButtons that they should call that method; you'll have to deal with them one at a time.
The "magical" way is, as #Sh_Khan suggests, to make a UIButton subclass that calls changeUIButtonBorder in its own initializer. You would then simply have to make sure that all your buttons are instances of that subclass.
For example, here's a UIButton subclass that's always red (assuming that all instances come from the storyboard):
class RedButton : UIButton {
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = .red
}
}

Is there an equivalent of CSS classes on controls in Swift and iOS?

I have a lot of UIButtons on a view that have to be styled through code. I need to give them all rounded borders, which cannot be done in XCode's interface builder.
So I'm wondering, is there a way in Swift to style a whole bunch of elements all at once, like using a CSS class to style stuff on the web?
Each button has an IBOutlet in my controller and it would be nice to style them all at the same time.
"which cannot be done in XCode's interface builder".
Sure it can, but you need to consider the various factors which limit you to think in terms of CSS.
Design an extension to a UIButton. Then make it IBDesignable. If you want to actually see it in IB, make it IBInspectable. Maybe your code will look something like this:
#IBDesignable
public class Button: UIButton {
#IBInspectable public var borderColor:UIColor? {
didSet {
layer.borderColor = borderColor?.cgColor
}
}
#IBInspectable public var borderWidth:CGFloat = 0 {
didSet {
layer.borderWidth = borderWidth
}
}
#IBInspectable public var cornerRadius:CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
}
In particular, pay attention to the cornerRadius inspectable property.
Finally here's a link that however old, I still find worthy of explaining things better than I can.