Kenji Crosland
Kenji Crosland

Reputation: 3034

Swift: Get all subviews of a specific type and add to an array

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?

Upvotes: 66

Views: 67711

Answers (12)

mojuba
mojuba

Reputation: 12207

A recursive one-liner for those who like functional style one-liners:

extension UIView {
    func subviews<T: UIView>(ofType type: T.Type) -> [T] {
        subviews.compactMap { $0 as? T } +
            subviews.flatMap { $0.subviews(ofType: type) }
    }
}

Upvotes: 2

Luca Iaco
Luca Iaco

Reputation: 3457

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 :)

Upvotes: -2

vadian
vadian

Reputation: 285039

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}

Upvotes: 119

matt
matt

Reputation: 534885

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)

Upvotes: 33

Mike Glukhov
Mike Glukhov

Reputation: 1830

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
    }
}

Upvotes: 0

Diego Carrera
Diego Carrera

Reputation: 2355

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 }

Upvotes: 8

Mustafa Alqudah
Mustafa Alqudah

Reputation: 49

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)

Upvotes: 3

Vinh Nguyen
Vinh Nguyen

Reputation: 826

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

Upvotes: 2

Mohammad Sadiq
Mohammad Sadiq

Reputation: 5241

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)

Upvotes: 47

ullstrm
ullstrm

Reputation: 10160

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)

Upvotes: 23

Zaid Pathan
Zaid Pathan

Reputation: 16820

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
}

Upvotes: 6

Kametrixom
Kametrixom

Reputation: 14973

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

Upvotes: 13

Related Questions