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

IosArraysSwiftClassSubview

Ios Problem Overview


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?

Ios Solutions


Solution 1 - Ios

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}

Solution 2 - Ios

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)

Solution 3 - Ios

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)

Solution 4 - Ios

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)

Solution 5 - Ios

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

Solution 6 - Ios

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 }

Solution 7 - Ios

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
}

Solution 8 - Ios

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)

Solution 9 - Ios

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

Solution 10 - Ios

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

Solution 11 - Ios

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

Solution 12 - Ios

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)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKenji CroslandView Question on Stackoverflow
Solution 1 - IosvadianView Answer on Stackoverflow
Solution 2 - IosMohammad SadiqView Answer on Stackoverflow
Solution 3 - IosmattView Answer on Stackoverflow
Solution 4 - IosullstrmView Answer on Stackoverflow
Solution 5 - IosKametrixomView Answer on Stackoverflow
Solution 6 - IosDiego CarreraView Answer on Stackoverflow
Solution 7 - IosMohammad Zaid PathanView Answer on Stackoverflow
Solution 8 - IosMustafa AlqudahView Answer on Stackoverflow
Solution 9 - IosVinh NguyenView Answer on Stackoverflow
Solution 10 - IosMike GlukhovView Answer on Stackoverflow
Solution 11 - IosLuca IacoView Answer on Stackoverflow
Solution 12 - IosSung Pyo ChoView Answer on Stackoverflow