So I managed to instantiate an object of a subclass that had a property (in this case storing a closure) not initialized. At least I believe so, thus I would appreciate somebody taking a look at this and comment. I had the following class (abbreviated here):
class TaskFile {
private static let ephemeralUrl = URL(fileURLWithPath: "/dev/null")
private(set) var location: URL
private var contents: Contents // a simple struct holding some Strings
private init(parsingFrom url: URL) throws {
location = url
contents = try Contents.loadFromPackage(packageUrl: url)
}
convenience init(withUrl: URL) throws {
guard url.isFileURL else { throw TaskFileError.locationIsNotFileURL }
if url.isFileUrlToExistingFile, url != Self.ephemeralUrl {
try self.init(parsingFrom: url)
} else {
let title = url.deletingPathExtension().lastPathComponent
self.init(withNewContentsToStoreAt: url, title: title)
try save()
}
}
private init(withNewContentsToStoreAt url: URL, title: String) {
location = url
contents = Contents(title: title)
}
static func ephemeralTaskFile() -> TaskFile {
return TaskFile(withNewContentsToStoreAt: ephemeralUrl, title: "New Task")
}
func save() throws {
// just some encoding of contents and writing to location
}
The "ephemeral TaskFile
" was just needed as basically an initial value in some places of my project and I thought I was clever by hiding the init
it needs to prevent TaskFile
s being created that had such "bad" URLs (the URLs for instances that are created based on user input would definitely be safe).
Generally the class worked very well so far, but then I came across a unit test for another class where I would need a mock/spy TaskFile
. So I did what I always do:
class SpyTaskFile: TaskFile {
var didSaveContents: () -> Void = { }
override func save() throws {
try super.save()
didSaveContents()
}
}
To my surprise, already existing tests that now (also) used this spy would give me a crash (bad mem access) when creating an instance, right when it was save()
ed, at the didSaveContents()
call. Confusingly, the property was set to 0x0, even though I had provided it a harmless noop as default value.
To further investigate I removed the default value and wrote my own init
for SpyTaskFile
and there I saw the issue: The compiler obviously wants me to call a designated init
of the superclass, but I had set them all to private. One facepalm later I fixed that by removing the first private init and changed the convenience initializer to a designated one:
init(withUrl url: URL) throws {
guard url.isFileURL else { throw TaskFileError.locationIsNotFileURL }
if url.isFileUrlToExistingFile, url != Self.ephemeralUrl {
location = url
contents = try Contents.loadFromPackage(packageUrl: url)
} else {
let title = url.deletingPathExtension().lastPathComponent
location = url
contents = Contents(title: title)
try save()
}
}
This works as expected as I was now able to call the new designated initializer from my subclass initializer, where I can set the didSaveContents
property. In fact, I don't even need to spell it out anymore, providing a default value works. The only drawback was the few lines of duplicated code in the if statement.
Oh, I should also mention that weirdly enough, if I made the didSaveContents
property optional, it does get properly defaulted to nil
, even with the bad private/convenience design.
I'm not sure whether it's a compiler bug (i.e. even with private designated initializer the property added in the subclass should properly init via default value) or just an "oversight" that would warrant a warning/error message only?