Reputation: 1430
I want to create a reusable button all over my app and was planning to design it with it's own .xib
file. The issue is that I can't connect an IBAction
to the custom button in the controllers where it's used.
I created a new .xib
file called SampleButton.xib
and added a button. This is what the hierarchy and the view looks like:
I then created a new swift file called SampleButton.swift
with a class called SampleButton
that's a subclass of UIButton
and assigned it as the File's Owner in my SampleButton.xib
file.
The contents of SampleButton.swift
are as follows:
import Foundation
import UIKit
@IBDesignable
class SampleButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
guard let view = loadViewFromNib() as? UIButton else {
return
}
view.frame = bounds
view.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth,
UIView.AutoresizingMask.flexibleHeight]
addSubview(view)
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.white.cgColor
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIButton
}
@IBAction func pressed(_ sender: Any) {
print("Called in here")
}
}
I can then create a new button in my storyboard and set it to custom and the class to SampleButton
. However now if I ctrl + drag from my button to my corresponding View Controller to create an IBAction
for the button, it's not called. The one in the SampleButton.swift
file is. Even if I delete the IBAction
in the SampleButton
file it's still not called.
Any help here? I want to be able to design the buttons separately and then have IBactions
for them in the controllers where they're used.
Upvotes: 1
Views: 4810
Reputation: 121
try following code:
override func viewDidLoad() {
super.viewDidLoad()
let myButton = Bundle.main.loadNibNamed("myButtonxibName", owner: self, options: nil)?[0] as? myButtonxibClassName
myButton.button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
self.view.addsubview(myButton)
}
@objc func buttonTapped() {}
Upvotes: 1
Reputation: 7113
I don't think it's possible to do this. Simpler way is to just set the target and action in view controllers. Something like:
class VC: UIViewController {
func viewDidLoad() {
sampleButton.addTarget(self, action: #selector(didClickOnSampleButton))
}
}
Upvotes: 0
Reputation: 4444
I encountered this same issue with some of my custom xib views and my initial thought was that I could set up my xib to be IBDesignable and then connect outlets from the storyboard rendering of my button in the view controller.
That didn't work.
So I setup a bit of a workaround using delegate callbacks from my custom views. I created IBOutlets for the view to the view controllers using them, then in viewDidLoad
I'd set the delegate and handle the button tap in the view controller
import UIKit
// defines a callback protocol for the SampleButtonView
protocol SampleButtonViewDelegate: class {
func sampleButtonTapped(_ button: SampleButton)
}
@IBDesignable
class SampleButton: UIView, NibLoadable {
// create IBOutlet to button if you want to register a target/action directly
@IBOutlet var button: UIButton!
// set delegate if you want to handle button taps via delegate
weak var delegate: SampleButtonViewDelegate?
// initializers to make it so this class renders in view controllers
// when using IBDesignable
convenience init() {
self.init(frame: .zero)
}
override init(frame: CGRect) {
super.init(frame: frame)
loadFromNib(owner: self)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadFromNib(owner: self)
}
@IBAction func buttonTapped(_ sender: Any) {
delegate?.sampleButtonTapped(_ button: self)
}
}
// here's a sample ViewController using this view and the delegate callback method
class ViewController: UIViewController {
@IBOutlet var sampleButtonView: SampleButton!
override func viewDidLoad() {
super.viewDidLoad()
sampleButtonView.delegate = self
}
}
extension ViewController: SampleButtonViewDelegate {
func sampleButtonTapped(_ button: SampleButton) {
// TODO: run logic for button tap here
}
}
For completeness I'll also add this NibLoadable protocol definition here.
// I used this for the @IBDesignable functionality to work and actually render
// my xib layouts in the storyboard view controller layouts using this class
import UIKit
/// Defines an interface for UIViews defined in .xib files.
public protocol NibLoadable {
// the name of the associated nib file
static var nibName: String { get }
// loads the view from the nib
func loadFromNib(owner: Any?)
}
public extension NibLoadable where Self: UIView {
/// Specifies the name of the associated .xib file.
/// Defaults to the name of the class implementing this protocol.
/// Provide an override in your custom class if your .xib file has a different name than it's associated class.
static var nibName: String {
return String(describing: Self.self)
}
/// Provides an instance of the UINib for the conforming class.
/// Uses the bundle for the conforming class and generates the UINib using the name of the .xib file specified in the nibName property.
static var nib: UINib {
let bundle = Bundle(for: Self.self)
return UINib(nibName: Self.nibName, bundle: bundle)
}
/// Tries to instantiate the UIView class from the .xib file associated with the UIView subclass conforming to this protocol using the owner specified in the function call.
/// The xib views frame is set to the size of the parent classes view and constraints are set to make the xib view the same size as the parent view. The loaded xib view is then added as a subview.
/// This should be called from the UIView's initializers "init(frame: CGRect)" for instantiation in code, and "init?(coder aDecoder: NSCoder)" for use in storyboards.
///
/// - Parameter owner: The file owner. Is usually an instance of the class associated with the .xib.
func loadFromNib(owner: Any? = nil) {
guard let view = Self.nib.instantiate(withOwner: owner, options: nil).first as? UIView else {
fatalError("Error loading \(Self.nibName) from nib")
}
view.frame = self.bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
}
}
You could also simply register the functions you defined in your view controller as the target/action functions for the button in the custom view.
override func viewDidLoad() {
super.viewDidLoad()
mySampleButtonView.button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
@objc func buttonTapped(_ sender: UIButton) {
// handle button tap action in view controller here...
}
Upvotes: 3
Reputation: 866
You don't need a Xib for what you're trying to do. Remove the loadViewFromNib()
and the pressed(_ sender: Any)
functions from your class above. Change your setup()
method to customize your button. I see that you want to add a border to it. Do something like this:
func setup() {
self.layer.borderWidth = 2
self.layer.borderColor = UIColor.white.cgColor
// * Any other UI customization you want to do can be done here * //
}
In your storyboard, drag and drop a regular UIButton
wherever you want to use it, set the class in the attributes inspector to SampleButton
, connect your IBOutlet
and IBAction
s as necessary, and it should be good to go.
Upvotes: 0