NSCoding required initializer in inherited classes in Swift - ios

I have class Foo which conforms to NSObject and NSCoding which I want to be able to persist with NSKeyedArchiver I want to create class Bar, a subclass of Foo that will also conform to NSObject and NSCoding. I am having a problem understanding how to create the required convenience init?(coder aDecoder: NSCoder) in the subclass.
so class Foo...
class Foo: NSObject, NSCoding {
let identifier:String
init(identifier:String) {
self.identifier = identifier
}
override var description:String {
return "Foo: \(identifier)"
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(identifier, forKey: "identifier")
}
required convenience init?(coder aDecoder: NSCoder) {
guard let identifier = aDecoder.decodeObjectForKey("identifier") as? String
else {
return nil
}
self.init(identifier:identifier)
}
}
Then class Bar ...
class Bar:Foo {
let tag:String
init(identifier:String, tag:String) {
self.tag = tag
super.init(identifier: identifier)
}
override var description:String {
return "Bar: \(identifier) is \(tag)"
}
}
I can get this to compile by adding the following methods on to make this NSCoding compliant
override func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(tag, forKey: "tag")
super.encodeWithCoder(aCoder)
}
this makes sense because I call super.encodeWithCoder(...) reusing the super makes this DRY. The problem I am having is creating the required convenience init?(...) the only way I can seem to get it to compile is by doing this...
required convenience init?(coder aDecoder:NSCoder) {
guard let identifier = aDecoder.decodeObjectForKey("identifier") as? String,
let tag = aDecoder.decodeObjectForKey("tag") as? String
else {
return nil
}
self.init(identifier:identifier, tag:tag)
}
I basically have copied the superclass required initializer and then added the additional decode method for the subclass property. This approach does not seem correct...
Is there a better way to implement this??

Right after you decode and assign all the subclass properties in the required init method, call:
super.init(coder: aDecoder)

Have thought about this for a while and believe that this is the correct way to implement this.
The reason is the way Swift enforces object initialization. Convenience initializers can only call the required initializers on self. Only the required initializer can call the init on super.
Therefore the only way to initialize the subclass object is to decode all of the required initialization parameters before you call the subclass required initializer...which then calls the super class initializer
Here is code you can copy to a playground https://gist.github.com/vorlando/dc29ba98c93eaadbc7b1

I have try your code in playground, it just auto to add the code when I tick the red circle of the Error.
The coding is like your function required convenience init.
sample code:
required convenience init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}

Related

Is it possible to initialize variable depending on another in class inheriting from UIViewController in Swift?

I'm trying to achieve a seemingly easy thing: initialize constant variable using another one from the same class which is inheriting from UIViewController. I had 2 ideas that worked, but have their problems and don't seem to be the best solution.
Idea 1 - problem: isn't constant
class MyViewController: UIViewController {
let db = Firestore.firestore()
let uid: String = UserDefaults.standard.string(forKey: "uid")!
lazy var userDocRef = db.collection("users").document(uid)
}
Idea 2 - problem: isn't constant and is optional
class MyViewController: UIViewController {
let db = Firestore.firestore()
let uid: String = UserDefaults.standard.string(forKey: "uid")!
var userDocRef: DocumentReference?
override func viewDidLoad() {
super.viewDidLoad()
userDocRef = db.collection("users").document(uid)
}
}
I think it should be possible to achieve that by overriding init(). I've tried couple of implementations I found Googling, but everyone I've tried gave me some kind of error. Few examples:
From this answer.
convenience init() {
self.init(nibName:nil, bundle:nil) // error: Argument passed to call that takes no arguments
userDocRef = db.collection(K.Firestore.usersCollection).document(uid)
}
From this article.
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)! // Property 'self.userDocRef' not initialized at super.init call
}
init() {
super.init(nibName: nil, bundle: nil) // Property 'self.userDocRef' not initialized at super.init call
userDocRef = db.collection(K.Firestore.usersCollection).document(uid)
}
I'm guessing I'm either missing something or those are outdated? I'm surprised such a simple task as overriding initializer is such a bother. What is the proper way to do it?
Your second try is quite close. You've really just missed this rule that you have to follow:
all properties declared in your class must be initialised before calling a super.init.
In your second try, userDocRef is not initialised in init(coder:) and initialised after a super.init call in init().
You should write it like this:
init() {
userDocRef = db.collection(K.Firestore.usersCollection).document(uid)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
userDocRef = db.collection(K.Firestore.usersCollection).document(uid)
super.init(coder: coder)
}
I don't think you can get rid of the duplicate code here... The best you can do is to create a static helper method that returns db.collection(K.Firestore.usersCollection).document(uid), which means making db static as well, which might be worse now that I think about it.
Note that anything from the storyboards will be created using init(coder:), so it is important that you do your initialiser there properly as well if you are using storyboards.

Swift error from required convenience for viewcontroller

I'm playing around with having a factory that sets up a login in a view controller to satisfy myself what is going on (this is old code I'm updating) I'm trying to make this as simple an example as possible.
It crashes!
View Controller
init(loginServerClass: String){
super.init(nibName: nil, bundle: nil)
}
//called to initialize the login server class
public init?(coder aDecoder: NSCoder, loginServerClass: String){
super.init(coder: aDecoder)
}
//called from storyboard
required convenience init?(coder aDecoder: NSCoder) {
self.init(coder: aDecoder)
}
using a LoginFactory
public protocol LoginFactoryProtocol{
static func createLogin () -> String
}
class LoginFactory : LoginFactoryProtocol {
static public func createLogin () -> String {
return "testlogintype"
}
}
So
1). It crashes with the minimal example above (bad access)
2). It crashes when I add my prefered convenience int as below:
required convenience init?(coder aDecoder: NSCoder) {
self.init(coder: aDecoder, loginServerClass: LoginFactory.createLogin() )
}
(This coder requires that replaced objects be returned from initWithCoder:)
How can I compile this minimum code?
Github link https://github.com/stevencurtis/initissue
If downvoters could explain why, that would be appreciated as I can't improve without knowing what needs to be added. I already added the error messages, a Github link with the problem. I've listed what I've tried, and I'm going from existing previously working code to a small example (therefore I HAVE tried things, the documentation is https://docs.swift.org/swift-book/LanguageGuide/Initialization.html#//apple_ref/doc/uid/TP40014097-CH18-XID_324 and similar questions on stack overflow have not helped (they seem to have the same issues i.e. (NSGenericException: This coder requires that replaced objects be returned from initWithCoder))). By definition this is a small example of the problem. I believe this is how a question should be structured (from https://stackoverflow.com/help/how-to-ask), so please do tell me: what else do I need to do to write a good question, please?
One solution (and I don't know if it is good practice) seems to be to use a single initializer that calls super.init
public required init?(coder aDecoder: NSCoder) {
loginServerClass = LoginFactory.createLogin()
super.init(coder: aDecoder)
}
and then have a single intializer for testing and mocking (or to create the viewcontroller otherwise from code)
init(loginServerClass: String){
self.loginServerClass = loginServerClass
super.init(nibName: nil, bundle: nil)
}

Does not conform to protocol 'NSCoding' - Swift 3

I have seen several questions similar to mine; however, those are pertaining to swift 2/1 and I am currently using swift 3. I believe Apple has changed it slightly.
class Person: NSObject, NSCoding {
var signature: UIImage
init(signature: UIImage) {
self.signature = signature
}
required convenience init(coder aDecoder: NSCoder) {
let signature = aDecoder.decodeObject(forKey: "signature") as! UIImage
self.init(signature: signature)
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encode(signature, forKey: "signature")
}
}
You will notice how Swift 3 now forces me to use required convenience init( instead of required init(. Perhaps that has something to do with it.
How can I resolve this issue? Thanks!
The encode method in Swift 3 has been renamed to
func encode(with aCoder: NSCoder)
When you get the do not conform error you can easily find out which required methods are missing
Press ⌘B to build the code.
Press ⌘4 to show the issue navigator.
Click on the disclosure triangle in front of the issue line.

'self' used before super.init call and property not initialized at super.init call

I'm trying to init an array of Int in my custom UIView:
var graphPoints:[Int]
required init?(coder aDecoder: NSCoder) {
RestApiManager.sharedInstance.requestData() { (json: JSON) in
for item in json[].array! {
let n=item["n"].intValue
print(n)
if(n>=0) {
self.graphPoints.append(n)
}
}
}
super.init(coder: aDecoder)
}
But int this row RestApiManager.sharedInstance.requestData() { i reiceved this error: 'self' used before super.init call
And at this row super.init(coder: aDecoder) the error: property self.graphPoint not initialized at super.init call
There are a few seperate issues here
The first related two you question being that you must call the super.init method before you reference self. Simply move this line to the beginning on the init method.
Unfortunately because you are preforming an asynchronous request this will still cause you problems as mentioned in the comments.
Also, note init?(coder aDecoder: NSCoder) is not intended for this use. Another issue is if you are requesting data from your API you should create a model object. As opposed to directly creating a UIView and then when you wish to display it to a user create a UIView from that model.
class GraphPointsModel {
var graphPoints:[Int]
init(graphPoints: [Int]) {
self.graphPoints = graphPoints
}
class func retreiveGraphPoints(handler: (GraphPointsModel -> ())) {
RestApiManager.sharedInstance.requestData() { (json: JSON) in
//Error checking...
//Create instance of GraphPointsModel
let instance = GraphPointsModel(...)
//Call handler to do with as you wish with result
handler(instance)
}
}
}
Swift requires all parameters to be initialized before calling super.init(). There are several ways to make this happen.
Declare graphPoints with an empty initializer like this:
var graphPoints:[Int] = [] or var graphPoints = [Int]()
You can also change the graphPoints to an Optional, like this:
var graphPoints:[Int]?
You can also leave the declaration alone, and just initialize it to an empty array before calling super.init()
You will also need to move your RestAPI call below your super.init call.
Hope this helps.
Maybe you must to do this:
super.init(coder: aDecoder)
put that before your requestData call.

Crazy behaviour with UIViewController inits in Swift

I'm trying to add simple initialiser to a UIViewController in Swift. So far it has been very frustrating...
First I tried adding it as a convenience init:
class ImageViewController: UIViewController {
var model: UIImage
convenience init(model: UIImage){
self.init(nibName: nil, bundle: nil)
self.model = model
}
....
If I do this, the compiler forces me to implement required init(coder aDecoder: NSCoder). I checked the definition of the UIViewController class and there's no such requirement, but anyway.
To make things worse, the compiler complains that the self.init(nibName: nil, bundle: nil) call has an erroneous extra argument in bundle:. Again, I checked the class definition and the initialiser signature requires both parameters.
So I decided to make it a designated init. It's not what I want, as I don't want to lose all the superclass initialisers.
Now it seems to be happy with the self.init(nibName: nil, bundle: nil) call, but it still insists that I implement init(coder aDecoder: NSCoder).
Any ideas of what's going on? I can't make head or tails of this...
The error messages are indeed confusing, but I think they come from the fact that
if the model property has no default value then the required initializers are no longer
inherited from the superclass. With an optional (or implicitly unwrapped optional) property
(which has the default value nil) your code compiles:
class ImageViewController: UIViewController {
var model: UIImage!
convenience init(model: UIImage) {
self.init(nibName: nil, bundle: nil)
self.model = model
}
}
If you don't want the model property to be optional, don't make it optional. Sure, it's a bitch to have to implement initWithCoder:, but it's better to have rock-solid, secure code.
class ImageViewController: UIViewController {
var model: UIImage
init(model: UIImage) {
self.model = model
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
This ensures that the only way an instance of ImageViewController can be created is by calling initWithModel: and therefore guarantees that model will always have a nonoptional value.
Maybe in the future, Apple will add a convenient way of doing this, but for now, I must sacrifice convenience for control.

Resources