Programmatically modifications will not apply to children class (viewController) - ios

I my iOS application maybe I need to override some viewControllers so I developed the following method to discover if the override ViewController exists I use it otherwise use from the original viewController.
internal class func getOverride<T: RBaseViewController>(id: String, mClass: AnyClass) -> T
{
let storyBoard = UIStoryboard(name: RConstants.Static.MAIN_STORYBOARD_NAME, bundle: nil)
let originalViewController = storyBoard.instantiateViewControllerWithIdentifier(id) as! T
var className = NSStringFromClass(mClass)
// Removing the prefix of class name if exists
if let index: String.Index = className.rangeOfString(".")?.startIndex.successor()
{
className = className.substringFromIndex(index)
}
// Override class name
var overrideClassName = className! + RConstants.Static.CLASSES_OVERRIDE_SUFFIX
if let mClass: AnyClass = NSClassFromString(overrideClassName)
{
RLog.debug("View controller override found")
// ViewController is overrode
if let overeideClass: T.Type = mClass as? T.Type
{
// Create instance and return
let overrodeViewController = overeideClass()
let view = originalViewController.view
originalViewController.view = nil
overrodeViewController.view = view
return overrodeViewController
}
}
return originalViewController
}
The Override viewController extends from the original one and just maybe some methods will change.
But the issue is that the modifications that I applied on the viewController is not affecting the overrode viewController.
For example in the parent viewController class I have the following code :
override func viewDidLoad()
{
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
title = "Map"
}
But in the Child viewController the title is empty, This issue is not just for title, Anything that is changed in the main viewController programmatically will not affect the child viewController.
Any suggestion ?

I found the issue.
By using xibfiles instead of storyboards this issue has fixed.
Seems that this is not possible to change the controller of a storyboard view.
in The above question, When I had set the view of originalViewController to nil all and set the new view to overrode view controller so the new view is not the same as old one, It's just a copy of it, So all the modifications shouldn't apply on the copy of it.

Related

Pass data from Parent UIviewController to a Container (ChildViewController) every time the value is changed

I have a UIViewController that has two containers and each Container is related to a UIViewController for some specific functionalities.
For people who are devaluating my question, it will be more helpful and appreciated if you put me on the right path instead.
what I am trying to do is pass data from the parent ViewController to the childViewControllers
I tried it using the protocol/delegate: But the problem is, I
couldn't assign the delegate to the childViewContainer since it
doesn't have an instance from the parent.
My second try was using the prepare function, but it doesn't work as well since the two containers load once the parent loads at first. so if the value is changed in the parentViewController I can't pass it again to the child.
Any Idea, please?
After a deeper digging I was able to find a solution for my own question.
here I am going to post if anyone else needs it in the future
so, first of all, I need it to lunch the ChildContoller from the parent controller and not from the storyboard ( so I deleted the segue between the parent and the child.
create a variable for childController like that:
lazy var firstChildViewController: FirstChildViewController = {
let storynoard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storynoard.instantiateViewController(identifier: "firstChild") as! FirstChildViewController
self.addChild(viewController)
self.view.addSubview(viewController.view)
return viewController
}()
same thing for the other one if you have two children
and then in the viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
firstChildViewController.view.isHidden = false
secondChildViewController.view.isHidden = true
}
and then in the FirstChildViewController:
override func viewDidLoad() {
super.viewDidLoad()
if let parent = self.parent as? ParentViewController {
parent.delegate = self
}
}
And the problem is solved
Hope it helps someone

get Nil when calling for IBOutlet properties in function

i’m working in swift and i’m trying to use the .frames to check if 2 objects of type CGRect intersect.
i have my View Controller Class and a CircleClass, the CircleClass creates a circle that has gesture recognition so i can drag the circles that i create where i want to, now i want to add the option that if at the end of the drag the subview intersects my trashimageView (image view that will always be in the low-right corner of the view for all devices it's like a trashcan) it can delete the circle or subView.
the problem is that when i try to call trashImageView.frame in a function “deleteSubView” that i’ve created in the View Controller i get nil and my app crashes.
But if the IBOutlet is in the VC and my function is defined in my VC, also i can call the trashImageView.frame (CGRect Value) in the viewDidLoad and there is fine, but not in my function, why do i get nil for this value??
class ViewController: UIViewController {
#IBOutlet var trashImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
//here i can print the CGRect value just fine
print("my imageView init: \(trashImageView.frame)")
}
func deleteSubView(subView: UIView){
// Here i get nil from the trashImageView.frame
if (subView.frame.intersects(trashImageView.frame)) {
print("intersection")
subView.removeFromSuperview()
}
}
}
i've checked that the Nil value is from the 'trashImageView.frame' and that the connection with the storyboard is good.
i call the function ‘delete subView’ from another class but should that matter? i don’t understand what is the error here, why do i get nil? help please.
Since your UIViewController is declared and instantiated using storyboard my guess is that you are creating the view controller using it's no arg initializer, i.e.: let controller = MyController() if you must create an instance of the controller programmatically do so by obtaining a reference to the Storyboard that contains the controller, i.e like this:
NOTE: Here I'm using "MyController" as the name of the class and the identifier that has been set in the storyboard.
func createMyController() -> MyController {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "MyController")
return controller as! MyController
}
I'd also add a guard for view load state in your deleteSubview(:subView) method, so something like this:
func deleteSubView(subView: UIView) {
guard isViewLoaded else { return }
// Here i get nil from the trashImageView.frame
if (subView.frame.intersects(trashImageView.frame)) {
print("intersection")
subView.removeFromSuperview()
}
}

Why do view objects of the destination ViewController default to nil in prepareForSegue?

I was wondering if someone could explain why the outlets that are view objects of the destination ViewController in the prepareForSegue() function are set to nil. I can only guess that it means that at the time PrepareForView is called, these objects are not created yet. Wouldn't it make sense though that by the time you have your destination ViewController object, you'd have the view object outlets associated with it initialized as well? I also know it's probably not good practice to directly modify the values of another ViewController's view, but I just want to understand the inner workings of Swift 3 better. Thanks!
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ItemController"
{
let indexPath = tableView.indexPathForSelectedRow?.row
if let itemStruct = itemList[indexPath!] as ItemStruct?
{
let correspondingItemController : ItemController = segue.destination as! ItemController
if let textView = correspondingItemController.textView
{
print("This is not nil!") //this will NOT get hit. Why is this still nil??
}
correspondingItemController.itemStruct = itemStruct
}
}
}
As you said at the time of prepareForSegue outlet objects are not yet created. ViewController loads/creates its view when the view property is accessed. When prepareForSegue is being called your destination view controller is intantiated but its view is yet to be loaded. You can force controller to load its view from prepareForSegue by accessing view property.
let correspondingItemController : ItemController = segue.destination as! ItemController
let _ = correspondingItemController.view // Forces controller to load its view.
Now you can access your outlets, but its not recommended. A good approach would be create variable in destination controller, set its value from prepareForSegue
// prepareForSegue
correspondingItemController.name = "something"
// Destination controller
var name:String
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
textView.text = name
}

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 Pass data between tabBarControllers

I am currently trying to pass data between two UINavigationControllers with a UITableViewController attached to each. I am navigating between these two controllers via a UITabBarController. I have been trying to use vacawama's solution on Changing VC issue in Swift. How to pass data between views in tab bar controller? I am using the following code.
import UIKit
class placeData: Equatable {
var description : String
var selected : Bool
init (description : String, selected : Bool) {
self.description = description
self.selected = selected
}
}
class PlacesTabBarController: UITabBarController {
var placeDataArray = [placeData]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
This is in the tabBarController custom class. In the other two controllers I declare the array I want to be pass between the view controllers and use the method in the link to populate the UITableViewController
var placeDataArray = [placeData]()
override func viewWillAppear(animated: Bool) {
placeDataArray = (self.tabBarController as PlacesTabBarController).placeDataArray
}
When the controllers load, however, the arrays are empty. In the example in the link, all the rest of the code is within the viewWillAppear function, where I need my array to be available to all of the corresponding tableView functions. I am not sure if I am just equating the arrays to zero on load. But my thought was that they would repopulate. Not sure what the correct way to go about this is. Any help would be great.
EDIT: My current structure is as follows:
UITabBarController
| |
UINavigation UINavigation
Controller Controller
| |
UITableView UITableView
Controller Controller
The array is populated when the first tab loads. What I am trying to do is have the populated array in the second view controller, and if I edit it in the second view controller, I want the edits to stay in the first. So I want it to be passed by reference.
I would subclass the UITabBarController and make it a delegate for the two UITableViewControllers.
CustomTabBarController
protocol CustomTabBarDelegate {
var places:Array<placeData> { get set }
}
class CustomTabBarController: UITabBarController, CustomTabBarDelegate {
var places = Array<placeData>()
override func viewDidLoad() {
places = [PlaceData(),PlaceData()]
var table1 = CustomTableViewController()
var table2 = CustomTableViewController()
table1.delegate = self
table2.delegate = self
var navController1 = UINavigationController(rootViewController: table1)
var navController2 = UINavigationController(rootViewController: table2)
self.viewControllers = [navController1, navController2]
}
}
Then your TableViewControllers simply access the delegates array like so.
CustomTableViewController
class CustomTableViewController: UITableViewController {
var delegate:CustomTabBarDelegate!
override func viewDidAppear() {
self.tableView.reloadData()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return delegate.places.count
}
}
Any changes to the array will be visible in each table after you .reloadData() - I have set the CustomTableViewController in my example to reload data every time the view appears, so when you change tabs they should refresh to show the latest changes.
It's worth mentioning that in time it would probably be cleaner to have a separate class that manages your data instead of holding the array in the TabBarController.
You can always use the app delegate..
Set a property there and call it from anywhere in your application.
let appDel: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var yourNeededData = appDel.yourPassingAroundDataProperty

Resources