Not long ago, the company decided to start mixed development with Swift in an old Objective-C project. During, a Crash related to the Swift class construction process was encountered. In the process of solving the problem, I have a deeper understanding of the Swift construction process, so I write this record, hoping to be helpful to the students who just entered the pit Swift development.
Crash review
The BaseiewController and AViewController classes are defined as follows:
// BaseViewController.h #import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface BaseViewController : UIViewController - (instancetype)initWithParamenterA:(NSInteger)parameterA; @end NS_ASSUME_NONNULL_END // BaseViewController.m #import "BaseViewController.h" @interface BaseViewController () @property (nonatomic, assign) NSInteger parameterA; @end @implementation BaseViewController - (instancetype)initWithParamenterA:(NSInteger)parameterA { self = [super init]; if (self) { self.parameterA = parameterA; } return self; } @endCopy the code
The above code defines the Objective-C class BaseViewController and customizes the constructor initWithParamenterA.
// AViewController.swift
import UIKit
class AViewController: BaseViewController {
let count: Int
init(count: Int, parameterA: Int) {
self.count = count
super.init(paramenterA: parameterA)
}
// Where does initCoder come from
required init? (coder aDecoder:NSCoder) {
fatalError("init(coder:) has not been implemented")}}Copy the code
The second block defines the Swift class AViewController, which inherits from BaseViewController, with a custom constructor init(Count: Int, parameterA: Int), which also calls the parent’s initWithParamenterA constructor. If you’re careful, you might notice that there’s init in the code, right? (Coder aDecoder: NSCoder) constructor, which is explained in more detail in the section where initCoder comes from.
So much code. Build run project, go to AViewController page, surprise, Crash. Console output:
`Fatal error: Use of unimplemented initializer 'init(nibName:bundle:)' for class 'XXX.AViewController'`
Copy the code
The AViewController did not implement init(nibName:bundle:), which caused the Crash.
For those of you who just entered the pit Swift, you may be a little confused. In Objective-C, there’s nothing wrong with that, but how did I Crash when I went to Swift?
Review of the construction process for Swift class types
If you want to understand the cause of the Crash, you need to know something about the class constructor that UIViewController belongs to.
Note: Most of this section is excerpted from Swift’s official Chinese tutorial.
Specify the constructor and convenience constructor
Swift provides two constructors for class types, the specified constructor and the convenience constructor.
Classes tend to have very few specified constructors, and it is common for a class to have only one. Every class must have at least one specified constructor. Specify the constructor syntax as follows:
init(parameters) {
statements
}
Copy the code
The convenience constructor is a secondary, helper constructor of a class. You can define convenience constructors to call specified constructors in the same class and provide default values for some parameters. Convenience constructors are typically provided for classes only when necessary.
The convenience constructor is written in the same style, but the init keyword is preceded by the convenience keyword and separated by a space:
convenience init(parameters) {
statements
}
Copy the code
Constructor proxy for class type
Rule 1
The specified constructor must call the specified constructor of its immediate parent.
Rule 2
Convenience constructors must call other constructors defined in the class.
Rule 3
The convenience constructor must finally call the specified constructor.
An easier way to remember is:
- Specifies that the constructor must always be propped up
- Convenience constructors must always be horizontal proxies
These rules can be illustrated by the following legend:
Inheritance and rewriting of class types
Unlike subclasses in Objective-C, subclasses in Swift do not inherit the constructor of their parent class by default. This mechanism in Swift prevents a simple constructor of a parent class from being inherited by a more refined subclass without being fully or incorrectly initialized when used to create a new instance of the subclass.
Automatic inheritance of constructors
As mentioned above, subclasses do not inherit the constructor of their parent class by default. However, the parent class constructor can be automatically inherited if certain conditions are met. In fact, this means that for many common scenarios you don’t have to override the parent class’s constructor, and you can inherit the parent class’s constructor with minimal cost while being safe.
Assuming you provide default values for all new attributes introduced in the subclass, the following two rules apply:
Rule 1
If a subclass does not define any specified constructors, it automatically inherits all specified constructors from its parent class. (Conversely, if the specified constructor is defined, the specified constructor of the parent class is not inherited.)
Rule 2
If a subclass provides implementations of all of the parent specified constructors — whether inherited through rule 1 or provided with a custom implementation — it automatically inherits all of the parent’s convenience constructors.
Even if you add more convenience constructors to your subclasses, these two rules still apply.
Pay attention to
A subclass may implement the specified constructor of its parent as a convenience constructor to satisfy Rule 2.
The specified constructor for UIViewController
UIViewController defines two specified constructors in Swift.
When you create a UIViewController using a StoryBoard, you end up calling:
init? (coder:NSCoder)
Copy the code
When you create something other than a StoryBoard, including code, Xib creation, you end up calling:
init(nibName nibNameOrNil: String? , bundle nibBundleOrNil:Bundle?).Copy the code
Analysis and solution
Now that we’re done with the Swift class type constructor, let’s look at the Swift class AViewController. AViewController defines a specified constructor init(count: Int, parameterA: Int), so according to rule 1 of constructor automatic inheritance, AViewController does not automatically inherit the specified constructors of its parent class, including init(nibName:bundle:). So AViewController does not implement init(nibName:bundle:).
Second, BaseViewController is an Objective-C class, so it doesn’t follow the rules of the Swift constructor. We can see that in the specified constructor of BaseViewController, initWithParamenterA, [super init] is called. This method is not the specified constructor of its parent class, but the compiler does not report an error if it writes this way.
@implementation BaseViewController - (instancetype)initWithParamenterA:(NSInteger)parameterA {// in objective-c, The specified constructor of a subclass does not need to force a call to the specified constructor of the parent class. // call init, compiler allows self = [super init]; if (self) { self.parameterA = parameterA; } return self; } @endCopy the code
And in the construction of an AViewController, The [super init] code in the specified constructor of BaseViewController will eventually call init(nibName:bundle:), which is not implemented by the current class (AViewController), causing the Crash. This corresponds to the console output:
Fatal error: Use of unimplemented initializer 'init(nibName:bundle:)' for class 'XXX.AViewController'
Copy the code
Let’s briefly summarize the reasons for Crash:
- A subclass
AViewController
The specified constructor is custom, but does not implement the specified constructor for the parent classinit(nibName:bundle:)
- The parent class
BaseViewController
Is directly called in the constructor of[super init]
, resulting in the final callAViewController
unrealizedinit(nibName:bundle:)
Crash.
In other words, if the subclass AViewController does not have a custom specified constructor or if the BaseViewController superclass follows rule 1 of the constructor agent of the class type, Crash will not occur.
Accordingly, a solution is in sight:
Method 1: Define a SwiftBaseViewController instead of BaseViewController, which specifies that the constructor does not allow calls to super.init, thus avoiding Crash:
import UIKit
class SwiftBaseViewController: UIViewController {
let parameterA: Int
init(parameterA: Int) {
self.parameterA = parameterA
// call super.init(), compilation failed
// Must call a designated Initializer of the superclass 'UIViewController'
// super.init()
// The specified constructor of the parent class must be called
super.init(nibName: nil, bundle: nil)}required init? (coder:NSCoder) {
fatalError("init(coder:) has not been implemented")}}Copy the code
The advantage of this approach is that it prevents calls to super.init directly from the compiler level, avoiding the possibility of programmer error.
The downside of this approach, however, is that you need to change the BaseViewController programming language. Migration costs are high.
Method 2: Modify the constructor implementation of BaseViewController by replacing self = [super init] with self = [super initWithNibName:nil bundle:nil].
@implementation BaseViewController
- (instancetype)initWithParamenterA:(NSInteger)parameterA {
//self = [super init];
self = [super initWithNibName:nil bundle:nil];
if (self) {
self.parameterA = parameterA;
}
return self;
}
@end
Copy the code
This approach forces the Objective-C BaseViewController class to follow the rules of the Swift constructor by calling the specified constructor of the parent class.
Method 3: Subclass AViewController
class AViewController: BaseViewController {
var count: Int = 0
// Use the convenience constructor
convenience init(count: Int, parameterA: Int) {
self.init(paramenterA: parameterA)
self.count = count}}Copy the code
The convenience constructor is used instead of the specified constructor. According to constructor automatic inheritance rule 1, the AViewController automatically inherits all the specified constructors of the parent class, including init(nibName:bundle:). The disadvantage of this approach is that the original constant attribute count needs to be changed to a variable and given a default value.
Where does initCoder come from
In a UIViewController subclass of Swift, if you specify a constructor, you have to implement constructor init? Coder aDecoder: NSCoder), why is that?
We can look at the UIViewController interface file, which follows the NSCoding protocol:
class UIViewController : NSCoding.Copy the code
Let’s look at the NSCoding protocol again:
protocol NSCoding {
func encode(with coder: NSCoder)
init? (coder:NSCoder) // NS_DESIGNATED_INITIALIZER
}
Copy the code
It defines a constructor that specifies init? (coder: NSCoder). This constructor is also a required constructor because of the protocol compliance.
Essential constructor
Adding the required modifier to the constructor of a class indicates that all subclasses of the class must implement the constructor.
According to constructor automatic inheritance rule 1, if a subclass defines the specified constructor, then it cannot inherit the specified constructor of its parent class. Coder: NSCoder is also a necessary constructor, so you must implement this method in a subclass.
So, this is an awkward situation. Storyboards are clearly not used in the project. However, it is very redundant to add this code every time:
required init? (coder aDecoder:NSCoder) {
fatalError("init(coder:) has not been implemented")}Copy the code
So is there any way to avoid repeating this code?
The answer is yes! The BaseViewController method declares that the method is unavailable, so all subclasses that inherit from BaseViewController don’t need to implement the method.
Swift version:
@available(*, unavailable, message: "Unsupported init(coder:)")
required init? (coder aDecoder:NSCoder) {
fatalError("init(coder:) has not been implemented")}Copy the code
Objective – C version:
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
Copy the code
Swift constructor knowledge pickup
In addition to the constructors mentioned above, there are a few other important points to cover here.
Default constructor
If a structure or class provides default values for all attributes and does not provide any custom constructors, Swift provides a default constructor for those structures or classes. This default constructor will simply create an instance where all property values are set to their default values.
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem(a)Copy the code
One-by-one constructor
If you’ve ever followed Swift, you’ve heard a lot about the difference between classes and structures. For those of you who are used to using classes, here’s one more reason to use structs.
As mentioned in the official documentation, constructs that do not define any custom constructors will automatically get memberwise Initializer. Unlike default constructors, structs can get member-by-member constructors even if stored properties have no default values.
struct Size {
var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)
// Swift 5.1 will even generate one-by-one constructors for you without the default value attributes. The omitted attributes will use the default values
let zeroByTwo = Size(height: 2.0)
let twoByZero = Size(width: 2.0)
Copy the code
In some scenarios, if you really need to customize a constructor, but you want to keep the member-by-member constructor, customize the constructor in Extension.
For classes, however, all constructors must be implemented themselves. So from the point of view of convenience, structure is undoubtedly a better choice.
Failable constructor
In Swift you can define a class, structure, or enumeration whose constructor can fail. By “failure,” I mean, for example, passing an invalid parameter to the constructor, missing a required external resource, or not meeting a required condition.
To deal with the possibility of failure during construction. You can add one or more failable constructors to the definition of a class, structure, or enumerated type. The syntax is to add a question mark (init?) after the init keyword. . For example, Int has the following failable constructor:
init? (exactly source:Float)
Copy the code
Recommended reading
For a more thorough and in-depth look at the Swift construction process, please read the following English and Chinese tutorials:
Swift Official Tutorial
Swift official Chinese tutorial