add to init function on UIButton extension - ios

I want to add a custom init to the UIButton class using an extension
extension UIButton {
init() {
backgroundColor = InterfaceDesign.button.backgroundColor
setTitleColor(InterfaceDesign.button.textColor, for: .normal)
}
}
The first problem is this error:
Designated initializer cannot be declared in an extension of 'UIButton'; did you mean this to be a convenience initializer?
Then if I click the red white dot to fix it, it inserts a convenience in front like so:
convenience init()
But then this error appers:
Initializer 'init()' with Objective-C selector 'init' conflicts with implicit initializer 'init()' with the same Objective-C selector
I already tried googling it but only found working code for subclasses of uibutton...
Thanks for your help

You can't do what you are trying to do. There are 2 interacting reasons for this.
You can't change a class' designated initializer from an extension.
You can create a convenience initializer in an extension, but a convenience initializer can't be an override of an existing initializer.
You are trying to redefine init(). You can't do that. You would need to create a convenience initializer that has different parameters than any existing initializer, and that convenience initializer needs to ultimately call the designated initializer.

struct Dummy { }
extension UIButton {
convenience init(dummy: Dummy) {
self.init()
backgroundColor = InterfaceDesign.button.backgroundColor
setTitleColor(InterfaceDesign.button.textColor, for: .normal)
}
}
UIButton(dummy: Dummy())
Even though the above code formally answers the question, consider doing this instead:
extension UIButton {
static var dummy: UIButton {
let button = UIButton()
button.backgroundColor = InterfaceDesign.button.backgroundColor
button.setTitleColor(InterfaceDesign.button.textColor, for: .normal)
return button
}
}
UIButton.dummy

Related

How to subclass a UIButton with its primaryAction initializer

We can initialize a new UIButton this way:
// myAction is a var of type UIAction
let btn = UIButton(primaryAction: myAction)
So I'm thinking cool, I can subclass this and stop using target-action. But here's the problem, Xcode can't seem to recognize the primaryAction initializer.
class Button: UIButton {
init(type: String, action: UIAction) {
super.init(primaryAction: action) // Error "Must call a designated initializer of the superclass 'UIButton'"
}
}
So it's not a designated initializer. How can I access this primaryAction property inside a UIButton subclass?
Because you can't initialize a superclass within a custom initializer with one of its convenience initializers, you have to perform the tasks of that convenience init manually. So after you've initialized the superclass with one of its designated inits:
class Button: UIButton {
init(type: String, action: UIAction) {
super.init(frame: .zero)
// register the action for the primaryActionTriggered control event
// and set the title and image properties to the action’s title and image
}
}
And it's not as straightforward as changing the value of a single property.
You set up a control so that it sends an action message to a target
object by associating both target and action with one or more control
events. To do this, send addTarget(_:action:for:): to the control for
each Target-Action pair you want to specify.
https://developer.apple.com/documentation/uikit/uicontrol/event
I figured it out. We can use addAction instead of addTarget. Requires iOS 14+.
func addAction(_ action: UIAction, for controlEvents: UIControl.Event) is available on all subclass of UIControl. Meaning we no longer have to scatter targets and #objc func all over our code. Just add a UIAction to your control.
class Button: UIButton {
init(type: String, action: UIAction) {
super.init(frame: .zero)
addAction(action, for: .touchUpInside)
}
}

Re-use animation throughout iOS views

Not sure if my thinking here is correct but I have similar animations I use throughout my iOS project and I would like to condense it to 1 file and reuse wherever I want.
A brief example. In my animations file I have a scale animation
Animations.swift
class Animations {
class func scaleSmall(_ view: UIView) {
let scaleAnim = POPBasicAnimation(propertyNamed: kPOPLayerScaleXY)
scaleAnim?.toValue = NSValue(cgSize: CGSize(width: 0.9, height: 0.9))
view.layer.pop_add(scaleAnim, forKey: "scaleSmallAnim")
}
}
Here I have one of my many swift files in my View folder and I would like to add that animation to the button
Button.swift
class Button: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
self.addTarget(self, action: #selector(Animations.scaleSmall(_:)), for: .touchDown)
}
}
I thought I would be able to reference the animation from an additional file however everytime I do it this way I get the same error
Argument of '#selector' refers to instance method 'scaleSmall' that is not exposed to Objective-C
Am I referencing this function wrong?
try changing class func scaleSmall(_ view: UIView) {
to
#objc class func scaleSmall(view: UIView) {
I've confirmed my comment, so I'm posting an answer. Methods for UIButton need to be bridged to Obj-C. That's what #Kostas Tsoleridis suggests with his answer as well - it is not mixing two languages in one file, you are just marking the method for the compiler. Other solution would be to inherit from NSObject by your Animations class.
Now, as your confusion mentioned in a comment - it worked, because your Button class inherits from UIButton which is both from Obj-C world, and also inherits from NSObject down the chain.
To also address the issue mentioned in a comment under #Kostas Tsoleridis answer (and to be honest I should have thought about it before) - you can't pass self as a target and use a method from another class (even a static one). To solve this, you can use a singleton instance of your Animations class, something like this :
class Animations {
static let sharedInstance = Animations()
#objc class func scaleSmall(_ view: UIView) {
// your code
}
}
let button = UIButton()
button.addTarget(Animations.sharedInstance, action: #selector(Animations.scaleSmall(_:)), for: .touchDown)

How come I can initialize a UIView without parameters, but its documentation does not have an empty initializer?

let view = UIView()
Why does this compile without an error when the only documented UIView initializer is init(frame: CGRect)?
Specifically, I am trying to write a new class that inherits from UIView, but this code throws an error:
class SquadHorizontalScrollViewCell: UIView {
init(var: FIRDataSnapshot){
super.init()
....
It says it must call a designated initializer.
UIView inherits from UIResponder which inherits from NSObject.
The NSObject.init() initializer is accessible on UIView, but not on subclasses of NSObject which replace this designated initializer.
Let's consider an example.
class A: NSObject {
init(_ string: String) { }
}
This leads to a compiler error for let a = A() - missing argument #1 for initializer because this initializer replaces the designated init() initializer for NSObject in the subclass.
You just need to define the initializer of the subclass as a convenience initializer:
class A: NSObject {
convenience init(_ string: String) {
self.init() // Convenience initializers must call a designated initializer.
}
}
let a = A() now compiles.
UIView can also compile with other designated initializers defined in the subclass, since its designated initializer is not known at compile time. As per the docs, if instantiating programmatically, init(frame:) is the designated initializer, otherwise init() is the designated initializer. This means that UIView inherits the NSObject designated initializer rather than replacing it as in the above example.
In your example:
class SquadHorizontalScrollViewCell: UIView {
init(var: FIRDataSnapshot){
super.init()
We see that the designated initializer is init(frame: CGRect), so you have to call this designated initializer instead.
A designated initializer should call its superclass designated initializer.
In this case super.init() is the designated initializer of NSObject not UIView.
It would be UIView's responsibility to call UIResponder init ,I guess it has no designated initializer, hence UIView will call Super.init in its init(frame:CGrect) initializer. check "Initializer Delegation"
for why let x = UIView() is ok , its because of this
Unlike subclasses in Objective-C, Swift subclasses do not inherit
their superclass initializers by default. Swift’s approach prevents a
situation in which a simple initializer from a superclass is inherited
by a more specialized subclass and is used to create a new instance of
the subclass that is not fully or correctly initialized. (Apple)
since UIView is objective c class it still can do it. but you won't be able to call SquadHorizontalScrollViewCell() unless you did not provide any initializer or you overrides the designated initializer of the superclass (UIView)
Check this link for more info
For UIView init(frame: CGRect) is default initializer. You must call it after initialize your instance variable. If you take view from NIB then init?(coder aDecoder: NSCoder) is called instead of init(frame: CGRect). So in that case you have to initialize your instance variable in awakeFromNib() method. In this case your code should be like this:
class SquadHorizontalScrollViewCell: UIView {
init(firDataSnapshot: FIRDataSnapshot){
// intialize your instance variable
super.init(frame: CGRectZero) // set your expected frame. For demo I have set `CGRectZero`
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
For more info you can check this link https://developer.apple.com/reference/uikit/uiview
At a certain point your view will need to be init with something, that is why the compilation is complaining, because it cannot find how to start the initialisation of your custom view. Because at the end, a view will be init from a xib (init(coder aDecoder: NSCoder)), or from a frame ( init(frame: CGFrame)). So here, the easiest way is to call super.init(frame: CGRectZero) at least in your custom init method.
init (var: FIRDataSnapshot) {
super.init(frame: CGRectZero)
}
// This method below is always needed when you want to override your init method
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
but you'll still need to set the size of your frame etc.
You'll notice if you create your own UIView subclass and only override init(frame:) with a log statement, then instantiate this new class using just init(), your init(frame:) is actually called with a zero-sized frame. So the designated initializer is still getting called.

UIStackView subclasses written in Swift may crash due to unimplemented init(frame:)

I have written a UIStackView subclass, but I am experiencing a strange run-time problem. Here is some sample code where it can be seen:
class SubclassedStackView: UIStackView {
init(text: String, subtext: String) {
let textlabel = UILabel()
let subtextLabel = UILabel()
textlabel.text = text
subtextLabel.text = subtext
super.init(arrangedSubviews: [textlabel, subtextLabel])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If you then use it such as this:
let stackView = SubclassedStackView(text: "Test", subtext: "Uh-oh!")
You get a runtime exception with the following message:
fatal error: use of unimplemented initializer 'init(frame:)' for class 'test.SubclassedStackView'
A look at the call stack shows that the base initializer -[UIStackView initWithArrangedSubviews:] is attempting to call init(frame: CGRect) on the subclass, which is intentionally left unimplemented.
Of course I could implement this extra initializer, weird as it would be for it to be called by the superclass, but in my real-life case this would force me to change my properties to use optional types (or implicitly unwrapped optionals) where I shouldn't have to do that.
I could also call init(frame:) instead of init(arrangedSubviews:) and subsequently call addArrangedSubview(view:) to add the arranged subviews. The run-time issue would disappear, but I don't wish to provide a frame.
Why does the superclass's initializer call the subclass's initializer? Can anyone suggest a way to work around this issue without introducing optionals?
Edit: Apple acknowledged this bug which should be fixed in iOS 10. http://www.openradar.me/radar?id=4989179939258368 Still applies to iOS 8-9 unfortunately.
I'm not sure if this will work for your needs, but I've managed to circumvent the problem with an extension on UIStackView:
extension UIStackView {
convenience init(text: String, subtext: String) {
let textlabel = UILabel()
let subtextLabel = UILabel()
textlabel.text = text
subtextLabel.text = subtext
self.init(arrangedSubviews: [textlabel, subtextLabel])
}
}
// ...
let sv = UIStackView(text: "", subtext: "") // <UIStackView: 0x7fcd32022c20; frame = (0 0; 0 0); layer = <CATransformLayer: 0x7fcd32030810>>
A look at the call stack shows that the base initialiser -[UIStackView initWithArrangedSubviews:] is attempting to call init(frame: CGRect) on the subclass, which is intentionally left unimplemented.
Why not just add the missing constructor?
override init(frame: CGRect) {
super.init(frame: frame)
}
The problem's coming from the frame initializer not being available since you've provided your own, and it needs that for its internal implementation. If the frame will be managed by AutoLayout anyways you don't need to be concerned with what it's actually set to initially, so you can just let it perform its internal routines necessary to initialize with your subviews. I don't see from the code above why you would need to add optionals..
init(arrangedSubviews: [] is a convenience initializer. As per documentation, you must call the superclass's designated initializer (which is init(frame:)) instead

Best practice to subclass UIKit classes

I'm using Swift, and I find myself having to subclass UIKit classes such as UIView and UIButton. I don't care about setting the frame since I'm using AutoLayout, so I don't want/need to use init(frame: CGRect).
class customSubclass: UIView {
var logo: UIImage
init(logo: UIImage) {
self.logo = logo
//compiler yells at me since super.init() isn't called before return from initializer
//so I end up doing this
super.init(frame: CGRectZero)
self.translatesAutoresizingMaskIntoConstraints = false
}
I also don't find it very sexy to set it's frame to CGRectZero.
Is there a way to having a custom initializer for a subclass of a UIView or UIButton without explicitly setting it's frame?
Note Every subclass is instantiated in code, so required init(coder: aDecoder) is in my code, but isn't actually doing anything.
Thanks!
During initialization of a subclass, you must call the designated initializer of a superclass. In your case, since you are creating these views programmatically, you must use super.init(frame: CGRect). As you mentioned, it would be useful to implement two designated initializers for your subclass, one of which takes in a frame:CGRect argument.
Please see the accepted answer to this question for a more thorough review.
If you're using autolayout in a storyboard or xib, then just override the init(coder:) method so that it calls the superclass's version, and have your convenience initializers pass CGRectZero or some other value in.
Instead of creating a new class and subclassing, you could try using extensions.
extension UIView {
var logo: UIImage = myImage
func myLogo() {
// code here
}
}

Resources