HiveBrain v1.2.0
Get Started
← Back to all entries
patternswiftMinor

Flatten to get all child controls of certain type in a UIView

Submitted by: @import:stackexchange-codereview··
0
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 :)

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 for
arrays 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 to
specify 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 @noescape
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:

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 recursively
flattens 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.