IBDesignable class don't seems to work properly - ios

I'm building a new iOS app with swift and trying to make a custom IBDesignable UIButton class to use in the designer.
My code does not seems to work either in the builder or the app itself at runtime.
The default value i gave to the IBInspectable vars does not apply to them.
#IBDesignable class MyButton: UIButton {
#IBInspectable var backColor: UIColor = UIColor.red {
didSet {
self.backgroundColor = backColor
}
}
#IBInspectable var textColor: UIColor = UIColor.white {
didSet {
self.setTitleColor(textColor, for: .normal)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setup()
}
private func setup() {
titleLabel?.font = UIFont.systemFont(ofSize: 17)
contentEdgeInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
layer.cornerRadius = (frame.height / 2)
setTitle(title(for: .normal)?.uppercased(), for: .normal)
}
}
I expect to see the button in the designer/app with a red background and a white text and none of these happens.
I'm using Swift 4.2 and Xcode 10.1.

add this two func s
override func awakeFromNib() {
super.awakeFromNib()
self.setup()
}
override func layoutSubviews() {
super.layoutSubviews()
self.setup()
}
no need for init

Setting a property default value does not invoke the didSet clause.
You need something like:
private func setup() {
titleLabel?.font = UIFont.systemFont(ofSize: 17)
contentEdgeInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
layer.cornerRadius = (frame.height / 2)
setTitle(title(for: .normal)?.uppercased(), for: .normal)
// I would do it differently, but this is just for demo purposes
// Also, can't assign a prop to itself, so using a temp
var tmp = self.backColor
self.backColor = tmp
tmp = self.textColor
self.textColor = tmp
}
Additionally, Xcode is not very good with IBDesignable, in swift or objective C.
Make sure the menu item: Editor -> Automatically Refresh Views is checked.
You can also explicitly do Editor -> Refresh All Views.
From my experience - this stops working after a while, and you need to quit Xcode and restart it to make it work again.

Make sure that your button is set to type .custom in your Interface Builder.
This is not obvious but the default button type is .system, which cannot be customized. I saw people having no problems with this but in my applications this was always a problem.
Another problem is that if you keep your inspectable values empty, the didSet won't be called and the default value won't be applied.
#IBInspectable var backColor: UIColor? = UIColor.red {
didSet {
updateBackground()
}
}
private func updateBackground() {
self.backgroundColor = backColor
}
func setup() {
...
updateBackground()
}

Related

Custom UIButton class does not show title

I have created a custom UIButton class. But the title does not show up. I only get the background color and the rounded corners. But the title is not visible.
Also when I run it in simulator, iOS 13+, its showing the title white. But when I run in my device i.e. iOS 12.4, its not working.
Here is my code:
public class CornerButton : UIButton {
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
self.layoutIfNeeded()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
self.layoutIfNeeded()
}
func setup() {
self.titleLabel?.font = UIFont(name: "Poppins-SemiBold", size: 12)
self.backgroundColor = UIColor(named: "BarColor")
self.setTitleColor(.white, for: .normal) // this line is not working
self.clipsToBounds = true
}
public override func layoutSubviews() {
self.roundCorners(corners: [.topRight,.bottomLeft], radius: 10)
}
}
You need to call super.layoutSubviews()
public override func layoutSubviews() {
super.layoutSubviews()
self.roundCorners(corners: [.topRight,.bottomLeft], radius: 10)
}
BTW here you don't need layoutSubviews as you use it when you need to make something that depends on the actual measured bounds of the view and as you set a static radius you can omit it

How to change custom UIButton's image on fly

I've create a custom UIButton class to make it look like drop down selection. I've small down arrow image on the right of button. I want to change the button's image to different images such as grey, white or red depending on various conditions. How to do it? Following is my code:
class DropDownButton: UIButton {
let dropDownImageGrey = UIImage(named: "Icons/DropDown/Grey")
let dropDownImageWhite = UIImage(named: "Icons/DropDown/White")
let dropDownImageRed = UIImage(named: "Icons/DropDown/Red")
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
self.imageEdgeInsets = UIEdgeInsets(top: 0, left: self.frame.width - 108, bottom: 0, right:0)
self.setImage(dropDownImageGrey, for: [])
self.titleEdgeInsets = UIEdgeInsets(top: 0, left: -13, bottom: 0, right:0)
}
}
override func viewDidLoad() {
// Change image to red one
dropDownButton.??? // How to change?
}
You may want to use setImage property of UIButton,
override func viewDidLoad() {
// Change image to red one
let dropDownButton = DropDownButton()
dropDownButton.setImage(dropDownButton.dropDownImageRed, for: .normal)
}
If the images are added in Assets.xcassets then you can use image literals or directly use the names like,
let dropDownImageGrey = UIImage(named: "Grey")
let dropDownImageWhite = UIImage(named: "White")
let dropDownImageRed = UIImage(named: "Red")
I think some answer above work. However, for the sake of production code, I suggest to use enum for the list of images.
class DropDownButton: UIButton {
enum ImageType: String {
case grey = "Icons/DropDown/Grey"
case white = "Icons/DropDown/White"
case red = "Icons/DropDown/Red"
var image: UIImage? { return UIImage(named: rawValue) }
}
var imageType: ImageType = .red {
didSet {
setImage(imageType.image, for: .normal)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
self.imageEdgeInsets = UIEdgeInsets(top: 0, left: self.frame.width - 108, bottom: 0, right:0)
self.titleEdgeInsets = UIEdgeInsets(top: 0, left: -13, bottom: 0, right:0)
}
}
override func viewDidLoad() {
// Change image to red one
dropDownButton.imageType = .red
}
Later, if you need to change the image type, simply set the imageType of the button. Like
dropDownButton.imageType = .grey
dropDownButton.imageType = .white
You can use an enum to define the various condition and add a method to get images. Based on the condition you can set the image. Example as below:
Define this enum outside of your class
enum DropdownCondition
{
case condition1
case condition2
case condition3
func getImage() -> UIImage? {
switch self {
case .condition1:
return UIImage(named: "Icons/DropDown/Grey")
case .condition1:
return UIImage(named: "Icons/DropDown/White")
case .condition1:
return UIImage(named: "Icons/DropDown/Red")
default:
return nil
}
}
}
in your viewDidLoad/init or any method call yourMethodWithSomeoCndition(.condition1) based on the condition.
override func viewDidLoad() {
// Change image to red one
let dropDownButton = DropDownButton()
//Call based on your condition
yourMethodWithSomeoCndition(.condition1)//This condition can change on the fly
}
func yourMethodWithSomeoCndition(_ condition:DropdownCondition)
{
self.dropDownButton.setImage(condition.getImage(), for: .normal)
}

Strange issue with 2 initializers in Swift

I want to create 2 initializers for my custom button class which looks like:
protocol CustomButtonProtocol {
var title: String { get set }
var cornerRadius: CGFloat { get set }
var backgroundColor: UIColor { get set }
}
struct Button: CustomButtonProtocol {
var title: String
var cornerRadius: CGFloat
var backgroundColor: UIColor
}
class CustomButton: UIButton {
var title: String? {
didSet {
self.setTitle(title, for: .normal)
}
}
var cornerRadius: CGFloat = 0.0 {
didSet {
self.layer.cornerRadius = cornerRadius
}
}
var color: UIColor? {
didSet {
self.backgroundColor = color
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
init(with frame: CGRect, button: Button) {
super.init(frame: frame)
self.title = button.title
self.cornerRadius = button.cornerRadius
self.backgroundColor = button.backgroundColor
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I want to have 2 initializers, the first one is simple init, the second one with my Button passed in, but the issue is that it changes ONLY background color but does not title and cornerRadius properties and I cannot find why it happens. Maybe I do not see something, could you please help me with finding the bug?
You have used didSet to update value to button property.
It won't call didSet while setting value inside init.
background color is getting set because you set backgroundColor by mistake in place of color
self.backgroundColor = button.backgroundColor
you should use/call below inside/outside init once.
self.setTitle(title, for: .normal)
self.layer.cornerRadius = cornerRadius
self.backgroundColor = color
Consider that property observers willSet / didSet are not called in init methods
From the documentation:
When you assign a default value to a stored property, or set its initial value within an initializer, the value of that property is set directly, without calling any property observers.
A solution is to call the code in the observers also in the init method
init(with frame: CGRect, button: Button) {
super.init(frame: frame)
self.title = button.title
self.setTitle(self.title, for: .normal)
self.cornerRadius = button.cornerRadius
self.layer.cornerRadius = self.cornerRadius
self.color = button.backgroundColor
self.backgroundColor = self.color
}
If color represents always the background color the property is actually redundant.

IBDesignable Subclass of TextField renders to IB but not to simulator / device

I created a subclass of UITextField that adds a floating button to the right hand side of the text-field. It's all good in the IB, but for some reason the button image does not show up in the simulator or on the device.
Using the view debugger, I can see the button is there, right where it is supposed to be. I can even tap it and get the target action to trigger. However, the image is not visible on the screen.
I tried messing with the clipsToBounds properties of both the TextView and the button to no avail.
I also know the image asset is working, because I made an an image view and set the image to the same image asset, and it appears just fine. The class for the TextField is below:
#IBDesignable
class ButtonedTextField: UITextField {
#IBInspectable var buttonImage: UIImage?
var button: UIButton?
var buttonAction: (() -> ())?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
configure()
}
#objc private func buttonActionWrap() {
buttonAction?()
}
private func configure() {
let button = UIButton(type: .custom)
button.addTarget(self, action: #selector(buttonActionWrap), for: .touchUpInside)
let height = self.frame.height
let buttonHeight = height - 10
button.frame = CGRect(x: self.frame.width - (buttonHeight + 10), y: 5, width: buttonHeight, height: buttonHeight)
button.setImage(buttonImage, for: .normal)
self.button = button
self.addSubview(button)
bringSubview(toFront: button)
}
}
Any thoughts?

How to create a custom button and use multiple times in best way in iOS?

I have created a custom UIButton in ios. I want to use the same custom button ten times with the same attributes but different titles. What is the most efficient and clever way to do this without repeating the same code for each button? Should it be a struct or a class or something else? How can I implement it? Attributes that I have changed for my custom button is as follows:
#IBOutlet weak var button_1: UIButton!
button_1.frame = CGRectMake(0.0, 0.0, button_1.frame.width, button_1.frame.height)
button_1.clipsToBounds = true
button_1.layer.cornerRadius = button_1.frame.width/2.0
button_1.layer.borderColor = UIColor.whiteColor().CGColor
button_1.layer.borderWidth=2.0
You can create your own class that extends from UIButton
import UIKit
#IBDesignable
final class CoyoteButton: UIButton {
var borderWidth: CGFloat = 2.0
var borderColor = UIColor.white.cgColor
#IBInspectable var titleText: String? {
didSet {
self.setTitle(titleText, for: .normal)
self.setTitleColor(UIColor.black,for: .normal)
}
}
override init(frame: CGRect){
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
setup()
}
func setup() {
self.clipsToBounds = true
self.layer.cornerRadius = self.frame.size.width / 2.0
self.layer.borderColor = borderColor
self.layer.borderWidth = borderWidth
}
}
To set a text to the button, just write myButton.titleText = "myText".
Then you can drag and drop a UIButton from the interface builder and then change the class of that button to your own MyOwnButton, or create one by code.
You need to create the extension of UIButton like this
extension UIButton {
class func attributedButton(frame: CGRect) -> UIButton {
let button = UIButton(frame: frame)
button.clipsToBounds = true
button.layer.cornerRadius = button_1.frame.width/2.0
button.layer.borderColor = UIColor.whiteColor().CGColor
button.layer.borderWidth = 2.0
return button
}
}
Now call this method like this way
let button = UIButton.attributedButton(frame: CGRectMake(20, 20, 10, 40)
button.setTitle("Title", forState: .Normal)
self.view.addSubview(button)
Now you can use this method every time you want to create a button with these fixed attributes.
question author marked correct answer (author: Brigadier), but it not works properly in swift 4 (maybe in other versions to, have not checked).
For example when I set button title it is not shown in run time. the reason is following: it is not called super.layoutSubviews().
The correct way is here:
#IBDesignable
class MyOwnButton: UIButton {
var borderWidth = 2.0
var boderColor = UIColor.whiteColor().CGColor
#IBInspectable
var titleText: String? {
didSet {
self.setTitle(titleText, forState: .Normal)
self.setTitleColor(UIColor.blackColor(), forState: .Normal)
}
}
override init(frame: CGRect){
super.init(frame: frame)
}
required init?(aDecoder: NSCoder) {
super.init(aDecoder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
setup()
}
func setup() {
self.clipsToBounds = true
self.layer.cornerRadius = self.frame.size.width / 2.0
self.layer.borderColor = borderColor
self.layer.borderWidth = borderWidth
}
}
I have a repo with a custom button.
You could use it: https://github.com/titopalito/CustomUIButton
It is not mine, I got it from a tutorial ;)
Best regards.

Resources