简体   繁体   中英

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
  • UITableView s 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:

  1. The UITableView did not extend its edges below the bottom UITabBar
  2. 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 (ie, 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 UITableViewController s 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. 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM