Use #IBInspectable with any UIView subclass within storyboards - swift

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.

Related

Subclassing Views

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 }
}

Why my background gradient view is not loading in my customer UINavigationController class

I'm trying to set a gradient to the background of my subclassed NavigationController. When I add a colour to the same code it works well but I can't seem to let my gradient show up. I created a subclass of a UIView that returns a CAGradientLayer as its background view.
Here is my subclassed UIView : (Note the colours are weird so I am sure its loading the right Gradient.
#IBDesignable
class GenericBackgrounView: UIView {
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
///The roundness for the corner
#IBInspectable var cornerRadius: CGFloat = 0.0 {
didSet{
setupGradient()
}
}
func setupGradient() {
//let gradientColors = [bgDarkColor.cgColor, bgDarkColor.blended(withFraction: 0.5, of: bgLightColor).cgColor, bgLightColor.cgColor]
let gradientColors = [UIColor.brown.cgColor, UIColor.red.blended(withFraction: 0.5, of: UIColor.cyan).cgColor, UIColor.yellow.cgColor]
gradientLayer.colors = gradientColors
gradientLayer.locations = ESDefault.backgroundGradientColorLocations
setNeedsDisplay()
}
var gradientLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override func awakeFromNib() {
super.awakeFromNib()
setupGradient()
}
override func prepareForInterfaceBuilder() {
setupGradient()
}
}
And Here is my UINavigationController :
class GenericNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
let backView = GenericBackgrounView(frame: self.view.frame)
backView.bounds = self.view.bounds
self.view.backgroundColor = UIColor.clear
self.view.addSubview(backView)
self.view.sendSubview(toBack: backView)
// Do any additional setup after loading the view.
}
}
Also note that my GenericBackgroundView works fine when I use it for any views I add in the interface builder.
I have been at this to long. I think I will suggest to Apple to setup some kind of Theming API in both code and Interface Builder... and the ability to add gradients straight into Interface Builder...
Thanks for you help.
Instead of setting it up in awakeFromNib() , try calling it in viewDidLayoutSubviews(). Reason is that in viewDidLayoutSubviews() will have the correct frame of the view , while in awakeFromNib() you wouldn't know the right frame of the view.From Apple Documentation.
Alright, I've tinkered a bit and found some working code. I would still love to understand the reason why this works and not the way I had it before. I hate feeling it works by magic...
here is the working code : (Remember that my gradient is in form of CAGradientLayer and I have made some static variable that has defaults.
import UIKit
class GenericNavigationController: UINavigationController {
let backViewGradient = Default.testGradientCALayer
override func viewDidLoad() {
super.viewDidLoad()
setupBackground()
}
func setupBackground() {
backViewGradient.frame = self.view.frame
self.view.layer.insertSublayer(backViewGradient, at: 0)
}
override func prepareForInterfaceBuilder() {
setupBackground()
}
}
What I'm wondering is how come since all the UIControls that are subclassed from UIView don't all work the same. They should all have a view that is the background and we should all be able to either add a layer or a subview to them and be able to get my previous code to work or my latest code too which does not work with TableViewCells.
I will leave this question open because I would love to know the truth behind this. I don't think I can fully grasp Swift or Xcode if it behaves somewhat magically and inconsistent.

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.

Subclass of UIButton with rounded corners (swift)

Using swift, I'd like like to create a button with rounded corners. To make this re-usable, my preference is to subclass UIButton, and have come up with the following:
import Foundation
import UIKit
class LoginButton: UIButton {
let corner_radius : CGFloat = 4.0
override func drawRect(rect: CGRect) {
super.drawRect(rect)
self.layer.cornerRadius = corner_radius
}
}
Unfortunately, this doesn't seem to work as I had hoped, even though it compiles fine. Perhaps I'm missing something - I'm very new to this!
Any ideas?
You need to use clipsToBounds to ensure that the containing view isn't drawn over the corner radius:
self.clipsToBounds = true
You need also to turn on the masksToBounds on the layer:
self.layer.masksToBounds = true

How to autoresize sublayers

I have an imageview and on button click I draw some bezier paths and some text layer too. On undo, I've removed last drawn path and text layer.
Now I want to resize my image View to set up in uper half portion of view and duplicated in lower half view, and want to resize all layers at the same time and also want to continue drawing after that.
Its too late to answer...
But may be this can help others. There is no autoresize masks for CALayers
You can manage that with some tricks..
- (void)layoutSubviews {
// resize your layers based on the view's new frame
layer.frame = self.bounds;
}
This will work if you have created a subclass of UIView. If not that, you can just set the bounds to the frame of CALayer anywhere if you have reference to your layer.
At least there are 3 options:
use delegate of CALayer
override layoutSublayers, and call it by setNeedsLayout()
use extension and call view's fitLayers
Example below, option 3:
Swift 5, 4
Code
extension UIView {
func fitLayers() {
layer.fit(rect: bounds)
}
}
extension CALayer {
func fit(rect: CGRect) {
frame = rect
sublayers?.forEach { $0.fit(rect: rect) }
}
}
Example
// inside UIViewController
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
view.fitLayers()
}
// or
// inside UIView
override func layoutSubviews() {
super.layoutSubviews()
fitLayers()
}