Sorry, meant to come back to this thread days ago...
As others have noted, Swift 2 doesn't seem to support what you want it to do. I see a workaround, though... There's still boilerplate code, but it seems less error-prone to me
First, we need a way to iterate over the enum cases (and provide their default values):
class StoryBookStateGenerator : AnyGenerator<StoryBookState> {
var optcase:StoryBookState? = nil
override func next() -> Generator.Element? {
if let cas = optcase {
switch cas {
case .Welcome: optcase = .Praise(nil, nil)
case .Praise: optcase = .InvitationToRepeat(nil, nil)
case .InvitationToRepeat: optcase = .TurnToNextPage(nil)
case .TurnToNextPage: optcase = .End
case .End: optcase = nil
}
} else {
optcase = .Welcome(nil)
}
return optcase
}
}
Second, we need a way to parse the source code and extract the case labels. For some reason, I couldn't get String(contentsOfFile) to work. Fortunately, someone on stackoverflow was kind enough to post a class that'll read each line of a file into a [String] (thanks, Martin R!). I'm not sure I should post somebody else's code, even though it's publicly posted, but it's the first answer here: http://stackoverflow.com/questions/24581517/read-a-file-url-line-by-line-in-swift
By using Martin R's StreamReader, and making a few assumptions about code structure, writing a simple parser is fairly easy:
class SimpleEnumParser {
let labels: [String]
init(file:String = __FILE__, line:Int = __LINE__) {
if let file = StreamReader(path: file) {
defer { file.close() }
let lineTextArr = Array(file)
var caseDeclStartLine:Int = 0
var caseDeclEndLine:Int = 0
var foundStartOfCaseDecl = false
func checkLine(line:String) -> Bool {
let trimmed = line.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
return !(trimmed == "" || trimmed.rangeOfString("//")?.startIndex == trimmed.startIndex)
}
parseLoop: for lineNum in line ..< lineTextArr.count {
let lineText = lineTextArr[lineNum]
switch foundStartOfCaseDecl {
case false:
if let _ = lineText.rangeOfString("case") {
caseDeclStartLine = lineNum
foundStartOfCaseDecl = true
}
case true:
if let _ = lineText.rangeOfString("case") {} else {
if checkLine(lineText) {
caseDeclEndLine = lineNum - 1
break parseLoop
}
}
}
}
labels = Array(lineTextArr[caseDeclStartLine...caseDeclEndLine])
.map {$0.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())}
.filter {checkLine($0)}
.map {$0.componentsSeparatedByCharactersInSet(NSCharacterSet.alphanumericCharacterSet().invertedSet)[1]}
} else {
labels = []
}
}
}
And this is how you'd use them to, um, "refactor" your boilerplate:
public enum StoryBookState {
// This is why Generators are good
public static let DOTLabelableItems = Array(StoryBookStateGenerator())
/******************************************
* Make sure that the SimpleEnumParser is *
* instantiated IMMEDIATELY (except for *
* newlines & comments) before you list all *
* your cases, otherwise it won't work. *
********************************************/
static let parser = SimpleEnumParser()
// It'll start parsing the lines when it finds one that contains "case"
case Welcome(String?)// Comments can go here...
// ... or here. And they're discarded, so you can say things like "case case case case" without messing anything up
case Praise(String?, String?)// Whitespace-only lines are ok, too, if you want to deliminate groups of cases:
case InvitationToRepeat(String?, String?)
case TurnToNextPage(String?)
case End
// It'll stop parsing when it gets to anything that doesn't start with "case" or "//"
func cases() {} // <- starts with "func"
/******************************************
* This can go anywhere EXCEPT between the *
* SimpleEnumParser instantiation and cases *
********************************************/
var fauxHashValue:Int {
switch self {
case .Welcome: return 0
case .Praise: return 1
case .InvitationToRepeat: return 2
case .TurnToNextPage: return 3
case .End: return 4
}
}
public var DOTLabel:String {
return StoryBookState.parser.labels[self.fauxHashValue]
}
}
It seems like their ought to be a way to programmatically generate fauxHashValue, but I can't think of one that doesn't involve custom build rules.
Speaking of which, if you can figure out how to call a custom preprocessor, you could probably automate the writing of StoryBookStateGenerator pretty easily, since it was copy/pasted from the output of this:
func generatorWriter(name:String, cases:[(label:String, defaultValue:String?)]) -> String {
guard cases.count > 0 else { return "" }
var text = ""
text += "class \(name)Generator : AnyGenerator<\(name)> {\n"
text += "\tvar optcase:\(name)? = nil\n"
text += "\toverride func next() -> Generator.Element? {\n"
text += "\t\tif let cas = optcase {\n"
text += "\t\t\tswitch cas {\n"
for i in 0 ..< (cases.count - 1) {
text += "\t\t\tcase .\(cases[i].label): optcase = .\(cases[i+1].label)"
if let value = cases[i+1].defaultValue {
text += "(\(value))\n"
} else {
text += "\n"
}
}
text += "\t\t\tcase .\(cases.last!.label): optcase = nil\n"
text += "\t\t\t}\n"
text += "\t\t\t} else {\n"
text += "\t\t\t\toptcase = .\(cases.first!.label)"
if let value = cases.first!.defaultValue {
text += "(\(value))\n"
} else {
text += "\n"
}
text += "\t\t\t}\n"
text += "\t\treturn optcase\n"
text += "\t}\n"
text += "}\n"
return text
}
let labels = ["Welcome", "Praise", "InvitationToRepeat", "TurnToNextPage", "End"]
let defaultValues:[String?] = ["nil", "nil, nil", "nil, nil", "nil", nil]
print(generatorWriter("StoryBookState", cases:Array(zip(labels, defaultValues))))
Anyway, I hope that helps! :-)