UIViewController setView nil automatically removes from superview - ios

Calling setView on a UIViewController removes the current view from its superview automatically. I couldn't find this documented. In my case I want to dynamically swap a UIViewController object for another while maintaining my view structure. I was planning to just relink the view to the new controller but alas, that doesn't work.
In general, automatically removing the view from its superview seems like a sensible decision. The documentation should reflect this though.
(For anyone thinking it's a really bad idea to swap a view controller object in this way, let me add that the controller I'm swapping in is a subclass of the existing controller. And this method is great for adding functionality to a view that is an extension of another.)

This is how I solved it:
UIView *viewToKeep = self.viewController.view;
UIView *superview = viewToKeep.superview;
self.viewController.view = nil; // removes the view from its superview
UIViewController *swapInViewController = [[UIViewController alloc] init];
swapInViewController.view = viewToKeep;
[superview addSubview: viewToKeep];
[viewToKeep applyConstraintsToFillSuperview]; // a helper to add auto layout constraints that make the view always fill it's parent
self.viewController = swapInViewController;

Doing this is definitely going against the grain in UIKit, and therefore very likely a bad idea.
That said, here's something which prevents the view from being automatically removed, which means you don't need to use addSubview to put it back where it was.
Only tested as far as you see below, caveat emptor, etc. In Swift:
class View: UIView {
var preventRemovalFromSuperView = false
override func removeFromSuperview() {
if !preventRemovalFromSuperView {
super.removeFromSuperview()
}
preventRemovalFromSuperView = false
}
}
let vc1 = UIViewController()
let vc2 = UIViewController()
let sv = UIView()
let v = View()
// Existing hierarchy
sv.addSubview(v)
vc1.view = v
// Swap view controllers
v.preventRemovalFromSuperView = true // Prevent automatic removal
vc1.view = nil // Prevent UIViewControllerHierarchyInconsistency exception
vc2.view = v
// Check that view was not automatically removed
v.isDescendantOfView(sv) // true

Related

Adding An UITableViewController on to a SubView

An UITableViewController pretty much takes up the entire view. I need a way to limit its height, width and add some shadows etc. For a clear explanation, I won't show the UITableViewController's contents.
Without the use of a storyboard, I subviewed the UITableViewController:
// In another UIViewController
let otherController = OtherController() // A subclass of UITableViewController
let otherControllerView = otherController.view
someView.addSubView(otherControllerView)
[...] // bunch of constraints
Notes:
In AppDelegate, if I set the rootController as OtherController(), everything works as it should. If I change it back to SomeView(), I see my modified tableView. If I should click it, it disappears.
This was the only thing that came close to my issue but sadly, I could not understand the answers provided as nothing made any sense to me.
I need to understand, why it disappears when touched etc.
view.bringSubviewToFront(...) proved futile. I'm gessing that a tableView should be rendered in its own controller and not in another view?
So just to answer this question, indeed you got two options. One is the best way, as suggested by Rakesha. Just use UITableView. Add it as a subview. Done.
And in the future, if you really want any controller to be added onto any UIView, remember you need to add that controller as a child. For example, in your case:
The controller of the view that will hold your UITableViewController will add such UITableViewController as a child.
self.addChild(yourUITableViewController)
self.whatEverViewContainer.addSubview(yourUITableViewController.view)
// Take note of the view of your tableViewController above^.
// Then setup the constraints of your yourUITableViewController.view below.
I hope this helps!
You must add the instance of UITableViewController's subclass as child view controller of the other view controller. You need to ensure few points in order to make it work. The points are as listed below:
Create the instance of your TableViewController
Add it as a child view controller of the other view controller
Add its view as a subview of the desired view (you may do these steps in viewDidLaod since they need to be done only once)
Keeping in mind the view cycle of a view controller. You must keep a weak reference of the child view controller aka TableViewController to adjust its view frame after the parent view controller has laid its subviews.
Code here:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let vc = TableViewController()
addChildViewController(vc)
view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
childVC = vc
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
childVC?.view.frame = view.frame
}

Multiple ViewControllers contained in a UIStackView

I have a UIStackView and I am dynamically adding UIViewControllers contained, here is my code;
[self addChildViewController:driverForm];
[self addChildViewController:marketingView];
[self.stackView insertArrangedSubview:driverForm.view atIndex:0];
[self.stackView insertArrangedSubview:marketingView.view atIndex:1];
[driverForm didMoveToParentViewController:self];
[marketingView didMoveToParentViewController:self];
After reading the documents it states I must call didMoveToParentViewController.
The problem I am facing is, the actions on the final UIViewController are not being called, but the first ViewController does. If I swap these round the first one works and the last one does not.
Simply add the view of your ViewController to your UIStackView like this:
yourStackView.addArrangedSubview(yourViewController.view)
Also, you don't need to be worried about the view being nil as it always returns UIView!
Note that the order is reversed, so the last appears first. To address this, assuming you have an array of view controllers, you can use stride to traverse your array inversely and add view controllers to your stack.
Here is a quick copy/pasta version for Swift 5:
private var childViewController: UIViewController
private var stackView: UIStackView?
// MARK: - UIViewController
override func loadView() {
super.loadView()
// 1. Add the child view controller to the parent.
addChild(childViewController)
// 2 Create and add the stack view containing child view controller's view.
stackView = UIStackView(arrangedSubviews: [childViewController.view])
stackView!.axis = .vertical
self.view.addSubview(stackView!)
// 3. Let the child know that it's been added!
childViewController.didMove(toParent: self)
}
UIStackView is for arranging multiple subviews in the same UIViewController class. how can you use it for different UIViewControllers?
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIStackView_Class_Reference/#//apple_ref/doc/uid/TP40015256-CH1-SW31

Fitting a UIScrollView to a UIView

I am trying to use a UIScrollView to show a series of UIViews. In my storyboard I have a View Controller containing a UIView that is constrained using AutoLayout.
View Controller (UIView in grey)
In order to call the UIScrollView I am using the following method:
func initScrollview() {
self.mainScrollView = UIScrollView(frame: self.mainView.bounds)
self.mainScrollView!.contentSize = CGSizeMake((self.mainView.bounds.width)*CGFloat(3), self.mainView.frame.height)
self.mainScrollView!.backgroundColor = UIColor.greenColor() // For visualization of the UIScrollView
self.mainScrollView!.pagingEnabled = true
self.mainScrollView!.maximumZoomScale = 1.0
self.mainScrollView!.minimumZoomScale = 1.0
self.mainScrollView!.bounces = false
self.mainScrollView!.showsHorizontalScrollIndicator = true;
self.mainScrollView!.showsVerticalScrollIndicator = false;
self.mainScrollView!.delegate = self
for i in 0...3 {
var tempView = SubView(frame: self.mainView.bounds)
pages.insert(tempView, atIndex: i)
self.mainScrollView!.addSubview(pages[i]);
}
self.mainScrollView!.scrollRectToVisible(CGRectMake(mainScrollView!.frame.size.width, 0, mainScrollView!.frame.size.width, mainScrollView!.frame.size.height), animated: false)
}
When I run my code, the UIScrollView does not fit the UIView. Instead it is too short and too wide. The result looks like this:
UIView in grey, UIScrollView in green
What am I doing wrong that is causing the UIScrollView to be incorrectly sized?
You should put the codes that you init the UI element sizes base on the screen size(UIView of UIViewController) in viewDidLayoutSubviews. Because in viewDidLoad, the screen didn't adjust its size yet,
In the above code, there no mention of adding the mainScrollView to the mainVew.
To whose view are you adding the mainScrollView? My opinion would be you are trying to add it to self.view whereas it should be to self.mainView
After the initScrollView() function is called try adding it the below code
self.mainView.addSubview(mainScrollView!)
This would probably be easier if you had placed all these views directly in your storyboard instead of programatically. I don't see anything in your code that can't be done visually in IB. Also, if you have autoLayout active in your storyboard, setting frames and sizes in code won't work. (auto-layout will change your values on the next pass)

How to SubView a ViewController into a SubView of another ViewController

I have two ViewController.
One ViewControllers contain a UITableView. And another contains a UIButton.
I have created a SubView Programmatically.Now i want to SubView the ViewController which contains the UITableView in other when i Press UIButton.
I searched all over the net but cannot find any stable solution.
Currently i am trying this:
bodyView =[[UIView alloc]initWithFrame:CGRectMake(0,120,containerView.frame.size.width,120)];
bodyView.backgroundColor = [UIColor redColor];
CustomTableVC *tableVC = [[CustomTableVC alloc]init];
[tableVC willMoveToParentViewController:self];
[bodyView addSubview:tableVC.view];
[self addChildViewController:tableVC];
[tableVC didMoveToParentViewController:self];
[containerView addSubview:bodyView];
You cannot.
You can only use the view property of your UIViewController to assign into UIView associated in your second UIViewController which is not recommended because UIViewController as per MVC pattern holds lot controller stuff which includes populating the view and resolving the inputs/touch, which is an overhead in your (using multiple of viewcontrollers without needed) case.
You need to use one UIViewController. Add UITableView only in it, and UIButton only in it. You only use one controller and multiple views.
The other approach, if you do not want to change your code, may also use ContainerView. But in that case you need to create separate ViewControllers for UIButton and UITableView. And if you want to fetch data inbetween the ViewControllers, that will be a huge pain and also a bad software design with so much coupling and less encapsulation.
i have tried this one and it's working for me.
#IBAction func moveToOther() {
var otherController = OtherViewController()
var bodyView = UIView(frame: CGRectMake(0,120, self.view.frame.size.width, 120))
bodyView.backgroundColor = UIColor.redColor();
bodyView.layer.borderWidth = 1.0
let tblCntrl = UITableViewController()
bodyView.addSubview(tblCntrl.tableView)
bodyView.clipsToBounds = true
otherController.addChildViewController(tblCntrl)
tblCntrl.didMoveToParentViewController(otherController)
otherController.view.addSubview(bodyView)
self.navigationController?.pushViewController(otherController, animated: true)
}
You should use only one view controller that contains both the table view & UIButton.
By default hide the table view.
Just hide the button and show the table view when the button is clicked.

UIScrollView with UIView (Swift)

I have implemented a UIScrollView with a UIView which I add when viewDidLoad() to the UIScrollView which is set to the UIViewControllers view. When I do this how ever the frame of the UIView with the setTranslatesAutoresizingMaskIntoConstraints(false) gets set to -101.0. This does not happen to another view that is displayed differently,but only happens to this view which is designed the same, and displayed with pushViewController from the navigationController.
The constraints are setup from the NIB/XIB files and I am confused why this is occurring.
Another thing to note is that, when this happens, no matter where I try and change the frame of the UIView, it has no affect.
EDIT:
CODE for viewDidLoad():
override func viewDidLoad(){
// call the super implementation
super.viewDidLoad();
// load our scrollview from our nib file
customScrollView = CustomScrollView.loadFromNib();
// set the resizeing mask to fill screen
customScrollView!.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
// load our uiview from our nib file
containerView = ContainerView.loadFromNib();
// we handle the constraint changes
containerView!.setTranslatesAutoresizingMaskIntoConstraints(false);
customScrollView!.addSubview(containerView!);
// intialize our refresh control
refreshControl = UIRefreshControl();
refreshControl!.tintColor = UIColor.whiteColor();
refreshControl!.addTarget(self, action: "onRefresh", forControlEvents: UIControlEvents.ValueChanged);
containerView!.addSubview(refreshControl!);
// add the view to our controller here
view = customScrollView!;
}
The answer ended up being a custom tableview. If anyone else is having this problem, I highly recommend ditching UIScrollView, because it is an utter disappointment from apple. The documentation is poor, and there are technical issues with it known to be true. There are also technical document notes for some issues on the class for anyone who is trying to do something with the class, be sure to read those.

Resources