Disabling waiting for idle state in UI testing of iOS apps - ios

Basically the problem is the same as this one: XCTestCase: Wait for app to idle
I am using perpetually repeating "background animations" in my views. The UI testing of Xcode/iOS wants to wait for all UIView animations to end before it considers the app idle and goes on with stuff like tapping buttons etc. It just doesn't work with the way we've designed the app(s). (Specifically, we have a button that is animated with UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse options, so it never stops.)
But I'm thinking there might be some way to turn off and/or shorten the state "Wait for app to idle". Is there? How? Is there any other way around this?

You actually can disable wait for app to idle. This is a hack and may not be stable. With animations disabled, and this hack enabled, I am seeing about a 20% performance gain (on top of the performance boost from disabling animations).
All you have to do is swizzle out the method that is called to idle the app and no-op it. That method is XCUIApplicationProcess waitForQuiescenceIncludingAnimationsIdle:
Here is my working solution in swift 3 - there is likely a better way but this works for a proof of concept.
Extend the XCTestCase class. I'll call mine MyTestCase
static var swizzledOutIdle = false
override func setUp() {
if !MyTestCase.swizzledOutIdle { // ensure the swizzle only happens once
let original = class_getInstanceMethod(objc_getClass("XCUIApplicationProcess") as! AnyClass, Selector(("waitForQuiescenceIncludingAnimationsIdle:")))
let replaced = class_getInstanceMethod(type(of: self), #selector(MyTestCase.replace))
method_exchangeImplementations(original, replaced)
MyTestCase.swizzledOutIdle = true
}
super.setUp()
}
#objc func replace() {
return
}
Note wait for app to idle will no longer appear in the logs.

Unfortunately using Apple's UI Testing you can't turn 'wait for app to idle' or poll other network activity, however you can use environment variables to disable animations in your app to make the tests more stable. In your setup method before your test set an environment variable like this.
override func setUp() {
super.setUp()
continueAfterFailure = false
let app = XCUIApplication()
app.launchEnvironment = ["UITEST_DISABLE_ANIMATIONS" : "YES"]
app.launch()
}
Now in your source code:
if (ProcessInfo.processInfo.environment["UITEST_DISABLE_ANIMATIONS"] == "YES") {
UIView.setAnimationsEnabled(false)
}
You can place that check in a specific view if you only want it to disable animations for that specific view or in a delegate file to disable animations throughout the app.

I used gh123man answer in Objective-C in case anyone needs it:
- (void)disableWaitForIdle {
SEL originalSelector = NSSelectorFromString(#"waitForQuiescenceIncludingAnimationsIdle:");
SEL swizzledSelector = #selector(doNothing);
Method originalMethod = class_getInstanceMethod(objc_getClass("XCUIApplicationProcess"), originalSelector);
Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (void)doNothing {
// no-op
}

I was using gh123man's solution in setUp() of a couple of test classes and it worked like a charm until updating to iOS 13.3. Since then app gets stuck in launching state.
Found that it still works if I move it to methods like disableWaitForIdle() and enableWaitForIdle() and call them only in the most granular manner (before and after the tap where I know the app will never become idle), e.g. like this:
#discardableResult func selectOption() -> Self {
disableWaitForIdle()
app.cells["Option"].firstMatch.waitAndForceTap(timeout: 20)
enableWaitForIdle()
return self
}

I translated to Objective-C, and successfully used, h.w.powers' Swift solution, in case anyone needs it.
For setting up:
XCUIApplication *app = [[XCUIApplication alloc] init];
app.launchEnvironment = #{#"UITEST_DISABLE_ANIMATIONS":#"YES"};
[app launch];
and then in your code
if ([[[NSProcessInfo processInfo] environment][#"UITEST_DISABLE_ANIMATIONS"] isEqualToString:#"YES"]) {
// do something like stopping the animation
}

For anyone who intermittently runs into this wait for app to idle problem, I also experienced it a few times while running local XCUITests. Quitting and re-opening the simulator has done the trick for me, not sure exactly why. Maybe some system UIKit stuff getting wacky after the simulator has been running for 2 weeks.

Related

Re-Assigning instance of AVAudioPlayer in iOS13 leads to BAD_ACCESS runtime error

I have an app, that is for many years in the AppStore and runs without any crashes (last deployment target iOS 12.4). I have some code for playing a sound on certain events in the app.
Now I tried to upgrade my app for iOS 13 and without changing any code related to that “playSound” thing, I always get this runtime error, when testing on a real device. Does not happen on simulator.
Thread 1: EXC_BAD_ACCESS (code=1, address=0x48)
PLEASE: Before you mark that question as “duplicate”, consider that this must have something to do with the release of iOS13, because before it didn’t happen and the code is just "usual".
Here is my code, also on gitHub.
I have a property in my ViewController to prevent ARC deallocating my AVAudioPlayer:
private var mySoundPlayer: AVAudioPlayer = AVAudioPlayer()
I have a routine, where the “play sound” should be performed (here happens the error, when assigning a new instance of AVAudioPlayer. The resourceURL is not the problem, the RE-ASSIGNING is the problem, I tested it with a new instance not being assigned and i did not crash.
// -------------------------------------------------
// MARK: Private Methods
// -------------------------------------------------
private func makeSoundEvent(_ soundEvent : SoundEvent) {
guard Settings().getSound() == .soundON else { return }
guard let urlToRessource : URL = soundEvent.getFileURLToSoundRessource() else { return }
do {
mySoundPlayer = try AVAudioPlayer(contentsOf: urlToRessource)
try? AVAudioSession.sharedInstance().setActive(true)
mySoundPlayer.play()
}
catch { print("Could not create AVPlayer for ressource \(urlToRessource)") }
}
And here I have the call of that subroutine, somewhere in my viewDidLoad() with a delay of 2 seconds.
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.makeSoundEvent(.startFanfare)
}
I think it somehow does not work because of the async thread. But since it is on the main thread, I thought this should work.
Just remove the initialisation and it will work
private var mySoundPlayer: AVAudioPlayer!
Cheers!!!
private var mySoundPlayer: AVAudioPlayer = AVAudioPlayer()
AVAudioPlayer doesn't have init() listed as a valid initializer. The reference page shows four separate initializers, all of which take at least one parameter. If you were using the line above in code prior to iOS 13.1 without crashing, you were initializing the audio player incorrectly and probably just getting lucky that it wasn't a problem.
I don't know what changed specifically with AVAudioPlayer in iOS 13.1, but the release notes show quite a few audio-related changes, and it seems likely that some change to that class introduced a dependency on something that happens at initialization time.

SKNode's action run completion block does not get called

I have a watchOS 4 app which displays SpriteKit animations (SKActions) on top of the UI. Everything works fine in simulator and also on device first couple of times, then after some time when app is in background, and it is started, animations just freeze and completion block for the most long-lasting animation is not called. Any idea what might be the issue?
This is how I run my actions, caller is waiting for completion closure in order to hide the spritekit scene:
private func runActions(with icon: SKShapeNode?, completion: #escaping () -> Void) {
if let icon = icon, let scaleAction = scaleAction, let bg = background {
self.label?.run(fadeInOutAction)
icon.run(scaleAction)
icon.run(fadeInOutAction)
bg.run(backgroundAction, completion: completion)
} else {
completion()
}
}
And yes, I am aware that SKScene is paused when app moves to background. I am doing this in willActivate of my InterfaceController:
if scene.scene?.isPaused == true {
scene.scene?.isPaused = false
}
I want to emphasize that this works first always. It begins to fail after the app has been backgrounded for some time. Especially if I start the app from complication and try to immediately fire these animations, then this freezing happens.
Can I answer my own question? I guess I can? Here goes:
I finally solved this. It turns out that the WKInterfaceScene in WatchKit has ALSO an isPaused property that you need to turn false sometimes. So now in willActivate of my InterfaceController I will also check that and turn it false if it is true. Since I made this change, I haven't seen a single hiccup, freeze or anything weird anymore.
Case closed, I guess. I leave this here for future generations who might face this issue.

addUIInterruptionMonitor(withDescription:handler:) not working on iOS 10 or 9

The following tests works fine on iOS 11. It dismisses the alert asking permissions to use the locations services and then zooms in in the map. On iOS 10 or 9, it does none of this and the test still succeeds
func testExample() {
let app = XCUIApplication()
var handled = false
var appeared = false
let token = addUIInterruptionMonitor(withDescription: "Location") { (alert) -> Bool in
appeared = true
let allow = alert.buttons["Allow"]
if allow.exists {
allow.tap()
handled = true
return true
}
return false
}
// Interruption won't happen without some kind of action.
app.tap()
removeUIInterruptionMonitor(token)
XCTAssertTrue(appeared && handled)
}
Does anyone have an idea why and/or a workaround?
Here's a project where you can reproduce the issue: https://github.com/TitouanVanBelle/Map
Update
Xcode 9.3 Beta's Changelogs show the following
XCTest UI interruption monitors now work correctly on devices and simulators running iOS 10. (33278282)
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let allowBtn = springboard.buttons["Allow"]
if allowBtn.waitForExistence(timeout: 10) {
allowBtn.tap()
}
Update .exists to .waitForExistence(timeout: 10), detail please check comments.
I had this problem and River2202's solution worked for me.
Note that this is not a fix to get the UIInterruptionMonitor to work, but a different way of dismissing the alert. You may as well remove the addUIInterruptionMonitor setup. You'll need to have the springboard.buttons["Allow"].exists test anywhere the permission alert could appear. If possible, force it to appear at an early stage of the testing so you don't need to worry about it again later.
Happily the springboard.buttons["Allow"].exists code still works in iOS 11, so you can have a single code path and not have to do one thing for iOS 10 and another for iOS 11.
Incidentally, I logged the base issue (that addUIInterruptionMonitor is not working pre-iOS 11) as a bug with Apple. It has been closed as a duplicate now, so I guess they acknowledge that it is a bug.
I used the #River2202 solution and it works better than the interruption one.
If you decide to use that, I strongly suggest that you use a waiter function. I created this one in order to wait on any kind of XCUIElement to appear:
Try it!
// function to wait for an ui element to appear on screen, with a default wait time of 20 seconds
// XCTWaiter was introduced after Xcode 8.3, which is handling better the timewait, it's not failing the test. It uses an enum which returns: 'Waiters can be used with or without a delegate to respond to events such as completion, timeout, or invalid expectation fulfilment.'
#discardableResult
func uiElementExists(for element: XCUIElement, timeout: TimeInterval = 20) -> Bool {
let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == true"), object: element)
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
guard result == .completed else {
return false
}
return true
}

Completion block in (animateWithDuration:animations:completion:) is unpredictably delayed

The code is too huge to post it here. My problem is the following. When I call animateWithDuration:animations:completion: (maybe with options) with duration == 0.3 it doesn't mean that the completion block will be called through the same delay. It is called through 2 seconds instead and it is too long for me.
This big delay usually appears before memory warnings but sometimes may work as expected.
Could anybody explain what may cause such a strange behaviour?
Are there any timers involved, like is the animation timer-triggered?
I had a similar problem when my animation was timer-triggered. It turned out the animation was started more than once. animationOngoing flag stopped animation from being started again before finishing.
// Timer function
func timerTextToggle(timer: NSTimer) {
if self.animationOngoing == false {
self.flipAnimation()
}
}
// Animation function
func flipAnimation() {
// important note: it's UIViewAnimationOptions,
// not UIViewAnimationTransition
self.animationOngoing = true
if self.animationToggle == false {
UIView.transitionFromView(self.singleTapLabel!,
toView: self.doubleTapLabel!,
duration: animDuration,
options: UIViewAnimationOptions.TransitionFlipFromBottom,
completion: {
(value: Bool) in
self.animationOngoing = false
})
} else {
UIView.transitionFromView(self.doubleTapLabel!,
toView: self.singleTapLabel!,
duration: animDuration,
options: UIViewAnimationOptions.TransitionFlipFromTop,
completion: {
(value: Bool) in
self.animationOngoing = false
})
}
self.animationToggle = !self.animationToggle
}
I experienced a similar problem to this, although without further information on your scenario, I don't know if this also applies to your issue.
I was calling becomeFirstResponder on a UITextField in the completion block of my animateWithDuration:delay:options:animations:completion. Logging showed the completion block was being called in a timely manner, but the keyboard was taking several seconds to show. This was only occurring on first launch of the keyboard.
This answer helped me solve this... turned out this was somehow linked to the iOS Simulator. This issue did not occur when I wasn't debugging the app... another classic example of chasing a bug for hours in the simulator that didn't actually exist.
The cause of this problem is found out. It is a lot of UIWebView objects rendered in the main thread. And it seems impossible to prevent their loading. Anyways time profiler show that a lot of time is spent to render them even if they are not visible on the screen.
And yes, I can't release them before memory warning event because of requirements

My iOS app freezes but no error appears

Does any body know what I need to check if app freezes after some time? I mean, I can see the app in the iPhone screen but no view responds.
I did some google and i found that, i've blocked the main thread somehow.
But my question is how to identify which method causes blocking of main thread? is there any way to identify?
Launch your app and wait for it to freeze. Then press the "pause" button in Xcode. The left pane should show you what method is currently running.
Generally, it is highly recommended to perform on the main thread all animations method and interface manipulation, and to put in background tasks like download data from your server, etc...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//here everything you want to perform in background
dispatch_async(dispatch_get_main_queue(), ^{
//call back to main queue to update user interface
});
});
Source : http://www.raywenderlich.com/31166/25-ios-app-performance-tips-tricks
Set a break point from where the freeze occurs and find which line cause that.
Chances may be,Loading of large data,disable the controls,overload in main thread,Just find out where that occurs using breakpoints and rectify based on that.
I believe it should be possible to periodically check to see if the main thread is blocked or frozen. You could create an object to do this like so:
final class FreezeObserver {
private let frequencySeconds: Double = 10
private let acceptableFreezeLength: Double = 0.5
func start() {
DispatchQueue.global(qos: .background).async {
let timer = Timer(timeInterval: self.frequencySeconds, repeats: true) { _ in
var isFrozen = true
DispatchQueue.main.async {
isFrozen = false
}
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + self.acceptableFreezeLength) {
guard isFrozen else { return }
print("your app is frozen, so crash or whatever")
}
}
let runLoop = RunLoop.current
runLoop.add(timer, forMode: .default)
runLoop.run()
}
}
}
Update October 2021:
Sentry now offers freeze observation, if you don't wanna roll this yourself.
I reached an error similar to this, but it was for different reasons. I had a button that performed a segue to another ViewController that contained a TableView, but it looked like the application froze whenever the segue was performed.
My issue was that I was infinitely calling reloadData() due to a couple of didSet observers in one of my variables. Once I relocated this call elsewhere, the issue was fixed.
Most Of the Time this happened to me when a design change is being called for INFINITE time. Which function can do that? well it is this one:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
Solution is to add condition where the function inside of viewDidLayoutSubviews get calls only 1 time.
It could be that another view is not properly dismissed and it's blocking user interaction! Check the UI Debugger, and look at the top layer, to see if there is any strange thing there.

Resources