Stupid Swift Tricks #3
So, Swift 1.2 was recently released, with lots of changes, mostly for the better1, including fixing several things mentioned in previous articles. Life on the bleeding edge… but, incremental compilation has arrived, so build types are drastically improved in most cases. Error messages are frequently more helpful. Default parameters don’t break trailing closure syntax. And of course there are many other fixes, and a bunch of exciting new things to play with.
Anyway, that’s not what I’m here to talk about today (although, I will be referring to Swift 1.2’s new Set<T> in a moment…). I’m here to talk about value types.
A lot of data types in Swift are value types – ie, passed by value instead of by reference. In fact, almost all of them are (at least, semantically). Curiously, almost all the discussion I’ve seen about them is around (im)mutability: the idea that it’s safe to pass value types around, knowing that if you pass them to other pieces of code, and those pieces of code mutate them, your original is safe, protected from those mutations, since they’ve modified a copy, not the original.
However, that’s not all there is to say about them. On the quiet, interesting things fall out of value semantics. One of them is that any in-place mutation of a value type is equivalent to pulling out the value, making a mutated copy, then writing it back into the original value (at least, conceptually – the implementation is, hopefully, a little more sophisticated).2
This has implications for property setters and observers, that turn out to be quite useful.
Consider the API to UICollectionView’s selections. There are three3 methods: one to retrieve a list of all the selected items, one to select an item, and one to deselect an item. You can also clear the selection – somewhat unintuitively – by passing nil as the indexPath to the method for selecting an item.
That’s a bunch of methods they had to implement, and yet it still doesn’t give you a particularly rich interface. I mean, you can use these as building blocks to make whatever you need, but wouldn’t it be nice if there were already methods to do things like “test if this index path is in the selection”, or “add/remove this list of items from the selection”?
Here’s my radical proposal for a new interface for selections:
var selection: Set<NSIndexPath>
Yeah. That’s it. It’s just a Set, here you go, have fun. Set already has a bunch of methods to read, test, compare, and mutate in various ways. It’s simple, it’s direct, it’s uncluttered, it’s powerful, and you can easily connect it to other bits of code that work with sequences.
Even though the UICollectionView is probably just holding the selection state in an NSMutableSet internally, we don’t get access to that. Instead we have to work through a limited wrapper around it.
Why?
Well, because the view needs to know about changes. It needs to reflect them visually, changing the state of the cells that have been selected or deselected.
In Objective-C, you could write a property setter. Then, any time you wrote a new selection into the view, it would reflect the new selection. But, if you called methods on it directly, it wouldn’t know. Because in Objective-C, NSSet & its mutable cousin are reference types. So:
// inside hypothetical UICollectionView
@property (nonatomic, strong) NSMutableSet* selection;
// client code
[collectionView.selection removeAllObjects];
This would clear the selection data – but the view won’t update on-screen, because the “value” of the selection hasn’t changed: the “value” is the pointer to the NSMutableSet instance, which is still the same object.
The only way you could make it work is to make the selection a mutable property containing an immutable set, and each time client code wants to make a change, it’s forced to pull it out, make a mutated copy, and write it back in:
// inside hypothetical UICollectionView
@property (nonatomic, strong) NSSet* selection;
// client code
collection.selection = [collection.selection setByAddingObject: item];
Which is annoying.
Hmm. Read the property. Make a copy with changes applied. Write it back in. Sound familiar?
Yeah, it’s the semantics of Swift mutable value-type properties.
Which means in Swift, you can write this:4
var selection: Set<Int> = []
{
didSet
{
let added = selection.subtract(oldValue)
let removed = oldValue.subtract(selection)
println("Added: \(added)")
println("Removed: \(removed)")
}
}
// Assigning a new Set value will trigger the observation,
// just like with a reference type (class instance):
selection = [1,2,3]
// "Added: [2, 3, 1]"
// "Removed: []"
// But unlike reference types, so does any kind of mutation:
selection.insert(4)
// "Added: [4]"
// "Removed: []"
selection.exclusiveOrInPlace([3,6])
// "Added: [6]"
// "Removed: [3]"
selection.removeAll()
// "Added: []"
// "Removed: [6, 2, 4, 1]"
…and the collection view knows exactly what it needs to update in the UI.
And, if you’re wondering about specifying whether or not the change should animate… we already have an API for that: performBatchUpdates:completion: (which is why insertItemsAtIndexPaths: doesn’t have an animated: parameter); selection could be made to work the same way.
It’s a shame it doesn’t already, otherwise we could bolt on this alternate interface using an extension. In the meantime, we could perhaps try this workaround, even if it is a bit icky:
extension UICollectionView
{
var selection: Set<NSIndexPath>
{
get { return Set((indexPathsForSelectedItems() as? [NSIndexPath]) ?? []) }
set { updateSelection(selection, to: newValue, animated: false) }
}
var animatedSelection: Set<NSIndexPath>
{
get { return selection }
set { updateSelection(selection, to: newValue, animated: true) }
}
private func updateSelection(from: Set<NSIndexPath>, to: Set<NSIndexPath>, animated: Bool)
{
let added = to.subtract(from)
let removed = from.subtract(to)
map(added) { self.selectItemAtIndexPath($0, animated: animated, scrollPosition: .None) }
map(removed) { self.deselectItemAtIndexPath($0, animated: animated) }
}
}
And yeah, it doesn’t do anything with the scroll position. I always felt that should be a separate bit of API… :P
Whether this is a good idea or not is left as an exercise to the reader, but it’s interesting to see how Swift’s facilities allow a whole lot of noise to potentially boil away into the ether.
It’s a beta, and there are some rather serious bugs, but they’re being addressed rapidly. I look forward to Beta 2. ↫
I considered digging in to see how Swift actually implements this in practice, but, given the rate at which Swift is changing and optimisations are being added, it would probably be out of date within a week or two. ↫
Plus some configuration stuff that’s orthogonal to this discussion. ↫
Ignore the fact that I’m using Int instead of NSIndexPath, it’s just to simplify the demo. ↫