I ran into an issue recently where I wanted a SwiftUI list multiple selection to preserve the order in which the user made the selection. You can bind the List to a Set object to enable multi-selection behavior. I thought I was so clever to go grab an OrderedSet, but it turns out List really wants a Set, and not something that’s just set-like.
Looking into it further, while Set conforms to SetAlgebra, OrderedSet does not.
Would it make sense (is it even possible) for OrderedSet to conform to SetAlgebra? A comment in the code says:
// OrderedSet does not directly conform to SetAlgebra because its definition
// of equality conflicts with SetAlgebra requirements. However, it still
// implements most SetAlgebra requirements (except insert, which is replaced
// by append).
Would it also make sense for SwiftUI to accept containers that conform to SetAlgebra, rather than Set specifically?
I realize I can’t really pitch SwiftUI enhancements here, I just mention it for motivation. Maybe this isn't even Evolution-worthy. But I feel like there’s some harmonization to be had here.
I'm not even sure SwiftUI can be enhanced to just accept OrderedSet as well as Set, since that would create a dependency on a third-party library. (I feel like I've seen a way to make something available in a library if the client has included the dependency, without forcing that dependency on the client, but that seems magical.)
I don’t actually see where equality is defined. I get that the question of comparing a Set to an OrderedSet could be complicated, or even nonsensical. Perhaps they are never strictly equal. Perhaps equality could be defined to mean "disregarding order" with additional methods for testing equality including order.
It already seems to have some accommodations for OptionSet, wouldn’t it make sense to accommodate ordering as well?
Where are these equatable expectations stated / documented?
Two ideas IRT how to do OrderedSet based selection:
start with having a way to construct a new ordered set from existing:
extension OrderedSet {
mutating func change(to newValue: Set<Element>) {
for old in self {
if !newValue.contains(old) {
remove(old)
}
}
for new in newValue {
if !contains(new) {
append(new)
}
}
}
}
this will not effect the order of existing items, items no longer in the set will be removed and new items will be added to the end.
Then, as SwiftUI wants "Set" give it a set made out of orderedSet:
private var cachedSet: Set<ID>!
var orderedSet: OrderedSet<ID> = [] {
didSet { cachedSet = nil }
}
var selection: Set<ID> {
get {
if cachedSet == nil {
cachedSet = Set(orderedSet)
}
return cachedSet
}
set {
orderedSet.change(to: newValue)
cachedSet = newValue
}
}
Make sure that "cachedSet" value is getting cleared if you change orderedSet.
This should satisfy SwiftUI and hopefully the price of converting from OrderedSet to Set after cached value invalidation will not be too high.
Ignore SwiftUI's "selection" and implement selection logic yourself (in which case you could use anything including OrderedSet. It's not hard at all.
I probably made my comment while you were typing, but see above the OrderedSet: Equatable conformance file.
OrderedSet is defined so that elements are in a, well, order. It doesn't really make theoretical sense such that equality ignores that compared to the logical/mathematical expectations of Set<Algebra>.
I meant: where are these logical/mathematical expectations of SetAlgebra are stated / documented? I wasn't able finding them in the SetAlgebra protocol.
Not hard? I'd sure love some hints on how to do this, as I'm having trouble imagining how I'd go about it.
I've definitely considered trying to keep track of the selection order as a side-effect of changing selection, but I'm not sure that works reliably (e.g. what happens if the user shift-clicks several items away from the current selection? I think you only get a single change notification, and no guarantee of the order of the items. You'd have to do some careful inspection of the list order to get it right.
[...] OrderedSet has an order-sensitive definition of equality (see above) that is incompatible with SetAlgebra 's documented semantic requirements. Accordingly, OrderedSet does not (cannot) itself conform to SetAlgebra .
This "SetAlgebra 's documented semantic requirements" is the droid I am looking for Could you point me to it? Specifically those that specify that SetAlgebra conformer should maintain [a, b] = [b, a] or something to that account.
do {
let x: Set = [0, 1]
let y: Set = [1, 0]
precondition(x.isSubset(of: y))
// succeeds (not actually a proof, just an example for illustration purposes)
precondition(x.union(y) == y)
}
do {
let x: OrderedSet = [0, 1]
let y: OrderedSet = [1, 0]
precondition(x.isSubset(of: y))
// fails because of the order difference between x and y (counter example)
precondition(x.union(y) == y)
}
Couldn’t x.union(y) be implemented such that it returns the y ordering? It seems the order returned here is arbitrary, as union can’t honor both orderings. I don't know if that would lead to consistency for all cases, but it's not clear to me how union should behave for ordered sets.
Alternatively, can there be a protocol for "ordered-without-duplicates" that can implement a subset of SetAlgebra?
Is there another "weaker" protocol, say, "SetProtocol" that both Set and OrderedSet would conform to? If so, SwiftUI could have used that instead of hardcoding "Set".
OTOH, would it be a great UX when user sees a number of selected items and has no clue what the order is among them (whilst this order matters somehow)? Maybe there needs to be a clue to the order via some small (1), (2), etc labels shown on the items?
mutating func change(to newValue: Set<Element>) {
...
for new in newValue {
if !contains(new) {
append(new)
}
}
}
In light of adding multiple items (e.g. via a shift click) this fragment needs to be changed to go in the current-on-screen sorting order of items. As written the order of the newly added items will be random.
I think the crux of the matter is that the equality operator, ==, is expected to be commutative. i.e. symmetrical w.r.t. to its operands. So it has no intrinsic bias towards the semantics of one operand or the other.
An approach involving methods (not operators) might be viable, though. e.g. if unordered sets had a hasSymmetricDifference(:) method.
In any case a new Setish protocol would have to be added, to represent a more limited set of constraints than what SetAlgebra requires.
I'm not sure about "documented" in Swift, but the mathematical semantics is certainly well-known and thus implicitly expected: Algebra of sets - Wikipedia
Here, you'll find that you'll have to ignore any order, if you want subsets and unions to work axiomatically.
For ordered sets, it is certainly possible to implement a set algebra, but that would then either have to break expectations of equality, or established expectations of how subsets work for ordered sets.
You can simply add a lens on your ordered set, that vends an ordinary set and only mutates the underlying ordered set when needed:
extension OrderedSet {
var elements: Set<Element> {
get { Set(self) }
set { self.update(withElementsFrom: newValue) }
}
private mutating func update(withElementsFrom other: Set<Element>) {
for old in self {
if !other.contains(old) { remove(old) }
}
for new in other {
if !self.contains(new) { append(new) }
}
}
}
Then you can simply use it as so:
import SwiftUI
import OrderedCollections
struct ListView: View {
@State private var selection: OrderedSet<Int> = []
// ^^^^^^^^^^
var body: some View {
VStack {
List(
Array(0..<1000),
id: \.self,
selection: $selection.elements,
// ^^^^^^^^^
rowContent: { i in Text("Row \(i)") }
)
Text("Selection: \(selection.description)")
}
}
}
Although, I'm not sure it's wise to have elements be settable. The order in which new elements will be added is undefined. It somewhat defeats the point of an ordered collection to add items in random order. Perhaps better to not have that 'convenience' and instead require users to add the elements manually, thus encouraging them to think about the order of addition.