Actually.. To answer my own query... I wrote something...
This is protocol OptionSetAssociated with it's extensions:
public protocol OptionSetAssociated: OptionSet where RawValue: BinaryInteger {
var store: [RawValue: Any] { get set }
}
extension OptionSetAssociated {
public init<T>(rawValue: RawValue, value: T) {
self.init(rawValue: rawValue)
self.store[rawValue] = value
}
fileprivate init(rawValue: RawValue, store: [RawValue: Any]) {
self.init(rawValue: rawValue)
self.store = store
}
fileprivate static func combinedStore(_ old: [RawValue: Any], new: [RawValue: Any]) -> [RawValue: Any] {
new.map {$0.key}.reduce(into: old) {
$0[$1] = new[$1] ?? old[$1]
}
}
fileprivate static func storeOverride(_ store: [RawValue: Any], member: RawValue?, value: Any?) -> [RawValue: Any] {
guard let member: RawValue = member else { return store }
var store: [RawValue: Any] = store
store[member] = value
return store
}
public func getValue<T>(key: RawValue) -> T? {
self.store[key] as? T
}
mutating public func formUnion(_ other: __owned Self) {
self = Self(rawValue: self.rawValue | other.rawValue, store: Self.combinedStore(self.store, new: other.store))
}
}
extension OptionSet where Self: OptionSetAssociated, Self == Element {
@discardableResult
public mutating func insert(
_ newMember: Element
) -> (inserted: Bool, memberAfterInsert: Element) {
let oldMember = self.intersection(newMember)
let shouldInsert = oldMember != newMember
var result = (
inserted: shouldInsert,
memberAfterInsert: shouldInsert ? newMember : oldMember)
if shouldInsert {
self.formUnion(newMember)
} else {
self.store = Self.storeOverride(
Self.combinedStore(self.store, new: newMember.store),
member: newMember.rawValue, value: newMember.store[newMember.rawValue])
result.memberAfterInsert.store[newMember.rawValue] = newMember.store[newMember.rawValue]
}
return result
}
@discardableResult
public mutating func remove(_ member: Element) -> Element? {
var intersectionElements = intersection(member)
guard !intersectionElements.isEmpty else {
return nil
}
let store: [RawValue: Any] = self.store
self.subtract(member)
self.store = store
self.store[member.rawValue] = nil
intersectionElements.store = Self.storeOverride([:], member: member.rawValue, value: store[member.rawValue])
return intersectionElements
}
@discardableResult
public mutating func update(with newMember: Element) -> Element? {
let previousValue: Any? = self.store[newMember.rawValue]
var r = self.intersection(newMember)
self.formUnion(newMember)
self.store[newMember.rawValue] = newMember.store[newMember.rawValue]
if r.isEmpty { return nil } else {
r.store = Self.storeOverride([:], member: newMember.rawValue, value: previousValue)
r.store[newMember.rawValue] = previousValue
return r
}
}
}
And here's struct that using this and working just like normal OptionSet does..
public struct TestSet: OptionSetAssociated {
public typealias RawValue = Int
public let rawValue: Int
public var store: [RawValue : Any] = [:]
public init(rawValue: RawValue) {
self.rawValue = rawValue
}
}
extension TestSet { // Members
public static var bool: TestSet { TestSet(rawValue: 1 << 0) }
public static var int: TestSet { TestSet(rawValue: 1 << 1) }
public static var string: TestSet { TestSet(rawValue: 1 << 2) }
public static var optString: TestSet { TestSet(rawValue: 1 << 3) }
public static func int(_ value: Int) -> TestSet {
TestSet(rawValue: TestSet.int.rawValue, value: value)
}
public static func string(_ value: String) -> TestSet {
TestSet(rawValue: TestSet.string.rawValue, value: value)
}
public static func optString(_ value: String?) -> TestSet {
TestSet(rawValue: TestSet.optString.rawValue, value: value)
}
}
extension TestSet { // Member options
public var int: Int? {
self.getValue(key: TestSet.int.rawValue) ?? ( self.contains(TestSet.int) ? Int() : nil ) // Returns default Int() if member is included and no value was given...
}
public var string: String? {
self.getValue(key: TestSet.string.rawValue) ?? ( self.contains(TestSet.string) ? String() : nil )
}
public var optString: String? { // Returns nil when value was not given
self.getValue(key: TestSet.optString.rawValue)
}
}
And here's extension for some debugging (I opted out previous dependencies to iterator):
extension TestSet: CustomStringConvertible {
public var description: String {
var members: [String] = []
var vars: [String] = []
if self.contains(TestSet.bool) { members.append("bool" ) }
if self.contains(TestSet.int) { members.append("int") }
if self.contains(TestSet.string) { members.append("string") }
if self.contains(TestSet.optString) { members.append("Optional<String>") }
if let int: Int = self.int { vars.append("Int(" + int.description + ")") }
if let string: String = self.string { vars.append("String(" + string + ")") }
if let optString: String = self.optString { vars.append("Optional<String>(" + optString + ")")}
return "Members: " + (members.isEmpty ? ["none"] : members).joined(separator: ", ") + "\nVariables: " + vars.joined(separator: ", ")
}
}
Try it. Initialization just like plain OptionSet..
var testset: TestSet = []
testset = [.int, .string("hello")]
testset.remove(.int)
testset.update(with: .string("world"))
testset.insert(.int(10))
print(testset.description)
Result:
Members: int, string
Variables: Int(10), String(world)
So, in this version, each member is allowed with one value (unless it's a tuple..) - which is removed when member leaves the set. Values are kept in variable store which is a dictionary with RawValue as it's key element. Because I used dictionary instead of KeyValuePairs<RawValue, Any> - it is required that RawValue conforms to Hashable(BinaryInteger).
In the example, default values are returned if no value is provided for member that does not use optional value, and then there's one member that has optional value. Also one traditional member without value at all.
Creation is identical to normal OptionSet, except that this needs:
public var store: [RawValue : Any] = [:]
Unfortunately, I am sure that I've forgotten to take something to account that makes my implementation still non-perfect, although it's not much or anything too important.. I just awhile ago had it in my mind, but lost it after that as fast as it came :)