Cast to a subclass from Storyboard - ios

Imagine i have a BaseViewController. Then i have 2 scenarios, New and Edit, where both shares the same UI and the most of logic. So i created class NewViewController and EditViewController, subclassing BaseViewController. The problem comes when i try to instantiate "BaseViewController" from the storyboard cause i want to specify which implementation is.
if isEdit {
storyboard.instantiateViewControllerWithIdentifier("baseVCIdentifier") as! EditViewController
} else {
storyboard.instantiateViewControllerWithIdentifier("baseVCIdentifier") as! NewViewController
}
Then i get an error:
Could not cast value of type 'Test.BaseViewController' (0x10ee5e0f0) to 'Test.EditViewController' (0x10ee5f000).
I dont wanto to have both ViewController on the storyboard since i dont want to redo the same UI 2 times.

You can do this using instantiateViewController(identifier:creator:).
I assume you have the view controller in a storyboard, with identifier template. The class assigned to the view controller in the storyboard should be the superclass:
let storyboard = UIStoryboard(name: "main", bundle: nil)
let viewController = storyboard.instantiateViewController(identifier: "template") { coder in
// this passes us with a coder for the storyboard, we can now init the preferred subclass.
if useSubclass {
return SpecialViewController(coder: coder)
} else {
return BaseViewController(coder: coder)
}
}
Here is the documentation

You can't do that. Instead of sub classing create 'interaction manager' classes, or state manager classes. The base view controller would then be provided with a manager instance as part of the segue and it would forward all UI interaction to the manager for processing. You then have a single VC in the storyboard as is required and you can supply a new or edit manager. The managers can also have specific instance variables which the view controller doesn't care about.

Related

programmatically changing views with Swift - not working

I have the following code which is intended to change views. This code is executed when my 'UITableViewRowAction' is selected and is intended to navigate to a different UITableViewController. However, I am getting the error below and I do not understand what I am doing incorrectly.
let storyBoard: UIStoryboard = UIStoryboard.init(name: "Main", bundle: nil)
let selectFolderViewController = storyBoard.instantiateViewController(withIdentifier: "SaveArticleTableViewController") as! SaveArticleTableViewController
self.navigationController?.pushViewController(selectFolderViewController, animated: true)
The error that i am getting in the console is:
Could not cast value of type 'UINavigationController' (0x106f5c898) to 'lifesci_PubMed.SaveArticleTableViewController' (0x104dbb658).
All my ViewControllers are embedded within a Navigation Controller
It's very likely that you have not properly set the custom class of the view controller in Storyboard. Here's an image ...
You would type "SaveArticleTableViewController" in that slot.
It's quite possible you have other problem as well, but that will get you going.
The error is actually quite descriptive here:
Could not cast value of type 'UINavigationController' to 'SaveArticleTableViewController'
At first I thought that this was because your view controller SaveArticleTableViewController is not actually set with a custom class. If you're using Interface Builder, make sure you've set the class type in the view controller's property inspector:
If you're doing it in code, make sure your custom class is inheriting properly.
But taking a leap of a guess, your target is "UINavigationController", but your custom class name that it's trying to cast to suggests that your target is a table view "SaveArticleTableViewController". Moreover, I'd bet that this is the tableview that is the source not the destination. I'd double check that you're actually instantiating the right view controller - typically you'd name this something like "SaveArticleDetailViewController", or guessing by your code, maybe it's "SelectFolderViewController", but that's up to you.
For example:
let storyBoard: UIStoryboard = UIStoryboard.init(name: "Main", bundle: nil)
let selectFolderViewController = storyBoard.instantiateViewController(withIdentifier: "SelectFolderViewController") as! SelectFolderViewController
self.navigationController?.pushViewController(selectFolderViewController, animated: true)
This also requires that you set self.navigationController to the containing nav controller.

In iOS, how to get the name of a UIStoryboard?

If I have reference to a UIStoryboard object, how do I get its name?
For example if I have
class ProfileVC: UIViewController {
override func viewDidLoad() {
let name = self.storyboard?.name // public .name property does not exist
let name = self.storyboard?.value(forKey: "name") // private property access works
}
}
Is there a way to get the name publicly? If not, why did Apple make this private?
Edit: Add use case
The app has multiple storyboards and there is navigation code that is able to navigate to a particular storyboard: navigate(to storyboardName: String). This code always instantiates a new storyboard object and its initial view controller. This is causing bugs in the UI.
I want to do a check to see if the window's root view controller's storyboard is the same as the destination storyboard to prevent re-instantiation.

Instantiation of ViewControllers from storyboard in Swift

I have a UIViewController that looks a bit like this:
class ProfileViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, ... {
...
convenience init(name: String) {
print("Init with student: \(name)")
self.init()
}
...
}
I have a corresponding Storyboard layout for this, embedded in a UINavigationViewController, linked to a UITabBarController. This seemed like the easiest way to design the layout, and is great for when there's only one instance of this VC required (which is how the app was originally designed).
I'd now like to create multiple tabs from this single design (between 1 and 3), and pass the VC init information programatically, but I'm unsure exactly of the best way to do this - as you can see above I've added a convenience init function based on some reading I've done as that seemed like a good place to start.
It's easy enough to create new named tabs based on the storyboard layout like this:
for user in (users)! {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "profileNC")
controller.tabBarItem.title = user.name
newTabs?.insert(controller, at: 0)
}
self.viewControllers = newTabs
But, doing it this way I don't get to call the init function to pass the UIViewController the information it needs to display correctly.
How can I create my ViewControllers, link the layout to the Storyboard and use the custom init function? Is this even possible?
Any suggestions gratefully appreciated!!
When using a storyboard you cannot use a custom initialiser. You will need to either set the property directly or use a configuration function on the view controller instance after you have created it.

How to pass data between Swift files without prepareForSegue?

I currently have three Swift files, one for the main view in a ViewController, and two more which are used for the two views within the first view, which are used for a Segmented Control.
As these don't use segues between each other, I can't use the prepareForSegue method to transfer data between them, so how do transfer the variables and such from one file to another?
This doesn't seem to be a duplicate as other cases such as the one commented are using segues, mine is not.
Are all three Swift classes view controller subclasses?
You have your main view controller with your segmented control. For each segmented, I would create a new view controller subclass.
On your main view controller, for each segment, use a 'Container View' instead of a UIView object.
This will create two new 'screens' in the storyboard, attached to your main view controller with a segue. These new screens will be UIViewControllers, you can change them to be your subclass's as normal.
You can now use your prepareForSegue function as normal to set data in your segmented control view controllers.
So you have something like viewControllerMain, which contains viewSegmentedOne and viewSegmentedTwo, and you want to be able to access `viewControllerMain.myProperty' ?
You can always navigate through the hierarchy to get parent views - but the easiest option could be to include a reference to viewControllerMain in each of the segmented controls
var myParentVC : ViewControllerMain?
then when you create the subviews
mySubView.myParentVC = self
If you are using storyboard for view controllers, then try like this:
let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Your_VC_Identifier");
viewController.Your_var = Your_value_to_assign
NOTE: Define Your_var in your ViewController class
You just need to create an instance of the view controller you want to display, this is easy such as calling on the storyboard instance (usually the presenting view controller has one) instantiateViewController(withIdentifier:), by using an identifier that you provide in the storyboard file.
One created you can pass the data you want by casting it to your view controller class and present it as you prefer.
One method would be using singleton class . https://cocoacasts.com/what-is-a-singleton-and-how-to-create-one-in-swift/ this is how you can make singleton class.
other method could be using nsuserdefaults.
You need to decide which approach is best according to your requirement.
try this:
let defaults = UserDefaults.standard()
defaults.set(yourdata, forKey: "someObject")
print(defaults.object(forKey: "someObject"))
You can try use Extensions for UIViewController
private var storedDataKey: UInt8 = 0
extension UIViewController {
var storedViewControllerData: UIViewController? {
get {
return objc_getAssociatedObject(self, &storedDataKey) as? UIViewController
}
set(newValue) {
objc_setAssociatedObject(self, &storedDataKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_ASSIGN)
}
}
}
This very useful you can send data with chain like:
viewControllerB.storedViewControllerData = viewControllerA.storedViewControllerData
or
func viewDidLoad() {
doSomething(self.storedViewControllerData)
}

How to access a view controller that is not the root view controller from AppDelegate.swift?

I think this is a beginner question. I currently have two view controllers:
class MainViewController: UIViewController {
var markersToDelete = Marker()
func deleteMarkers() {}
}
class MapViewController: UIViewController {
var markersToDelete = Marker()
func deleteMarkers() {}
}
The MapViewController is instantiated with a modal segue in the storyboard. I know that I can access MainViewController in AppDelegate.swift with this code:
var mainViewController = self.window!.rootViewController as! MainViewController
How can I do the same thing with MapViewController?
EDIT
Based on the comments below I am updating this to include more detail. Here is what I am trying to accomplish:
I have IBActions in both view controllers that appends Marker items to the
markersToDelete array. The method deleteMarkers is used to find the corresponding items in the Core Data persistent store and delete them; it is called by AppDelegate when applicationDidEnterBackground or when applicationWillTerminate.
This is the reason why I want to access both view controllers from app delegate. I am starting to think there could be a better way to do this.
Some other answers suggest ways of insantiating the controller, but they seem incorrect since you mentioned the controller is instantiated through a segue. There is no straight forward way of doing what you want, and for a good reason, it should be hard to access things from unrelated code. In your very simple example, this code would work, given the controllers have already been presented:
var mainViewController = self.window!.rootViewController as! MainViewController
let mapViewController = mainViewController.presentedViewController
But I would strongly discourage this type of code inside AppDelegate, it makes a lot of assumptions and will break easily if your navigation structure changes. Be more specific about what you're trying to achieve and you may get answers that lead you to a better architecture.
EDIT
Given the new information provided by the asker, this is a proposed solution to the more specific problem:
I understand you have Marker objects in your Core Data context, and you wish to batch delete some of them. What about adding a bool property to Marker class, and setting it to true for the objects you wish to delete. Then, in AppDelegate, you fetch the markers which have that property set to true, and delete them. This way you don't need to mantain an array, and there is no coupling between classes.
You can access your map controller by calling it's storyboard ID, which you'll need to set in Storyboard. Then just do:
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
var mapViewController = storyboard. instantiateViewControllerWithIdentifier("storyboardID") as! MapViewController
You need to set storyboard ID to your mapViewController. Then You can access it from appDelegate.
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main",bundle: nil)
var destViewController : UIViewController
destViewController = mainStoryboard.instantiateViewControllerWithIdentifier("YourViewController") as! UIViewController
You should start by encapsulating your code which is responsible for interaction with CoreData framework API.
Create a singleton class like below and use this class only for insertion/deletion/update operations. This way will ensure loose coupling among your view controllers .
You can get reference to the sharedInstance of DataManager in any class. . Using sharedInstance you can call methods related to your Database operations.
class DataManager {
class var sharedInstance : DataManager {
struct Singleton {
static let instance = DataManager()
}
return Singleton.instance
}
func deleteMarkers() {
//Your logic here
}
}

Resources