2

I have a UIViewController with a UITableView that fills the screen for an ad-supported version. MyViewController is embedded in a UINavigationController and it has a UITabBarController at the bottom.

There will be 2 versions of this app:

1) Paid - I have this configured on a storyboard. It works as desired.

2) Ad Supported - Try as I may, I can't get the banner to draw in the right spot. I'm trying to do this:

topLayoutGuide
tableview
standard height padding
bannerView (50 height)
standard height padding
bottomLayoutGuide

Instead, the bannerView is being drawn on top of the tableView, rather than between the tableView and the bottomLayoutGuide

enter image description here

I call a method I created called configureBannerView from viewDidLoad. Here' the relevant portion of the code that lays out the view in Visual Format Language:

var allConstraints = [NSLayoutConstraint]()

let horizontalTableViewConstraint = NSLayoutConstraint.constraintsWithVisualFormat(
    "H:|[tableView]|",
    options: NSLayoutFormatOptions.AlignAllCenterY,
    metrics: nil,
    views: views)
allConstraints += horizontalTableViewConstraint

let horizontalBannerViewConstraint = NSLayoutConstraint.constraintsWithVisualFormat(
    "H:|[leftBannerViewSpacer]-[bannerView(320)]-[rightBannerViewSpacer(==leftBannerViewSpacer)]|",
    options: NSLayoutFormatOptions.AlignAllCenterY,
    metrics: nil,
    views: views)
allConstraints += horizontalBannerViewConstraint

let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
    "V:|[topLayoutGuide][tableView]-[leftBannerViewSpacer(50)]-[bottomLayoutGuide]|",
    options: [],
    metrics: metrics,
    views: views)
    allConstraints += verticalConstraints

I can't figure out why this isn't working. Below is the complete configureBannerView method.

func configureBannerView() {
    if adSupported == false {
        // Do nothing, leave it alone
    } else {

        // remove existing constraints
        tableView.removeConstraints(tableView.constraints)
        tableView.translatesAutoresizingMaskIntoConstraints = false

        // create dictionary of views
        var views: [String : AnyObject] = [
            "tableView" : tableView,
            "topLayoutGuide": topLayoutGuide,
            "bottomLayoutGuide": bottomLayoutGuide]

        // Create a frame for the banner
        let bannerFrame = CGRect(x: 0, y: 0, width: kGADAdSizeBanner.size.width, height: kGADAdSizeBanner.size.height)
        // Instnatiate the banner in the frame you just created
        bannerView = GADBannerView.init(frame: bannerFrame)
        bannerView?.translatesAutoresizingMaskIntoConstraints = false
        // add the bannerView to the view
        view.addSubview(bannerView!)
        // add bannerView to the view dictionary
        views["bannerView"] = bannerView

        // Create spacers for left and right sides of bannerView
        // 32.0 = leftSpacer left pad + leftSpacer right pad + rightSpacer left pad + rightSpacer right pad
        // Calculate width of spacer
        let spacerWidth = (screenSize.width - kGADAdSizeBanner.size.width - 32.0) / 2

        // Instantiate left and right pads
        // 50.0 = height of bannerView
        let leftBannerViewSpacer = UIView(frame: CGRect(x: 0, y: 0, width: spacerWidth, height: 50.0))
        let rightBannerViewSpacer = UIView(frame: CGRect(x: 0, y: 0, width: spacerWidth, height: 50.0))

        leftBannerViewSpacer.translatesAutoresizingMaskIntoConstraints = false
        rightBannerViewSpacer.translatesAutoresizingMaskIntoConstraints = false

        // add the spacers to the subview
        view.addSubview(leftBannerViewSpacer)
        view.addSubview(rightBannerViewSpacer)

        // add to the views dictionary
        views["leftBannerViewSpacer"] = leftBannerViewSpacer
        views["rightBannerViewSpacer"] = rightBannerViewSpacer


        // Create metric for tabBarHeight
        let tabBarHeight = tabBarController?.tabBar.frame.height

        // Create a dictionary of metrics
        let metrics: [String : CGFloat] = ["tabBarHeight": tabBarHeight!]


        var allConstraints = [NSLayoutConstraint]()

        let horizontalTableViewConstraint = NSLayoutConstraint.constraintsWithVisualFormat(
            "H:|[tableView]|",
            options: NSLayoutFormatOptions.AlignAllCenterY,
            metrics: nil,
            views: views)
        allConstraints += horizontalTableViewConstraint

        let horizontalBannerViewConstraint = NSLayoutConstraint.constraintsWithVisualFormat(
            "H:|[leftBannerViewSpacer]-[bannerView(320)]-[rightBannerViewSpacer(==leftBannerViewSpacer)]|",
            options: [NSLayoutFormatOptions.AlignAllCenterY],
            metrics: nil,
            views: views)
        allConstraints += horizontalBannerViewConstraint

        let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
            "V:|[topLayoutGuide][tableView]-[leftBannerViewSpacer(50)]-[bottomLayoutGuide]|",
            options: [],
            metrics: metrics,
            views: views)
        allConstraints += verticalConstraints

        NSLayoutConstraint.activateConstraints(allConstraints)

Thank you for reading. I welcome suggestions to resolve my erroneous code.

Adrian
  • 16,233
  • 18
  • 112
  • 180
  • 1
    (may not be applicable but worth putting on your radar) if you are targeting iOS 9.0 and up, it be much easier to just use a Stack View. – ishaq Jan 28 '16 at 21:03
  • @ishaq I hadn't thought of that, but this looks pretty simple. What I'm trying to do isn't rocket science, but this looks even simpler to implement. http://stackoverflow.com/a/31577312/4475605 – Adrian Jan 28 '16 at 21:21
  • 1
    Well, I hope it helped :) – ishaq Jan 28 '16 at 21:22
  • Thanks again for suggesting `UIStackView`. That, combined with SwiftArchitect's suggestion and some inspiration from his AutoLayout article on his site got me squared away. I went through my entire project and redid most of my VCs w/ StackView. Much simpler, much less code, much better looking layout, and zero constraint errors logged. – Adrian Jan 29 '16 at 19:08

3 Answers3

1

Storyboard solution

Do not add constraints programmatically unless all other avenues have failed: there is no way to see what you are doing until you build, link, run.

A much simpler solution requiring much less code is to hold on to references of your views an constraints from Interface Builder, and:

constraintToTweak.constant = newValue

Unlike the other properties, the constant may be modified after constraint creation. Setting the constant on an existing constraint performs much better than removing the constraint and adding a new one that's just like the old but for having a new constant.

or

constraintToTweak.active = false

The receiver may be activated or deactivated by manipulating this property.  Only active constraints affect the calculated layout.  Attempting to activate a constraint whose items have no common ancestor will cause an exception to be thrown. Defaults to NO for newly created constraints.

SwiftArchitect
  • 47,376
  • 28
  • 140
  • 179
  • Thank you. So it sounds like you're suggesting I lay out on storyboard with both views on it, then `ctrl + drag` constraints from the storyboard to the VC and tweak them programmatically? That is very cool! I didn't know you could create `IBOutlets` from the storyboard. That's a LOT simpler! – Adrian Jan 29 '16 at 01:07
  • `IBOutlets` from constraints, that is ;) – Adrian Jan 29 '16 at 01:24
  • Correct. You can't do much with them (although I have seen some engineers remove them at runtime if it's a one-time operation), but you can change their value or activate/deactivate them. An added bonus is that these 2 values can be animated, which can create nice visual effects. – SwiftArchitect Jan 29 '16 at 03:30
  • Hmm...this is like whack-a-mole. I ended up going with a `UIStackView` for my `tableView` and `bannerView`. `bannerView.hidden = true` it stays as it's configured in interface builder. While the frame draws when and where it's supposed to, but the ad doesn't appear in the frame even though the request is sent successfully. This doesn't "get me to the dance" just yet, but I think i'm dealing w/ an AdMob issue at this point. Thank you for helping me cut 200 lines of code ;) – Adrian Jan 29 '16 at 04:06
1

With ishaq's and SwiftArchitect's assistance, I figured it out. It turns out, the key to getting the GADBannerView to display properly below a UITableView without adding it as a footer was super simple. I ended up chopping 100 lines of needless code by doing the following:

1) UIStackView: If you haven't used this before, stop what you're doing now and follow this tutorial.

I added my tableView: UITableView and the bannerView: GADBannerView in interface builder to a vertical UIStackView

2) I created IBOutlets (I had tableView already) for both of them on MyViewController.

3) My refactored configureBannerView looks like this.

// I added these properties at the top. I did not know you could drag 
// constraints from Interface Builder onto the ViewController
@IBOutlet weak var bannerViewHeight: NSLayoutConstraint!
@IBOutlet weak var bannerViewWidth: NSLayoutConstraint!


func configureBannerView() {
    if adSupported == true {
        loadAd() // follow Google's documentation to configure your requests
    } else {
        bannerView.hidden = true // this hides the bannerView if it's not needed

    // Removes constraint errors when device is rotated
    bannerViewWidth.constant = 0.0  
    bannerViewHeight.constant = 0.0

    }
}

Thanks to ishaq & SwiftArchitect for pointing me in the right direction.

Adrian
  • 16,233
  • 18
  • 112
  • 180
0

Add a new adMob UIViewController as root controller and addSubview of the old root controller.

I had a programmatic very similar problem case having to add adMob to a UITabBarController root controller with tab Controllers that were UINavigationController. Them all making hard resistance in trying to resize them internally, the ads were just typed over the application views. I might have just not been lucky enough finding a working way that path. Read a lot Stackoverflow and Google hints.

I also believe Apples and Googles recommendations are like having the ads below and kind of separate from but tight to the app. The Android Admob banners appears the same way, and same behavior is wanted. (Did the same project for Android recently).

Make a new app root controller, a ViewController with Admob

To be reusable and smoothly implementable in future projects, about a new adUnitID is all needed to be put in (and the name of the old root controller). The banner will be below the applications screen.

Just add this class file to the project and make it the root controller in the AppDelegate.swift file.

import UIKit
import GoogleMobileAds

class AdsViewController: UIViewController, GADBannerViewDelegate {

    let adsTest         = true
    let ads             = true //false // 
    let adUnitIDTest    = "ca-app-pub-3940256099942544/2934735716"
    let adUnitID        = "ca-app-pub-xxxxxxxxxxxxxxxx/xxxxxxxxxx"
    let tabVc           = MainTabBarController()

    var bannerView: GADBannerView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Add the adMob banner
        addBanner(viewMaster: view)
        // Add the old root controller as a sub-view
        view.addSubview(tabVc.view)
        // Make its constraints to fit the adMob
        if(ads) {
            tabVc.view.translatesAutoresizingMaskIntoConstraints = false
            tabVc.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
            tabVc.view.bottomAnchor.constraint(equalTo: bannerView.topAnchor).isActive = true
            tabVc.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
            tabVc.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        // To be notified of rotation
        NotificationCenter.default.addObserver(self, selector: #selector(AdsViewController.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
        }
    }

/*******************************

Ads banner standard engine

*******************************/

    func addBanner(viewMaster: UIView) {
        if(ads) {
            // We instantiate the banner with desired ad size.
            bannerView = GADBannerView(adSize: kGADAdSizeSmartBannerPortrait)
            addBannerViewToView(bannerView, viewMaster: viewMaster)
            if(adsTest) {
                bannerView.adUnitID = adUnitIDTest
            } else {
                bannerView.adUnitID = adUnitID
            }

            bannerView.rootViewController = self
            bannerView.load(GADRequest())
            bannerView.delegate = self
        }
    }

    func addBannerViewToView(_ bannerView: GADBannerView, viewMaster: UIView) {
        bannerView.translatesAutoresizingMaskIntoConstraints = false
        viewMaster.addSubview(bannerView)
        viewMaster.addConstraints(
        [NSLayoutConstraint(item: bannerView,
                            attribute: .bottom,
                            relatedBy: .equal,
                            toItem: viewMaster.safeAreaLayoutGuide,
                            attribute: .bottom,
                            multiplier: 1,
                            constant: 0),
         NSLayoutConstraint(item: bannerView,
                            attribute: .centerX,
                            relatedBy: .equal,
                            toItem: viewMaster,
                            attribute: .centerX,
                            multiplier: 1,
                            constant: 0)
        ])
    }

/*******************************

Rotation (change SmartBanner)

*******************************/
    @objc func rotated() {
        if UIDevice.current.orientation.isPortrait {
            bannerView.adSize = kGADAdSizeSmartBannerPortrait

        }
        if UIDevice.current.orientation.isLandscape {
            bannerView.adSize = kGADAdSizeSmartBannerLandscape
        }
    }
}
Jan Bergström
  • 736
  • 6
  • 15