Alternative to enum with associated value?

Hi,

I have an interesting data modelling issue. In essence, in my app there will be a list of profiles, and then "profiles categories". There will always be a default category, the "All" category, which the user cannot delete/modify, and which contains (you guessed it) all the profiles. The user can also create their own "custom" categories, to which they can add profiles.

Naturally, I gravitated towards using an enum to model ProfilesCategory, since this seemed like a good use case for an either/or type, where the type category is either "All", or a custom one:

enum ProfilesCategory {
    case all
    case custom(name: String, profileIDs: Set<Profile.ID>)
}

However, the user is also able to add/delete profiles from profile categories – but enums with associated values don't allow in-place mutation, so I'd end up copying the profileIDs Set every time. This would be fine if the category had a small amount of profiles, but this isn't scalable.

So, I was wondering if there was a good alternative to using an enum with associated values such as this which gives me the same modelling characteristics and in-place mutation.

I wouldn't treat the "All" category as a different thing than custom categories. Instead I would treat it as a normal category that just happens to not constrain what is inside

struct Profile: Identifiable {
    var id: Int
    static var allProfiles: [Profile] {
        [Profile(id: 123), Profile(id: 23), Profile(id: 42), Profile(id: 999)]
    }
}

struct Category: Equatable {
    var name: String
    var filter: Set<Profile.ID>?

    /// categories that will be shown to the user
    static var allCategories: [Category] {
        [Category(name: "All profiles", filter: nil)] + Self.customCategories
    }
    /// categories that can be modified by the user
    static var customCategories: [Category] = [
        Category(name: "funny profiles", filter: [42, 999]),
        Category(name: "my favourite", filter: [42]),
    ]

    func isCustom() -> Bool {
        Self.customCategories.contains(self)
    }

    func shouldShowDeleteIconInTheUI() -> Bool {
        self.isCustom()
    }

    func profilesInCategory() -> [Profile] {
        if let ids = self.filter {
            return Profile.allProfiles.filter({ ids.contains($0.id) })
        } else {
            return Profile.allProfiles
        }
    }

    mutating func addProfile(id: Int) {
        self.filter = self.filter ?? []
        self.filter?.insert(id)
    }
}

Not the most elegant solution but you could encapsulate it into a method.

mutating func modifyProfileIDs(_ f: (inout Set<Profile.ID>) throws -> Void) rethrows {
  switch self {
  case .all: fatalError("expected .custom case")
  case .custom(name: let name, profileIDs: var ids):
    self = .all
    defer { self = .custom(name: name, profileIDs: ids) }
    try f(&ids)
  }
}
2 Likes