63

I have a custom class of buttons in a UIView that I'd like to add to an array so that they're easily accessible. Is there a way to get all subviews of a specific class and add it to an array in Swift?

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
Kenji Crosland
  • 2,914
  • 6
  • 31
  • 40

12 Answers12

117

The filter function using the is operator can filter items of a specific class.

let myViews = view.subviews.filter{$0 is MyButtonClass}

MyButtonClass is the custom class to be filtered for.

To filter and cast the view to the custom type use compactMap

let myViews = view.subviews.compactMap{$0 as? MyButtonClass}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • 2
    This worked, but the syntax was a little off: Should be let myViews = view.subViews.filter({$0 is MyButtonClass}) – Kenji Crosland Aug 29 '15 at 01:55
  • 4
    The syntax is called "Trailing Closure" symtax: *`If a closure expression is provided as the function’s only argument and you provide that expression as a trailing closure, you do not need to write a pair of parentheses () after the function’s name when you call the function.`* – vadian Aug 29 '15 at 04:53
  • I'm curious if there's a way to make this method chainable – Trip Mar 17 '16 at 19:00
  • 2
    It appears you can't use a trailing closure if it's called on an object that appears after "in" in a "for in" loop: for myButton in view.subViews.filter{$0 is MyButtonClass} { } has error: "Anonymous closure argument not contained in a closure" – cncool Jun 23 '16 at 02:12
  • top top answer!! – iOSer Mar 07 '18 at 05:14
  • 1
    This solution is not recursive, it only lists direct subviews. – Vilmir Jun 27 '19 at 13:04
  • @Vilmir Right, the question doesn’t mention *recursion*. – vadian Jun 27 '19 at 14:16
46

Here you go

    extension UIView {

    /** This is the function to get subViews of a view of a particular type 
*/
    func subViews<T : UIView>(type : T.Type) -> [T]{
        var all = [T]()
        for view in self.subviews {
            if let aView = view as? T{
                all.append(aView)
            }
        }
        return all
    }


/** This is a function to get subViews of a particular type from view recursively. It would look recursively in all subviews and return back the subviews of the type T */
        func allSubViewsOf<T : UIView>(type : T.Type) -> [T]{
            var all = [T]()
            func getSubview(view: UIView) {
                if let aView = view as? T{
                all.append(aView)
                }
                guard view.subviews.count>0 else { return }
                view.subviews.forEach{ getSubview(view: $0) }
            }
            getSubview(view: self)
            return all
        }
    }

You can call it like

let allSubviews = view.allSubViewsOf(type: UIView.self)
let allLabels = view.allSubViewsOf(type: UILabel.self)
Mohammad Sadiq
  • 5,070
  • 28
  • 29
32

So many of the answers here are unnecessarily verbose or insufficiently general. Here's how to get all subviews of a view, at any depth, that are of any desired class:

extension UIView {
    func subviews<T:UIView>(ofType WhatType:T.Type) -> [T] {
        var result = self.subviews.compactMap {$0 as? T}
        for sub in self.subviews {
            result.append(contentsOf: sub.subviews(ofType:WhatType))
        }
        return result
    }
}

How to use:

let arr = myView.subviews(ofType: MyButtonClass.self)
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 2
    For some reason I am getting the error : Instance method 'subViews(ofType:)' requires that 'UITextView.Type' inherit from 'UIView – humblePilgrim Jun 09 '20 at 16:25
  • @humblePilgrim Well UITextView _does_ inherit from UIView — you'd have to be in a pretty strange world where it didn't. Passing `UITextView.self` into that method works fine for me. – matt Jun 09 '20 at 17:19
  • 1
    @humblePilgrim true when using Swift 4.2, use this variation instead: func subviews(ofType WhatType: AnyClass) -> [T] { ... } – michael-martinez May 07 '21 at 09:04
  • just FYI I'd name this childViews (or something else) rather than subviews – Fattie Feb 15 '23 at 19:22
  • @Fattie I don't see why. There's no danger of confusion. – matt Feb 15 '23 at 19:48
23

To do this recursively (I.e. fetching all subview's views aswell), you can use this generic function:

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
13

I can't test it right now but this should work in Swift 2:

view.subviews.flatMap{ $0 as? YourView }

Which returns an array of YourView

Here's a tested, typical example, to get a count:

countDots = allDots!.view.subviews.flatMap{$0 as? Dot}.count
Fattie
  • 27,874
  • 70
  • 431
  • 719
Kametrixom
  • 14,673
  • 7
  • 45
  • 62
8

From Swift 4.1, you can use new compactMap (flatMap is now depcrecated): https://developer.apple.com/documentation/swift/sequence/2950916-compactmap (see examples inside)

In your case, you can use:

let buttons:[UIButton] = stackView.subviews.compactMap{ $0 as? UIButton }

And you can execute actions to all buttons using map:

let _ = stackView.subviews.compactMap{ $0 as? UIButton }.map { $0.isSelected = false }
Diego Carrera
  • 2,245
  • 1
  • 13
  • 16
6

If you want to update/access those specific subviews then use this,

for (index,button) in (view.subviews.filter{$0 is UIButton}).enumerated(){
    button.isHidden = false
}
Mohammad Zaid Pathan
  • 16,304
  • 7
  • 99
  • 130
4
func allSubViews(views: [UIView]) {
    for view in views {
        if let tf = view as? UITextField {
             // Do Something
        }
        self.allSubViews(views: view.subviews)
    }
}

self.allSubViews(views: self.view.subviews)
  • you're not exactly answering the OP's question. Your `func` should return an array of views, since that's what the OP asked; and why `UITextField`? `MyCustomButton` would be more appropriate. – dirkgroten Jan 03 '18 at 21:29
2

For this case, I think we could use Swift's first.where syntax, which is more efficient than filter.count, filter.isEmpty.

Because when we use filter, it will create a underlying array, thus not effective, imagine we have a large collection.

So just check if a view's subViews collection contains a specific kind of class, we can use this

let containsBannerViewKind = view.subviews.first(where: { $0 is BannerView }) != nil

which equivalent to: find me the first match to BannerView class in this view's subViews collection. So if this is true, we can carry out our further logic.

Reference: https://github.com/realm/SwiftLint/blob/master/Rules.md#first-where

Vinh Nguyen
  • 816
  • 1
  • 13
  • 27
0

Let me post my variation of this) but this, finds the first of T

extension UIView {

    func firstSubView<T: UIView>(ofType type: T.Type) -> T? {
        var resultView: T?
        for view in subviews {
            if let view = view as? T {
                resultView = view
                break
            }
            else {
                if let foundView = view.firstSubView(ofType: T.self) {
                    resultView = foundView
                    break
                }
            }
        }
        return resultView
    }
}
Mike Glukhov
  • 1,758
  • 19
  • 18
-1

Swift 5

func findViewInside<T>(views: [UIView]?, findView: [T] = [], findType: T.Type = T.self) -> [T] {
    var findView = findView
    let views = views ?? []
    guard views.count > .zero else { return findView }
    let firstView = views[0]
    var loopViews = views.dropFirst()
    
    if let typeView = firstView as? T {
        findView = findView + [typeView]
        return findViewInside(views: Array(loopViews), findView: findView)
    } else if firstView.subviews.count > .zero {
        firstView.subviews.forEach { loopViews.append($0) }
        return findViewInside(views: Array(loopViews), findView: findView)
    } else {
        return findViewInside(views: Array(loopViews), findView: findView)
    }
}

How to use:

findViewInside(views: (YourViews), findType: (YourType).self)
-2

I've gone through all the answers above, they cover the scenario where the views are currently displayed in the window, but don't provide those views which are in view controllers not shown in the window.

Based on @matt answers, I wrote the following function which recursively go through all the views, including the non visible view controllers, child view controllers, navigation controller view controllers, using the next responders

(Note: It can be definitively improved, as it adds more complexity on top of the recursion function. consider it as a proof of concept)

    /// Returns the array of subviews in the view hierarchy which match the provided type, including any hidden
    /// - Parameter type: the type filter
    /// - Returns: the resulting array of elements matching the given type
    func allSubviews<T:UIView>(of type:T.Type) -> [T] {
        var result = self.subviews.compactMap({$0 as? T})
        var subviews = self.subviews

        // *********** Start looking for non-visible view into view controllers ***********
        // Inspect also the non visible views on the same level
        var notVisibleViews = [UIView]()
        subviews.forEach { (v) in
            if let vc = v.next as? UIViewController {
                let childVCViews = vc.children.filter({$0.isViewLoaded && $0.view.window == nil }).compactMap({$0.view})
                notVisibleViews.append(contentsOf: childVCViews)
            }
            if let vc = v.next as? UINavigationController {
                let nvNavVC = vc.viewControllers.filter({$0.isViewLoaded && $0.view.window == nil })
                let navVCViews = nvNavVC.compactMap({$0.view})
                notVisibleViews.append(contentsOf: navVCViews)
                // detect child vc in not visible vc in the nav controller
                let childInNvNavVC = nvNavVC.compactMap({$0.children}).reduce([],+).compactMap({$0.view})
                notVisibleViews.append(contentsOf: childInNvNavVC)
            }
            if let vc = v.next as? UITabBarController {
                let tabViewControllers = vc.viewControllers?.filter({$0.isViewLoaded && $0.view.window == nil }) ?? [UIViewController]()
                // detect navigation controller in the hidden tab bar view controllers
                let vc1 = tabViewControllers.compactMap({$0 as? UINavigationController})
                vc1.forEach { (vc) in
                    let nvNavVC = vc.viewControllers.filter({$0.isViewLoaded && $0.view.window == nil })
                    let navVCViews = nvNavVC.compactMap({$0.view})
                    notVisibleViews.append(contentsOf: navVCViews)
                    // detect child vc in not visible vc in the nav controller
                    let childInNvNavVC = nvNavVC.compactMap({$0.children}).reduce([],+).compactMap({$0.view})
                    notVisibleViews.append(contentsOf: childInNvNavVC)
                }
                // ad non-navigation controller in the hidden tab bar view controllers
                let tabVCViews = tabViewControllers.compactMap({($0 as? UINavigationController) == nil ? $0.view : nil})
                notVisibleViews.append(contentsOf: tabVCViews)
            }
        }
        subviews.append(contentsOf: notVisibleViews.removingDuplicates())

        // *********** End looking for non-visible view into view controllers ***********

        subviews.forEach({result.append(contentsOf: $0.allSubviews(of: type))})

        return result.removingDuplicates()
    }

    extension Array where Element: Hashable {
        func removingDuplicates() -> [Element] {
            var dict = [Element: Bool]()
            return filter { dict.updateValue(true, forKey: $0) == nil }
        }
    }

Sample usage:

let allButtons = keyWindow.allSubviews(of: UIButton.self)

Note: If a modal view controller is currently presented, the above script does not find views which are contained in the presentingViewController. (Can be expanded for that, but I could not find an elegant way to achieve it, as this code is already not elegant by itself :/ )

Probably is not common to have this need, but maybe helps someone out there :)

Luca Iaco
  • 3,387
  • 1
  • 19
  • 20