Add view controller to container view overwriting view - ios

I've tried following numerous examples and other questions on SO. The view controller is being added but it is placing the view container into the view and not the view container. Here is a shot of what I mean.
I have a tab bar on the primary view controller and a container view. When an item on the tab bar is tapped, it is supposed to grab the controller and load it into the container view. Here is the code for how I am doing it.
#IBOutlet weak var tabBar: UITabBar!
#IBOutlet weak var containerView: UIView!
var currentViewController: UIViewController!
var radarViewController: UIViewController!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let storyboard = self.storyboard
let currentViewController = storyboard?.instantiateViewController(withIdentifier: "WeatherCurrentViewController") as! WeatherCurrentViewController
self.currentViewController = UINavigationController(rootViewController: currentViewController)
let radarViewController = storyboard?.instantiateViewController(withIdentifier: "WeatherRadarViewController") as! WeatherRadarViewController
self.radarViewController = UINavigationController(rootViewController: radarViewController)
}
func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
if item.tag == 1 {
containerView.addSubview(currentViewController!.view)
currentViewController.didMove(toParentViewController: self)
}
if item.tag == 2 {
containerView.addSubview(radarViewController!.view)
radarViewController.didMove(toParentViewController: self)
}
}
This is Swift 3. I don't know what it is I am doing incorrectly. Any help is appreciated.
Update
Although I never got the switching of view to work programmatically after numerous changes/tries, I did find another way of using segues with multiple view containers (here. Not the ideal since it ties up more memory but it works.

The view controller is just a wrapper around the UIView. So in the end you will always have the controllers view that is added as a subview to the container view.
The point of this is you will still get all of the notification for the view controller such as rotation change, layouts...
I use a class which is an UIView subclass. It looks something like this:
import UIKit
class ContentControllerView: UIView {
weak var parrentViewController: UIViewController?
private(set) var currentController: UIViewController?
func setViewController(controller: UIViewController) {
guard let parrentViewController = parrentViewController else {
print("ContentControllerView error: You need to set a parrentViewController to add a new view controller")
return
}
if controller.view != currentController?.view {
currentController?.willMove(toParentViewController: nil) // Notify the current controller it will move off the parent
controller.willMove(toParentViewController: parrentViewController) // Notify the new controller it will move to the parent
parrentViewController.addChildViewController(controller) // Add child controller
currentController?.view.removeFromSuperview()
currentController?.didMove(toParentViewController: nil) // Notify the current controller it did move off the parent
controller.view.translatesAutoresizingMaskIntoConstraints = false // Disable this to add custom constraints
self.addSubview(controller.view) // Add as subview
// Assign new constraints
self.addConstraint(NSLayoutConstraint(item: self, attribute: .left, relatedBy: .equal, toItem: controller.view, attribute: .left, multiplier: 1.0, constant: 0.0))
self.addConstraint(NSLayoutConstraint(item: self, attribute: .right, relatedBy: .equal, toItem: controller.view, attribute: .right, multiplier: 1.0, constant: 0.0))
self.addConstraint(NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: controller.view, attribute: .top, multiplier: 1.0, constant: 0.0))
self.addConstraint(NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: controller.view, attribute: .bottom, multiplier: 1.0, constant: 0.0))
self.layoutIfNeeded()
currentController?.removeFromParentViewController() // remove the current controller from parrent
controller.didMove(toParentViewController: parrentViewController) // Notify the new controller it did move to the parent
currentController = controller
}
}
}
The view can be added in the storyboard but then in the code you need to assign the view controller:
self.contentControllerView.parrentViewController = self
self.contentControllerView.setViewController(controller: controller)

Related

Programmatically Added Constraint Not Working

I've been trying to add constraints programmatically to a view that I'm also adding programmatically to my view controller. However, it seems like the constraints are not being followed.
The view has been added to the story board for the view controller, but isn't actually added to the view controller's view until later on (See screenshot below).
I've tried adding a variety of constraints but none of them have worked so far. I've simplified it now to the single constraint below and even this will not work. What am I doing wrong?
#IBOutlet var loadingView: LoadingView!
override func viewDidLoad() {
super.viewDidLoad()
displayLoadingView(true)
}
func displayLoadingView(display: Bool) {
if display {
view.addSubview(loadingView)
let widthConstraint = NSLayoutConstraint(item: loadingView, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: 50.0)
view.addConstraint(widthConstraint)
}
}
set translatesAutoresizingMaskIntoConstraints = false to any view you are settings constraints programatically.
from the apple doc: translatesAutoresizingMaskIntoConstraints
If you want to use Auto Layout to dynamically calculate the size and position of your view, you must set this property to false, and then provide a non ambiguous, nonconflicting set of constraints for the view.
You don't set all necessary constraints, that can be the reason. Consider following rough example. MyView interface is defined in standalone xib file. Hope it helps:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard let myView = loadFromNib("MyView") else {
return
}
view.addSubview(myView)
myView.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-15-[myView]-15-|", options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: nil, views: ["myView": myView]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-15-[myView]-15-|", options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: nil, views: ["myView": myView]))
}
func loadFromNib(cls: String) -> UIView? {
return NSBundle.mainBundle().loadNibNamed(cls, owner: nil, options: nil)[0] as? UIView
}
}

view hierarchy is not prepared

I am a newbie swift developer, following code examples in a good iOS 9 Swift book. The book has just demonstrated how to add a constraint (in code) between two objects (label, button) that are in two different branches of the view hierarchy.
myLabel is in viewBB container, which is in the top view controller.
myButton is in the top view controller, outside of viewBB.
The example code worked fine. But for an exercise, I wanted to create a second label2 inside of viewBB, and constrain its position to be offset from myLabel. So both labels (I think) are in viewBB.
Creating label2 in code and adding the constraints in code works fine.
But... if I create label2 in the interface builder, and try to remove the IB constraints and replace them with my own, the build succeeds but the app crashes with the usual "view hierarchy not prepared" error.
*** The interesting thing is the error message talks about a label and a button, suggesting that the book example code (I called it BLOCK 0 below) is the offender.
What am I doing wrong? (I've read everything I can find on the error message, including 4 posts in this forum. But I'm at a loss to know why the book code is failing.)
The error message:
2016-02-25 12:52:32.796 TwoViewConstraints[5713:1860768]
The view hierarchy is not prepared for the constraint: NSLayoutConstraint:0x7c9ce990 UILabel:0x7c9dbec0'Label'.centerX ==
UIButton:0x7c9deed0'Button'.centerX
When added to a view, the constraint's items must be descendants of that view (or the view itself). This will crash if the constraint needs to be resolved before the view hierarchy is assembled. Break on -[UIView(UIConstraintBasedLayout) _viewHierarchyUnpreparedForConstraint:] to debug.
Message from debugger: Terminated due to signal 15
Here is my code:
class ViewController: UIViewController {
#IBOutlet weak var myLabel: UILabel!
#IBOutlet weak var myButton: UIButton!
#IBOutlet weak var centerConstraint: NSLayoutConstraint!
#IBOutlet weak var viewBB: UIView!
#IBOutlet weak var lab2cxh: NSLayoutConstraint!
#IBOutlet weak var lab2cxv: NSLayoutConstraint!
#IBOutlet weak var label2: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// BLOCK 0 - works fine, from the iOS9 book
// myLabel and myButton were created with the interface builder
// Goal is to remove an IB constraint, and replace it with one
// that is built in code. (Because label and buttons are in
// different branches of the view hierarchy).
//
// remove auto center constraint from parent label
viewBB.removeConstraint(centerConstraint)
// center the label and the button across views
let ccx = NSLayoutConstraint(item: myLabel,
attribute: NSLayoutAttribute.CenterX,
relatedBy: NSLayoutRelation.Equal,
toItem: myButton,
attribute: NSLayoutAttribute.CenterX,
multiplier: 1.0,
constant: 0)
self.view.addConstraint(ccx)
// BLOCK 1 - Create a second label object in code
// this code works fine with the two added constraints below
// create a second label object and add it to the view
// let label2 = UILabel()
// label2.translatesAutoresizingMaskIntoConstraints = false
// label2.text="Label2"
// viewBB.addSubview(label2)
// BLOCK 2 - create a second label object in the interface builder
// Suggested horiz/vert constraints are set in the IB.
// So now I want to remove them, and replace them with my own,
// as was done in the Swift book example code in BLOCK 0.
// remove original label 2 constraints
viewBB.removeConstraint(lab2cxv)
viewBB.removeConstraint(lab2cxh)
// adding these two constraints works fine when the label2 object
// is created in code. But when I create label2 in the interface
// builder and try to remove the IB constraints (as was done in the
// first block of code), I get the error:
//
// ... "The view hierarchy is not prepared
// for the constraint "Label.CenterX to Button.CenterX", which is
// the first block of code.
//
// NOTE** To me, it looks like the error is coming from the
// BLOCK 0 constraint, since it cites a label and a button, not
// two labels.
// What am I doing wrong?
let c2h = NSLayoutConstraint(item: label2,
attribute: NSLayoutAttribute.CenterY,
relatedBy: NSLayoutRelation.Equal,
toItem: myLabel,
attribute: NSLayoutAttribute.CenterY,
multiplier: 1.0,
constant: -50)
self.viewBB.addConstraint(c2h)
// constrain label 2 to be diagonally left of label one
let c2v = NSLayoutConstraint(item: label2,
attribute: NSLayoutAttribute.CenterX,
relatedBy: NSLayoutRelation.Equal,
toItem: myLabel,
attribute: NSLayoutAttribute.CenterX,
multiplier: 1.0,
constant: -100)
self.viewBB.addConstraint(c2v)
}
I did the whole piece of code over again, from scratch, and it worked. Go figure. Here is the code that worked properly. As far as I can see, it is pretty much the same code, with the exception of a minor name change here and there.
I have no idea why this version works, and the previous version did not. It makes me wonder if something was different in the Interface Builder setup.
class ViewController: UIViewController {
#IBOutlet weak var viewbb: UIView!
#IBOutlet weak var myLabel: UILabel!
#IBOutlet weak var centerConstraint: NSLayoutConstraint!
#IBOutlet weak var myButton: UIButton!
#IBOutlet weak var label2: UILabel!
#IBOutlet weak var label2centerConstraint: NSLayoutConstraint!
#IBOutlet weak var label2Yconstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// remove the IB centering constraint, then add our own between label and button
viewbb.removeConstraint(centerConstraint)
// make a new constraint to center label over button
let newcon = NSLayoutConstraint(item: myLabel,
attribute: NSLayoutAttribute.CenterX,
relatedBy: NSLayoutRelation.Equal,
toItem: myButton,
attribute: NSLayoutAttribute.CenterX,
multiplier: 1.0,
constant: 0)
self.view.addConstraint(newcon)
// remove the IB centering constraint, then add our own between label and button
viewbb.removeConstraint(label2centerConstraint)
viewbb.removeConstraint(label2Yconstraint)
// make a new constraint to center label over button
let newcon2 = NSLayoutConstraint(item: label2,
attribute: NSLayoutAttribute.CenterX,
relatedBy: NSLayoutRelation.Equal,
toItem: myLabel,
attribute: NSLayoutAttribute.CenterX,
multiplier: 1.0,
constant: -100) // left of mylabel
// make a new constraint to center label over button
let newcon3 = NSLayoutConstraint(item: label2,
attribute: NSLayoutAttribute.CenterY,
relatedBy: NSLayoutRelation.Equal,
toItem: myLabel,
attribute: NSLayoutAttribute.CenterY,
multiplier: 1.0,
constant: 0) // left of mylabel
self.view.addConstraint(newcon3)
self.view.addConstraint(newcon2)
}

Preventing the white gap that appears on swiping UIPageViewController

I have implemented the UIPageViewController in this manner:
GalleryViewController: is the container of the PageViewController
PageViewController: is the pageViewController which I added it to GalleryViewController as a subview.
PageContentViewController: I put in it UIImageView to be the content of the page view controller.
Everything is going well, but when I swipe between images, weird things happen. There is white gap appears upside.
The weird thing is when I finish scrolling it stretches automatically.
This is the code of GalleryViewController, which is the container of the PageViewController:
import UIKit
class PageViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var startIndex:Int = 0
var pageViewController : UIPageViewController!
override func viewDidLoad() {
self.navigationController?.hidesBarsOnTap = true;
reset()
}
func reset() {
/* Getting the page View controller */
pageViewController = self.storyboard?.instantiateViewControllerWithIdentifier("PageViewController") as UIPageViewController
self.pageViewController.dataSource = self
let pageContentViewController = self.viewControllerAtIndex(0)
self.pageViewController.setViewControllers([pageContentViewController!], direction: UIPageViewControllerNavigationDirection.Forward, animated: true, completion: nil)
self.pageViewController.view.frame = CGRectMake(0, 0, self.view.frame.width, self.view.frame.height)
self.addChildViewController(pageViewController)
self.view.addSubview(pageViewController.view)
self.pageViewController.didMoveToParentViewController(self)
println("\(startIndex) start index")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
var index = (viewController as PageContentViewController).pageIndex!
index++
if(index >= Constants.Statics.images.count){
return nil
}
return self.viewControllerAtIndex(index)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
var index = (viewController as PageContentViewController).pageIndex!
if(index <= 0){
return nil
}
index--
return self.viewControllerAtIndex(index)
}
func viewControllerAtIndex(index : Int) -> UIViewController? {
if((Constants.Statics.images.count == 0) || (index >= Constants.Statics.images.count)) {
return nil
}
let pageContentViewController = self.storyboard?.instantiateViewControllerWithIdentifier("PageContentViewController") as PageContentViewController
pageContentViewController.pageIndex = index
return pageContentViewController
}
}
What can I do to prevent that stretching and the white gap on swiping?
UPDATED:
A strange thing has happened, I changed the pageViewController Transition Style to Page Curl, the problem did not appear. Sounds like it is about scrolling!
The origin of the bug is the constraints of your UIImageView.
When you begin scroll, only viewWillAppear is called, and autolayout is not yet calculated, when the scroll is finished, and view is shown, the view start calculate autolayout, and viewDidAppear happens after autolayout is completed.
you can check that by adding the code below in PageContentViewController
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = UIImage(named: "UpcomingGamesBg")
println("viewDidLoad imageView Frame : \(imageView.frame) pageIndex : \(pageIndex)")
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
println("viewWillAppear imageView Frame : \(imageView.frame) pageIndex : \(pageIndex)")
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
println("viewDidAppear imageView Frame : \(imageView.frame) pageIndex : \(pageIndex)")
}
And because, the layout of your imageView refer to top layout guide, and top layout guide can have different value before and after calculating autolayout, you have this bug.
So to resolve that, you can change the constraints of your image view and make them relatives to the parent view (because the pageContentViewController is like a subview and the the pagerViewController will manage the top and the bottom margin), like that :
With that you can fix this bug, but be careful you should add some contraints to your pageViewController.view
Update
For example to support navigationBar / TabBar, you should add some constraints in viewDidLoad (after view.addSubview(pageViewController.view) ):
pageViewController.view.setTranslatesAutoresizingMaskIntoConstraints(false)
let topConstraint = NSLayoutConstraint(item: pageViewController.view, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: topLayoutGuide, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 0)
let bottomConstaint = NSLayoutConstraint(item: pageViewController.view, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: bottomLayoutGuide, attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant: 0)
let leadingConstraint = NSLayoutConstraint(item: pageViewController.view, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.Leading, multiplier: 1.0, constant: 0)
let trailingConstraint = NSLayoutConstraint(item: pageViewController.view, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.Trailing, multiplier: 1.0, constant: 0)
view.addConstraints([topConstraint, bottomConstaint, leadingConstraint, trailingConstraint])
Best regards,
Check definitely example here: https://github.com/dimpiax/UIPageViewControllerScrolling
But if you want to remove empty blank on swipe, you can remove bounce, for example:
for value in view.subviews {
if let scrollView = value as? UIScrollView {
scrollView.bounces = false
}
}
The auto stretching is because the ImageView instances are not constrained to aspect fit. In your PageContentViewController, specify contentMode value for your images to fix the issue. For example:
self.imageView.contentMode = UIViewContentModeScaleAspectFit
So it appears that the view's frame is not correct before the animation takes place. Try setting up the frame before it starts to come on screen. Something like:
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
var index = (viewController as PageContentViewController).pageIndex!
if(index <= 0){
return nil
}
index--
// Setting up the new view's frame
var newVC = self.viewControllerAtIndex(index)
newVC.view.frame = self.pageViewController.view.bounds
return newVC
}
Have you tried those? You should select your content view controller first. Note: click on the controller, not the view
uncheck Adjust Scroll View Insets
uncheck Under Top Bars
uncheck Under Bottom Bars
Look like a layout issue.
Since UIPageViewController with .Scroll type use a UIScrollView to make it,
try to add self.automaticallyAdjustsScrollViewInsets = false to your viewDidLoad
If it doesn't work, try to remove self.navigationController?.hidesBarsOnTap = true
And if it doesn't work again, please provide a demo project, so I can figure out what is wrong.
Set imageView's contentMode to AspectFill ...

Display ADBannerView with UITableViewController inside UITabBarController

EDIT
Thanks to #LeoNatan I have now got a complete working solution. If anyone finds this and would like the solution, it's available on GitHub.
Original Question
I'm trying to get iAds (or any other view for that matter, although it may be specific to ADBannerView) to be displayed just above a UITabBar. I've gone about a few different ways of doing this, but haven't come up with a solution that satifies the following:
Works on iOS 7 and 8
Works with and without the iAd displayed
Works in landscape and portrait
Works on iPhone and iPad
UITableViews insets correctly update
The only solution I have so far that has worked has been to have my UITableView inside a UIViewController, and adding the UITableView and ADBannerView to the view property of the UIViewController. I moved away from this for 2 reasons:
The UITableView did not extend its edges below the bottom UITabBar
I need to subclass UITableViewController, not UIViewController
I have a bannerView property on my AppDelegate and a shouldShowBannerView property to decide whether or not to show the iAd, and share a single instance. The AppDelegate then sends out notifications when iAds should be displayed or hidden (i.e., when an iAd is loaded and when the user has paid to remove the iAds). The "base" of the code works as such:
func showiAds(animated: Bool) {
if !self.showingiAd {
let delegate = UIApplication.sharedApplication().delegate as AppDelegate
if let bannerView = delegate.bannerView {
println("Showing iAd")
self.showingiAd = true
if (bannerView.superview != self.view) {
bannerView.removeFromSuperview()
}
// let bannersSuperview = self.view.superview! // Bottom inset incorrect
let bannersSuperview = self.view // Banner is shown at the top screen. Crashes on iOS 7 (at bannersSuperview.layoutIfNeeded())
// let bannersSuperview = self.tableView // The is the same as self.view (duh)
// let bannersSuperview = self.tabBarController!.view // Bottom inset incorrect
// Added the view and the left/right constraints allow for the proper height
// to be returned when bannerView.frame.size.height is called (iOS 7 fix mainly)
bannersSuperview.addSubview(bannerView)
bannersSuperview.addConstraints([
NSLayoutConstraint(item: bannerView, attribute: .Left, relatedBy: .Equal, toItem: bannersSuperview, attribute: .Left, multiplier: 1, constant: 0),
NSLayoutConstraint(item: bannerView, attribute: .Right, relatedBy: .Equal, toItem: bannersSuperview, attribute: .Right, multiplier: 1, constant: 0),
])
bannersSuperview.layoutIfNeeded()
let bannerViewHeight = bannerView.frame.size.height
var offset: CGFloat = -self.bottomLayoutGuide.length
if (UIDevice.currentDevice().systemVersion as NSString).floatValue < 8 {
// Seems to be needed for some reason
offset -= bannerViewHeight
}
let bannerBottomConstraint = NSLayoutConstraint(item: bannerView, attribute: .Bottom, relatedBy: .Equal, toItem: bannersSuperview, attribute: .Bottom, multiplier: 1, constant: offset + bannerViewHeight)
// self.bannerBottomConstraint = bannerBottomConstraint
bannersSuperview.addConstraint(bannerBottomConstraint)
bannersSuperview.layoutSubviews()
// bannerSuperview.setNeedsLayout()
bannersSuperview.layoutIfNeeded()
// Previously, this values was the height of the banner view, so that it starts off screen.
// Setting this to 0 and then doing an animation makes it slide in from below
bannerBottomConstraint.constant = offset
bannersSuperview.setNeedsUpdateConstraints()
UIView.animateWithDuration(animated ? 10 : 0, animations: { () -> Void in
// Calling layoutIfNeeded here will animate the layout constraint cosntant change made above
bannersSuperview.layoutIfNeeded()
})
} else {
println("Cannot show iAd when bannerView is nil")
}
}
}
func hideiAds() {
if self.showingiAd {
self.showingiAd = false
let delegate = UIApplication.sharedApplication().delegate as AppDelegate
if let bannerView = delegate.bannerView {
if bannerView.superview == self.view {
bannerView.removeFromSuperview()
}
}
}
}
I then check in my viewWillAppear: and viewDidDisappear: methods if an iAds is/should be displayed and calling showiAds(false) and hideiAds() as required.
No matter what I do, I don't seem to be able to get it to work. A couple of other things I've tried but scrapped the code for:
Adding the iAd in the UITabBarController, which then alerts the UITableViewControllers that the iAd was shown/hidden. Modifying the content/scroll indicator insets did not work well, and was ofter reset by the UITableViewController to fit above/below the navigation/tab bar.
(as above) setting the content/scroll indicator insets myself, but I could not get it consistent without attempting to emulate (using (top|bottom)LayoutGuide) in viewDidLayoutSubviews, but this seems very costly?
I did, at one point, have it working by adding the ADBannerView to some view from within the UITableViewController, but it would crash on iOS 7 (something about tableView must call super -layoutSubviews)
EDIT
I have created a UIViewController subclass with the intent of using it to house UITableViewControllers via a Container View. Here is what I have so far, followed by a couple of issues:
class AdvertContainerViewController: UIViewController {
var tableViewController: UITableViewController?
var showingiAd = false
var bannerBottomConstraint: NSLayoutConstraint?
private var bannerTopOffset: CGFloat {
get {
var offset: CGFloat = 0
if let tabBar = self.tabBarController?.tabBar {
offset -= CGRectGetHeight(tabBar.frame)
}
if let bannerView = AppDelegate.instance.bannerView {
let bannerViewHeight = bannerView.frame.size.height
offset -= bannerViewHeight
}
return offset
}
}
override func viewDidLoad() {
super.viewDidLoad()
if self.childViewControllers.count > 0 {
if let tableViewController = self.childViewControllers[0] as? UITableViewController {
self.tableViewController = tableViewController
tableViewController.automaticallyAdjustsScrollViewInsets = false
self.navigationItem.title = tableViewController.navigationItem.title
}
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if AppDelegate.instance.shouldShowBannerView {
self.showiAds(false)
}
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
let delegate = AppDelegate.instance
NSNotificationCenter.defaultCenter().addObserver(self, selector: "showiAds", name: "BannerViewDidLoadAd", object: delegate)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "hideiAds", name: "RemoveBannerAds", object: delegate)
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
if self.showingiAd {
self.hideiAds()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
println("View did layout subviews")
if self.showingiAd {
if let bannerView = AppDelegate.instance.bannerView {
let bannerViewHeight = CGRectGetHeight(bannerView.frame)
if let bottomConstraint = self.bannerBottomConstraint {
let bannerTopOffset = self.bottomLayoutGuide.length + bannerViewHeight
if bottomConstraint.constant != bannerTopOffset {
println("Setting banner top offset to \(bannerTopOffset)")
bottomConstraint.constant = -bannerTopOffset
bannerView.superview?.setNeedsUpdateConstraints()
bannerView.superview?.updateConstraintsIfNeeded()
}
}
println("Bottom layout guide is \(self.bottomLayoutGuide.length)")
let insets = UIEdgeInsetsMake(self.topLayoutGuide.length, 0, self.bottomLayoutGuide.length + bannerViewHeight, 0)
self.updateTableViewInsetsIfRequired(insets)
}
}
}
private func updateTableViewInsetsIfRequired(insets: UIEdgeInsets) {
if let tableView = self.tableViewController?.tableView {
if !UIEdgeInsetsEqualToEdgeInsets(tableView.contentInset, insets) {
println("Updating content insets to \(insets.top), \(insets.bottom)")
tableView.contentInset = insets
}
if !UIEdgeInsetsEqualToEdgeInsets(tableView.scrollIndicatorInsets, insets) {
println("Updating scroll insets to \(insets.top), \(insets.bottom)")
tableView.scrollIndicatorInsets = insets
}
}
}
func showiAds() {
self.showiAds(true)
// self.showiAds(false)
}
func showiAds(animated: Bool) {
if !self.showingiAd {
let delegate = UIApplication.sharedApplication().delegate as AppDelegate
if let bannerView = delegate.bannerView {
println("Showing iAd")
self.showingiAd = true
if (bannerView.superview != self.view) {
bannerView.removeFromSuperview()
}
let bannersSuperview = self.view.superview!
// Added the view and the left/right constraints allow for the proper height
// to be returned when bannerView.frame.size.height is called (iOS 7 fix mainly)
bannersSuperview.addSubview(bannerView)
bannersSuperview.addConstraints([
NSLayoutConstraint(item: bannerView, attribute: .Left, relatedBy: .Equal, toItem: bannersSuperview, attribute: .Left, multiplier: 1, constant: 0),
NSLayoutConstraint(item: bannerView, attribute: .Right, relatedBy: .Equal, toItem: bannersSuperview, attribute: .Right, multiplier: 1, constant: 0),
])
bannersSuperview.layoutIfNeeded()
let bannerBottomConstraint = NSLayoutConstraint(item: bannerView, attribute: .Top, relatedBy: .Equal, toItem: bannersSuperview, attribute: .Bottom, multiplier: 1, constant: 0)
self.bannerBottomConstraint = bannerBottomConstraint
bannersSuperview.addConstraint(bannerBottomConstraint)
bannersSuperview.layoutSubviews()
bannersSuperview.layoutIfNeeded()
let topInset = self.navigationController?.navigationBar.frame.size.height ?? 0
let insets = UIEdgeInsetsMake(topInset, 0, -self.bannerTopOffset, 0)
// Previously, this values was the height of the banner view, so that it starts off screen.
// Setting this to 0 and then doing an animation makes it slide in from below
bannerBottomConstraint.constant = self.bannerTopOffset
bannersSuperview.setNeedsUpdateConstraints()
UIView.animateWithDuration(animated ? 0.5 : 0, animations: { () -> Void in
// Calling layoutIfNeeded here will animate the layout constraint cosntant change made above
self.updateTableViewInsetsIfRequired(insets)
bannersSuperview.layoutIfNeeded()
})
} else {
println("Cannot show iAd when bannerView is nil")
}
}
}
func hideiAds() {
if self.showingiAd {
self.showingiAd = false
let delegate = UIApplication.sharedApplication().delegate as AppDelegate
if let bannerView = delegate.bannerView {
if bannerView.superview == self.view {
bannerView.removeFromSuperview()
}
}
}
}
}
Issues so far:
Using self.view as the superview causes a crash on rotate Auto Layout still required after sending -viewDidLayoutSubviews to the view controller. Gathered.AdvertContainerViewController's implementation needs to send -layoutSubviews to the view to invoke auto layout.
I'm not calculating the content insets correctly; when the iAd is shown, the top jumps up slightly and the bottom in below the top of the banner
The table view doesn't show the scroll indicators. This seems to be a known issue but I cannot find a solution
At the request of Leo Natan I have create a repo on GitHub that I will update with any attempts I make, and explain issues here. Currently, the issues are as follows:
First Tab:
Top of table moves down when iAd is shown (iOS 8)
Table cannot be scrolled (iOS 7)
Top of table view jumps when iAd shows (iOS 7)
Rotation often breaks the offset of the iAd, hiding it behind the tab bar (iOS 7 and 8)
Second Tab:
There are no scroll bars (iOS 7 and 8)
Scroll inset it not set (iOS 7)
Rotation often breaks the offset of the iAd, hiding it behind the tab bar (iOS 7 and 8)
The best solution is to use view controller containment. Use a view controller subclass that will house both the ad view and the table view controller's view, and add the table view controller as a child of the container view controller. This should take care of content insets correctly. On each layout of the container controller's view, position the table controller view hierarchy correctly after positioning the ad view. If you wish to hide the ad view, simply hide or remove it from the container hierarchy, and extend the table controller's view hierarchy fully. When working with hierarchies, remember to always use the table controller's view and not the tableView directly.
My answer was adapted into the following GitHub repo:
https://github.com/JosephDuffy/iAdContainer
The best that is that you download the AD suite from Apple site, there are tabbar controller and navigation controller containment example.
Apple provides you an abstract view controller that can handle by itself the ADBanner flow without interrupting its presentation, maximizing the showing time.
You can use this https://developer.apple.com/library/ios/samplecode/iAdSuite/Introduction/Intro.html apple sample and modified it according to your needs. Such as bool variable to take care of when iAds is shown or not.
There in code you can see BannerViewController class that contains all the logic. You can also write ADmob code there to use.

How To Properly Add Child View Controller in iOS 8 With Swift

I've been going through the documentation and still seem to be on a sticking point.
I have a view controller object C_SelectPhoto. This has a container view. Inside the container view I want the childed view controller, C_SelectPhotoControllerView, to fit inside it. It will just be an array of photos. However, setting the frame and adding the child view controller is not working. If I move the x value of the desired child view controller, no effect happens.
To figure out what is going on I color coded everything. The container, below, is orange. The view the container expects, according to the storyboard is yellow. The view I actually want to fit in there is red.
Here is the storyboard:
Here is my controller code for C_SelectPhoto
class C_SelectPhoto:Controller
{
#IBOutlet weak var selectPhotoControllerView: UIView!
var _collectionViewController:C_SelectPhotoControllerView!
//TODO PERMISSION IS NEEDED BEFORE FETCHING
func initController()
{
_collectionViewController = Controller.STORYBOARD.instantiateViewControllerWithIdentifier("selectPhotoControllerView") as C_SelectPhotoControllerView
displayControllerViewController()
}
//show the photo selection
private func displayControllerViewController()
{
addChildViewController(_collectionViewController)
_collectionViewController.view.frame = CGRectMake(100, 0, 500, 500)
self.view.addSubview(_collectionViewController.view)
_collectionViewController.didMoveToParentViewController(self)
}
}
However the result is produces is below:
First, the yellow class shouldn't be added at all, I wanted only the red (the UICollectionViewController class). Second, I can tell the red class is being added to the wrong spot because its x value hasn't moved it over at all.
So my question is:
How can I add a UIContainerViewController, as a child to the main view controller, C_SelectPhoto, but have the UIContainerViewController frame FIT the container I have in the main view controller?
Thank you!!!
NOTE: The views I am trying to add are UICollectionViewControllers. When I add a UIViewController, the framing works just fine, but as you can see when adding the UICollectionViewControllers, the framing does NOT work, and they are getting added to random offsets and are not respecting my attempts to size them with frame assignments.
use following Extension for adding childViewController On View
extension UIViewController {
func configureChildViewController(childController: UIViewController, onView: UIView?) {
var holderView = self.view
if let onView = onView {
holderView = onView
}
addChildViewController(childController)
holderView.addSubview(childController.view)
constrainViewEqual(holderView, view: childController.view)
childController.didMoveToParentViewController(self)
childController.willMoveToParentViewController(self)
}
func constrainViewEqual(holderView: UIView, view: UIView) {
view.translatesAutoresizingMaskIntoConstraints = false
//pin 100 points from the top of the super
let pinTop = NSLayoutConstraint(item: view, attribute: .Top, relatedBy: .Equal,
toItem: holderView, attribute: .Top, multiplier: 1.0, constant: 0)
let pinBottom = NSLayoutConstraint(item: view, attribute: .Bottom, relatedBy: .Equal,
toItem: holderView, attribute: .Bottom, multiplier: 1.0, constant: 0)
let pinLeft = NSLayoutConstraint(item: view, attribute: .Left, relatedBy: .Equal,
toItem: holderView, attribute: .Left, multiplier: 1.0, constant: 0)
let pinRight = NSLayoutConstraint(item: view, attribute: .Right, relatedBy: .Equal,
toItem: holderView, attribute: .Right, multiplier: 1.0, constant: 0)
holderView.addConstraints([pinTop, pinBottom, pinLeft, pinRight])
}}
Updated for Swift 5+
Just one line in your view controller to add child view controller.
Super scalable methods in the extension if you want to add it on any custom view.
public extension UIViewController {
/// Adds child view controller to the parent.
///
/// - Parameter child: Child view controller.
func add(_ child: UIViewController) {
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
/// It removes the child view controller from the parent.
func remove() {
guard parent != nil else {
return
}
willMove(toParent: nil)
removeFromParent()
view.removeFromSuperview()
}
}
How to use:
Adding: In the view controller where you want to add the child view controller.
// let yourChildViewController = Load fro the storyboard or XIB
add(yourChildViewController)
Removing:
yourChildViewController.remove()
If you want the red controller to be the child controller, delete the yellow one, and control-drag from the container to the red controller. There's no need to add it in code, or do any resizing. The red controller will be set to the same size as the container in the storyboard.

Resources