Crazy behaviour with UIViewController inits in Swift - ios

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.

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.

Using CoreData and Dependency Injection - Thread 1: Fatal error: init(coder:) has not been implemented

I'm trying to learn how to use CoreData and the correct way to implement it, currently I have watched this video on youtube (link below). At the moment it all makes sense, however when I go from one viewController to my HomeVC (which is part of a tab bar controller) I get the below error. Does anyone have any idea as to why? Any help is greatly appreciated, many thanks!!
https://www.youtube.com/watch?v=OYRo3i9z-lM
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
\\ Thread 1: Fatal error: init(coder:) has not been implemented
}
Function to home:
func toHome() {
self.dismiss(animated: true) {
if let destVC = UIStoryboard(name: "Home", bundle: nil).instantiateViewController(withIdentifier: "TabBarHomeVC") as? UITabBarController {
if let homeVC = destVC.viewControllers?.first as? HomeVC {
homeVC.persistenceManager = PersistenceManager.shared
self.present(destVC, animated: true, completion: nil)
}
}
}
}
HomeVC:
class HomeVC: UIViewController {
var persistenceManager: PersistenceManager
init(persistenceManager: PersistenceManager) {
self.persistenceManager = persistenceManager
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
PersistenceManager:
import Foundation
import CoreData
final class PersistenceManager {
private init() {}
static let shared = PersistenceManager()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "CoreDataApp")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
lazy var context = persistentContainer.viewContext
}
If you work with segues then the UIViewController is instantiated from the Storyboard, which is done through the init?(coder:) initializer. You making it unusable makes the HomeVC unable to instantiate itself from Storyboard since it crashes there.
The property var persistenceManager: PersistenceManager makes Swift unable to inherit init(coder:) from UIViewController (explanation) and so you have to provide one for yourself where you initialize all the variables you added, all to get the UIViewController into a usable state.
Try to make var peristenceManager: PersistenceManager optional since you assign it later anyway or assign it a default value in the same line or assign it a value during init?(coder:) so it is initalized. Also call super.init(coder:) inside your init?(coder:) since there it loads all the settings from the Storyboard.
With Storyboard you can’t give things in any initializer so you have to set it after the initializer did run. You can use a static factory function where you initialize the vc instance, then immediately set what you want and then return the vc in a usable state.
So how can I make homeVC.persistenceManager = PersistenceManager.shared, into HomeVC(persistenceManager: PersistenceManager.shared) by passing it through the tabBarController straight to HomeVC?
You can’t use that initializer since the only one being called is the init?(coder:) initializer. You can change the variable to:
var persistenceManager: PersistenceManager!
Then you can set the variable after init(coder:) was called and you have your instance.
Here the UITabBarController initializes the HomeVC, this guy here had the same issue, where to initialize his UIViewController being embedded, maybe it helps you out. The answer uses a call that is being called before a UIViewController is being shown:
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
// Initialization code here.
// Also make sure its only called once if you want
}
He even constructs the UIViewController himself if a certain tab is asked for.
You can ofcourse set it as you do already, since the UITabBarController is not shown yet. Keep it simple as you do already.

How to subclass a UIViewController and add properties in swift?

I want to make a new kind of view controller in Swift that has an additional property that must be explicitly initialized. It doesn't make any sense for this property to be nil or have a default value and the controller will only be initialized programmatically. I tried defining it like this:
class MyController : UIViewController {
var prop: Int
init(prop: Int) {
self.prop = prop
super.init()
}
required init(coder aDecoder: NSCoder?) {
fatalError("don't serialize this")
}
}
I tried running this but it crashed because super.init() tries to run the nib constructor which isn't defined, so I tried adding that:
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
But now the compiler complains that prop isn't being initialized. And this is my question: how can I initialize prop correctly here? I don't want a default value, and anything I set will override the correct value that I set in the other initializer.
I kinda hacked around it by setting some default value in the nib init, but then having my first init do this
self.prop = prop
super.init()
self.prop = prop
But other than being really weird and ugly, that makes me worried that now it is possible to initialize my view controller from a nib and end up with the default value, which would be bad.
What is the correct and idiomatic way to do this in Swift?
At some point the view controller must be initialized by calling init(nibName:bundle:) or init(coder:)
Try this:
class MyViewController: UIViewController {
var prop: Int
init(prop: Int, nibName nibNameOrNil: String? = nil, bundle nibBundleOrNil: Bundle? = nil) {
self.prop = prop
super.init(nibName:nibNameOrNil, bundle: nibBundleOrNil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Try the following
class MyController: UIViewController {
var prop: Int
required init?(coder aDecoder: NSCoder) {
fatalError()
}
init(prop: Int) {
self.prop = prop
super.init(nibName: nil, bundle: nil)
}
}

Where to put super.init() when init() also initializes member variable and references "self"

I'm learning Swift, and I now have this code:
import SpriteKit
import GameplayKit
class Player: GKEntity {
var spriteComponent : SpriteComponent
init(imageName: String) {
super.init() // gives error: self.spriteComponent not initialized at super.init call
let texture = SKTexture(imageNamed: imageName)
spriteComponent = SpriteComponent(entity: self, texture: texture, size: texture.size())
// super.init() Placing it here gives error on line above: "self used before super.init() call"
addComponent(spriteComponent)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I have seen other questions about this, but I can't imagine that I would have to create a dummy initializer (with zero args) in SpriteComponent and call that like:
var spriteComponent = SpriteComponent()
in order to call super.init() before referencing "self"
Can anyone explain why I have to do this idiotic tap-dancing? There surely must be a better way to do it right? Swift can't be that *&%&/(% right?
You must initialize all non-optional properties declared in your subclass before you can call super.init()
So you have two ways of solving such issues:
Change property type to optional (either SpriteComponent? or SpriteComponent!).
Initialize every property either when you declare it or in your initializer before calling super.init
In your case first option suits better.
You can find more info about Swift two-phase initialization here:
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html
This tutorial may also be helpful: (Adding Properties to Subclasses paragraph):
https://www.raywenderlich.com/121603/swift-tutorial-initialization-part-2
All the non-optional properties must be initialised before the object is created. Make your property an optional.
spriteComponent is a non-optional var. Therefore it has to be initialised before calling super.init (explains the 1st mentioned error).
You can not solve this by calling super.init later, because the constructor of SpriteComponent needs a reference to self, which is only available after calling super.init. (explains the second error)
As a solution you can make spriteComponent an unwrapped optional:
var spriteComponent : SpriteComponent!
This instructs the compiler to allow spriteComponent not to be initialised, and gives you the responsibility do do it yourself at a later point in time.
This "Tap-Dancing" has a reason.
In swift, class initialization is a two-phase-initializtion :
Phase #1 - All stored properties are given some initial value (nil is also fine) by the class that defined them
Phase #2 - Now each class may change the initial value and use self
Why is that? Safety mainly - knowing in phase #2 that all properties has some values.
Therefore, in your code, you may not need an empty dummy initializer, but turning your sprite component to an optional could be handy :
class Player: GKEntity{
var spriteComponent : SpriteComponent? = nil // optional
init(imageName: String)
{
super.init() // now OK
let texture = SKTexture(imageNamed: imageName)
spriteComponent = SpriteComponent(entity: self, texture: texture, size: texture.size())!
addComponent(spriteComponent!) // unwrap here
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
}

NSCoding required initializer in inherited classes in Swift

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")
}

Resources