I have a simple app with a Home View, a Content View and a Settings View. I am trying to show an AdMob Interstitial when I navigate from the Home View to the Content View. The navigation between views is using a combination of NavigationStack and Navigation Links. It all works fine when I don't show the ad. When I do show the ad, all of the Navigation Links are no longer responsive.
using SwiftUI 5.0 on iOS 17.0 in XCode 15
Expected flow: HOME >> AD >> CONTENT >> SETTINGS When ad is shown HOME >> AD >> CONTENT >> no response (back button works then CONTENT link is broken)
Source code is below (to try it you'll have to install the GoogleMobileAds library and add the requisite plist fields). To replicate, run this app (wait a sec for an ad to load) and click on the 'To Content' link. A test ad should show. When you dismiss it you see the Content View. Click on 'To Settings' and the link doesn't work. Click 'Back' and the 'To Content' link doesn't work anymore.
The app flow is HOME >> CONTENTVIEW (with Ad) >> SETTINGSVIEW
[UPDATED WITH SIMPLIFIED CODE]
App Code:
import SwiftUI
import GoogleMobileAds
@main
struct AdsTestApp: App {
let adManager = InterstitialAdsManager.shared
init(){
initMobileAds()
}
var body: some Scene {
WindowGroup {
HomeView()
}
}
func initMobileAds() {
GADMobileAds.sharedInstance().start(completionHandler: nil)
GADMobileAds.sharedInstance().disableSDKCrashReporting()
InterstitialAdsManager.shared.loadInterstitialAd()
}
}
HomeView Code
import SwiftUI
struct HomeView: View {
@State var showAd: Bool = true
var body: some View {
NavigationStack{
VStack {
NavigationLink(destination: ContentView(showAd: $showAd)){
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("To Content")
}
}
.padding()
}
}
}
#Preview {
HomeView()
}
ContentView Code
import SwiftUI
struct ContentView: View {
@Binding var showAd: Bool
let adsManager = InterstitialAdsManager.shared
var body: some View {
ZStack() {
NavigationLink {
SettingsView()
} label: {
Label("To Settings", systemImage: "slider.horizontal.3")
.font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/)
.frame(maxWidth: .infinity, maxHeight: .infinity )
}
}
.background(.yellow)
.onAppear(){
if(showAd){
adsManager.displayInterstitialAd()
}
showAd.toggle()
}
}
}
SettingsView Code
import SwiftUI
struct SettingsView: View {
var body: some View {
VStack() {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
.font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/)
.frame(maxWidth: .infinity, maxHeight: .infinity )
.padding(.all)
}
.background(.green)
}
}
#Preview {
SettingsView()
}
InterstitialAdManager Code
import Foundation
import GoogleMobileAds
class InterstitialAdsManager: NSObject, GADFullScreenContentDelegate, ObservableObject {
// Properties
@Published var interstitialAdLoaded:Bool = false
var interstitialAd:GADInterstitialAd?
static let shared = InterstitialAdsManager()
override init() {
super.init()
}
func loadInterstitialAd(){
GADInterstitialAd.load(withAdUnitID: "ca-app-pub-3940256099942544/4411468910", request: GADRequest()) { [weak self] add, error in
guard let self = self else {return}
if let error = error{
print(": \(error.localizedDescription)")
self.interstitialAdLoaded = false
return
}
#if DEBUG
print(": Ad Loading succeeded")
#endif
self.interstitialAdLoaded = true
self.interstitialAd = add
self.interstitialAd?.fullScreenContentDelegate = self
}
}
func displayInterstitialAd(){
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
guard let root = window?.rootViewController else {
return
}
if let ad = interstitialAd{
ad.present(fromRootViewController: root)
self.interstitialAdLoaded = false
}else{
print(": Ad not ready")
self.interstitialAdLoaded = false
self.loadInterstitialAd()
}
}
// Failure notification
func ad(_ ad: GADFullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
#if DEBUG
print(": Failed to display interstitial ad: \(error.localizedDescription)")
#endif
self.loadInterstitialAd()
}
// Indicate notification
func adWillPresentFullScreenContent(_ ad: GADFullScreenPresentingAd) {
#if DEBUG
print(": Displayed an interstitial ad")
#endif
self.interstitialAdLoaded = false
}
// Close notification
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
#if DEBUG
print(": Interstitial ad closed")
#endif
self.loadInterstitialAd()
}
}
I finally found an answer which is basically build a navigation stack that doesn't use NavigationStack or NavigationLink. These constructs do not play well with the need for the AdMob interstitial to have access to the rootViewController. I got the answer from this blog (this is not my blog and I am not affiliated) about how to build a router using UIHostingController and pushing the View objects on and off in the form of UIViewControllers.
Edit: adding the solution here:
There are three parts to the solution:
Here is the Code for the Router classes:
import SwiftUI
enum AppRoute: Equatable { case Home case Game case Settings case Instructions }
class Router<Route: Equatable>: ObservableObject {
}
struct RouterHost<Route: Equatable, Screen: View>: UIViewControllerRepresentable {
}
struct RouterView: View {
}
Then the app code:
The app:
Home View:
Content View:
Settings View:
Ad Object Code: