I am trying to use IBInspectable to add borders to my views.
extension UIView {
private func getBorder(integer: Int) -> UIRectEdge {
if integer == 1 {
return .top
} else if integer == 2 {
return .left
} else if integer == 3 {
return .right
} else if integer == 4 {
return .bottom
}
return .all
}
#IBInspectable var border: Int? {
get {
return self.border
}
set (value) {
self.border = value
for v in addBorder(edges: self.getBorder(integer: self.border!)) {
self.addSubview(v)
}
}
}
#IBInspectable var borderColor: UIColor? {
get {
return self.borderColor
}
set (value) {
self.borderColor = value //EXC_BAD_ACCESS here
for v in addBorder(edges: self.getBorder(integer: self.border!), color: borderColor!) {
self.addSubview(v)
}
}
}
private func addBorder(edges: UIRectEdge, color: UIColor = UIColor.white, thickness: CGFloat = 1) -> [UIView] {
...
}
}
The crash occurs on the line self.borderColor = value (in the set for the borderColor).
All it says in the debug log is (lldb). The crash itself says:
Thread 1: EXC_BAD_ACCESS (code=2, address=0x7fff53cc5fe8)
Here is my storyboard:
How can I fix this issue? Thanks!
You have an infinite recursion there, that is causing the crash. Basically within the setter of borderColor you're calling the setter for the same property, resulting the infinite recursion.
This happens because class extensions are not allowed to have stored properties, so Swift doesn't generate a backstore for your property, instead it treats it like a computed property, and calls the setter whenever you try to set the property.
There are two solutions that I can think of at this time, that will solve your problem:
Subclass UIView, add the two properties there, update the class in IB to match the name of your new class.
Use associated objects in your UIView accessors (objc_setAssociatedObject()/ objc_getAssociatedObject()) instead of direct iVar reference. You will not need to subclass and to update your xibs, however this solution is a little bit messier than the first one.
Related
This question already has answers here:
Swift 4.2 Setter Getter, All paths through this function will call itself
(3 answers)
Why does Swift not allow stored properties in extensions?
(1 answer)
Closed 3 years ago.
I have a warning that I can not delete
All paths through this function will call itself
My code:
extension UIView {
var isLowAlpha: Bool {
get {
return self.isLowAlpha
}
set {
self.isUserInteractionEnabled = !newValue
self.alpha = newValue ? 0.3 : 1
}
}
}
I can not modify the code with this, because I have an error extension must not contain stored property ..:
extension UIView {
var isLowAlpha: Bool {
didSet {
self.isUserInteractionEnabled = !isLowAlpha
self.alpha = isLowAlpha ? 0.3 : 1
}
}
}
What is the solution?
One possible solution is to revert the process:
var isLowAlpha: Bool {
get {
return !isUserInteractionEnabled
}
set {
isUserInteractionEnabled = !newValue
alpha = newValue ? 0.3 : 1
}
}
or better, since you are not interested in the getter, make it a function:
func setIsLowAlpha(_ lowAlpha: Bool) {
self.isUserInteractionEnabled = !lowAlpha
self.lowAlpha = newValue ? 0.3 : 1
}
Anyway, looking at your code, you probably want to implement that a view is disabled. That's usually a task for UIControl subclasses, not UIView.
Also note the same can be implemented using a wrapper view:
class AlphaView: UIView {
var isLowAlpha: Bool = false {
didSet {
isUserInteractionEnabled = !isLowAlpha
alpha = isLowAlpha ? 0.3 : 1
}
}
}
And put your views inside.
In the first case you were recursively calling the variable's getter so it would never return. The way to fix this would be with a stored property _isLowAlpha, but unfortunately, as the second error mentions, Swift extensions cannot contain stored variables; they can only contain computed properties. If you really needed to add another property you would need to instead subclass UIView instead of making an extension.
However, in this case you can kind of cheat as long as you are not setting the UIView's alpha property elsewhere by directly using the alpha property:
extension UIView {
var isLowAlpha: Bool {
get {
return self.alpha == 0.3;
}
set {
self.alpha = newValue ? 0.3: 1
self.isUserInteractionEnabled = !newValue
}
}
}
So I'm fairly new to the whole UIAppearance approach to doing things, and doing it with swift. Hurray for not a ton of documentation out there.
I'm trying to set my border radius through UIAppearance with something along the lines of:
CircleButton.appearance.roundBorderRadius = 9
My CircleButton class implementation:
public class CircleButton : UIButton{
#nonobjc var roundBorderRadius: CGFloat? {
get { return self.layer.cornerRadius }
set {
self.layer.cornerRadius = newValue!
}
}
}
And I hook everything up in Storyboard to a ViewController that contains a CircleButton. No Compilation or Build errors.
However, at runtime I'm getting a:
"Thread 1: EXC_BAD_ACCESS" error on:
CircleButton.appearance.roundBorderRadius = 9
Any advice?
Remove #nonobjc and add dynamic, then change the type from CGFloat? to CGFloat and remove the ! after newValue in the setter, like so:
public class CircleButton: UIButton {
dynamic var roundBorderRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
}
}
}
I can’t figure out how to incorporate a design pattern in to my Swift class which has a property with the following requirements:
Property with default value and is IBInspectable
accompanying setProperty:animated: method
All of the ways I’ve tried require a separate private ‘instance' variable e.g. _property, like below:
#IBInspectable var progress: CGFloat = 0.5 {
didSet {
_progress = progress
setNeedsDisplay()
}
}
private var _progress: CGFloat = 0.5
func setProgress(progress: CGFloat, animated: Bool)
{
if _progress == progress
{
return
}
if animated
{
// Animate changes
// [Animation code]
}
else
{
// No need to animate changes
setNeedsDisplay()
}
titleLabel?.text = "\(NSInteger(progress * 100))%"
// Update value
_progress = progress
}
This doesn’t account for getting the property value though. As a property cannot have a get and didSet method.
So what is the correct way of having a property which is IBInspectable and can be set in the setAnimated method without automatically updating, bypassing the animation?
I Set CornerRadius and BorderWidth for UIbutton in User Defined Runtime Attributes. Without adding layer.borderColor it works Well and Display border in black color. But when add layer.borderColor does not work(does not show border).
For Swift:
Swift 3:
extension UIView {
#IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
#IBInspectable var borderWidth: CGFloat {
get {
return layer.borderWidth
}
set {
layer.borderWidth = newValue
}
}
#IBInspectable var borderColor: UIColor? {
get {
return UIColor(cgColor: layer.borderColor!)
}
set {
layer.borderColor = newValue?.cgColor
}
}
}
Swift 2.2:
extension UIView {
#IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
#IBInspectable var borderWidth: CGFloat {
get {
return layer.borderWidth
}
set {
layer.borderWidth = newValue
}
}
#IBInspectable var borderColor: UIColor? {
get {
return UIColor(CGColor: layer.borderColor!)
}
set {
layer.borderColor = newValue?.CGColor
}
}
}
I got answer. Change borderColor instead of layer.borderColor:
and add this code in .m file:
#import <QuartzCore/QuartzCore.h>
#implementation CALayer (Additions)
- (void)setBorderColorFromUIColor:(UIColor *)color
{
self.borderColor = color.CGColor;
}
#end
Tick properties in Attribute Inspector
Swift 4, Xcode 9.2 - Use IBDesignable and IBInspectable to build custom controls and live preview the design in Interface Builder.
Here is a sample code in Swift, place just below the UIKit in ViewController.swift:
#IBDesignable extension UIButton {
#IBInspectable var borderWidth: CGFloat {
set {
layer.borderWidth = newValue
}
get {
return layer.borderWidth
}
}
#IBInspectable var cornerRadius: CGFloat {
set {
layer.cornerRadius = newValue
}
get {
return layer.cornerRadius
}
}
#IBInspectable var borderColor: UIColor? {
set {
guard let uiColor = newValue else { return }
layer.borderColor = uiColor.cgColor
}
get {
guard let color = layer.borderColor else { return nil }
return UIColor(cgColor: color)
}
}
}
If you go to the Attributes inspectable of the view, you should find these properties visually, edit the properties:
The changes are also reflected in User Defined Runtime Attributes:
Run in build time and Voila! you will see your clear rounded button with border.
The explanation, perhaps being lost in some of the other answers here:
The reason that this property is not being set is that layer.borderColor needs a value with type CGColor.
But only UIColor types can be set via Interface Builder's User Defined Runtime Attributes!
So, you must set a UIColor to a proxy property via Interface Builder, then intercept that call to set the equivalent CGColor to the layer.borderColor property.
This can be accomplished by creating a Category on CALayer, setting the Key Path to a unique new "property" (borderColorFromUIColor), and in the category overriding the corresponding setter (setBorderColorFromUIColor:).
There is a much better way to do this! You should use #IBInspectable. Check out Mike Woelmer's blog entry here:
https://spin.atomicobject.com/2017/07/18/swift-interface-builder/
It actually adds the feature to IB in Xcode! Some of the screenshots in other answers make it appear as though the fields exist in IB, but at least in Xcode 9 they do not. But following his post will add them.
This works for me.
Swift 3, Xcode 8.3
CALayer extension:
extension CALayer {
var borderWidthIB: NSNumber {
get {
return NSNumber(value: Float(borderWidth))
}
set {
borderWidth = CGFloat(newValue.floatValue)
}
}
var borderColorIB: UIColor? {
get {
return borderColor != nil ? UIColor(cgColor: borderColor!) : nil
}
set {
borderColor = newValue?.cgColor
}
}
var cornerRadiusIB: NSNumber {
get {
return NSNumber(value: Float(cornerRadius))
}
set {
cornerRadius = CGFloat(newValue.floatValue)
}
}
}
In case of Swift, function doesn't work. You'll need a computed property to achieve the desired result:
extension CALayer {
var borderColorFromUIColor: UIColor {
get {
return UIColor(CGColor: self.borderColor!)
} set {
self.borderColor = newValue.CGColor
}
}
}
You have set the data values for the radius and the width set to be a string, but it should properly be to be set to a number, not a string
When you get it working, this will not be visible while looking at the storyboard, but will be when the app is running unless you have taken steps to make it #IBDesigneable.
I have found a good answer about this problem, look at here How to hide iOS7 UINavigationBar 1px bottom line
but i want to know how to implement it with swift, i've tried in this way
func findHairlineImageViewUnder(view:UIView!) {
if view is UIImageView && view.bounds.size.height <= 1.0 {
return view
}
var subview: UIView
for subview in view.subviews {
var imageView:UIImageView = self.findHairlineImageViewUnder(subview)
if imageView {
return imageView
}
}
}
i can't make it because the compiler told me
'UIView' is not convertible to ()
cannot convert the expression's type 'AnyObject' to type 'UIImageView'
Type 'UIImageView' does not conform to protocol 'BoooleanType'
i know why these errors came out but how can i fix it?
This extension should do it.
extension UINavigationController {
func hairLine(hide hide: Bool) {
//hides hairline at the bottom of the navigationbar
for subview in self.navigationBar.subviews {
if subview.isKindOfClass(UIImageView) {
for hairline in subview.subviews {
if hairline.isKindOfClass(UIImageView) && hairline.bounds.height <= 1.0 {
hairline.hidden = hide
}
}
}
}
}
}
Just call it like this:
navigationController?.hairLine(hide: true)
There are a few problems in your code.
You haven't specified a return type, so the compiler is trying to figure it out for you but is having trouble. You should specify that your return type is UIView?; it should be an optional because you may not be able to find the view and will need to return nil.
You don't need to declare subview before your for loop; the for loop will do it implicitly.
Declaring imageView to a UIImageView will not work without casting. However, you don't really need to make it a UIImageView in the first place and you can let swift deal with it.
By making the change in 1, you can now simplify the inside of your for loop using optional binding and just do if let foundView = self.findHairlineImageViewUnder(subview) { ... }
If you never find the view you're looking for, you never return anything from the function. That's going to cause a compile error in swift, so make sure to return nil at the very end.
Here is a working implementation with the above fixes:
func findHairlineImageViewUnder(view:UIView!) -> UIView? {
if view is UIImageView && view.bounds.size.height <= 1.0 {
return view
}
for subview in view.subviews as [UIView] {
if let foundView = self.findHairlineImageViewUnder(subview) {
return foundView
}
}
return nil
}