Accessing Data Model in UITabBarController from a Struct - ios

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)

Related

Swift NavigationBar Press "Back" to get values, why?

I am using some values to perform some calculations. For testing purposes I show in Label1 a value as string, since it is stored as a string and in Label2 I show a casted value as a Double since I need them at the end as doubles for my calculations.
The weird thing is, that when I access the ViewController the first time it doesn't show any values. But if I go back and klick on it again using the navigation controller it actually works. But I need the values right away cause my original intention is as I said, not showing some labels but rather making some calculations with it.
I made a little gif to show you what the problem is but I have problem with adding photos. Basically what happens is, that I click on the ViewController with the labels and nothing is showed. I go back and press again and the values will be showed in the labels.
Why is that and how can it be showed right away/ used for calculations right away
Thanks for the help. :)
class AHPfinalPreferencesViewController: UIViewController {
var ahpPrios = [AHPPriorityStruct]()
let decoder = JSONDecoder()
#IBOutlet weak var label1: UILabel!
#IBOutlet weak var label2: UILabel!
let ajkpXc = globaLajkpXc
let ajkpXijr = globaLajkpXijr
let valueA = globaLajkpXc
let valueB = Double(globaLajkpXijr)
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
UserService.ahpPref(for: User.current) { (ahpPrios) in
self.ahpPrios = ahpPrios
print("This is our AHP PRIOS", ahpPrios)
for ahpPrio in ahpPrios {
print(ahpPrio)
}
print("this is the global ajk. ", self.ajkpXc)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Mark: - Get Data
label1.text = valueA
label2.text = "\(String(describing: valueB))"
// MARK: - Set Values for calculation
// setValues()
// ahpCalculation()
}
}
Could it be because of the globalVariables? I know that it is not the right way to do it but for my purposes its absolutely "okay"
import Foundation
import FirebaseAuth.FIRUser
import FirebaseDatabase
import FirebaseUI
import FirebaseAuth
import CodableFirebase
var globaLajkpXc: String = String()
var globaLajkpXijr: String = String()
var globaLajkpXqpa: String = String()
struct UserService {
static func ahpPref(for user: User, completion: #escaping ([AHPPriorityStruct]) -> Void) {
let ref = Database.database().reference().child("AHPRatings").child(user.uid)
ref.observe(DataEventType.value, with: { snapshot in
guard let value = snapshot.value else { return }
do {
let ahpPrios = try FirebaseDecoder().decode(AHPPriorityStruct.self, from: value)
print(ahpPrios)
// MARK: - lets store the values in the actual constants :)
let ajkpXc = ahpPrios.ajkpXc
let ajkpXijr = ahpPrios.ajkpXijr
let ajkpXqpa = ahpPrios.ajkpXqpa
globaLajkpXc = ajkpXc ?? "no Value"
globaLajkpXijr = ajkpXijr ?? "no Value"
globaLajkpXqpa = ajkpXqpa ?? "no Value"
} catch let error {
print(error)
}
})
}
}
[1]: https://i.stack.imgur.com/VKxaE.png
You are calling UserService's ahpPref in your controller's viewWillAppear. BUT you are attempting to put your valueA (globaLajkpXc's value) to your label in your controller's viewDidLoad.
So what does that mean? Do you know which of these two controller's life cycle method gets called and when they do get called?
To solve your problem, have your label assigning value code
label1.text = globaLajkpXc
move in the completion block of your ahpPref (in the viewWillAppear).
Here's the Apple's documentation about the UIViewController's lifecycle: https://developer.apple.com/library/archive/referencelibrary/GettingStarted/DevelopiOSAppsSwift/WorkWithViewControllers.html
Also, below this line: globaLajkpXqpa = ajkpXqpa ?? "no Value"
add your completion call, like:
completion([ahpPrios]).
This should make my answer above work.

Index getting nil in view Controllers

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!")
}
}
}

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"]

iOS sharing data between viewcontrollers

I want to share some data (an array of custom objects)
from different ViewController, when tab changed.
1 = TabController
2 = ViewController
3 = ViewController
4 = SplitViewController
5 = MapView
6 = ViewController
7 = TableViewController
I want to share data between:
7 to 3, 7 to 2
What is the best way to do this?
You could do something like this:
class DataSource {
static let sharedInstance = DataSource()
var data: [AnyObject] = []
}
Usage:
DataSource.sharedInstance.data
Another simple solution is creating a view bag to hold data to be shared between VC:
import Foundation
class ViewBag
{
internal static var internalDictionary = Dictionary<String, AnyObject>()
class func get(key: String) -> AnyObject?
{
return internalDictionary[key]
}
class func add(key: String, data: AnyObject)
{
internalDictionary[key] = data
}
}
class MyClass
{
}
// Example
let myClassArray = [MyClass(),MyClass(),MyClass(),MyClass()]
ViewBag.add("myKey", data: myClassArray)
ViewBag.get("myKey")?.count // You must do a proper casting here
What's the data?A string?NSNotification is best.A few data?Save to NSUserDefaults.A lot of data?Save to file and read it.
Here is example code for find vc along view controller chain:
let vc7 = UIViewController()
let tabBarVC = vc7.splitViewController?.tabBarController
let vc2 = tabBarVC?.viewControllers?[1]
let vc3 = tabBarVC?.viewControllers?[2]

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