0

Description

I have enums with case names that correspond to class names. I use the enum to populate sections/rows of a UITableView. When a user selects a row, I'd like to instantiate the corresponding class and instantiate an object of that class.

// example enum:
enum BodiceEnum: String, CaseIterable {
    case AddBraCups
    case AddOrRemoveBoning
    // other cases hidden
    }

// example classes:
class AddBraCups { // implementation hidden }
class AddOrRemoveBoning { // implementation hidden}

Additional Context

I made a lookup table to connect the "Section" enum cases to their corresponding detail enum cases:

var alterationsLookupTable: [(String,[Any])] = [
    ("Bodice",BodiceEnum.allCases),
    ("Neckline",NecklineEnum.allCases),
    ("Sides",SidesEnum.allCases),
    ("Sleeves or Straps",SleevesOrStrapsEnum.allCases),
    ("Back of Dress",BackOfDressEnum.allCases),
    ("Seams",SeamsEnum.allCases),
    ("Hem",HemEnum.allCases),
    ("Skirt",SkirtEnum.allCases),
    ("Veils",VeilsEnum.allCases),
    ("Prom - Straps",PromStrapsEnum.allCases),
    ("Prom - Take in/out",PromTakeInOrOutEnum.allCases),
    ("Prom - Hem",PromHemEnum.allCases),
    ("Tux",TuxEnum.allCases),
]

The current UITableView section corresponds to an index in this alterationsLookupTable array.

Once I get the correct section's type, I switch over the corresponding enum cases for that section. I'm switching over itemsTuple.0 and then using the current indexPath.row value as an index in itemsTuple.1

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
        //var alterationDetailsArray: [String]()
        let section = indexPath.section
        var cellText: String = ""
        let itemTuple = alterationsLookupTable[section]
        switch itemTuple.0 {
        case "Bodice":
            let items = itemTuple.1 as! [BodiceEnum]
            cellText = items[indexPath.row].readable
        case "Neckline":
             let items = itemTuple.1 as! [NecklineEnum]
            cellText = items[indexPath.row].readable
        case "Sides":
            let items = itemTuple.1 as! [SidesEnum]
            cellText = items[indexPath.row].readable
        case "Sleeves or Straps":
            let items = itemTuple.1 as! [SleevesOrStrapsEnum]
            cellText = items[indexPath.row].readable
        case "Back of Dress":
            let items = itemTuple.1 as! [BackOfDressEnum]
            cellText = items[indexPath.row].readable
        case "Seams":
            let items = itemTuple.1 as! [SeamsEnum]
            cellText = items[indexPath.row].readable
        case "Hem":
            let items = itemTuple.1 as! [HemEnum]
            cellText = items[indexPath.row].readable
        case "Skirt":
            let items = itemTuple.1 as! [SkirtEnum]
            cellText = items[indexPath.row].readable
        case "Veils":
            let items = itemTuple.1 as! [VeilsEnum]
            cellText = items[indexPath.row].readable
        case "Prom - Straps":
            let items = itemTuple.1 as! [PromStrapsEnum]
            cellText = items[indexPath.row].readable
        case "Prom - Take in/out":
            let items = itemTuple.1 as! [PromTakeInOrOutEnum]
            cellText = items[indexPath.row].readable
        case "Prom - Hem":
            let items = itemTuple.1 as! [PromHemEnum]
            cellText = items[indexPath.row].readable
        case "Tux":
            let items = itemTuple.1 as! [TuxEnum]
            cellText = items[indexPath.row].readable
        default:
            cellText = "not valid cell text"
            
        }
        cell.textLabel?.text = cellText
        return cell
    }
}

I've been looking at this but I can't seem to make it work. My (little) understanding of the problem is Swift's type safety. I'm guessing there is Swifty (idiomatic) way to make this happen.

Update 2:

Here’s an example of one Alteration Section —> Hem and one of it’s subclasses —> AddHemLace

class Hem : Codable {
    var minCost: Float
    var maxCost: Float
    var actualCost: Float

    var name: String {
        let thisType = type(of: self)
        return String(describing: thisType)
    }

    init(minCost: Float, maxCost: Float, actualCost: Float) {
        self.minCost = minCost
        self.maxCost = maxCost
        self.actualCost = actualCost
    }

    convenience init() {
        self.init(minCost: -1, maxCost: -1, actualCost: -1)
    }
}

class AddHemLace : Hem {
    var costDetails: String?
    var costUnit: String?
    var units: Int = 1
    var secondaryCost: Float = 0.0
    var secondaryCostDetails: String?

    var totalCost : Float {
        return self.actualCost * Float(self.units) + self.secondaryCost
    }

    init() {
        let min: Float = 50.00
        let max: Float = 80.00
        let actual: Float = min
        super.init(minCost: min, maxCost: max, actualCost: actual)
    }

    required init(from decoder: Decoder) throws {
        fatalError("init(from:) has not been implemented")
    }
}

Problem: I couldn’t figure out how to populate the UITableView

I made related enumerations that had a parallel structure to my classes.

enum AlterationSectionsEnum: String, CaseIterable  {   
    case Hem
    // other cases
}

enum HemEnum: String, CaseIterable {
    case cutAndReplaceHem
    // other cases
}

I then used a big switch statement, a lookup table and some string parsing foo to fill the UITableView. Lots of code smell.

Question

As I attempt to understand this answer, I’m seeing now it would have been possible to use my classes directly to populate the UITableView.

I’m not sure where to start with this statement:

“For example, it should provide a method to populate an NSView or table cell. This way, each class can define its own kind of UI to present for its particular configurable parameters…”

See screenshot of UITableView

Community
  • 1
  • 1
Matt Green
  • 123
  • 5
  • 1
    **_When a user selects a row, I'd like to instantiate the corresponding class and instantiate an object of that class._** I do not think you need enum for that purpose. Please provide more context. – OOPer May 19 '19 at 23:02
  • 1
    Why is there an enum in the first place? I.e. what purpose do the enum cases serve, that the classes themselves can't fulfill? – Alexander May 19 '19 at 23:05
  • I made edits to the post – Matt Green May 19 '19 at 23:35
  • Thanks for updating. But I cannot see where you are trying to instantiate your classes, and how you want to use them. What's `readable`? – OOPer May 19 '19 at 23:43
  • I tried instantiating my classes using `NSClassFromString(className)` where `className` is a the rawValue of the selected enum. I tried this: https://stackoverflow.com/questions/24030814/swift-language-nsclassfromstring/32265287#32265287 – Matt Green May 19 '19 at 23:46
  • `readable` is an extension I wrote for my enums that turn Strings like `addPremadeCapSleeves` into: `Add premade cap sleeves` – Matt Green May 19 '19 at 23:48
  • Where do you put `NSClassFromString(className)`? And please show the extension where you are defining `readable`. – OOPer May 19 '19 at 23:59
  • In my opinion, you need to bind each cases to an instance, not a class. When you can provide how you instantiate the classes and how you want to use them, you would get better solutions. – OOPer May 20 '19 at 00:28

2 Answers2

0

This sort of switching and force down casting into one of many possible types are a severe code smell, indicating a missed opportunity to apply object oriented design.

It seems to me that you would benefit greatly from a DressCustomization protocol, which your various customizations conform to. Then, make a set of classes that conform to this protocol. You need one DressCustomization class per "category" of customization. By "category", I mean a set of customizations that share similar configurable parameters, use cases and UI. For example

  • A "ElongateCustomization" is one that just stores the name of some area of a dress, and a length by which to elongate it
  • A "ColorCustomization" is one that stores the name of some area of a dress, and a color to use for it
  • An "AddOnCustomization" is one that stores the name of a feature to add on

The DressCustomization protocol is a unifying protocol that lets you store/handle any one of these customizations in a uniform way. For example, it should provide a method to populate an NSView or table cell. This way, each class can define its own kind of UI to present for its particular configurable parameters. E.g. the views vended by ElongateCustomization presents a picker for a dress section, and a number input for the elongation length. The views vended by ColorCustomization presents a picker for a dress section, and a color palette for input.

These DressCustomization objects are also part of your data model. Your various table cells can store references to them (for every DressCustomization class, you'll have a corresponding subclass of UITableViewCell), and can act on them directly. No casting required.

Fundamentally, in almost any case where you find yourself branching on one of many values, and doing one thing out of many possible things, that's a strong indication that you can leverage polymorphism. This way, you can easily define new customizations, and not hunt around your code base adding in new switch cases, force casts and other such nonsense.

Alexander
  • 59,041
  • 12
  • 98
  • 151
  • Thank you. I think I have some redundancy here. I do have related classes for alterations and then a parallel structure in enums. I'm going to write up another question that brings your ideas together with my structure. – Matt Green May 20 '19 at 00:37
  • @MattGreen I would use enums storing the data associated with "one of the above" cases, but only when I don't need it to be extensible, and don't need to define any behaviour that differs between the cases. In your situation, you need every type of alteration to have its own kind of UI, which is a significant enough difference in behaviour that I think makes traditional polymorphism a better choice. – Alexander May 20 '19 at 00:58
  • Thank you Alexander. I posted a new question with more context, hoping for some suggestions on how I might do that: https://stackoverflow.com/questions/56213250/design-swift-class-protocol-method-to-populate-uitableviewcell – Matt Green May 20 '19 at 01:03
  • Fundamentally, this question is trying to solve the problem of "How do I present the appropriate UI for every table cell?". You're doing so by implementing the `UITableViewController.tableView(_:cellForRowAt:)` method. Within you're implementation, you're using the section and row to find the appropriate tuple from within your `alterationsLookupTable`, and then you're trying to decide how to display the UI for the particular case. The fundamental issue, is that rather than asking your model what kind of UI it needs, you're trying to make the decision for it. – Alexander May 20 '19 at 01:13
  • Instead, you could have an `customizations: [DressCustomization]`, and have the `DressCustomization` protocol require conforming types to implement a method like `createTableViewCell -> UITableViewCell`. Rather than having one monolithic `tableView(_:cellForRowAt:)` that's trying to make all the decisions, have it delegate to to the `DressCustomization` objects, and have them provide the cell for you. – Alexander May 20 '19 at 01:15
0

Problem: I couldn’t figure out how to populate the UITableView

Why don't you make a direct type representing the section-row hierarchy?

Prepare some protocol representing a row:

protocol AlterationType {
    var name: String {get}
    //... other common properties or methods
}

And define a struct representing a section:

struct AlterationSection {
    var sectionName: String
    var alterations: [AlterationType]
}

With these above, you can declare a data model of your table view as an Array of AlterationSection:

var alterationSections: [AlterationSection] = [
    //...
]

With this Array, you can write your tableView(_:cellForRowAt:) simply as:

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        //### `dequeueReusableCell(withIdentifier:)` may return nil,
        //### You should better use `dequeueReusableCell(withIdentifier:for:)` instead.
        //### (You need to register the identifier "Cell" in any of the available ways.)
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        let alterationSection = alterationSections[indexPath.section]
        cell.textLabel?.text = alterationSection.alterations[indexPath.row].name
        return cell
    }

You can initialize alterationSections as:

class Hem : Codable, AlterationType { //<-Add conformance to `AlterationType`
    //...
}

class AddHemLace : Hem {
    //...
}
var alterationSections: [AlterationSection] = [
    AlterationSection(sectionName: "Hem", alterations: [
        AddHemLace(),
        //...
    ]),
    AlterationSection(sectionName: "...", alterations: [
        //...
    ]),
    //...
]

OOPer
  • 47,149
  • 6
  • 107
  • 142