SwiftUI: Admob Interstitial Ad is not being presented on rootViewController - swift

CODED IN SWIFTUI
I have implemented a settings page within my SwiftUI app that is presented upon clicking the button within the view. This view is wrapped within a sheet. You can see the code below:
TabsBar(showOptionsTab: $showOptionsTab)
.sheet(isPresented: $showOptionsTab)
{
OptionsTab(showOptionsTab: self.$showOptionsTab)
}
I have implemented an interstitial within this "OptionsTab" that is created when the options tab is loaded within the .onAppear() call and presented when the back button is selected. You can the code from the OptionsTab below:
#State private var interstitial : GADInterstitial!
VStack
{
... other code
Button(action:
{
if ( self.interstitial.isReady)
{
let root = UIApplication.shared.windows.first?.rootViewController
self.interstitial.present(fromRootViewController: root!)
}
self.showOptionsTab.toggle()
})
{
Image(systemName: "xmark")
.font(.largeTitle)
}
}
.onAppear
{
self.interstitial = GADInterstitial(adUnitID: "addID")
let req = GADRequest()
self.interstitial.load(req)
}
I have additional code within the button that logs the status of the interstitial, and it shows it as being READY, and the present code is being hit... but it never presents itself.
I have implemented this exact code in the ContentView, and the interstitial loads perfectly fine. Something about this code being within the sheet is causing the interstitial not to present.
Is the rootViewController the incorrect view to load the interstitial on when in a view that is presented from a sheet? Otherwise, what else am I doing wrong?
Thanks for the help in advance!

Try this
TabsBar(showOptionsTab: $showOptionsTab)
.sheet(isPresented: $showOptionsTab)
{
OptionsTab(showOptionsTab: self.$showOptionsTab)
.OnDisapear
{
if ( self.interstitial.isReady)
{
let root = UIApplication.shared.windows.first?.rootViewController
self.interstitial.present(fromRootViewController: root!)
}
}
}
}
And then add the state interstitial variable within this view, and onAppear of the ContextView, create the interstitial there.

Solution 2:
(not tested but should work):
present on the presentingViewController instead of the rootViewController
if ( self.interstitial.isReady)
{
let root = UIApplication.shared.windows.first?.rootViewController?.presentingViewController
self.interstitial.present(fromRootViewController: root!)
}
check the reason, at the end of the answer
Solution 1:
it sounds like that there's something that prevents Interstitial ad to be presented when a sheet view is already presented
in my case i wanted to present the ad immediately after the sheet view is presented, which didn't work
what worked is, Presenting the ad, and after it being dismissed, the sheet view will be presented , and it worked!
for the Interstitial i used this code
import SwiftUI
import GoogleMobileAds
import UIKit
final class Interstitial:NSObject, GADInterstitialDelegate{
var interstitialID = "ca-app-pub-3940256099942544/4411468910"
var interstitial:GADInterstitial = GADInterstitial(adUnitID: "ca-app-pub-3940256099942544/4411468910")
var sheet: (() -> Void)? = nil //this closure will be executed after dismissing the ad
override init() {
super.init()
LoadInterstitial()
}
func LoadInterstitial(){
let req = GADRequest()
self.interstitial.load(req)
self.interstitial.delegate = self
}
func showAd(then sheet:(() -> Void)?){
if self.interstitial.isReady{
let root = UIApplication.shared.windows.first?.rootViewController
self.interstitial.present(fromRootViewController: root!)
self.sheet = sheet
}
else{
print("Not Ready")
}
}
func interstitialDidDismissScreen(_ ad: GADInterstitial) {
self.interstitial = GADInterstitial(adUnitID: interstitialID)
LoadInterstitial()
if let presentSheet = sheet {
presentSheet()
}
}
}
here we pass whatever we want to happen to the closure which will be called when interstitialDidDismissScreen is called
now instead of switching the state when button is pressed, it will be switched after the ad is dimissed
at the top of your contentView:
struct HistoryView: View {
private var fullScreenAd: Interstitial!
init() {
fullScreenAd = Interstitial()
}
}
then in your button:
Button(action: {
self.interstitial.showAd {
self.showOptionsTab.toggle()
}
})
Note: i used the code of interstitial Ad from this gist
Also, AFAIK we can't tell if it's an bug in swiftUI, or it's something googleAdmob was supposed to take care of ?, there's no SwiftUI support From google so we can't blame them.
the problem AFAIK, that in iOS 13, grabbing the root ViewController will return the View Controller that presented the sheet, and when trying to present the ad, UIKit will complain that you are trying to present A view controller while a view controller is already present(the sheet),
Solution ? simple, present the Ad, on the presented view Controller(The sheet)

Related

Admob Ad Inflates, but overlaps existing views in SwiftUI

I have this app that I am trying to use AdMob with that is made with SwiftUI. I had been using official AdMob documentation with SwiftUI to help me do this. I have one issue however, the ad successfully gets loaded, but the ad is located underneath another view, much like a ZStack without a ZStack present. No matter where I put it, it gets placed beneath a view. I have used the .frame() attribute, and it works using the BannerAd view, but then there is a giant white space there when the ad can't be loaded. Here are screenshots that may help solve the issue:
(Image on the left) This is what happens using the Google provided code (They say that the banner would reload into its own view when the ad is loaded, but does not appear to happen) As you can see, the add is below the name of the current herb.
(Image on the right)This is what happens when using the .frame() attribute before the ad is loaded. The ad loads correctly, but leaves this white space when not loaded or unable to load.
Here is the code to make the banner work with SwiftUI
import SwiftUI
import Foundation
import GoogleMobileAds
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)
// Tell the delegate the initial ad width.
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
// Notify the delegate of ad width changes.
self.delegate?.bannerViewController(
self, didUpdate: self.view.frame.inset(by: self.view.safeAreaInsets).size.width)
}
}
}
struct BannerAd: UIViewControllerRepresentable {
#State private var viewWidth: CGFloat = .zero
private let bannerView = GADBannerView()
private let adUnitID = "ca-app-pub-3940256099942544/6300978111"
// Make the view to be used in SwiftUI
func makeUIViewController(context: Context) -> some UIViewController {
let bannerViewController = BannerViewController()
bannerView.adUnitID = adUnitID
bannerView.rootViewController = bannerViewController
bannerViewController.view.addSubview(bannerView)
bannerViewController.delegate = context.coordinator
return bannerViewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
if(viewWidth != .zero){
// Nothing happens
} else {
// Return when the size is greater than 0
//TODO: Fix !!!! This does not push other views down as it should, a frame could be set, but the ad does not push everything down or up
return
}
// Request a banner ad with the updated viewWidth.
bannerView.adSize = GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(viewWidth)
bannerView.load(GADRequest())
}
}
}
The code may be long, but you can ignore the coordinator as that is only for listeners, not nay real displaying of the advertisement.
What I want to happen exactly is for the Ad to move every object down so that when the ad is loaded, it will fit respectfully at the top of the page. I have looked at all sorts of sources, but I cannot find any solutions to this anywhere. Please help me, and thank you!

How to change a macOS menubar icon?

I'm trying to change a macOS menubar icon at the click of a button based on this question.
However, when I run the app and click the button, I get a Could not cast value of type 'SwiftUI.AppDelegate' (0x20dfafd68) to 'MenuBarTest.AppDelegate' (0x10442c580). error on this line:
let appDelegate = NSApplication.shared.delegate as! AppDelegate
Main Swift File
#main
struct MenuBarTestApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var statusItem: NSStatusItem?
var popOver = NSPopover()
func applicationDidFinishLaunching(_ notification: Notification) {
let menuView = MenuBarView()
popOver.behavior = .transient
popOver.animates = true
popOver.contentViewController = NSViewController()
popOver.contentViewController?.view = NSHostingView(rootView: menuView)
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let MenuButton = statusItem?.button {
MenuButton.image = NSImage(named: "settings_bw")
MenuButton.image?.size = NSSize(width: 18.0, height: 18.0)
MenuButton.action = #selector(MenuButtonToggle)
}
}
func setIcon(colorType:String) {
var icon = NSImage(named: "settings_color")
if colorType != "color" {
icon = NSImage(named: "settings_bw")
}
statusItem?.button?.image = icon
}
#objc
func MenuButtonToggle(sender: AnyObject) {
if popOver.isShown {
popOver.performClose(sender)
} else {
if let MenuButton = statusItem?.button {
self.popOver.show(relativeTo: MenuButton.bounds, of: MenuButton, preferredEdge: NSRectEdge.minY)
popOver.contentViewController?.view.window?.makeKey()
}
}
}
}
MenuBar View
struct MenuBarView: View {
var body: some View {
VStack {
Button(action: {
let appDelegate = NSApplication.shared.delegate as! AppDelegate
appDelegate.setIcon(colorType: "color")
}, label: {
Text("Change Icon")
})
}
.frame(width: 150, height: 100)
}
}
First, I'll answer the question as asked, but then I'll follow up with why it actually doesn't matter because it's solving the wrong problem.
Accessing the App Delegate in a SwiftUI lifecycle app
When you're using SwiftUI to manage your app lifecycle, you don't use an app delegate directly. SwiftUI will set the application's app delegate to an instance of its own class, SwiftUI.AppDelegate. This is what you saw when trying to cast it to MenuBarTest.AppDelegate. You can't cast it to your class, because it's not an instance of your class.
As a backwards compatibility feature, SwiftUI's app delegate lets you provide your own delegate instance to that it will forward to. This is wired up with the NSApplicationDelegateAdaptor property wrapper. If you want to access an instance, that delegate instance property is the way to get at it (not NSApplication.shared.delegate).
Don't even use the App Delegate
You can disregard everything I said so far, because this is just a bad design, and you shouldn't use it. The App Delegate should be strictly limited to code related to managing the life cycle of your app and how it interacts with the system. It should not be a kitchen-junk-drawer of any code that fits.
Having random code right in the App Delegate is the AppKit equivalent to putting all your code in the main() function of a C program. It's done for brevity, but rarely a good idea. This is what you'll often see in tutorials, which aim to keep file and line count at a minimum, to illustrate some other point (e.g. Related to menu bar configuration, in this case).
As you'll notice, none of the code in this app delegate does much that's specific to the App life cycle. It just calls out to NSStatusBar. This code can just be put in any other appropriate place, like any other stateful SwiftUI code.

Menu Bar Popover is missing from application's elements tree on macOS

I'm currently trying to write simple UI Tests for an App that comes with a popover in the macOS menu bar. One of the tests is supposed to open the menu bar popover and interact with its content. The problem is that the content seems to be completely absence from the application's element tree.
I'm creating the pop-up like so:
let view = MenuBarPopUp()
self.popover.animates = false
self.popover.behavior = .transient
self.popover.contentViewController = NSHostingController(rootView: view)
…and show/hide it on menu bar, click like this:
if let button = statusItem.button {
button.image = NSImage(named: NSImage.Name("MenuBarButtonImage"))
button.action = #selector(togglePopover(_:))
}
#objc func togglePopover(_ sender: AnyObject?) {
if self.popover.isShown {
popover.performClose(sender)
} else {
openToolbar()
}
}
func openToolbar() {
guard let button = menuBarItem.menuBarItem.button else { return }
self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
NSApp.activate(ignoringOtherApps: true)
}
When I dump the element tree, the popover is not present:
[…]
MenuBar, 0x7fef747219d0, {{1089.0, 1.0}, {34.0, 22.0}}
StatusItem, 0x7fef74721b00, {{1089.0, 1.0}, {34.0, 22.0}}
[…]
Everything works when I compile the app and click around, I just can't make it work when it comes to automated UI testing. Any ideas?
Okay, after spending a lot of time with it, this solved my problem.
First, I had to add the Popover to the app's accessibility children like so:
var accessibilityChildren = NSApp.accessibilityChildren() ?? [Any]()
accessibilityChildren.append(popover.contentViewController?.view as Any)
NSApp.setAccessibilityChildren(accessibilityChildren)
However, this didn't seem to solve my problem at first. I'm using an App Delegate in a SwiftUI application. After tinkering with it for quite some time, I figured out that the commands I've added in my App.swift didn't go too well with my changes to the accessibility children in the App Delegate. After removing the commands from the Window Group, everything worked as expected.
#main
struct MyApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
// .commands {
// CommandGroup(replacing: CommandGroupPlacement.appSettings) {
// Button("Preferences...") { showPreferences() }
// }
// }
}
}

SwiftUI - Display view on UIWindow

I'm trying to display a custom SwiftUI view similar to a Toast in Android.
My issue is that I would like to display this particular view above everything else, using the current UIWindow.
Currently, while working on static func displayToastAboveAll() located in my ToastView, this is how far i got
public struct ToastView: View {
static func displayToastAboveAll() {
let window = UIApplication.shared.windows.filter { $0.isKeyWindow }.first // window
let viewToShow = ToastView(my params) // my view to display
// This part I'm not sure of
let hostingController = UIHostingController(rootView: viewToShow)
window?.addSubview(hostingController.view)
}
public var body: some View {
// MyDesign
}
}
Any idea how should I use the window to put the ToastView at its proper place, and still being able to navigate within the app (and use the outlets) while having the view displayed ?
I managed to do what I wanted.
Basically, this code is working, but I had to remove some constraints from my SwiftUI view and add them with UIKit using the static func.
Also, I had to pass by a modifier (see below) and put ToastView init in private.
public struct ToastModifier: ViewModifier {
public func body(content: Content) -> some View {
content
}
}
extension View {
public func toast() -> some View {
ToastView.displayToastAboveAll()
return modifier(ToastModifier())
}
}
This is done to force the use of either modifier (SwiftUI, by doing .toast, just like you'd do .alert) or directly by calling the static func ToastView.displayToastAboveAll() (UIKit).
Indeed, I dont wont this Toast to be a part of the view, I want to trigger it like an alert.
Finally, special warning because passing ToastView into UIHostingViewController will mess with some of the animations.
I had to rewrite animations in UIKit in order to have a nice swipe & fade animation.

How to test if the app is presenting a certain view controller?

I'm pretty new to XCode UI tests and I'm trying to run a test where I fill two text labels and then I press a button. After the button is pressed the app should make an URL call and be redirected to another view controller. I want to check if at the end of this operation the second view controller is displayed.
To test this, I have written the following test:
let app = XCUIApplication()
app.launch()
let ownerTextField = app.textFields["ownerTextField"]
ownerTextField.tap()
ownerTextField.typeText("UserA")
let repositoryTextField = app.textFields["repositoryTextField"]
repositoryTextField.tap()
repositoryTextField.typeText("AppB")
app.buttons["SearchButton"].tap()
XCTAssertTrue(app.isDisplayingResults)
Where isDisplayingResults is
extension XCUIApplication {
var isDisplayingResults: Bool {
return otherElements["resultView"].exists
}
}
I have set up the identifier of the View controller inside its swift file class:
override func viewDidLoad() {
super.viewDidLoad()
view.accessibilityIdentifier = "resultView"
...
Nonetheless to say, the test fails. How can I get a success?
It's so simple.
If after clicking the URL, Viewcontroller is presenting means your previous VC button doesn't exist on screen.
So Just check for previous VC button exists or not.
If app.buttons["SearchButton"].esists()
{ //write if code
} else {
// Write else code
}