What's the correct way to add a UIImageView or UITextView to a custom UIButton class? - ios

I've built a custom UIButton class but I'm struggling to add objects without affecting the behaviour of the button. I've shown the class below. When I use the class in my main code and add a target, the button only works in areas not covered by the image or text. How can I add objects and have the entire button area behave in the same way?
class TestButton: UIButton {
var myText: UITextView!
var myImageView: UIImageView!
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.blue
let myImage = UIImage(named: "AnImage")
myImageView = UIImageView(image: myImage)
myImageView.frame = CGRect(x: 10, y: 10, width: (myImage?.size.width)!, height: (myImage?.size.height)!)
addSubview(myImageView)
myText = UITextView()
myText.font = UIFont(name: "Courier", size: 12)!
myText.isEditable = false
addSubview(myText)
}
}

A guess here, hope it works.
UIImageView and UITextView does not normally allow "user interaction", meaning that the user can not tap them and expect the app to react based on that. That is probably why, when you tap on the Image view, that event is not passed through to the UIButton below.
Fortunately the fix is easy. You can set the boolean property isUserInteractionEnabled to true and you should be in business again.
So in your case:
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.blue
let myImage = UIImage(named: "AnImage")
myImageView = UIImageView(image: myImage)
myImageView.frame = CGRect(x: 10, y: 10, width: (myImage?.size.width)!, height: (myImage?.size.height)!)
myImageView.isUserInteractionEnabled = true
addSubview(myImageView)
myText = UITextView()
myText.font = UIFont(name: "Courier", size: 12)!
myText.isEditable = false
myText.isUserInteractionEnabled = true
addSubview(myText)
}
Update (after your comment)
OK so I just tried to create a quick project with your button and...it seems to work, I can click on the imageView and I can click on the label and still get an answer from my function, so there must be some difference in how we've set this up.
Here is my MyButton button, awfully close to the one you've made, the only difference is that I've added a backgroundColor to myImageView, a frame on myText and isUserInteractionEnabled = false on myText
class MyButton: UIButton {
var myText: UITextView!
var myImageView: UIImageView!
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.blue
let myImage = UIImage(named: "AnImage")
myImageView = UIImageView(image: myImage)
myImageView.frame = CGRect(x: 10, y: 10, width: (myImage?.size.width)!, height: (myImage?.size.height)!)
myImageView.backgroundColor = UIColor.red
addSubview(myImageView)
myText = UITextView()
myText.font = UIFont(name: "Courier", size: 12)!
myText.frame = CGRect(x: 10, y: 80, width: 100, height: 30)
myText.isEditable = false
myText.isUserInteractionEnabled = false
addSubview(myText)
}
}
And here is my ViewController where I use the button
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = MyButton(frame: CGRect(x: 10, y: 100, width: 200, height: 200))
button.myText.text = "Hello"
button.addTarget(self, action: #selector(didTapButton(_:)), for: .touchUpInside)
view.addSubview(button)
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func didTapButton(_ sender: MyButton) {
print("It's alive!!")
}
}
Which gives me this pretty UI
And when I tap either the red image, the blue button itself or the "Hello" label I can see this in my console:
It's alive!!
It's alive!!
It's alive!!
So the good news is that it seems to work, now we just need to figure out what the difference between your setup and my setup is :)
Hope that works and helps.

Related

addTarget on a Custom UI Button not working programmatically

I created a custom UIButton with this initialiser :
class CustomButton : UIButton{
override init(frame: CGRect) {
super.init(frame: frame)
setUpButtoninClass(frame)
addTarget(self, action: #selector(handleTap), for:.touchUpInside )
}
fileprivate func setUpButtoninClass(_ frame: CGRect) {
let padding : CGFloat = 16
self.frame = frame
layer.shadowColor = UIColor.darkGray.cgColor
layer.shadowOpacity = 0.3
layer.shadowOffset = .zero
layer.shadowRadius = 10
layer.cornerRadius = frame.width/2
backgroundColor = UIColor(white: 0.9, alpha: 1)
let buttonView = UIView(frame: frame)
buttonView.layer.cornerRadius = frame.width/2
buttonView.backgroundColor = .white
addSubview(buttonView)
let imageView = UIImageView(image: UIImage(named: "pen")?.withRenderingMode(.alwaysTemplate))
imageView.tintColor = UIColor(white: 0.7, alpha: 1)
buttonView.addSubview(imageView)
imageView.anchor(top: buttonView.topAnchor, leading: buttonView.leadingAnchor, bottom: buttonView.bottomAnchor, trailing: buttonView.trailingAnchor, padding: UIEdgeInsets.init(top: padding, left: padding, bottom: padding, right: padding))
}
#objc func handleTap(){
print("I'm here")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}}
In the initialiser I'm adding a target but when I actually initialise the custom button in the VC the #selector method (handleTap) is not called.
This is the implementation of custom Button in VC:
class ViewController: UIViewController {
let circularButton = CustomButton(frame: CGRect(x: 0, y: 0, width: 70, height: 70))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(circularButton)
circularButton.center = view.center
}
I also tried to add the target when initialising the CustomButton in the VC but nothing changed.
I would like to know where I'm making a mistake in setting up the button.
EDIT 1 :
this is the Debug View Hierarchy
OMG, after debug your code, buttonView and imageView is on the top. Button is behide. You can set the color to debug it more easily. Delete 2 views above make your code works perfectly
I think it's your fault here,
Touch is not detected because you added an ImageView to the top of UIButton.
Try this, or this one,
buttonView.isUserInteractionEnabled = true
imageView.isUserInteractionEnabled = true

Swift: Target not getting called on a subclassed UIButton

CustomButton.swift
class CustomButton: UIButton {
override func draw(_ rect: CGRect) {
//drawing code
}
}
ViewController.swift
let testCustom = CustomButton()
testCustom.draw(CGRect(x: 0, y: 0, width: 0, height: 0))
testCustom.isUserInteractionEnabled = true
testCustom.addTarget(self, action: #selector(Start(_:)), for: .touchUpInside)
self.view.addSubview(testCustom)
#objc func Start(_ sender: CustomButton) {
print("pressed start")
}
The button appears on screen but the function does not get called upon pressing the button. Any ideas why?
I also tried the function and addTarget code within the CustomButton.swift but couldn't get that to trigger either.
Thanks for any help!
The following is a simple example of how to instantiate a UIButton subclass in a view controller (UIViewController). It's tested under Swift 4.2.
// Subclassing UIButton //
import UIKit
class MyButton: UIButton {
var tintColor0: UIColor!
var tintColor1: UIColor!
var borderColor: UIColor!
var backColor: UIColor!
var cornerRadius: CGFloat!
required init(frame: CGRect, tintColor0: UIColor, tintColor1: UIColor, borderColor: UIColor, backColor: UIColor, cornerRadius: CGFloat, titleString: String) {
super.init(frame: frame)
self.tintColor0 = tintColor0
self.tintColor1 = tintColor1
self.borderColor = borderColor
self.backColor = backColor
self.cornerRadius = cornerRadius
self.setTitle(titleString, for: .normal)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
self.setTitleColor(tintColor0, for: .normal)
self.setTitleColor(tintColor1, for: .highlighted)
self.layer.borderColor = borderColor.cgColor
self.layer.cornerRadius = cornerRadius
self.layer.borderWidth = 1.0
self.layer.backgroundColor = backColor.cgColor
}
}
// View controller //
import UIKit
class ViewController: UIViewController {
// MARK: - Variables
// MARK: - IBOutlet
// MARK: - IBAction
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
let buttonRect = CGRect(x: 20.0, y: 160.0, width: 100.0, height: 32.0)
let myButton = MyButton(frame: buttonRect, tintColor0: UIColor.black, tintColor1: UIColor.gray, borderColor: UIColor.orange, backColor: UIColor.white, cornerRadius: 8.0, titleString: "Hello")
myButton.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
view.addSubview(myButton)
}
#objc func buttonTapped(_ sender: UIButton) {
print("Hello!?")
}
}
#MuhammadWaqasBhati made a good point asking about the frame.
I was using addSublayer to draw the path I had created onto the screen. My mistake here was that I was setting values in the draw() function and adding the CAShapeLayer with addSublayer, but a frame for the button wasn't being set.
Even though the layer that is drawn is a sublayer of the button, it appears at the coordinates and dimensions provided for the layer, without any relation to the frame of its "parent" button.
The frame of the button could be (0, 0, 0, 0) or (0, 0, 100, 100) and the image drawn in addSublayer could still be at (250, 200, 75, 80) so that the visible image would be in one spot of the screen, but the actual button is in an unrelated spot to what is visible in its sublayer.

Button and Image Alignment issues in UIButton

So I have a UIBarbuttonItem that I am currently designing based off of a layout that I have done.
import Foundation
import UIKit
class LocationManager: UIBarButtonItem {
var viewController: MainViewController?
lazy var customButton : UIButton = {
let customButton = UIButton(type: .system)
customButton.setImage(UIImage(named: "downArrow"), for: .normal)
customButton.imageEdgeInsets = UIEdgeInsetsMake(0, 20, 0, -10)
guard let customFont = UIFont(name: "NoirPro-SemiBold", size: 20) else {
fatalError("""
Failed to load the "CustomFont-Light" font.
Make sure the font file is included in the project and the font name is spelled correctly.
"""
)
}
customButton.semanticContentAttribute = UIApplication.shared
.userInterfaceLayoutDirection == .rightToLeft ? .forceLeftToRight : .forceRightToLeft
customButton.titleLabel?.font = customFont
customButton.setTitleColor(UIColor.black, for: .normal)
return customButton
}()
override init() {
super.init()
setupViews()
}
#objc func setupViews(){
customView = customButton
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I correctly do the job of using both an image and title and setting the image insets for the button and on load the appearance is great. However, when I leave the screen and come back it seems as though everything is thrown out of wack the image gets moved back and sometimes there will be two images and one will have a distorted size.
Is there anything wrong with my custom button implementation that I am missing.
I have included images for before and after
I suggest you to make your custom button class, then make title and image by adding subviews. In this case UIImageView and UILabel. Because UIButton inherits from UIView you can easy do this. I've never had problems using this way.
Here is the code I've written for you:
import UIKit
class ViewController: UIViewController {
lazy var customButton: CustomButton = {
let button = CustomButton(frame: CGRect(x: 50,
y: 200,
width: view.frame.width - 100,
height: 50))
// This mask for rotation
button.autoresizingMask = [.flexibleLeftMargin,
.flexibleRightMargin,
.flexibleTopMargin,
.flexibleBottomMargin]
button.attrTitleLabel.text = "San Francisco, CA"
button.addTarget(self, action: #selector(chooseCity), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
view.addSubview(customButton)
}
#objc func chooseCity() {
print("Choose city button has pressed")
}
}
class CustomButton: UIButton {
private let arrowImageSize: CGSize = CGSize(width: 20, height: 20)
private let sideOffset: CGFloat = 10
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
addSubview(attrTitleLabel)
addSubview(arrowImageView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var attrTitleLabel: UILabel = {
let label = UILabel()
label.font = UIFont(name: "NoirPro-SemiBold", size: 20)
label.textColor = .black
return label
}()
lazy var arrowImageView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "arrow_down")
iv.contentMode = .scaleAspectFit
return iv
}()
override func layoutSubviews() {
super.layoutSubviews()
arrowImageView.frame = CGRect(x: self.frame.width - arrowImageSize.width - sideOffset,
y: self.frame.height/2 - arrowImageSize.height/2,
width: arrowImageSize.width,
height: arrowImageSize.height)
attrTitleLabel.frame = CGRect(x: sideOffset, y: 0, width: self.frame.width - sideOffset*2 - arrowImageSize.width, height: self.frame.height)
}
}
How it looks:

Custom UITextField class only applied to first text field

I have made my first custom class in Swift and I want to use it for the text fields in my app. The problem is that it only works for the first textfield in the view controller, the class is not applied to the second one (the second one has no rounded corners, default placeholder color etc.). I have double checked that the right "custom class" is set in the storyboard for both textfields. Why isn't the class applied to both fields? Seems like a thing with a simple solution but I haven't found any...
Here is the class:
import UIKit
class RoundedUITextField: UITextField {
override init(frame: CGRect) {
super.init(frame: frame)
self.setAttributes()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setAttributes() {
self.layer.cornerRadius = 20.0
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 15, height: self.frame.height))
self.leftView = paddingView
self.leftViewMode = UITextFieldViewMode.always
self.textColor = UIColor.white
self.alpha = 0.7
let str = NSAttributedString(string: (self.placeholder)!, attributes: [NSForegroundColorAttributeName:UIColor.white])
self.attributedPlaceholder = str
}
}
Here is the view controller code:
class LogInViewController: UIViewController {
#IBOutlet weak var password: RoundedUITextField!
#IBOutlet weak var userName: RoundedUITextField!
override func viewDidLoad() {
super.viewDidLoad()
userName.setAttributes()
// Do any additional setup after loading the view.
}
I discovered now that the problem lies in the "setAttributes", I thought the attributes where set in the class but they're actually only set in viewDidLoad for one of them (I put it there in the beginning and forgot to remove it)... I want this to be done in the "init" of the class for all of the fields...
Put your self.setAttributes() in your awakeFromNib method and remove it from init(frame: CGRect)
override func awakeFromNib() {
super.awakeFromNib()
self.setAttributes()
}
your custom class code will be like this
import UIKit
class RoundedUITextField: UITextField {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
self.setAttributes()
}
func setAttributes() {
self.layer.cornerRadius = 20.0
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 15, height: self.frame.height))
self.leftView = paddingView
self.leftViewMode = UITextFieldViewMode.always
self.textColor = UIColor.white
self.alpha = 0.7
let str = NSAttributedString(string: (self.placeholder)!, attributes: [NSForegroundColorAttributeName:UIColor.white])
self.attributedPlaceholder = str
}
}
Hope this helps

Custom Clear Button

I want to create custom clear button on UITextField, that is to use rightView and put image there, the problem is attaching the original clear button event to that custom rightView.
In Objective-C i can do that this way:
SEL clearButtonSelector = NSSelectorFromString(#"clearButton");
// Reference clearButton getter
IMP clearButtonImplementation = [self methodForSelector:clearButtonSelector];
// Create function pointer that returns UIButton from implementation of method that contains clearButtonSelector
UIButton * (* clearButtonFunctionPointer)(id, SEL) = (void *)clearButtonImplementation;
// Set clearTextFieldButton reference to “clearButton” from clearButtonSelector
UIButton *_clearTextFieldButton = clearButtonFunctionPointer(self, clearButtonSelector);
[_clearTextFieldButton setImage:[UIImage imageNamed:#"icon_remove"] forState:UIControlStateNormal];
self.hasClearButtonAsRightView = YES;
now how to convert this to Swift?
or any ideas to workaround it?
You can add a custom button as right view of the UITextField like this
class CustomTextField : UITextField
{
override init(frame: CGRect) {
super.init(frame: frame)
let clearButton = UIButton(frame: CGRect(origin: .zero, size: CGSize(width: 15, height: 15))
clearButton.setImage(UIImage(named: "clear.png")!, forState: UIControlState.Normal)
self.rightView = clearButton
clearButton.addTarget(self, action: "clearClicked:", forControlEvents: .touchUpInside)
self.clearButtonMode = .never
self.rightViewMode = .always
}
func clearClicked(sender: UIButton)
{
self.text = ""
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Implementing a custom text field as suggested in the other answers is not a good idea. You should try to use extensions rather than inheritance if at all possible, because with inheritance you are much more likely to need to make major changes to your codebase in response to changes, whereas using extensions you are much more flexible to change.
I strongly suggest that instead of implementing a custom text field, you extend the UITextField class like this:
extension UITextField {
func applyCustomClearButton() {
clearButtonMode = .Never
rightViewMode = .WhileEditing
let clearButton = UIButton(frame: CGRectMake(0, 0, 16, 16))
clearButton.setImage(UIImage(name: "iCFieldClear")!, forState: .Normal)
clearButton.addTarget(self, action: "clearClicked:", forControlEvents: .TouchUpInside)
rightView = clearButton
}
func clearClicked(sender:UIButton) {
text = ""
}
}
Then to use it you just do this:
yourTextField.applyCustomClearButton()
Here is my solution in Swift 3. In addition to the already existing answer, I also made sure that both left and right views of the textfield (i.e. the search magnifier image view and the custom clear button) have a padding to their left/right by overriding leftViewRect() and rightViewRect(). Otherwise, they will stick right on the edges of the textfield.
class CustomTextField: UITextField
{
fileprivate let searchImageLength: CGFloat = 22
fileprivate let cancelButtonLength: CGFloat = 15
fileprivate let padding: CGFloat = 8
override init( frame: CGRect )
{
super.init( frame: frame )
self.customLayout()
}
required init?( coder aDecoder: NSCoder )
{
super.init( coder: aDecoder )
self.customLayout()
}
override func leftViewRect( forBounds bounds: CGRect ) -> CGRect
{
let x = self.padding
let y = ( bounds.size.height - self.searchImageLength ) / 2
let rightBounds = CGRect( x: x, y: y, width: self.searchImageLength, height: self.searchImageLength )
return rightBounds
}
override func rightViewRect( forBounds bounds: CGRect ) -> CGRect
{
let x = bounds.size.width - self.cancelButtonLength - self.padding
let y = ( bounds.size.height - self.cancelButtonLength ) / 2
let rightBounds = CGRect( x: x, y: y, width: self.cancelButtonLength, height: self.cancelButtonLength )
return rightBounds
}
fileprivate func customLayout()
{
// Add search icon on left side
let searchImageView = UIImageView()
searchImageView.contentMode = .scaleAspectFit
let searchIcon = UIImage( named: "search_magnifier" )
searchImageView.image = searchIcon
self.leftView = searchImageView
self.leftViewMode = .always
// Set custom clear button on right side
let clearButton = UIButton()
clearButton.setImage( UIImage( named: "search_cancel" ), for: .normal )
clearButton.contentMode = .scaleAspectFit
clearButton.addTarget( self, action: #selector( self.clearClicked ), for: .touchUpInside )
self.rightView = clearButton
self.clearButtonMode = .never
self.rightViewMode = .whileEditing
}
#objc fileprivate func clearClicked( sender: UIButton )
{
self.text = ""
}
}
with iOS 14, none of the solution were working for me. the clear button was getting wrong offset for different device sizes.
I had the image. if you dont have it, you can download it from SF Symbols. the name is xmark.circle.fill
In the end, I used this
let customClearButton = UIButton.appearance(whenContainedInInstancesOf: [UITextField.self])
customClearButton.setImage(UIImage(named: "icon-x"), for: .normal)
Updated to Swift 5, based on #marmoy answer:
public func addClearAllCustomButton() {
clearButtonMode = .never
rightViewMode = .whileEditing
let clearButton = UIButton(frame: rightViewRect(forBounds: bounds))
clearButton.setImage(UIImage(named: "clearAll"), for: .normal)
clearButton.addTarget(self, action: #selector(didTouchClearAllButton(sender:)), for: .touchUpInside)
rightView = clearButton
}
public func removeClearAllButton() {
rightViewMode = .never
}
#objc func didTouchClearAllButton(sender: UIButton) {
text = ""
}
For rigth padding & listen the clear delegate of textfield
class SearchBoxTextField: UITextField {
override open func awakeFromNib() {
super.awakeFromNib()
self.initialize()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func initialize() {
let clearButton = UIButton(frame: CGRect(x: 0, y: 0, width: 12, height: 12))
clearButton.setImage(UIImage(named: "removeIcon")!, for: .normal)
let clearView = UIView(frame: CGRect(x: 0, y: 0, width: 22, height: 12))
clearView.addSubview(clearButton)
self.rightView = clearView
clearButton.addTarget(self, action: #selector(clearClicked), for: .touchUpInside)
self.clearButtonMode = .never
self.rightViewMode = .whileEditing
}
#objc func clearClicked(sender:UIButton) {
self.text = ""
_ = self.delegate?.textFieldShouldClear?(self)
}
}

Resources