Background
I'm working on a server controlled user feedback flow right now, and I've found that it's represented quite nicely as a set of enums:
enum FeedbackScreen {
indirect case questions(header: String?, subHeader: String?, questions: [String], next: FeedbackScreen)
case confirmation(header: String, subHeader: String?)
}
In practice, however, I'm still left with some run-time type checking:
class ConfirmationViewController: UIViewController {
let headerLabel = UILabel()
let subHeaderLabel = UILabel()
required init(with confirmationScreenModel: FeedbackScreenModel) {
super.init(nibName: nil, bundle: nil)
// Can't enforce this at compile time
guard case let .confirmation(header: header, subHeader: subHeader) = confirmationScreenModel else {
fatalError("I can only take confirmation models!")
}
headerLabel.text = header
subHeaderLabel.text = subHeader
}
}
I could alternatively choose to wrap the usable properties for each enum in a struct, but that just adds verbosity and creates an additional layer of indirection that makes the enum approach less effective overall:
enum FeedbackScreenModel {
struct QuestionsModel {
var header: String?
var subHeader: String?
var questions: [String]
var nextScreenModel: FeedbackScreenModel
}
struct ConfirmationModel {
var header: String
var subHeader: String?
}
indirect case questions(QuestionsModel)
case confirmation(ConfirmationModel)
}
Proposed Solution
The goal of this pitch is to improve the compile-time safety for this enum use-case, as well as make enums themselves more practical to use as-is without the need for additional layers of indirection.
// Typealiased
typealias ConfirmationModel = FeedbackScreenModel where case .confirmation
// With a variable
let confirmationScreenModel: FeedbackScreenModel where case .confirmation = .confirmation(header: "Hello", subHeader: "World")
// With a function
func printConfirmation(confirmationModel: FeedbackScreenModel where case .confirmation) {...}
Following the Enum type with a where case
clause would, at compile time, generate a hidden protocol that only the specific enum case conforms to. It can then be used as follows:
func printConfirmation(confirmationModel: FeedbackScreenModel where case .confirmation) {
print("\(confirmationModel.0) \(confirmationModel.1)")
}
// Invalid, the enum case constraint must be explicit
let bar1 = FeedbackScreenModel.confirmation(header: "bar", subHeader: "1")
printConfirmation(confirmationModel: bar1)
// Valid, prints 'bar Optional("2")'
let bar2: FeedbackScreenModel where case .confirmation = .confirmation(header: "bar", subHeader: "2")
printConfirmation(confirmationModel: bar2)
// Valid, prints 'bar Optional("3")'
let bar3: FeedbackScreenModel = .confirmation(header: "bar", subHeader: "3")
if let confirmationModel where case .confirmation = bar3 as? FeedbackScreenModel {
printConfirmation(confirmationModel: confirmationModel)
}
// Valid, prints 'bar Optional("4")'
let bar4: FeedbackScreenModel = .confirmation(header: "bar", subHeader: "4")
switch bar4 {
case let confirmationModel as FeedbackScreenModel where case .confirmation:
printConfirmation(confirmationModel: confirmationModel)
default:
break
}
Note that the above printConfirmation()
method used tuple notation for accessing properties. This is to match how Swift enums currently store associated values as tuples. On implementation of SE-0155 (Normalize Enum Case Representation), much of what is proposed above evolves into:
// Typealiased (post SE-0155)
typealias ConfirmationModel = FeedbackScreenModel where case .confirmation(header:subHeader:)
// With a variable (post SE-0155)
let confirmationScreenModel: FeedbackScreenModel where case .confirmation(header:subHeader:) = .confirmation(header: "Hello", subHeader: "World")
// printConfirmation (post SE-0155)
func printConfirmation(confirmationModel: FeedbackScreenModel where case .confirmation(header:subHeader:)) {
print("\(confirmationModel.header) \(confirmationModel.subHeader)")
}
Thoughts?