I am in the process of writing a small set of classes to implement some messages. I've defined a base class called NetworkMessage which contains the core functionality of all messages. At this point, unless performance becomes an issue, I'm planning to use NSCoding to handle the serialization because it's pretty straightforward to use, or at least I thought so.
Swift is getting in my way, or perhaps my understanding of it is.
I think I understand the need for "required" initializers, and the fact that a subclass needs to be sure to call one as part of its initialization.
In my case, and I've only just started these classes, I've created a second, intermediary class called TileRelatedMessage that subclasses NetworkMessage to provide some extra functionality specific to a subset of the message kinds.
I've then created the first of several subclasses of the intermediary, only to find that even though TileRelatedMessage ensures that the rules of initialization are followed, my lowest level class will not compile unless I create what seems to me, a completely pointless override of the NSCoding initializer.
In the code below, if I comment out the 'init(coder:)' initializer I get an error from the compiler. I understand why I get that if I don't include it in TileRelatedMessage, however it makes no sense to me that I have to include it in DiscardATileMessage. The initializer I have in DiscardATileMessage calls an initializer in TileRelatedMessage which in turn calls a required initializer in NetworkMessage. What is missing that leaves this unsafe in any way? Through this chain, it's guaranteed that every class property has been initialized.
I think I understand why Swift enforces some of these rules, however they seem so hit and miss a lot of the time, that even after dabbling with Swift for 12 months now, it is incredibly frustrating to say the least.
Here is the code in question; I've tried to limit it:
enum MessageKind : Int {
/// The sender (opponent) wants a new tile
case requestATile
/// ... and others...
}
public class NetworkMessage : NSCoding {
let kind: MessageKind
/// Initialises the message object with the specified kind.
///
/// - Parameter withMessageKind: the message kind
init(withMessageKind: MessageKind) {
self.kind = withMessageKind
}
/// Encodes the message.
///
/// - Parameter aCoder: the coder that will contain the results of the encoding.
public func encode(with aCoder: NSCoder) {
aCoder.encode(self.kind.rawValue, forKey: "kind")
}
/// Decodes a message from the supplied decoder.
///
/// - Parameter aDecoder: the decoder
public required init?(coder aDecoder: NSCoder) {
self.kind = MessageKind.init(rawValue: aDecoder.decodeInteger(forKey: "kind"))!
}
/// Encodes the message as a stream of bytes, encapsulated within a Data object.
///
/// - Returns: A Data object containing the byte stream representation of the message.
public func data() -> Data {
return NSKeyedArchiver.archivedData(withRootObject: self)
}
/// Constructs a NetworkMessage from the supplied byte stream.
///
/// - Parameter fromData: A Data object containing the byte stream representation of the message.
/// - Returns: A NetworkMessage, decoded from the input byte stream.
static func networkMessage(fromData: Data) -> NetworkMessage? {
return NSKeyedUnarchiver.unarchiveObject(with: fromData) as? NetworkMessage
}
}
public class TileRelatedMessage : NetworkMessage {
var tileID : Int = 0
override init(withMessageKind: MessageKind) {
super.init(withMessageKind: withMessageKind)
}
init(withTileID tileID: TileID, andMessageKind kind: MessageKind) {
self.tileID = tileID
super.init(withMessageKind: kind)
}
public override func encode(with aCoder: NSCoder) {
aCoder.encode(self.tileID, forKey: "tileID")
super.encode(with: aCoder)
}
public required init?(coder aDecoder: NSCoder) {
self.tileID = aDecoder.decodeInteger(forKey: "tileID")
super.init(coder: aDecoder)
}
}
public class DiscardATileMessage : TileRelatedMessage {
init(withTileID: TileID) {
super.init(withTileID: withTileID, andMessageKind: .discardATile)
}
/// Compiler returns:
/// 'required' initializer 'init(coder:)' must be provided by subclass of 'TileRelatedMessage'
///
///public required init?(coder aDecoder: NSCoder) {
/// super.init(coder: aDecoder)
///}
}