34

Having a devil of a time trying to figure this out. I asked a similar question here: Swift: Get all subviews of a specific type and add to an array

While this works, I realized there are many subviews and sub-sub views, and so I need a function that starts at the main UIView, cycles through all the subviews (and their subviews until there aren't any left) and adds it to an array for a custom button class which I have named CheckCircle.

Essentially I'd like to end up with an array of CheckCircles which constitute all the CheckCircles added to that view programmatically.

Any ideas? Here's what I've been working on. It doesn't seem to be appending any Checkcircles to the array:

    func getSubviewsOfView(v:UIView) -> [CheckCircle] {
        var circleArray = [CheckCircle]()
        // Get the subviews of the view

        var subviews = v.subviews

        if subviews.count == 0 {
            return circleArray
        }

        for subview : AnyObject in subviews{
  if let viewToAppend = subview as? CheckCircle {
        circleArray.append(viewToAppend as CheckCircle)
      }
            getSubviewsOfView(subview as! UIView)
        }
        return circleArray
    }
Community
  • 1
  • 1
Kenji Crosland
  • 2,914
  • 6
  • 31
  • 40

7 Answers7

43

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

Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127
37

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)
shtnkgm
  • 1,396
  • 15
  • 17
33

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
}
Aaron Brager
  • 65,323
  • 19
  • 161
  • 287
  • Thank you. It worked. I decided to change one small part: if let viewToAppend = subview as? CheckCircle { circleArray.append(viewToAppend as CheckCircle) } – Kenji Crosland Aug 30 '15 at 21:55
  • 1
    @KenjiCrosland If you want to do that, I'd just do `if let subview = subview as? CheckCircle` and `append(subview)`. No need to rename it or cast inside the `if let` scope. – Aaron Brager Aug 30 '15 at 21:58
19

My approach with swift 3 and generics!

private func getSubviewsOf<T: UIView>(view: UIView) -> [T] {
    var subviews = [T]()

    for subview in view.subviews {
        subviews += getSubviewsOf(view: subview) as [T]

        if let subview = subview as? T {
            subviews.append(subview)
        }
    }

    return subviews
}

To fetch all UILabel's in a view hierarchy, just do this:

let allLabels: [UILabel] = getSubviewsOf(view: theView)
ullstrm
  • 9,812
  • 7
  • 52
  • 83
0

Based on Vasily Bodnarchuk's, Aaron Brager's, and ullstrm's answers.

Why another one?

I personally do not like to have as [XXX] and let specific: [Type], all over but rather pass the type into function calls, e.g.

let scrollViews = view.getNestedSubviews(ofType: UIScrollView.self)
print(scrollViews) // outputs: [UIScrollView]

I also renamed All to Nested because it conveys the recursive nature of the function to the API caller better.

Details

Swift 4.x, Xcode 9.1+

Solution

extension UIView {

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

    func getNestedSubviews<T: UIView>() -> [T] {
        return UIView.getNestedSubviews(view: self) as [T]
    }
}

Usage

let scrollViews = view.getNestedSubviews(ofType: UIScrollView.self)
print(scrollViews) // outputs: [UIScrollView]
Jon Willis
  • 6,993
  • 4
  • 43
  • 51
0

Find (private) Subviews By String:

I would like to add the possibility of detecting even private classes, hidden by Apple from us developers, in general finding classes by a given string:

func recursiveSubviews() -> [UIView] {

    if subviews.isEmpty {
        return subviews
    }
    return subviews + subviews.flatMap { $0.recursiveSubviews() }
}

func recursiveSubviews(className: String) -> [UIView] {
    let all = recursiveSubviews()
    let filtered = all.filter {
        String(describing: type(of: $0)) == className
    }
    return filtered
}

Now you can run:

activeWindow.recursiveSubviews(className: "_UIContextMenuContainerView")
blackjacx
  • 9,011
  • 7
  • 45
  • 56
-1

the UITextField is no longer on the top layer of subviews, so I use this method:

@implementation UISearchBar (changeFont)

- (void)setFont:(UIFont *)font {
    for (UIView *v in [self subviews]) {
        if ([v isKindOfClass:[UITextField class]]) {
            UITextField *tf = (UITextField *)v;
            tf.font = font;
            UILabel *l = (UILabel *)[tf valueForKey:@"placeholderLabel"];
            l.font = font;
            break;
        } else if (v.subviews.count) {
            for (UIView *v1 in v.subviews) {
                if ([v1 isKindOfClass:[UITextField class]]) {
                    UITextField *tf = (UITextField *)v1;
                    tf.font = font;
                    UILabel *l = (UILabel *)[tf valueForKey:@"placeholderLabel"];
                    l.font = font;
                    break;
                } else if (v1.subviews.count) {
                    for (UIView *v2 in v1.subviews) {
                        if ([v2 isKindOfClass:[UITextField class]]) {
                            UITextField *tf = (UITextField *)v2;
                            tf.font = font;
                            UILabel *l = (UILabel *)[tf valueForKey:@"placeholderLabel"];
                            l.font = font;
                            break;
                        }
                    }
                }
            }
        }
    }
}

a bit long winded, but should account for the textfield moving deeper in the future

Halpo
  • 2,982
  • 3
  • 25
  • 54