Dismiss In-App AppStore rating in UITests in Swift - swift

Hello dear developers,
I'm currently stuck due to a problem with In-App AppStore rating (SKStoreReviewController).
Here is the situation, I've a screen "FirstScreen" with a button. When I tap on it, I'm going to the next screen "SecondScreen" and an in app alert for AppStore rating pop over.
I'm trying to find a solution for my UITests in order to dismiss this Alert.
I tried many solutions but I'm looking for one which do not depends on string (I don't want to localize the content of this Alert):
override func setUp() {
app = XCUIApplication()
app.launch()
addUIInterruptionMonitor(withDescription: "System Dialog") { (alert) -> Bool in
let allowButton = alert.buttons.element(boundBy: 1)
if allowButton.exists {
allowButton.tap()
}
}
}
I also tried to add an interaction ("app.swipeUp()") when I'm coming to "SecondScreen" in order to trigger this handler.
I've also tried another solution, as I know when this alert will be triggered:
let dismissButton = XCUIApplication(bundleIdentifier: "com.apple.springboard").buttons.element(boundBy: 1)
if dismissButton.exists {
dismissButton.tap()
}
No one worked and I'm still stuck :(
Does anybody found a solution in order to dismiss this alert ?
Thanks

Swiping up doesn't work but ironically swiping down does. Here is a very simplistic example
import UIKit
import StoreKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
SKStoreReviewController.requestReview()
}
}
}
import XCTest
class UITests: XCTestCase {
override func setUp() {
continueAfterFailure = false
}
func test() {
let app = XCUIApplication()
app.launch()
sleep(5)
app.swipeDown()
sleep(3)
}
}

Related

Keyboard overlaying action sheet in iOS 13.1 on CNContactViewController

This seems to be specific to iOS 13.1, as it works as expected on iOS 13.0 and earlier versions to add a contact in CNContactViewController, if I 'Cancel', the action sheet is overlapping by keyboard. No actions getting performed and keyboard is not dismissing.
Kudos to #GxocT for the the great workaround! Helped my users immensely.
But I wanted to share my code based on #GxocT solution hoping it will help others in this scenario.
I needed my CNContactViewControllerDelegate contactViewController(_:didCompleteWith:) to be called on cancel (as well as done).
Also my code was not in a UIViewController so there is no self.navigationController
I also dont like using force unwraps when I can help it. I have been bitten in the past so I chained if lets in the setup
Here's what I did:
Extend CNContactViewController and place the swizzle function in
there.
In my case in the swizzle function just call the
CNContactViewControllerDelegate delegate
contactViewController(_:didCompleteWith:) with self and
self.contact object from the contact controller
In the setup code, make sure the swizzleMethod call to
class_getInstanceMethod specifies the CNContactViewController
class instead of self
And the Swift code:
class MyClass: CNContactViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.changeImplementation()
}
func changeCancelImplementation() {
let originalSelector = Selector(("editCancel:"))
let swizzledSelector = #selector(CNContactViewController.cancelHack)
if let originalMethod = class_getInstanceMethod(object_getClass(CNContactViewController()), originalSelector),
let swizzledMethod = class_getInstanceMethod(object_getClass(CNContactViewController()), swizzledSelector) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
// dismiss the contacts controller as usual
viewController.dismiss(animated: true, completion: nil)
// do other stuff when your contact is canceled or saved
...
}
}
extension CNContactViewController {
#objc func cancelHack() {
self.delegate?.contactViewController?(self, didCompleteWith: self.contact)
}
}
The keyboard still shows momentarily but drops just after the Contacts controller dismisses.
Lets hope apple fixes this
I couldn't find a way to dismiss keyboard. But at least you can pop ViewController using my method.
Don't know why but it's impossible to dismiss keyboard in CNContactViewController. I tried endEditing:, make new UITextField firstResponder and so on. Nothing worked.
I tried to alter action for "Cancel" button. You can find this button in NavigationController stack, But it's action is changed every time you type something.
Finally I used method swizzling. I couldn't find a way to dismiss keyboard as I mentioned earlier, but at least you can dismiss CNContactViewController when "Cancel" button is pressed.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
changeImplementation()
}
#IBAction func userPressedButton(_ sender: Any) {
let controller = CNContactViewController(forNewContact: nil)
controller.delegate = self
navigationController?.pushViewController(controller, animated: true)
}
#objc func popController() {
self.navigationController?.popViewController(animated: true)
}
func changeImplementation() {
let originalSelector = Selector("editCancel:")
let swizzledSelector = #selector(self.popController)
if let originalMethod = class_getInstanceMethod(object_getClass(CNContactViewController()), originalSelector),
let swizzledMethod = class_getInstanceMethod(object_getClass(CNContactViewController()), swizzledSelector) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
PS: You can find additional info on reddit topic: https://www.reddit.com/r/swift/comments/dc9n3a/bug_with_cnviewcontroller_ios_131/
Fixed in iOS 13.4
Tested in Xcode Simulator
NOTE: This bug is now fixed. This question and answer were applicable only to some particular versions of iOS (a limited range of iOS 13 versions).
The user can in fact swipe down to dismiss the keyboard and then tap Cancel and see the action sheet. So this issue is regrettable and definitely a bug (and I have filed a bug report) but not fatal (though, to be sure, the workaround is not trivial for the user to discover).
Thanks, #GxocT for your workaround, however, the solution posted here is different from the one you posted on Reddit.
The one on Reddit works for me, this one doesn't so I want to repost it here.
The difference is on the line with swizzledMethod which should be:
let swizzledMethod = class_getInstanceMethod(object_getClass(self), swizzledSelector) {
The whole updated code is:
class MyClass: CNContactViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.changeImplementation()
}
func changeCancelImplementation() {
let originalSelector = Selector(("editCancel:"))
let swizzledSelector = #selector(CNContactViewController.cancelHack)
if let originalMethod = class_getInstanceMethod(object_getClass(CNContactViewController()), originalSelector),
let swizzledMethod = class_getInstanceMethod(object_getClass(self), swizzledSelector) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
// dismiss the contacts controller as usual
viewController.dismiss(animated: true, completion: nil)
// do other stuff when your contact is canceled or saved
...
}
}
extension CNContactViewController {
#objc func cancelHack() {
self.delegate?.contactViewController?(self, didCompleteWith: self.contact)
}
}
Thanks #Gxoct for his excellent work around. I think this is very useful question & post for those who are working with CNContactViewController. I also had this problem (till now) but in objective c. I interpret the above Swift code into objective c.
- (void)viewDidLoad {
[super viewDidLoad];
Class class = [CNContactViewController class];
SEL originalSelector = #selector(editCancel:);
SEL swizzledSelector = #selector(dismiss); // we will gonna access this method & redirect the delegate via this method
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
Creating a CNContactViewController category for accessing dismiss;
#implementation CNContactViewController (Test)
- (void) dismiss{
[self.delegate contactViewController:self didCompleteWithContact:self.contact];
}
#end
Guys who are not so familiar with Swizzling you may try this post by matt
One thing to always take into account is that swizzler method is executed only once. Make sure that you implement changeCancelImplementation() in dispatch_once queue so that it is executed only once.
Check this link for description
Also this bug is found only in iOS 13.1, 13.2 and 13.3

windowWillClose and button action not called Swift

I'm designing a mac app with Xcode 10 (beta) and I got an issue with the Preference Window Controller
I have in my Main.storyboard a NSWindowController of custom class PreferenceWindowController with a toolbar. Here are its connections :
Here is the full class :
class PreferenceWindowController: NSWindowController, NSWindowDelegate {
#IBAction func didClickAuthor(_ sender: Any) {
print("author")
}
#IBAction func didClickTypo(_ sender: Any) {
print("typo")
}
override func windowDidLoad() {
super.windowDidLoad()
}
func windowWillClose(_ notification: Notification) {
print("willClose")
}
}
The window is initiated via the AppDelegate class with this code :
let storyboard = NSStoryboard(name: "Main",bundle: nil)
if let wc = storyboard.instantiateController(withIdentifier: "PreferenceWindowController") as? PreferenceWindowController
{
wc.showWindow(self)
}
The window opens as expected, with the toolbar clickable, but no functions from PreferenceWindowController are called at all, neither the closing of the window, nor the clicks on the toolbar.
I checked every connections, every class name, and I really don't know what's wrong...
SOLUTION
The solution is to store the PreferenceViewController class inside the AppDelegate class as a variable.
My solution :
var preferenceWindowController:PreferenceWindowController? = nil
#IBAction func clickPreferences(_ sender: Any) {
if let wc = storyboard.instantiateController(withIdentifier: "PreferencesWindowController") as? PreferenceWindowController {
let window = wc.window
preferenceWindowController = wc
wc.showWindow(self)
}
}
Thank you for helping !
The comment above seems like it could be on the right track. Based on the code context you've included in your question, it looks like the window controller you create will only have a lifetime for that function call.
Try making the window controller an instance variable. This is normally how I wire things up in an App delegate that creates window controllers. It's a simple pattern that works well.

Admob interstitial doesn't work

I'm dealing with the Admob interstitials and in particular I'm trying to display an interstitial when a particular ViewController of my app loads. I used the code provided by the official Admob Interstitial guide but it doesn't work :https://developers.google.com/admob/ios/interstitial?hl=it. I also followed this video here :https://youtu.be/ahmQQ3OeY0Y?t=787 (minute 13.04 stop the video). If you look at the code is the same as in the guide. My objective is to display the ad when the RequestViewController appears so I try to present the ad in the viewDidAppear function. Anyway it doesn't work, the console displays this error:
To get test ads on this device, call: request.testDevices =
#[ kGADSimulatorID ]
AppDelegate.swift
import UIKit
import UserNotifications
import GoogleMobileAds
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate,GADInterstitialDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
GADMobileAds.configure(withApplicationID: "ca-app-pub-*******")
}
}
This is the ViewController where I present the ad:
RequestviewController.swift
class RequestViewController: UIViewController {
var myInterstitial : GADInterstitial?
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
//display ad
myInterstitial = createAndLoadInterstitial()
if (myInterstitial?.isReady)!{
print("\n\n\n\n\nReady")
myInterstitial?.present(fromRootViewController: self)
}else { print("\n\n\n\nAd wasn't ready") }
}
func createAndLoadInterstitial()->GADInterstitial {
let interstitial = GADInterstitial(adUnitID: "ca-app-pub-3940256099942544/4411468910") //test Ad unit id
interstitial.delegate = self
interstitial.load(GADRequest())
return interstitial
}
func interstitialDidReceiveAd(ad: GADInterstitial!) {
print("interstitialDidReceiveAd")
}
func interstitial(ad: GADInterstitial!, didFailToReceiveAdWithError error: GADRequestError!) {
print(error.localizedDescription)
}
func interstitialDidDismissScreen(ad: GADInterstitial!) {
print("\n\n\n\n\n\ninterstitialDidDismissScreen")
myInterstitial = createAndLoadInterstitial()
}
you need to write it like,
appDelegate.createAndLoadInterstitial()
in place of,
appDelegate.myInterstitial?.present(fromRootViewController: self)
It looks like the root problem is this section of RequestViewController:
myInterstitial = createAndLoadInterstitial()
if (myInterstitial?.isReady)!{
print("\n\n\n\n\nReady")
myInterstitial?.present(fromRootViewController: self)
}else { print("\n\n\n\nAd wasn't ready") }
Interstitials are loaded asynchronously, which means the call to load() will return before the ad is loaded and ready to display. If your app is calling load() in createAndLoadInterstitial and then checking isReady immediately afterwards, there's no time for the ad to actually load, and isReady will always be false.
A good way to deal with this could be to create the interstitial in your first view controller, and then show it before transitioning to RequestViewController. That would give the ad time to load, and would still display it during the same transition.
FWIW, transition time between ViewControllers is a great place in the flow of your app to use an interstitial, so well done there.
Also, the line:
To get test ads on this device, call: request.testDevices = #[ kGADSimulatorID ]
isn't an error. The Android and iOS SDKs always print that to the log so publishers will know how to get test ads for a particular device or emulator. If you were running on a hardware device instead of the iOS simulator, you'd see a unique identifier for the device in that line, which you could then use in your code to get test ads.

Launch Main app from MenuBar app

I've managed to create an Application and I don't want it to be on the Dock until a user clicks the MenuBar Item, then it launches the app and if a user quits the Main app, the menubar Item still remains.
Ok after days of research, I finally figured how to do it.
Swift 2.3
func toggleApp(sender: AnyObject) {
if self.window!.visible {
self.window.orderOut(window)
NSApplication.sharedApplication().setActivationPolicy(NSApplicationActivationPolicy.Accessory)
} else {
NSApplication.sharedApplication().setActivationPolicy(NSApplicationActivationPolicy.Regular)
self.window!.makeKeyAndOrderFront(nil)
NSApp.activateIgnoringOtherApps(true)
}
}
And this is for keeping the Menubar when the user quits(CMD Q) the app:
func applicationShouldTerminate(sender: NSApplication) -> NSApplicationTerminateReply {
// Cancel terminate if pref set
self.window.close()
NSApplication.sharedApplication().setActivationPolicy(NSApplicationActivationPolicy.Accessory)
return NSApplicationTerminateReply.TerminateCancel
}
Hope it helps someone.
in Swift 5:
func toggleApp(sender: AnyObject) {
if self.window.visible {
self.window.orderOut(window)
NSApplication.shared.setActivationPolicy(NSApplication.ActivationPolicy.accessory)
} else {
NSApplication.shared.setActivationPolicy(NSApplication.ActivationPolicy.regular)
self.window!.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
}
}

UI Testing Failure when displaying UIAlertController with no buttons

We're using a UIAlertController as a loading indicator while a network request occurs. There are no actions associated with this UIAlertController as it is closed automatically when the network activity is completed. We display this alert after the user taps the login button for our app.
When we run our tests, they fail at the point right after this with:
UI Testing Failure - Did not receive view did disappear notification within 2.0s
Per other answers on SO, I've tried to use addUIInterruptionMonitor to handle the alert, but without any success. I think this is because there are no actionable buttons on the UIAlertController. Since there's no action that can be taken on the alert, the interruption monitor just looks like this:
addUIInterruptionMonitor(withDescription: "Loading") { handler in
return true
}
Even with this though, I get the same error. How can I work around this?
EDIT: Relevant UI testing code below:
class UI_Tests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = true
XCUIApplication().launch()
}
func testLogin() {
let app = XCUIApplication()
let tablesQuery = app.tables
let secureTextField = tablesQuery.cells.containing(.staticText, identifier:"PIN").children(matching: .secureTextField).element
secureTextField.tap()
secureTextField.typeText("1234")
app.buttons["Login"].tap()
addUIInterruptionMonitor(withDescription: "Loading") { handler in
return true
}
// Test failure occurs here.
let searchField = tablesQuery.searchFields.element(boundBy: 0)
searchField.tap()
searchField.typeText("hey")
}
}
After reaching out to Apple DTS, it turns out that when displaying a UI interruption / UIAlertController that dismisses based on a timeout, that you need to combine the UI interruption monitor with a timeout-based expectation (otherwise, the interruption monitor will return before the alert has dismissed!).
Using the UI testing example in the question, this approach looks like this:
class UI_Tests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = true
XCUIApplication().launch()
}
func testLogin() {
let app = XCUIApplication()
let tablesQuery = app.tables
let secureTextField = tablesQuery.cells.containing(.staticText, identifier:"PIN").children(matching: .secureTextField).element
secureTextField.tap()
secureTextField.typeText("1234")
app.buttons["Login"].tap()
addUIInterruptionMonitor(withDescription: "Loading") { alert in
self.expectation(for: NSPredicate(format: "exists == false"), evaluatedWith: alert, handler: nil);
self.waitForExpectations(timeout: 10, handler: nil)
return true
}
// Test failure occurs here.
let searchField = tablesQuery.searchFields.element(boundBy: 0)
searchField.tap()
searchField.typeText("hey")
}
}
This expectation will wait for 10 seconds to be filled. If the alert doesn't dismiss after 10 seconds, the expectation won't be met and the test will fail, but if it does, the test will succeed.