Swift Tricks: Searching for Objects by Type
I was working on an app a few days ago, and managed to nerd snipe myself within a single line of Swift code. I thought it’d make a good short writeup.
But first, some background: in the app, we had a very basic tree structure of objects. Each one had a parent and some children, and each was potentially of a different concrete type. For the sake of discussion, think of a UIView hierarchy: the parent is the superview, the children are subviews, and each view might be any arbitrary subclass of UIView.
My goal in this instance was to find the first child of a certain parent of a
particular type, and get it returned to me as that type. (Let’s call the type
SpecialView
.) My first attempt looked something like this:
var specialView: SpecialView? {
return view.subviews.first(where: { $0 is SpecialView }) as! SpecialView
}
This rubbed me the wrong way, specifically because of the is SpecialView
/as!
SpecialView
combination. It seemed like I should be able to do the type check
once and get the object back as that type, maybe using as?
.
Thankfully, there’s a Swift function for that: flatMap
. It’s very
similar to map
, in that it applies a closure to every element in a
collection, but differs in that it drops nil
s out of the result. This led me
to my second revision:
var specialView: SpecialView? {
return view.subviews.flatMap({ $0 as? SpecialView }).first
}
The nice thing about this approach is that the collection returned from
flatMap
is of type [SpecialView]
, so the trailing call to first
provides a
view of the type I want.
However, there’s a cost here: where previously we’d stop enumerating subviews
once we found the first SpecialView, with flatMap
we run all the way through
the array before getting the first result. If subviews
is an especially large
array, this could become a performance hotspot very quickly.
Once again, the Swift standard library comes to the rescue, this time with
lazy
. This handy property returns a special collection that
reimplements flatMap
(along with several other related functions) so that they
don’t perform all their work right away. Instead, this lazy collection can store
the closure we pass to it, evaluating it only when a later call needs a result –
like first
.
This brought me to the implementation I settled on:
var specialView: SpecialView? {
return view.subviews.lazy.flatMap({ $0 as? SpecialView }).first
}
It’s very similar to the previous iteration, but the strategic application of
lazy
saves work in the case that subviews
is very large (and the SpecialView
isn’t at the end of the array). It also reads fairly concisely, even if you’re
unsure what lazy
does – and the phrasing of “lazy flat map” lends itself well
to searching documentation or other resources.
I’m always interested in other opinions about these sorts of problems! Let me
know on Twitter how you’d write this expression, or about other neat
applications of lazy
.