Index getting nil in view Controllers - ios

Index of View controller in Navigation array always getting as nil. If i print arrayOfVCs, i can see the list of controllers still i am getting index as always nil
public func removeFromStack(controller : UIViewController) -> () {
if let currentWindow = UIApplication.shared.keyWindow {
let arrayOfVCs = (currentWindow.rootViewController as!
UINavigationController).viewControllers
if let index = arrayOfVCs.index(of: controller) {
(currentWindow.rootViewController as!
UINavigationController).viewControllers.remove(at: index)
}
}
}

Note that in an extension, self refers to the object that you are calling this extension method on, so you don't need to get the window, get the top VC and stuff like that. Just use self.
Also, you are passing UserProfileController.self into the method, which is not an object of view controller but a metatype. If you don't have an accessible VC instance and want to use the metatype to find the correct item to remove from the stack, you can change the extension to something like this:
extension UINavigationController {
func removeFromStack<T: UIViewController>(vcType: T.Type) {
if let index = viewControllers.index(where: { type(of: $0) == vcType }) {
viewControllers.remove(at: index)
} else {
print("Oops! The type of VC you specified is not in the stack!")
}
}
}

Related

Accessing Data Model in UITabBarController from a Struct

I have an app that uses the UITabBarController and I have a model which I use to pass information between the various Tabs
class BaseTBController: UITabBarController {
var title: String = ""
var valueData = Double()
override func viewDidLoad() {
super.viewDidLoad()
}
Normally within any of the Tabs in the TabBars, I can just do:
let tabbar = tabBarController as! BaseTBController
let valueData = Int(tabbar.valueData) == 0 ? Int(UserDefaults.standard.double(forKey: UDKeys.valueData)) : Int(tabbar.valueData)
Now, the situation I'm in is like this, I would like to use the data from the Tabbar model data in a helper function (as struct)
Doing this, it doesn't work. Wondering if there is a way to do this and my googling/SO searching skills are just not up to par. TX
struct DataFieldOptions{
static func showValueAs() -> String {
let tabbar = tabBarController as! BaseTBController
let valueData = Int(tabbar.valueData) == 0 ? Int(UserDefaults.standard.double(forKey: UDKeys.valueData)) : Int(tabbar.valueData)
return String(valueData)
}
This seems a bit of an odd approach. If you are adding a "helper" I would expect you'd have a data-manager class rather than using a var in the tab bar controller.
However, tabBarController is an Optional property of a view controller. If you want to access it from outside the view controller you need to give it a reference.
As a side note, you're showing -> String but you're returning an Int ... I'm going to guess you're really planning on formatting the value as a string in some way, so here's how to do it returning a string:
struct DataFieldOptions{
static func showValueAs(vcRef vc: UIViewController) -> String {
// safely make sure we have access to a tabBarController
// AND that it is actually a BaseTBController
if let tbc = vc.tabBarController as? BaseTBController {
let v = Int(tbc.valueData) == 0 ? Int(UserDefaults.standard.double(forKey: UDKeys.valueData)) : Int(tbc.valueData)
return "\(v)"
}
return ""
}
}
Then, from a view controller, you would call it like this:
let str = DataFieldOptions.showValueAs(vcRef: self)

filtering array of custom view controllers for property?

I have an array of viewControllers, various sub classes not all the same VC. I want to filter the array containing them all to identify that which matches my chosen filter.
Issue being that as its a variety of different types of VC being processed as just 'ViewControllers 'i cant filter by a clear property.
Is there an approach where I can define a property that will be on all the viewControllers and then filter by it?
a thought was i could sublass VC and then subclass the subclass for each controller, that way i can cast them all to that type and check that parameter
Here is the code: However i want to swap 'title' for a custom property
contentViewControllers = contentControllers()
if self.pushedTitle != nil && self.pushedID != nil, self.pushedPage != nil {
if let i = contentViewControllers.index(where: { $0.title == self.pushedPage }) {
return selectContentViewController(contentViewControllers[i])
}
edit: update on VC init
let text = TextViewController(textView: TextView.init(frame: UIScreen.main.bounds), pageID: page.pageid, pageTitle: page.title)
let navigationController = UINavigationController(rootViewController: text)
contentList.append(navigationController)
I think the best way to do this is to add a protocol like:
protocol PropertyProtocol {
var customProperty: String { get }
}
Then make your view controllers conform to the PropertyProtocol
class ViewController: UIViewController, PropertyProtocol {
var customProperty: String {
return "My property for view Controller"
}
...
}
Then you just do:
let i = contentViewControllers.index(where: { (viewController) -> Bool in
if let viewController = viewController as? PropertyProtocol {
return viewController.customProperty == pushedPage
}
return false
})
Of course, name your protocol to be more descriptive than in my case.
Since you already have a TextViewController subclass you can do:
let i = contentViewControllers.index(where: { (viewController) -> Bool in
var vc = viewController
if let navVC = viewController as? UINavigationController {
vc = navVC.viewControllers.first!
}
if let textViewController = vc as? TextViewController {
return textViewController.pageID == pushedPage
}
return false
})
Last option and the one maybe you should choose is add a new variable next to contentList - var viewControllerIndexes = [String : Int]() and then when you do:
contentList.append(navigationController)
viewControllerIndexList[page.pageid] = contentList.count - 1
Then when you need a index for a page ID you dont need to search but just do let index = viewControllerIndexList["myPageID"]

Create a UIViewController class instance from string (view controller string name) without using Storyboards or NIB/XIB?

I wanted to instantiate view controllers from push notifications, most tutorials seem to suggest passing the identifier for the VC and matching them up with the storyboardID.
Issue is my app is made programatically and so I cant add storyboard identifiers.
How can i resolve this programatically so i can reference a VC via a string?
Update:
My point is that if the notification parses a string of what VC to instantiate e.g. "Deashboard" i cant then load via identifier that matches that string, id need a massive switch statement for every possibility that then runs your code in each case
When a view controller is not associated with storybaord or nib file, it's (class's) default initiazer provides an instance of view controller.
Try this and See:
Objective-C
ViewController *viewController = [[ViewController alloc] init];
[self.navigationController pushViewController:viewController animated:true];
Swift
let viewController = ViewController()
navigationController?.pushViewController(viewController, animated: true)
Update
To convert your string class name into view controller, try following extension.
extension NSObject {
func viewControllerFromString(viewControllerName: String) -> UIViewController? {
if let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String {
print("CFBundleName - \(appName)")
if let viewControllerType = NSClassFromString("\(appName).\(viewControllerName)") as? UIViewController.Type {
return viewControllerType.init()
}
}
return nil
}
}
Now, get your view controller from class string
if let viewController = viewControllerFromString(viewControllerName: "ViewController") as? ViewController {
navigationController?.pushViewController(viewController, animated: true)
}
Similar functionality with String extension:
extension String {
func getViewController() -> UIViewController? {
if let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String {
print("CFBundleName - \(appName)")
if let viewControllerType = NSClassFromString("\(appName).\(self)") as? UIViewController.Type {
return viewControllerType.init()
}
}
return nil
}
}
if let viewController = "ViewController".getViewController() as? ViewController {
navigationController?.pushViewController(viewController, animated: true)
}
Add Extension to String:
extension String {
public func getViewController() -> UIViewController? {
if let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String {
if let viewControllerType = Bundle.main.classNamed("\(appName).\(self)") as? UIViewController.Type {
return viewControllerType.init()
}
}
return nil
}
}
Example:
let className = "CustomViewController"
if let controller = className.getViewController() {
navigationController?.pushViewController(controller, animated: true)
}

How can I get the name of presentingViewController around a UINavigationController in Swift 2?

In my application I have a modal view that can be called from two different viewControllers. Let’s call them MainViewController and DetailViewController. This modal view is also embedded in a UINavigationController.
I’m trying to do an if/else statement based off of which ViewController triggered the modal to appear.
The code that I currently have in the modal's ViewController is:
if presentingViewController is DetailTableViewController {
//Update a current Distance
if let managedObjectContext = (UIApplication.sharedApplication().delegate as? AppDelegate)?.managedObjectContext {
distance.name = name!
distance.length = length!
if let distanceImage = distanceImageView.image {
distance.image = UIImagePNGRepresentation(distanceImage)
}
do {
try managedObjectContext.save()
} catch {
print(error)
return
}
}
} else if presentingViewController is ViewController {
//Save a new Distance
if let managedObjectContext = (UIApplication.sharedApplication().delegate as? AppDelegate)?.managedObjectContext {
distance = NSEntityDescription.insertNewObjectForEntityForName("Distance", inManagedObjectContext: managedObjectContext) as! Distance
distance.name = name!
distance.length = length!
if let distanceImage = distanceImageView.image {
distance.image = UIImagePNGRepresentation(distanceImage)
}
do {
try managedObjectContext.save()
} catch {
print(error)
return
}
}
}
It appears though that the result of presentingViewController is only returning the UINavigationController that it’s embedded in. Is there some way that I can get around that controller and test against the view that segues to the UINavigationController in the first place?
I'm working with iOS 9 and Swift 2. Thanks in advance for any help!
I guess what you are presenting is navigationcontroller. So one can test for navigationcontrollers property i.e viewControllers which will return an array of view controllers i.e either MainViewController and DetailViewController. So here one can use filter operation for array to check the controller one wants to test.As shown below.
let controllerList = (presentingViewController as? UINavigationController).viewControllers
let isControllerExsist = controllerList.filter{$0 is MainViewController}
if isControllerExsist.count>0
{
print("isMainViewCntroller")
}
if it's between specifically only two view controllers you can pass a Bool to the destination view controller.
In your firstVC:
let destinationVC = DestinationVC()
// or storyboard.instantiateViewControllerWithIdentifier("destinationVC") as! DestinationVC
destinationVC.isDetail = true
showViewController(destinationVC)
In your destinationVC, in your -viewDidLoad:
if isDetail {
if let managedObjectContext = (UIApplication.sharedApplication().delegate as? AppDelegate)?.managedObjectContext {
distance.name = name!
distance.length = length!
if let distanceImage = distanceImageView.image {
distance.image = UIImagePNGRepresentation(distanceImage)
}
do {
try managedObjectContext.save()
} catch {
print(error)
return
}
}
} else {
//Save a new Distance
if let managedObjectContext = (UIApplication.sharedApplication().delegate as? AppDelegate)?.managedObjectContext {
distance = NSEntityDescription.insertNewObjectForEntityForName("Distance", inManagedObjectContext: managedObjectContext) as! Distance
distance.name = name!
distance.length = length!
if let distanceImage = distanceImageView.image {
distance.image = UIImagePNGRepresentation(distanceImage)
}
do {
try managedObjectContext.save()
} catch {
print(error)
return
}
}
}
and in your secondVC set the bool isDetail to false
I would recommend using prepareForSegue and populate a field on that view controller you're segueing to (make it a weak var!). If the view controller you're segueing to is the navigation controller, which it probably is, you'll need to make a custom navigation controller with the var, and then the presentingViewController will be this custom type, and will have the presenting view controller. Either that or in the navigation controller's prepareForSegue populate another var on your modal view.

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

Resources