Best Practice to Implement a Failable Initializer in Swift

Best practice to implement a failable initializer in Swift

Update: From the Swift 2.2 Change Log (released March 21, 2016):

Designated class initializers declared as failable or throwing may now return nil or throw an error, respectively, before the object has been fully initialized.


For Swift 2.1 and earlier:

According to Apple's documentation (and your compiler error), a class must initialize all its stored properties before returning nil from a failable initializer:

For classes, however, a failable initializer can trigger an
initialization failure only after all stored properties introduced by
that class have been set to an initial value and any initializer
delegation has taken place.

Note: It actually works fine for structures and enumerations, just not classes.

The suggested way to handle stored properties that can't be initialized before the initializer fails is to declare them as implicitly unwrapped optionals.

Example from the docs:

class Product {
let name: String!
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}

In the example above, the name property of the Product class is
defined as having an implicitly unwrapped optional string type
(String!). Because it is of an optional type, this means that the name
property has a default value of nil before it is assigned a specific
value during initialization. This default value of nil in turn means
that all of the properties introduced by the Product class have a
valid initial value. As a result, the failable initializer for Product
can trigger an initialization failure at the start of the initializer
if it is passed an empty string, before assigning a specific value to
the name property within the initializer.

In your case, however, simply defining userName as a String! does not fix the compile error because you still need to worry about initializing the properties on your base class, NSObject. Luckily, with userName defined as a String!, you can actually call super.init() before you return nil which will init your NSObject base class and fix the compile error.

class User: NSObject {

let userName: String!
let isSuperUser: Bool = false
let someDetails: [String]?

init?(dictionary: NSDictionary) {
super.init()

if let value = dictionary["user_name"] as? String {
self.userName = value
}
else {
return nil
}

if let value: Bool = dictionary["super_user"] as? Bool {
self.isSuperUser = value
}

self.someDetails = dictionary["some_details"] as? Array
}
}

How to implement a failable initializer for a class conforming to NSCoding protocol in Swift?

UPDATE: This was addressed in the Swift 2.2 update, and you no longer have to assign a nil value and call super prior to failing an initializer.

For version of Swift prior to 2.2:

You actually have to initialize your values before returning nil, unfortunately.

Here's the working solution:

class CustomMedia : NSObject, NSCoding {
var videoTitle: String?
var videoURL: NSURL!

init?(title: String?, urlString: String) {
super.init()

if let url = NSURL(string: urlString) {
self.videoURL = url
self.videoTitle = title
} else {
self.videoURL = nil
self.videoTitle = nil
return nil
}
}

func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(self.videoTitle, forKey: PropertyKey.videoTitle)
aCoder.encodeObject(self.videoURL, forKey: PropertyKey.videoURL)
}

required init(coder aDecoder: NSCoder) {
videoTitle = aDecoder.decodeObjectForKey(PropertyKey.videoTitle) as? String
videoURL = aDecoder.decodeObjectForKey(PropertyKey.videoURL) as! NSURL
}
}

Swift Failable Initializer Can’t Return Nil?

Apparently it’s a bug that’ll be fixed as of Swift 2.2 https://stackoverflow.com/a/26497229/5792198

In the meantime, you can initialize all the properties with dummy data and set a flag. At the end of all the init code check the flag and return nil.

Clarification on Failable Initializers for Classes in Swift

As stated by @mustafa in this post:

According to Chris Lattner this is a bug. Here is what he says:

This is an implementation limitation in the swift 1.1 compiler,
documented in the release notes. The compiler is currently unable to
destroy partially initialized classes in all cases, so it disallows
formation of a situation where it would have to. We consider this a
bug to be fixed in future releases, not a feature.


Source

Failable initializers and stored properties

The problem is that each property declared with let in a class must be populated before the init does return.

In your case the init is not populating the 2 constant properties.

In Swift 2.1 each constant property of a class must be populated even when a failable initializer does fail.

class Foo {
let a: Int?
let b: Int?

init?() {
return nil // compile error
}
}

More details here.

Struct

On the other hand you can use a struct where a failable initializer can return nil without populating all the let properties.

struct Person {
let name: String

init?(name:String?) {
guard let name = name else { return nil }
self.name = name
}
}

Creating a second failable initializer using a convenience initializer

In Swift 3, a convenience initializer must call a designated initializer in the same class before any attempt to access self is made in the convenience initializer.

I suggest you change your convenience initializer to be like this:

convenience init?(barnivoreDictionary: [String: AnyObject]) {
guard
let id = barnivoreDictionary[Constants.kID] as? Int,
let companyName = barnivoreDictionary[Constants.kCompanyName] as? String,
let address = barnivoreDictionary[Constants.kAddress] as? String,
let city = barnivoreDictionary[Constants.kCity] as? String,
let state = barnivoreDictionary[Constants.kState] as? String,
let postal = barnivoreDictionary[Constants.kPostal] as? String,
let country = barnivoreDictionary[Constants.kCountry] as? String,
let phone = barnivoreDictionary[Constants.kPhone] as? String,
let email = barnivoreDictionary[Constants.kEmail] as? String,
let url = barnivoreDictionary[Constants.kURL] as? String,
let checkedBy = barnivoreDictionary[Constants.kCheckedBy] as? String,
let notes = barnivoreDictionary[Constants.kNotes] as? String,
let status = barnivoreDictionary[Constants.kStatus] as? String,
let statusColor = barnivoreDictionary[Constants.kStatusColor] as? String else {
return nil
}

self.init(id: id, companyName: companyName, address: address, city: city, state: state, postal: postal, country: country, phone: phone, email: email, url: url, checkedBy: checkedBy, notes: notes, status: status, statusColor: statusColor, alcoholType: alcoholType)
}

Note that you don't need to call self.init... inside the guard.

Parameterless Failable initializer for an NSObject subclass

As you are subclassing NSObject, you cannot have a failable no-parameter initialiser as NSObject's no parameter initialiser is not failable.

You could create a class factory method that returns an instance or nil depending on the iOS version

How to guard initialization of property that may fail

Update Another approach, that doesn't use guard, would be to use a switch, as optionals map to the Optional enum:

init?() {
switch URL(string: "http://www.google.com") {
case .none:
myURL = NSURL()
return nil
case let .some(url):
myURL = url
}
}

although you'd still get a url local variable.


Original answer

You can declare your initializer as a failable one and return nil in case the url string parsing fails, instead of throwing a fatal error. This will make it more clear to clients your the class that the initializer might fail at some point. You still won't get rid of the guard, though.

init?() {

guard let url = URL(string: "http:www.google.com") else {
// need to set a dummy value due to a limitation of the Swift compiler
myURL = URL()
return nil
}
myURL = url

}

This add a little complexity on the caller side, as it will need to check if the object creation succeeded, but it's the recommended pattern in case the object initializer can fail constructing the object. You'd also need to give up the NSObject inheritance as you cannot override the init with a failable version (init?).

You can find out more details about failable initializers on the Swift blog, Apple's documentation, or this SO question.



Related Topics



Leave a reply



Submit