For a project I needed a way to get a reference to a UISwitch contained in the contentView of a UITableViewCell. I remembered from my research for my Swift talk that there was this handy is operator, so I was excited to tweet that I had finally found the first good use for it.
My original proposition was:
I was slightly embarrassed by the fact that I had taken the screenshot before checking Xcode for warnings. One problem here is that view is a UIView subclass and requires casting to be returned as UISwitch. Also a function returning something needs to have returns for all code paths. So there is a return nil missing at the bottom.
It turns that there are multiple ways to skin a cat. From this initial post an interesting game of Code Golf ensued. One that taught me about several interesting ways to approach this problem, some more elegant than others. First of all, I didn’t know what Code Golf was before this time. It’s basically people competing for who can formulate the shortest solution to a programming problem. Strictly speaking, people are trying to use the least characters, but with Swift I place more value on elegance and uniqueness of the approach.
Swiftly Code Golfing
So, let’s go through the variants and judge them in terms of elegance and teaching value:
Variant 1: if-for-let
func switchInCell(cell: UITableViewCell) -> UISwitch? { for view in cell.contentView.subviews { if let ret = view as? UISwitch { return ret } } return nil }
This is a classic for loop where an if let tries to cast the enumerated item as UISwitch, which is evaluated as false if the cast fails.
Variant 2a: filter
func switchInCell(cell: UITableViewCell) -> UISwitch? { return cell.contentView.subviews.filter { (view) -> Bool in return view is UISwitch }.first as? UISwitch }
Here the loop is carried out by the functional filter function. The block is repeated for each element in the subviews array and returns true if view is UISwitch. From this you get an array containing zero or all switches. We take the first element (or the last if we wanted to save one more character), but again we have to cast it into UISwitch for the function return.
Variant 2b: shorter filter
func switchInCell(cell: UITableViewCell) -> UISwitch? { return cell.contentView.subviews.filter { $0 is UISwitch }.first as? UISwitch }
Orta, who has a background in Ruby and functional programming, gets extra points for using the dollar parameters in daily use. You can use these in lieu of the parameter list. Shorter and slightly more elegant than 2a.
Variant 3: for+where
Orta also had his hand in this contender by suggesting to use a where clause in combination with for.
func switchInCell(cell: UITableViewCell) -> UISwitch? { for view in cell.contentView.subviews where view is UISwitch { return view as? UISwitch } }
This was the first suggestion that taught me something new. I knew about using where with if-let statements, but not that you could use it in conjunction with for. But so far we still have to use this ugly cast with the return.
Variant 4: flat map
This “wart” was remedied in the suggestion by Joseph Lord. A flat map to the rescue!
func switchInCell(cell: UITableViewCell) -> UISwitch? { cell.contentView.subviews.flatMap { $0 as? UISwitch }.first }
The flatMap infers the element type of the resulting array from the returned values. Magic! We don’t need a type cast anymore.
Variant 5: for-case-let
Boom! My mind was blown a third time by Andy Calderbank
func switchInCell(cell: UITableViewCell) -> UISwitch? { for case let retView as UISwitch in cell.contentView.subviews { return retView } }
No type cast, and you see a combination of words that you have to read several times to grasp the ingenuity: for case let as in. The case selector lets you do the for enumeration but filter out certain elements via a let. If variant 1 and 3 had a child, this would be it.
Variant 6: generic extension
Several people contributed to my final solution, the one I ended up using. The first person suggesting a generic approach was Alexsander Akers. Davide De Franceschi gave me the idea of extending CollectionType as opposed to UIView. I had used the approach of passing T.Type in generic methods previously when writing my own Parse SDK for use in a client project.
extension CollectionType { func firstElementOfType<T>(type: T.Type) -> T? { return flatMap { $0 as? T }.first } } func switchInCell(cell: UITableViewCell) -> UISwitch? { return cell.contentView.subviews.firstElementOfType(UISwitch) }
This takes flat map (variant 4) and makes it generic. This way any collection gains the ability to return the first element of a given type. And because of the flatMap the result already has the correct type, namely T.
Conclusion
We learned several new ways to pick out a specific element from a collection. I found the discussion exhilerating and uniquely instructive. In particular combining for + where and for + case let were new to me. I am sure that I will soon find more uses for these.
Playing Code Golf in Swift is not about brevity, but mostly about beauty, elegance and code readability. Because of this I ended up with an approach that reads like plain english to me: “from the cell’s content view’s subviews, get the first element of type UISwitch”. Anybody can understand that.
I am sure that this was not last last game of Swift Golf we played. Stay tuned on my Twitter @cocoanetics to take part in the next game we might play.
Categories: Recipes