Access var from rootViewController in CollectionViewCell in Swift - ios

I would like to access a variable from my rootViewController from within a different viewController (it‘s a CollectionViewCell).
window!.rootViewController = ViewController()
I declare the var like so:
import UIKit
class ViewController: UIViewController {
var testString : String = "Test";
override func viewDidLoad() {
[…]
And try to access it this way:
import UIKit
class MainCollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
[…]
super.init(frame: frame)
let mainView = self.window!.rootViewController
var testStringFromMainView = mainView.test
[…]
But all I keep getting is:
Type of expression is ambiguous without more context
Strange thing is, when I try for example
mainView.view.backgroundColor = UIColor.redColor()
it works.
I can’t figure out what I am doing wrong. Any help is appreciated!

You must conditionally cast the rootViewController. Your current code only knows that it is a UIViewController, but in order for it to use your variable, it needs to know that it is an instance of your subclassed view controller, ViewController.
Replacing your MainCollectionViewCell.init with this should fix the problem:
if let cvc = self.window!.rootViewController as? ViewController {
var testStringFromMainView = cvc.test
}
Please note that due to the conditional unwrapping, which is much safer than forced unwrapping, this code will not be executed if the rootViewController is not an instance of class ViewController. In other words, you need to look into global variables if your app will have multiple view controllers.

It doesn't work because rootViewController is a type of UIViewController and doesn't have a test property. Anyway, that doesn't matter as you shouldn't be trying to do what you're trying to do - it isn't appropriate for the cell to be trying to navigate up to the root view controller. Anything you need in the cell should be passed (see dependency injection) from the root view controller 'down' to through the view controllers to the cell. In this way your code is logical and the dependencies are obvious. What you're trying to do is hide a dependency in the bottom of your view hierarchy. You can make it work (with a cast), but you shouldn't.

Related

How to check if a view controller is of type UIHostingController

i have a UIHostingController that is hosting a SwiftUI view called CatalogView.
when showing it, an environment object is attached, so basically from UIKit it is shown like this:
let rootCatalogView = CatalogView()
let appState = AppState.get()
let catalogView = UIHostingController(rootView: rootCatalogView.environmentObject(appState))
navigationController.pushViewController(catalogView, animated: true)
now at a later time i need to check if this UIHostingController is in the list of navigationController.viewControllers
the type(of:) is showing the following, which kind of make sense:
UIHostingController<ModifiedContent<CatalogView, _EnvironmentKeyWritingModifier<Optional<AppState>>>>
things like vc.self is UIHostingController.Type or vc.self is UIHostingController< CatalogView >.Type both return false (vc is an element of navigationController.viewControllers
the following obviously works, it returns true, but any change in the initialisation of the UIHostingController will change its type
vc.isKind(of: UIHostingController<ModifiedContent<CatalogView, _EnvironmentKeyWritingModifier<Optional<StoreManager>>>>.self)
how can i check if the view controller is of type UIHostingController?
or at least how can i cast the controller to UIHostingController so that i can check its rootview?
Yes, you can't cast to generic class, but you can declare protocol and implement it only for UIHostingViewController
private protocol AnyUIHostingViewController: AnyObject {}
extension UIHostingController: AnyUIHostingViewController {}
and somewhere in your code
func justMakeMeHappy(viewController: UIViewController) {
if viewController is AnyUIHostingViewController {
print("That is hosting view controller")
} else {
print("That s not uihosting view controller 🤣")
}
}
Due to the generic parameter, we cannot cast the ViewController to find if it is a UIHostingController without knowing the full constraint.
I should note that this it not an ideal fix and it is really just a work around.
UIHostingController is a subclass of UIViewController so we could do the following.
Create a computed property on UIViewController that returns the name of the class that is used to create UIViewController. This gives us something to search for in the list of ViewControllers contained in the UINavigationController
extension UIViewController {
var className: String {
String(describing: Self.self)
}
}
Create a few UIViewController subclasses and our UIHostingController
class FirstViewController: UIViewController {}
class SecondViewController: UIViewController {}
class MyHostingController<Content>: UIHostingController<Content> where Content : View {}
let first = FirstViewController()
let second = SecondViewController()
let hosting = UIHostingController(rootView: Text("I'm in a hosting controller"))
let myHosting = MyHostingController(rootView: Text("My hosting vc"))
We can then add these to a UINavigationController.
let nav = UINavigationController(rootViewController: first)
nav.pushViewController(second, animated: false)
nav.pushViewController(hosting, animated: false)
nav.pushViewController(myHosting, animated: false)
Now that we have some ViewControllers inside our UINavigationController we can now iterate across them and find a ViewController that has a className that contains what we are looking for.
for vc in nav.viewControllers {
print(vc.className)
}
This would print the following to the console:
FirstViewController
SecondViewController
UIHostingController<Text>
MyHostingController<Text>
You can then for-where to find the ViewController in the hierarchy.
for vc in nav.viewControllers where vc.className.contains("UIHostingController") {
// code that should run if its class is UIHostingController
print(vc.className)
}
for vc in nav.viewControllers where vc.className.contains("MyHostingController") {
// code that should run if its class is MyHostingController
print(vc.className)
}
As I said above, this is not an ideal solution but it may help you until there is a a better way of casting without knowing the generic constraint.

Create a UIViewController concrete class from its type

I wish to create a concrete class from UIViewController type, something like this
func create(with type : UIViewController.Type)->UIViewController{
return type.init(coder: NSCoder())!
}
Apparently, UIViewController's designated initializer is only init(coder : NSCoder). And, when I try to pass in NSCoder() (as shown in the above case), the app crashes.
Anyone knows a better solution in creating a UIViewController concrete class from its type? Or am I pass in the wrong NSCoder in this case?
Code completion does not show this option, but this compiles and runs without a problem:
func create(with type : UIViewController.Type) -> UIViewController {
return type.init()
}
If you just want to create the view controller programmatically or from a XIB then just use the base constructor.
let viewController = MyViewController()
If you have a XIB with the same file name as the class then it will load it automatically.
If you want to load it from a storyboard then you need to define an identifier for the view controller in the storyboard and then call:
let storyboard = UIStoryboard(name: "storyboardName", bundle: nil)
storyboard.instantiateViewController(withIdentifier: "myViewControllerIdentifier")
EDIT
If you want a special create function, you can actually create an extension for UIViewController like this:
extension UIViewController {
static func create() -> Self {
return self.init()
}
}
Then you can call let myViewController = MyViewController.create(). However, unless you want to do something special in that create function it's a bit unnecessary.

Pass data to View Controller embedded inside a Container View Controller

My view controller hierarchy is the following:
The entry point is a UINavigationController, whose root view controller is a usual UITableViewController. The Table View presents a list of letters.
When the user taps on a cell, a push segue is triggered, and the view transitions to ContainerViewController. It contains an embedded ContentViewController, whose role is to present the selected letter on screen.
The Content View Controller stores the letter to be shown as a property letter: String, which should be set before its view is pushed on screen.
class ContentViewController: UIViewController {
var letter = "-"
#IBOutlet private weak var label: UILabel!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
label.text = letter
}
}
On the contrary, the Container View Controller should not know anything about the letter (content-unaware), since I'm trying to build it as reusable as possible.
class ContainerViewController: UIViewController {
var contentViewController: ContentViewController? {
return childViewControllers.first as? ContentViewController
}
}
I tried to write prepareForSegue() in my Table View Controller accordingly :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
// Not executed:
containerViewController.contentViewController?.letter = letter
}
}
but contentViewController is not yet created by the time this method is called, and the letter property is never set.
It is worth mentioning that this does work when the segue's destination view controller is set directly on the Content View Controller -- after updating prepareForSegue() accordingly.
Do you have any idea how to achieve this?
Actually I feel like the correct solution is to rely on programmatic instantiation of the content view, and this is what I chose after careful and thorough thoughts.
Here are the steps that I followed:
The Table View Controller has a push segue set to ContainerViewController in the storyboard. It still gets performed when the user taps on a cell.
I removed the embed segue from the Container View to the ContentViewController in the storyboard, and I added an IB Outlet to that Container View in my class.
I set a storyboard ID to the Content View Controller, say… ContentViewController, so that we can instantiate it programmatically in due time.
I implemented a custom Container View Controller, as described in Apple's View Controller Programming Guide. Now my ContainerViewController.swift looks like (most of the code install and removes the layout constraints):
class ContainerViewController: UIViewController {
var contentViewController: UIViewController? {
willSet {
setContentViewController(newValue)
}
}
#IBOutlet private weak var containerView: UIView!
private var constraints = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
setContentViewController(contentViewController)
}
private func setContentViewController(newContentViewController: UIViewController?) {
guard isViewLoaded() else { return }
if let previousContentViewController = contentViewController {
previousContentViewController.willMoveToParentViewController(nil)
containerView.removeConstraints(constraints)
previousContentViewController.view.removeFromSuperview()
previousContentViewController.removeFromParentViewController()
}
if let newContentViewController = newContentViewController {
let newView = newContentViewController.view
addChildViewController(newContentViewController)
containerView.addSubview(newView)
newView.frame = containerView.bounds
constraints.append(newView.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor))
constraints.append(newView.topAnchor.constraintEqualToAnchor(containerView.topAnchor))
constraints.append(newView.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor))
constraints.append(newView.bottomAnchor.constraintEqualToAnchor(containerView.bottomAnchor))
constraints.forEach { $0.active = true }
newContentViewController.didMoveToParentViewController(self)
}
} }
In my LetterTableViewController class, I instantiate and setup my Content View Controller, which is added to the Container's child view controllers. Here is the code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
if let viewController = storyboard?.instantiateViewControllerWithIdentifier("ContentViewController"),
let contentViewController = viewController as? ContentViewController {
contentViewController.letter = letter
containerViewController.contentViewController = contentViewController
}
}
}
This works perfectly, with an entirely content-agnostic container view controller. By the way, it used to be the way one instantiated a UITabBarController or a UINavigationController along with its children, in the appDidFinishLaunching:withOptions: delegate method.
The only downside of this I can see: the UI flow ne longer appears explicitly on the storyboard.
The only way I can think of is to add delegation so that your tableViewController implements a protocol with one method to return the letter; then you have containerViewController setting its childViewController (the contentViewController) delegate to its parent. And the contentViewController can finally ask its delegate for the letter.
At your current solution the presenting object itself is responsible for working both with the "container" and the "content", it doesn't have to be changed, but such solution not only has the issues like the one you described, but also makes the purpose of the "container" not very clear.
Look at the UIAlertController: you are not configuring its child view controller directly, you are not even supposed to know it exists when using the alert controller. Instead of configuring the "content", you are configuring the "container" which is aware of the content interfaces, lifecycle and behavior and doesn't expose it. Following this approach you achieve a properly divided responsibility of the container and content, minimal exposure of the "content" allows you to update the "container" without a need to update the way it is used.
In short, instead of trying to configure everything from a single place, make it so you configure only the "container" and let it configure the "content" when and where it is needed. E.g. in the scenario you described the "container" would set data for the "content" whenever it initializes the child controllers. I'm using "container" and "content" instead of ContainerViewController and ContentViewController because the solution is not strictly based on the controllers because you might as well replace it wth NSObject + UIView or UIWindow.

How to access the master's tableview from detail view in iOS

A portion of my app has an embedded master-detail section. Each detail view is using a custom UIViewController. When I change the value of something inside one of these UIViewControllers I need to be able to grey out one of the table rows in the master UITableViewController.
The closest I have seen to a solution is to use NSNotificationCenter to bubble up any changes, though this feels a little untidy..
Another solution is to use delegates? But I haven't come across any example solutions or tutorials in how to use this in Swift?
I've also experimented just trying to access the table view by navigating back up the hierarchy:
let navController = self.splitViewController!.viewControllers[0];
navController.tableView.reloadData()
I know the example above is wrong, but I don't know how to access the master view that way, or even if it is the right approach.
Oh, I am trying to call reloadData() because in the master view there is some logic which checks the condition as to wether to grey out a table row is applicable (i'm using Core Data)
I've seen that you figured this one out already. However a cleaner and more future proof way would be to use a delegate protocol:
protocol DetailViewControllerDelegate: class {
func reloadTableView()
}
Then add a delegate property to your DetailViewController class and implement the call to the delegate:
class DetailViewController: UIViewController {
weak var delegate: DetailViewControllerDelegate?
....
func reloadMasterTableView() {
delegate?.reloadTableView()
}
}
And then in your MainViewController implement the delegate method:
extension MainViewController: DetailViewControllerDelegate {
func reloadTableView() {
tableView.reloadData()
}
}
Don't forget to set the delegate on your DetailViewController instances when you create them:
let detailViewController = DetailViewController()
detailViewController.delegate = self
I would suggest you use NSNotificationCenter .
If you want to to do it via Navigation controller here is to code should work for you in swift.
let navController: UINavigationController = self.splitViewController!.viewControllers[0] as! UINavigationController
let controller: MasterViewController = navController.topViewController as! MasterViewController
controller.tableView.reloadData()
Since I was able to access my viewController, I was able to access the parent viewcontroller like so:
func reloadMasterTableView(){
let navVC: UINavigationController = self.splitViewController!.viewControllers[0] as! UINavigationController
let sectionsVC : UIMasterViewController = navVC.topViewController as! UIMasterViewController
sectionsVC.tableView.reloadData()
}

Mock UIViewController defined in Storyboard

I have a UIViewController-class instantiated via Storyboard that contains a constant property. For testing, I want to replace/mock whatever the value of the view controller.
I can actually do this by subclassing and defining a new constant and by overriding the methods that use it. However, I do not know how to instantiate the ViewController, since it's not in the storyboard.
It's important that all views and all other functionality of the original ViewController is still present, of course. How to go about it?
If i understand you need to access a property from another ViewController outside your Storyboard without presenting it. Since you're using swift, all you need to do is instantiate the class itself i believe. For example if the ViewController that is not in the storyboard has a class named "SecondController", and the variable inside second controller is called "stringVar" then all you need to do is this:
var secondVC = SecondController()
secondVC.stringVar = "new string value"
Example:
//SecondVC
import UIKit
class SecondViewController: UIViewController {
var something:String! = "String Value";
}
//Main VC
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var secondVC = SecondViewController()
secondVC.something = "Another String Value"
println(secondVC.something)
}
}

Resources