AdMob inline adaptive banner in NavigationSplitView sidebar

TL;DR

How to implement AdMob inline adaptive banner ad in NavigationSplitView's sidebar on iPad with SwiftUI, so it takes the entire width of its parent and the view's height adjusts to the ad's height?


Details

I'm trying to insert an AdMob inline adaptive banner ad in the sidebar of a NavigationSplitView on iPad. As I'm using SwiftUI, I tried to replicate the implementation from this Google example. One problem is that the example is for an adaptive anchor banner ad—not what I'm looking for.

I've made a few attempts over the past few days, with varying results. I can't seem to make it truly adaptive. Two attempts that display the ad but with fixed size are presented below.


Attempt 1

struct InlineAdaptiveBannerAdView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> some UIViewController {
        let viewController = UIViewController()
        let adSize = GADInlineAdaptiveBannerAdSizeWithWidthAndMaxHeight(280, 150)
        let bannerView = GADBannerView(adSize: adSize)
        bannerView.adUnitID = "ca-app-pub-3940256099942544/2934735716"
        bannerView.rootViewController = viewController
        let request = GADRequest()
        request.scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        bannerView.load(request)
        viewController.view.addSubview(bannerView)
        return viewController
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}

I didn't know how to get the width of the sidebar programmatically, so I set it to 320 with .navigationSplitViewColumnWidth() and set the ad width to 280 (adjusted for padding).

This code displays the ad nicely (at least along X-axis), but the size is fixed.


Attempt 2

struct InlineAdaptiveBannerAdView: UIViewControllerRepresentable {
    // viewWidth is set to .zero in the Google sample
    @State private var viewWidth: CGFloat = CGFloat(280.0)
    private let bannerView = GADBannerView()
    private let adUnitID = "ca-app-pub-3940256099942544/2934735716"
    
    func makeUIViewController(context: Context) -> some UIViewController {
        let bannerViewController = BannerViewController()
        bannerView.adUnitID = adUnitID
        bannerView.rootViewController = bannerViewController
        bannerView.delegate = context.coordinator
        bannerView.translatesAutoresizingMaskIntoConstraints = false
        // Removed the constraints from the sample
        bannerViewController.view.addSubview(bannerView)
        bannerViewController.delegate = context.coordinator
        return bannerViewController
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        guard viewWidth != .zero else { return }
        bannerView.adSize = GADInlineAdaptiveBannerAdSizeWithWidthAndMaxHeight(viewWidth, 150)
        let request = GADRequest()
        request.scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        bannerView.load(request)
        print("View height: \(uiViewController.view.frame.height)")
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, BannerViewControllerWidthDelegate, GADBannerViewDelegate {
        let parent: InlineAdaptiveBannerAdView
        
        init(_ parent: InlineAdaptiveBannerAdView) {
            self.parent = parent
        }
        
        // MARK: BannerViewControllerWidthDelegate methods
        func bannerViewController(_ bannerViewController: BannerViewController, didUpdate width: CGFloat) {
            parent.viewWidth = width
        }
        
        // MARK: GADBannerViewDelegate methods
        func bannerViewDidReceiveAd(_ bannerView: GADBannerView) {
            print("Did receive ad")
            print("Ad height: \(bannerView.adSize.size.height)")
        }
        
        func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) {
            print("Did not receive ad: \(error.localizedDescription)")
        }
    }
}

protocol BannerViewControllerWidthDelegate: AnyObject {
    func bannerViewController(_ bannerViewController: BannerViewController, didUpdate width: CGFloat)
}

class BannerViewController: UIViewController {
    weak var delegate: BannerViewControllerWidthDelegate?
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        delegate?.bannerViewController(self, didUpdate: view.frame.inset(by: view.safeAreaInsets).size.width)
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        coordinator.animate { _ in
            // Do nothing
        } completion: { _ in
            self.delegate?.bannerViewController(self, didUpdate: self.view.frame.inset(by: self.view.safeAreaInsets).size.width)
        }
    }
}

If I leave the default .zero for viewWidth, the whole process doesn't get through guard viewWidth != .zero else { return }.

With viewWidth set to 280, the ad shows, but the size is never updated.

Both viewDidAppear(_:) and viewWillTransition(to:with:) never get called, even if the ad gets displayed, so the width is never updated.

As for height, according to Google developer's guide (which I can't link to on this forum):

The height is either zero or maxHeight, depending on which API you're using. The actual height of the ad is made available when it's returned.

So, I can read the ad's height when it is presented, but I still don't know how to really update the view's size.


My brain is fried. Am I missing something very simple and obvious here?

It turns out that Attempt 2 works even with initial viewWidth = .zero. However, the problem is that if I insert the banner in a List, viewDidAppear(animated:) never triggers, so width never gets updated.

Does anyone know how to work around this?

AdMob inline adaptive banner in NavigationSplitView sidebar
 
 
Q