Swift: Recursively cycle through all subviews to find a specific class and append to an array

Based on Aaron Brager and ullstrm answers

Details

  • Xcode 9.1, Swift 4,
  • Xcode Version 10.3 (10G8), Swift 5

Solution

extension UIView {

    class func getAllSubviews<T: UIView>(from parenView: UIView) -> [T] {
        return parenView.subviews.flatMap { subView -> [T] in
            var result = getAllSubviews(from: subView) as [T]
            if let view = subView as? T { result.append(view) }
            return result
        }
    }

    class func getAllSubviews(from parenView: UIView, types: [UIView.Type]) -> [UIView] {
        return parenView.subviews.flatMap { subView -> [UIView] in
            var result = getAllSubviews(from: subView) as [UIView]
            for type in types {
                if subView.classForCoder == type {
                    result.append(subView)
                    return result
                }
            }
            return result
        }
    }

    func getAllSubviews<T: UIView>() -> [T] { return UIView.getAllSubviews(from: self) as [T] }
    func get<T: UIView>(all type: T.Type) -> [T] { return UIView.getAllSubviews(from: self) as [T] }
    func get(all types: [UIView.Type]) -> [UIView] { return UIView.getAllSubviews(from: self, types: types) }
}

Usage sample

var allViews = UIView.getAllSubviews(from: simpleView)
func printResult(with text: String) {
    print("\n==============================================")
    print("\(text):\n\(allViews.map { $0.classForCoder } )")
}
printResult(with: "UIView.getAllSubviews(from: simpleView)")

allViews = UIView.getAllSubviews(from: simpleView) as [UILabel]
printResult(with: "UIView.getAllSubviews(from: simpleView) as [UILabel]")

allViews = UIView.getAllSubviews(from: simpleView, types: [UIStackView.self, UILabel.self])
printResult(with: "UIView.getAllSubviews(from: simpleView, types: [UIStackView.self, UILabel.self])")

allViews = simpleView.getAllSubviews()
printResult(with: "simpleView.getAllSubviews()")

allViews = simpleView.getAllSubviews() as [UILabel]
printResult(with: "simpleView.getAllSubviews() as [UILabel]")

allViews = simpleView.get(all: UILabel.self)
printResult(with: "simpleView.get(all: UILabel.self)")

allViews = simpleView.get(all: [UIStackView.self, UILabel.self])
printResult(with: "simpleView.get(all: [UIStackView.self, UILabel.self])")

Output of the sample

==============================================
UIView.getAllSubviews(from: simpleView):
[UILabel, UIButton, UILabel, UILabel, UILabel, UIStackView]

==============================================
UIView.getAllSubviews(from: simpleView) as [UILabel]:
[UILabel, UILabel, UILabel, UILabel]

==============================================
UIView.getAllSubviews(from: simpleView, types: [UIStackView.self, UILabel.self]):
[UILabel, UILabel, UILabel, UILabel, UIStackView]

==============================================
simpleView.getAllSubviews():
[UILabel, UIButton, UILabel, UILabel, UILabel, UIStackView]

==============================================
simpleView.getAllSubviews() as [UILabel]:
[UILabel, UILabel, UILabel, UILabel]

==============================================
simpleView.get(all: UILabel.self):
[UILabel, UILabel, UILabel, UILabel]

==============================================
simpleView.get(all: [UIStackView.self, UILabel.self]):
[UILabel, UILabel, UILabel, UILabel, UIStackView]

Storyboard of the sample

enter image description here

Other info

Also, I suggest to work with weak references. Array with weak references to objects


Your main problem is that when you call getSubviewsOfView(subview as! UIView) (recursively, within the function), you aren't doing anything with the result.

You also can delete the count == 0 check, since in that case the for…in loop will just be skipped. You also have a bunch of unnecessary casts

Assuming your desire is to get a flat array of CheckCircle instances, I think this adaptation of your code should work:

func getSubviewsOfView(v:UIView) -> [CheckCircle] {
    var circleArray = [CheckCircle]()

    for subview in v.subviews as! [UIView] {
        circleArray += getSubviewsOfView(subview)

        if subview is CheckCircle {
            circleArray.append(subview as! CheckCircle)
        }
    }

    return circleArray
}

You can implement it simply by extending UIView and defining the following functions.

Swift4 Code

extension UIView {
    func findViews<T: UIView>(subclassOf: T.Type) -> [T] {
        return recursiveSubviews.compactMap { $0 as? T }
    }

    var recursiveSubviews: [UIView] {
        return subviews + subviews.flatMap { $0.recursiveSubviews }
    }
}

Usage

findViews(subclassOf: UILabel.self)
findViews(subclassOf: CheckCircle.self)