Reputation: 21
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()
}
}
Upvotes: 2
Views: 345
Reputation: 21
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 Foundation
import SwiftUI
enum AppRoute: Equatable { case Home case Game case Settings case Instructions }
class Router<Route: Equatable>: ObservableObject {
var routes = [Route]()
var onPush: ((Route) -> Void)?
var onPop: (() -> Void)?
init(initial: Route? = nil) {
if let initial = initial {
routes.append(initial)
}
}
func push(_ route: Route) {
routes.append(route)
onPush?(route)
}
func pop() {
routes.removeLast()
onPop?()
}
}
struct RouterHost<Route: Equatable, Screen: View>: UIViewControllerRepresentable {
let router: Router<Route>
@ViewBuilder
let builder: (Route) -> Screen
func makeUIViewController(context: Context) -> UINavigationController {
let navigation = UINavigationController()
for route in router.routes {
navigation.pushViewController(
UIHostingController(rootView: builder(route)), animated: false
)
}
router.onPush = { route in
navigation.pushViewController(
UIHostingController(rootView: builder(route)), animated: true
)
}
router.onPop = {
navigation.popViewController(animated: true)
}
return navigation
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
}
typealias UIViewControllerType = UINavigationController
}
struct RouterView: View {
@StateObject var router = Router(initial: AppRoute.Home)
var body: some View {
RouterHost(router: router) { route in
switch route {
case .Home: StartupView()
case .Settings: SettingsView()
case .Game: GameTableView()
case .Instructions: InstructionsView()
}
}.environmentObject(router)
.accentColor(Color("textDark"))
.navigationBarBackButtonHidden(true)
.background(Color("backgroundGreen"))
.ignoresSafeArea(.all)
}
}
Then the app code:
The app:
import SwiftUI
import GoogleMobileAds
@main
struct AdsTestApp: App {
@StateObject var router = Router(initial: AppRoute.Home)
@State var showAd: Bool = true
let adV = InterstitialAd.shared
init(){
initMobileAds()
}
var body: some Scene {
WindowGroup {
RouterView(showAd: $showAd)
}
}
func initMobileAds() {
GADMobileAds.sharedInstance().start(completionHandler: nil)
GADMobileAds.sharedInstance().disableSDKCrashReporting()
InterstitialAd.shared.loadAd()
}
}
Home View:
import SwiftUI
struct HomeView: View {
@State var path = NavigationPath()
@Binding var showAd: Bool
@EnvironmentObject var router: Router<AppRoute>
var body: some View {
NavigationStack(path: $path){
VStack {
Button {
router.push(.Content)
} label: {
Text("To Content")
}
}
.padding()
}
}
}
#Preview {
HomeView(showAd: .constant(true))
}
Content View:
import SwiftUI
struct ContentView: View {
@Environment (\.isPresented) var isPresented
@EnvironmentObject var router: Router<AppRoute>
@Binding var showAd: Bool
var body: some View {
VStack() {
Spacer()
Button {
router.pop()
} label: {
Text("Home")
.frame(maxWidth: .infinity )
}
Button {
router.push(.Settings)
} label: {
Text("Settings")
.frame(maxWidth: .infinity )
}
Spacer()
}
.background(.yellow)
.background(){
InterstitialAdView(isPresented: $showAd)
}
}
}
#Preview {
ContentView(showAd: .constant(false))
}
Settings View:
import SwiftUI
struct SettingsView: View {
var body: some View {
VStack() {
Text(/*@START_MENU_TOKEN@*/"Settings"/*@END_MENU_TOKEN@*/)
.font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/)
.frame(maxWidth: .infinity, maxHeight: .infinity )
.padding(.all)
}
.background(.green)
}
}
#Preview {
SettingsView()
}
Ad Object Code:
import GoogleMobileAds
import SwiftUI
import UIKit
class InterstitialAd: NSObject, ObservableObject {
var interstitialAd: GADInterstitialAd?
static let shared = InterstitialAd()
func loadAd() {
let req = GADRequest()
/*
Test ads: ca-app-pub-3940256099942544/4411468910
*/
let id = "ca-app-pub-3940256099942544/4411468910"
GADInterstitialAd.load(withAdUnitID: id, request: req) { interstitialAd, err in
if let err = err {
#if DEBUG
print("🟡: Failed to display interstitial ad: \(err.localizedDescription)")
#endif
return
}
self.interstitialAd = interstitialAd
}
}
}
struct InterstitialAdView: UIViewControllerRepresentable {
let interstitialAd = InterstitialAd.shared.interstitialAd
@Binding var isPresented: Bool
@Environment(\.presentationMode) var presentationMode
init(isPresented: Binding<Bool>) {
self._isPresented = isPresented
}
func makeUIViewController(context: Context) -> UIViewController {
let view = UIViewController()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1)) {
self.showAd(from: view)
}
return view
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func showAd(from root: UIViewController) {
if let ad = interstitialAd {
ad.present(fromRootViewController: root)
} else {
print("Ad not ready")
self.isPresented.toggle()
InterstitialAd.shared.loadAd()
}
}
class Coordinator: NSObject, GADFullScreenContentDelegate {
var parent: InterstitialAdView
init(_ parent: InterstitialAdView) {
self.parent = parent
super.init()
parent.interstitialAd?.fullScreenContentDelegate = self
}
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
InterstitialAd.shared.loadAd()
parent.isPresented.toggle()
}
}
}
Upvotes: 0