patternswiftMinor
Flatten to get all child controls of certain type in a UIView
Viewed 0 times
allcontrolstypeuiviewgetchildcertainflatten
Problem
extension Array {
func flatten(transform: (T) -> TResult?, childArray: (T) -> [T]) -> [TResult] {
var result = [TResult]()
for original in self {
let transformed = transform(original)
if transformed != nil {
result.append(transformed!)
}
result += childArray(original).flatten(transform, childArray: childArray)
}
return result
}
}Example usage to get all UILabels in a view (including it's subviews) :
let labels = someView.subviews.flatten(
{ $0 as? UILabel },
childArray: { $0.subviews })The code can probably be simplified/optimized, but how?
Solution
Let's start with the method and parameter names (this part is highly
opinion based :)
recursively a tree-like structure and maps the elements to some values.
Swift already has a
arrays and other types) which does a similar thing, but only one level
deep. Therefore I would call the method
Also the descendants of a node in a tree are usually called "children",
so I would choose that as name for the second parameter. (And perhaps
you want to generalize the code for other types, such as sequences.)
iterates of the array elements, therefore I would write that as
can be simplified with optional binding:
Then we have:
Now let's have a look how the method is called:
What strikes me is that you have to specify
In
If the method were called on
specify only once how to descend to the "child nodes".
This means that instead of an
(free) generic function which takes the "root of the tree" as the
first argument:
The for-loop can be replaced by the built-in
The function is called as
Finally, you can mark the two closure parameters with the
attribute. This is new in Swift 1.2 (described in the
Xcode 6.3 release notes) and tells the compiler that
the closure is used only during this function call and
does not outlive the lifetime of the call. This allows some
compiler optimizations:
More thoughts (inspired by this blog post):
Swift has built-in methods for the second and third task:
and
flattens the given object (in your case the tree of subviews):
and then use it as
The advantage is that you have a simpler function and a clear
separation into different tasks.
There is a small drawback: an array of all subviews is created first,
and then the labels are extracted. But that is probably tolerable if
the number of subviews is not too large.
(One could work around this problem by defining a method which returns
a "lazy sequence", posted separately as Recursive flattening of Swift sequences.)
opinion based :)
flatten() does not describe well what you method does: It traverses recursively a tree-like structure and maps the elements to some values.
Swift already has a
flatMap() function (and similar methods forarrays and other types) which does a similar thing, but only one level
deep. Therefore I would call the method
recursiveFlatMap().Also the descendants of a node in a tree are usually called "children",
so I would choose that as name for the second parameter. (And perhaps
you want to generalize the code for other types, such as sequences.)
for original in self { ... }iterates of the array elements, therefore I would write that as
for element in self { ... }let transformed = transform(original)
if transformed != nil {
result.append(transformed!)
}can be simplified with optional binding:
if let transformed = transform(element) {
result.append(transformed)
}Then we have:
extension Array {
func recursiveFlatMap(transform: (T) -> TResult?, children: (T) -> [T]) -> [TResult] {
var result = [TResult]()
for element in self {
if let transformed = transform(element) {
result.append(transformed)
}
result += children(element).recursiveFlatMap(transform, children: children)
}
return result
}
}Now let's have a look how the method is called:
let labels = someView.subviews.recursiveFlatMap(
{ $0 as? UILabel },
children: { $0.subviews })What strikes me is that you have to specify
subviews twice: In
someView.subviews and in $0.subviews.If the method were called on
someView itself then we would have tospecify only once how to descend to the "child nodes".
This means that instead of an
Array extension method we have a(free) generic function which takes the "root of the tree" as the
first argument:
func recursiveFlatMap(root: T, transform: (T) -> TResult?, children: (T) -> [T]) -> [TResult] {
var result = [TResult]()
if let value = transform(root) {
result.append(value)
}
for child in children(root) {
result += recursiveFlatMap(child, transform, children)
}
return result
}The for-loop can be replaced by the built-in
flatMap() method:func recursiveFlatMap(root: T, transform: (T) -> TResult?, children: (T) -> [T]) -> [TResult] {
var result = [TResult]()
if let value = transform(root) {
result.append(value)
}
result += children(root).flatMap( { recursiveFlatMap($0, transform, children) } )
return result
}The function is called as
let labels = recursiveFlatMap(someView,
{ $0 as? UILabel },
{ $0.subviews as! [UIView] })Finally, you can mark the two closure parameters with the
@noescapeattribute. This is new in Swift 1.2 (described in the
Xcode 6.3 release notes) and tells the compiler that
the closure is used only during this function call and
does not outlive the lifetime of the call. This allows some
compiler optimizations:
func recursiveFlatMap(root: T,
@noescape transform: (T) -> TResult?,
@noescape children: (T) -> [T]) -> [TResult]
{
var result = [TResult]()
if let value = transform(root) {
result.append(value)
}
result += children(root).flatMap( { recursiveFlatMap($0, transform, children) } )
return result
}More thoughts (inspired by this blog post):
recursiveFlatMap() does actually three things:- Recursively traverse a tree-like structure,
- transform each object, and
- select the objects for which the transformation succeeded.
Swift has built-in methods for the second and third task:
map()and
filter(). So you could just define a function that recursivelyflattens the given object (in your case the tree of subviews):
func recursiveFlatten(root: T, @noescape children: (T) -> [T]) -> [T] {
return [root] + children(root).flatMap( { recursiveFlatten($0, children) } )
}and then use it as
let labels = recursiveFlatten(someView, { $0.subviews as! [UIView] })
.filter( { $0 is UILabel } )
.map( { $0 as! UILabel } )The advantage is that you have a simpler function and a clear
separation into different tasks.
There is a small drawback: an array of all subviews is created first,
and then the labels are extracted. But that is probably tolerable if
the number of subviews is not too large.
(One could work around this problem by defining a method which returns
a "lazy sequence", posted separately as Recursive flattening of Swift sequences.)
Code Snippets
for original in self { ... }for element in self { ... }let transformed = transform(original)
if transformed != nil {
result.append(transformed!)
}if let transformed = transform(element) {
result.append(transformed)
}extension Array {
func recursiveFlatMap<TResult>(transform: (T) -> TResult?, children: (T) -> [T]) -> [TResult] {
var result = [TResult]()
for element in self {
if let transformed = transform(element) {
result.append(transformed)
}
result += children(element).recursiveFlatMap(transform, children: children)
}
return result
}
}Context
StackExchange Code Review Q#86860, answer score: 3
Revisions (0)
No revisions yet.