When inheriting from UIControl my app crashes as soon as I add a property to my UIControl Class and a hit test is performed (EXC_BAD_ACCESS) => E.g. mouse over the Control:
class ReloadButton: UIControl {
var stopRotating: Bool = true
init(coder aDecoder: NSCoder!)
{
super.init(coder: aDecoder)
}
init(frame aRect: CGRect){
super.init(frame: aRect)
}
}
If I remove the property stopRotating it won't crash. If I change the inheritance to UIButton instead of UIControl the crash won't happen.
Is there a specific function which needs to be added to UIControl to handle hit tests?
UPDATE: I created a minimalistic sample project on github: https://github.com/Aranir/hit_test
With the beta 5 Xcode the error messages have become more explicit.
Apparently the method
override init(){
super.init()
}
needed to be implemented for it to work. This is now also necessary if the class inherits from UIButton
Related
I want to add a tab recognizer to a custom .xib file view in my ios swift app. Here's the code from the owner class of the .xib file:
import UIKit
class WordLabel: UILabel {
#IBOutlet weak var wordLabel: UILabel!
#IBOutlet var wordFrame: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
//I added the tab recognizer here
wordLabel.isUserInteractionEnabled = true
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(labelClicked(_:)))
wordLabel.addGestureRecognizer(gestureRecognizer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
Bundle.main.loadNibNamed(K.wordLabelNibName, owner: self, options: nil)
addSubview(wordFrame)
wordFrame.frame = self.bounds
wordFrame.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
//should happen when label is tapped
#objc func labelClicked(_ sender: Any) {
print("UILabel clicked")
}
}
When I ran the project on a phone simulator, there were no errors.
App Running On The Simulator
But when I clicked the labels that showed up, the message was not printed onto the console (meaning that the action was not triggered). What am I doing wrong?
Thank you for your help.
github link to my project:
https://github.com/ShaungYi/PolyLibrum/blob/main/PolyLibrum/View/BookReader/WordLabel/WordLabel.swift
When you use WordLabel custom UILabel with xib, init(frame:) constructor is not called. init(coder:) is called instead. Because of that, your gesture recognizer doesn't work. You must to move the gesture recognizer assignment to commonInit() method.
I solved the problem on my own. It seems that I set the owner class of the .xib file to inherit from a UILabel. When I changed the superclass to the generic UIView, everything worked out perfectly! I don't really understand how this fixed the problem, but my theory is that previously, the owner class relegated the UIGesture event to the UILabel class instead of itself, thus not triggering the bound gesture recognizer. Now That I diverted that to the owner class, there's no such problem.
So for anyone else who has my problem- set the owner class' superclass to the generic UIView if possible?
Thanks again to Furkan Kaplan for his help.
When I create a subclass of UIView or UIViewController with a stored property, Xcode will not compile my project unless I include an implementation of required init?(coder aDecoder: NSCoder). Currently, I have the following implementation to shut the compiler up:
required init?(coder aDecoder: NSCoder) {
fatalError()
}
I understand why I'm required to include this initializer; my subclass needs to conform to the NSCoding protocol because its superclass conforms to it, and this initializer is part of the NSCoding protocol so it needs to work with my class, i.e. initialize all of my class's stored properties (which the superclass version of the initializer won't do).
I imagine that a correct implementation would look something like this:
class MyView: UIView {
let label: UILabel
override init(frame: CGRect) {
label = UILabel()
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
if let label = aDecoder.decodeObject() as? UILabel {
self.label = label
} else {
return nil
}
super.init(coder: aDecoder)
}
override func encode(with aCoder: NSCoder) {
aCoder.encode(label)
super.encode(with: aCoder)
}
}
However, considering that my application has over 50 custom views and view controllers, correctly implementing this function in every custom view and view controller is a lot of work.
So, I'm wondering if it's necessary to implement this initializer correctly, or if I can just leave it throwing a fatal error. In other words, will this initializer ever be called if I don't call it in my own code? I think I read that it might be called by a Storyboard, but my app doesn't use any Storyboards.
This initialiser will be called if an instance of your view is used in a storyboard scene.
It is up to you whether to create a functioning initialiser or not, but it should mostly be a matter of copying code from init(frame:)
It provides an NSCoder instance as a parameter, which you need only if you are using iOS serialization APIs. This is not used often, so you can ignore it. If you are curious to learn, serialisation converts an object in a byte stream that you can save on disk or send over the network.
During the initalization of a view controller, you usually allocate the resources that the view controller will need during its lifetime. So, this include model objects or other auxiliary controllers, like network controllers.
I have a subclass of UITextView, which needs to have a specific default appearance. So far, I've been able to achieve this by overriding the initialize() class function, which has been deprecated in Swift 3.1.
public class CustomTextView : UITextView {
override public class func initialize() {
self.appearance().backgroundColor = .green
}
}
Is there a way to achieve the same thing pure Swift?
I'm working around the loss of the class method initialize by using a construct like this:
class CustomTextView : UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame:frame, textContainer: textContainer)
CustomTextView.doInitialize
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
CustomTextView.doInitialize
}
static let doInitialize : Void = {
CustomTextView.appearance().backgroundColor = .green
}()
}
This construct has the advantage that doInitialize will be initialized only once, so the code connected with it will run only once (in this case we'll configure the appearance proxy only once); and it is early enough to affect even the first instance created (that is, every CustomTextView you ever make will in fact be green).
Question
In cases where I know init?(coder:) and other storyboard/nib specific initializers will not be called, can I avoid the requirement to implement or call them in UI subclasses?
Background
Many UIKit classes including UIViewController, UIView and subclasses of UIControl (UIButton, UITextField, etc) adopt the NSCoding protocol. The NSCoding init?(coder:) method is used to instantiate these classes from a storyboard or nib.
NSCoding protocol:
public protocol NSCoding {
func encode(with aCoder: NSCoder)
init?(coder aDecoder: NSCoder)
}
Classes that adopt a protocol with an initializer must implement that initializer as required. Required initializers must be implemented by all subclasses.
I often build iOS apps without storyboards or nibs. I implement UIViewController, UIView and UIControl subclasses completely in code. Yet, I must implement init?(coder:) in subclasses to appease the compiler if I want to provide my own init methods (which I often do). The following examples illustrate this.
The following does not compile
class CustomView: UIView {
init() {
super.init(frame: CGRect.zero)
}
}
// Error:'required' initializer 'init(coder:)' must be provided by subclass of 'UIView'
The following does compile because I provided an implementation of init?(coder:). For code-only UI subclasses, I typically implement 'init(coder:)' by throwing a fatal error to assert that I don't expect it to be called.
class CustomView: UIView {
init() {
super.init(frame: CGRect.zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("NSCoding not supported")
}
}
Subclasses of CustomView also need to implement 'init(coder:)' for reasons stated above.
class SubClassOfCustomView: CustomView {
let someProperty: Int
init(someProperty: Int) {
self.someProperty = someProperty
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("NSCoding not supported")
}
}
UI Subclasses & #available(*, unavailable)
*The code below was written and tested in Swift 3
The crux of this solution is creating base subclasses that your custom UI subclasses inherit from. In the examples below, these subclasses are named BaseViewController, BaseView and BaseButton. These subclasses include an initializer that defaults arguments of the superclass's designated initializer that are hidden from their subclasses.
init(coder:) must be implemented in all subclasses since it is a required initializer of the UI superclasses. You can get around this by placing the attribute #available(*, unavailable) above an implementation of that initializer.
The available attribute is used "to indicate the declaration’s lifecycle relative to certain platforms and operating system versions". Using this attribute with the following arguments: #available(*, unavailable) makes the block of code that follows unavailable on all versions of all platforms.
UIViewController
class BaseViewController: UIViewController {
// This initializer hides init(nibName:bundle:) from subclasses
init() {
super.init(nibName: nil, bundle: nil)
}
#available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("NSCoding not supported")
}
}
class CustomViewController: BaseViewController {
let someProperty: Int
init(someProperty: Int) {
self.someProperty = someProperty
super.init()
}
}
let customViewController = CustomViewController(someProperty: 1)
UIView
class BaseView: UIView {
// This initializer hides init(frame:) from subclasses
init() {
super.init(frame: CGRect.zero)
}
#available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("NSCoding not supported")
}
}
class CustomView: BaseView {
let someProperty: Int
init(someProperty: Int) {
self.someProperty = someProperty
super.init()
}
}
let customView = CustomView(someProperty: 1)
UIButton - UIControl Subclass
This UIButton example illustrates how to subclass UIControl subclasses.
internal class BaseButton: UIButton {
// This initializer hides init(frame:) from subclasses
init() {
super.init(frame: CGRect.zero)
}
#available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("NSCoding not supported")
}
}
class CustomButton: BaseButton {
let someProperty: Int
init(someProperty: Int) {
self.someProperty = someProperty
super.init()
}
}
let customButton = CustomButton(someProperty: 1)
Considerations
I don't advise regularly using #available(*, unavailable) to avoid implementing required initializers. It's beneficial in reducing redundant code that will not be called in this case (since you don't plan to use storyboard/nibs). The appearance of #available(*, unavailable) is reduced by using it in the base classes (and having custom subclasses inherit from the base classes) as opposed to in every custom subclass.
I'm aware that this works in Swift 2 and 3. Its possible that future versions of Swift will not allow this. However, I hope the Swift team comes up with a better way to avoid this redundant code in custom UI subclasses.
Out of curiosity, I tried initiating a BaseViewController subclass from a storyboard. I expected the app to crash with a selector not found error but it called the init?(coder) method even though it was hidden from all platforms. This may be because the available attribute does not hide the init?(coder) initializer from Objective C and that is where storyboard instantiation code runs.
UIKit often makes use use of classes and inheritance whereas the Swift community encourages structs and protocol oriented programming. I include the following headers above my base UI class declarations to discourage base UI classes from becoming a dumping ground for global settings and functionality.
/**
* This base UIViewController subclass removes the requirement to override init(coder:) and hides init(nibName:bundle:) from subclasses.
* It is not intended to create global functionality inherited by all UIViewControllers.
* Alternatively, functionality can be added to UIViewController's via composition and/or protocol oriented programming.
*/
/**
* This base UIView subclass removes the requirement to override init(coder:) and hides init(frame:) from subclasses.
* It is not intended to create global functionality inherited by all UIViews.
* Alternatively, functionality can be added to UIView's via composition and/or protocol oriented programming.
*/
Reference: I found the Initialization section of the Swift Language Guide helpful in understanding the rules for Initializers.
I come from Obj-C and I'm struggling on doing something super basic in Swift!
I have a custom UICollectionViewCell:
class CustomCell: UICollectionViewCell
{
// Outlets
// ***************************
#IBOutlet weak var button: UIButton!
// Init
// ***************************
required init?(coder aDecoder: NSCoder)
{
super.init(coder:aDecoder)
setup()
}
override init(frame: CGRect)
{
super.init(frame: frame)
setup()
}
func setup()
{
button.backgroundColor = .white
}
}
The cell is loaded from an external .xib file, so init(coder:) is called for the initialization but my button is not ready.
If I change to button?.backgroundColor the app doesn't crash but obviously nothing happen.
I can call my setup() function in layoutSubviews() and it works, but it's definitely not the right place to be.
How do I solve this massive problem? lol
Edit
Probably I have to call setup() from awakeFromNib(), right?
I usually don't use external .xib, I'm not familiar with them
Edit: Sorry It seems youe edited your question before my answer, it seems as you load it from XIB, then you can run the awakeFromNib which will be called when you register a nib using this method:
Apple Source UICollectionView
Apple Source UITableView
--- old post below ---
In Xcode 6 you have to provide additional init(coder:) initializer in
classes like RDCell, which is the subclass of UICollectionViewCell.
This initializer is called instead of init(frame:) when the class gets
initialized from a storyboard or a xib file. That’s not our case, but
we still need to provide init(coder:). We can use the solution
provided to us by Xcode. In Issue Navigator click on an error that
says “'required' initializer 'init(coder:)' must be provided by
subclass of 'UICollectionViewCell'“,
Source