Is there a way to reset the app between tests in Swift XCTest UI?
Asked Answered
R

18

90

Is there an API call within XCTest that I can put into the setUP() or tearDown() to reset the app between tests? I looked in the dot syntax of XCUIApplication and all I saw was the .launch()

OR is there a way to call a shell script in Swift? I could then call xcrun in-between test methods to reset the simulator.

Rightist answered 13/10, 2015 at 16:18 Comment(2)
Funny I couldn't find this question asked back when I wrote this one. I blame SO for poor query results. Anywho, feel free to delete this "dupe", I solved the problem awhile ago using an elegant solution with fast lane/ gitlab-ci.yml file.Rightist
How did you manage to solve it using gitlab-ci.yml file? Could you please share something.Averroes
M
99

You can add a "Run Script" phase to build phases in your test target to uninstall the app before running unit tests against it, unfortunately this is not between test cases, though.

/usr/bin/xcrun simctl uninstall booted com.mycompany.bundleId

Update


Between tests, you can delete the app via the Springboard in the tearDown phase. Although, this does require use of a private header from XCTest. (Header dump is available from Facebook's WebDriverAgent here.)

Here is some sample code from a Springboard class to delete an app from Springboard via tap and hold:

#Swift 4:

import XCTest

class Springboard {

    static let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
    
    /**
     Terminate and delete the app via springboard
     */
    class func deleteMyApp() {
        XCUIApplication().terminate()
        
         // Force delete the app from the springboard
        let icon = springboard.icons["Citizen"]
        if icon.exists {
            let iconFrame = icon.frame
            let springboardFrame = springboard.frame
            icon.press(forDuration: 1.3)
        
            // Tap the little "X" button at approximately where it is. The X is not exposed directly
            springboard.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + 3) / springboardFrame.maxX, dy: (iconFrame.minY + 3) / springboardFrame.maxY)).tap()
        
            springboard.alerts.buttons["Delete"].tap()
        }
    }
 }

#Swift 3-:

import XCTest

class Springboard {

    static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")
    
    /**
     Terminate and delete the app via springboard
     */
    class func deleteMyApp() {
        XCUIApplication().terminate()
        
        // Resolve the query for the springboard rather than launching it
        springboard.resolve()
        
        // Force delete the app from the springboard
        let icon = springboard.icons["MyAppName"]
        if icon.exists {
            let iconFrame = icon.frame
            let springboardFrame = springboard.frame
            icon.pressForDuration(1.3)
            
            if #available(iOS 13.0, *) {
                springboard.buttons["Remove App"].tap()
                springboard.alerts.buttons["Delete App"].tap()
                springboard.alerts.buttons["Delete"].tap()
            } else {
            
                // Tap the little "X" button at approximately where it is. The X is not exposed directly
                let xPosition = CGVector(dx: (iconFrame.minX + 3) / springboardFrame.maxX,
                                     dy: (iconFrame.minY + 3) / springboardFrame.maxY)
                springboard.coordinate(withNormalizedOffset: xPosition).tap()
                springboard.alerts.buttons["Delete"].tap()
            }
        }
    }
 }

And then:

override func tearDown() {
    Springboard.deleteMyApp()
    super.tearDown()
}

The private headers were imported in the Swift bridging header. You'll need to import:

// Private headers from XCTest
#import "XCUIApplication.h"
#import "XCUIElement.h"

Note: As of Xcode 10, XCUIApplication(bundleIdentifier:) is now exposed by Apple, and the private headers are no longer needed.

Megaton answered 23/3, 2016 at 1:12 Comment(23)
This works really well. You just have to add the PrivateHeaders folder from WebDriveAgent to your project (no need to add the whole thing) and then make a bridging header that #imports the needed headers as mentioned.Arbiter
Great answer! Is there a smarter way of getting "MyAppName"? I tried using NSBundle-bundleWithIdentifier/Path, however the test app doesn't have a reference to the application bundle. My project has many targets, each with a different name, and I want to be able to use the Springboard class across all targets.Korwin
I found that I can use XCUIApplication().label. However, this requires the app to be launched for several seconds before label can be resolved. This is not a good solution, because you practically need to launch the app and hold it in that state when what you really want to do is delete it.Korwin
Can this be done after a test started? If so how can I reinstall the app?Nematic
Yes, it can. You just need to call XCUIApplication().launch(). That will install and run the last built copy of the app.Megaton
Why #import "XCUIElement.h" ? We are just redeclaring methods on XCUIApplication without anything concerning XCUIElement.Achromatize
XCUIApplication inherits from XCUIElement, and resolve is a method on XCUIElement. If you just cherry pick the few methods you need, you could just tack resolve onto the XCUIApplication category and avoid the extra import.Megaton
@ChaseHolland its also possible to subclass XCTestCase and launch Springboard.deleteMyApp() only onceFlare
@Flare yeah, I call it at the end of tearDown() in my test base classMegaton
This answer is really terrible solution, but it actually works.Valeda
Can someone clarify how to get this working on Xcode 10.1 and Swift 4? I don't understand what "imported in the Swift bridging header" means given the information in the answer.Icelandic
As of Xcode 10, you don't actually need to import those headers anymore. Apple now supports automating other applications, including the springboard: let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")Megaton
@ChaseHolland - This one works when i use it in the teardown, but the same code / method call does not work in Class teardown. Any idea why ?Dosage
You might want to add a sleep(1) call between tapping the "x" rounded button and the "Delete" button on the alert.Orson
The little "x" button has an accessibility identifier of "DeleteButton" and can be tapped by running icon.buttons["DeleteButton"].tap() after the long press rather than using the CGVector.Supporter
For iOS 13, since the UI for deleting apps changed, you can use springboard.buttons["Delete App"].tap()Graticule
As of iOS 13.4, I am now getting an error when I call app.launch() after Springboard.deleteMyApp(): The request was denied by service delegate (SBMainWorkspace) for reason: NotFound ("Application "com.serpentisei.studyjapanese" is unknown to FrontBoard").Waistcloth
The above error seems to be introduced in Xcode 11.4, regardless of simulator iOS version. It occurs whenever you launch your application, delete the app using the above technique, and then attempt to launch it again (even if this is across separate tests). I have filed FB7666257.Waistcloth
@Ruxel I ended up building a whole system to run simctl commands from my tests by writing a file in a known directory, and then having an Automator folder action on my mac that reads the file and runs the command. It's painfully complicated, not sure I can really recommend it...Waistcloth
@ChrisVasselli I have same exact issue. If you run tests one-by-one, it works. But that's not the same ofcMonopteros
Not sure when exactly this was fixed, but I just tried in Xcode 12.4 and I am no longer getting the error, and this solution seems to be working again.Waistcloth
/usr/bin/xcrun simctl uninstall booted com.mycompany.bundleId worked for me 👍Fractocumulus
For what it's worth, Apple responded to my feedback request, and confirmed that this has now been fixed in the latest versions of Xcode.Waistcloth
S
45

At this time, the public API provided by Xcode, the Simulator and the Swift Package Manager does not appear have any method callable from setUp() and tearDown() XCText subclasses to "Reset Contents and Settings" for the simulator.

There are other possible approaches which use public APIs:

  1. Application Code. Add some myResetApplication() application code to put the application in a known state. However, device (simulator) state control is limited by the application sandbox ... which is not much help outside the application. This approach is OK for clearing application controllable persistance.

  2. Shell Script. Run the tests from a shell script. Use xcrun simctl erase all or xcrun simctl uninstall <device> <app identifier> or similar between each test run to reset the simulator (or uninstall the app). see StackOverflow: "How can I reset the iOS Simulator from the command line?"

xcrun simctl --help
# Uninstall a single application
xcrun simctl uninstall --help  
xcrun simctl uninstall <device> <app identifier>

# Erase a device's contents and settings.
xcrun simctl erase <device>
xcrun simctl erase all      # all existing devices

# Grant, revoke, or reset privacy and permissions
simctl privacy <device> <action> <service> [<bundle identifier>]
  1. Xcode Schema Script Action. Add xcrun simctl erase all (or xcrun simctl erase <DEVICE_UUID>) or similar commands to an Xcode Scheme section such as the Test or Build section. Select the Product > Scheme > Edit Scheme… menu. Expand the Scheme Test section. Select Pre-actions under the Test section. Click (+) add "New Run Script Action". The command xcrun simctl erase all can be typed in directly without requiring any external script.

Options for invoking 1. Application Code to reset the application:

A. Application UI. [UI Test] Provide a reset button or other UI action which resets the application. The UI element can be exercised via XCUIApplication in XCTest routines setUp(), tearDown() or testSomething().

B. Launch Parameter. [UI Test] As noted by Victor Ronin, an argument can be passed from the test setUp() ...

class AppResetUITests: XCTestCase {

  override func setUp() {
    // ...
    let app = XCUIApplication()
    app.launchArguments = ["MY_UI_TEST_MODE"]
    app.launch()

... to be received by the AppDelegate ...

class AppDelegate: UIResponder, UIApplicationDelegate {

  func application( …didFinishLaunchingWithOptions… ) -> Bool {
    // ...
    let args = ProcessInfo.processInfo.arguments
    if args.contains("MY_UI_TEST_MODE") {
      myResetApplication()
    }

C. Xcode Scheme Parameter. [UI Test, Unit Test] Select the Product > Scheme > Edit Scheme… menu. Expand the Scheme Run section. (+) Add some parameter like MY_UI_TEST_MODE. The parameter will be available in ProcessInfo.processInfo.

// ... in application
let args = ProcessInfo.processInfo.arguments
if args.contains("MY_UI_TEST_MODE") {
    myResetApplication()
}

D. Direct Call. [Unit Test] Unit Test Bundles are injected into the running application and can directly call some myResetApplication() routine in the application. Caveat: Default unit tests run after the main screen has loaded. see Test Load Sequence However, UI Test Bundles runs as a process external to the application under test. So, what works in the Unit Test gives a link error in a UI Test.

class AppResetUnitTests: XCTestCase {

  override func setUp() {
    // ... Unit Test: runs.  UI Test: link error.
    myResetApplication() // visible code implemented in application
Sponson answered 18/11, 2015 at 7:31 Comment(4)
xcrun simctl erase all is a great suggestion - thanks!Mayworm
Instead of 3rd solution, you can softly uninstall your app in your test target build phase. See my answer.Tims
Looks like in Xcode 13 for reseting the app and simulator, the simulator needs to NOT be running. In my schema script I've added killall "Simulator" before xcrun simctl erase all. Also, to make it work the first time I had to kill it manually.Smashandgrab
Additionally, since it was not mentioned before, the "Provide build settings from" can be left to None, it's working, although I'm not sure what's the meaning for that.Smashandgrab
C
15

Updated for swift 3.1 / xcode 8.3

create bridging header in test target:

#import <XCTest/XCUIApplication.h>
#import <XCTest/XCUIElement.h>

@interface XCUIApplication (Private)
- (id)initPrivateWithPath:(NSString *)path bundleID:(NSString *)bundleID;
- (void)resolve;
@end

updated Springboard class

class Springboard {
   static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")!
   static let settings = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.Preferences")!

/**
Terminate and delete the app via springboard
*/

class func deleteMyApp() {
   XCUIApplication().terminate()

// Resolve the query for the springboard rather than launching it

   springboard.resolve()

// Force delete the app from the springboard
   let icon = springboard.icons["{MyAppName}"] /// change to correct app name
   if icon.exists {
     let iconFrame = icon.frame
     let springboardFrame = springboard.frame
     icon.press(forDuration: 1.3)

  // Tap the little "X" button at approximately where it is. The X is not exposed directly

    springboard.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + 3) / springboardFrame.maxX, dy: (iconFrame.minY + 3) / springboardFrame.maxY)).tap()

     springboard.alerts.buttons["Delete"].tap()

     // Press home once make the icons stop wiggling

     XCUIDevice.shared().press(.home)
     // Press home again to go to the first page of the springboard
     XCUIDevice.shared().press(.home)
     // Wait some time for the animation end
     Thread.sleep(forTimeInterval: 0.5)

      let settingsIcon = springboard.icons["Settings"]
      if settingsIcon.exists {
       settingsIcon.tap()
       settings.tables.staticTexts["General"].tap()
       settings.tables.staticTexts["Reset"].tap()
       settings.tables.staticTexts["Reset Location & Privacy"].tap()
       settings.buttons["Reset Warnings"].tap()
       settings.terminate()
      }
     }
    }
   }
Centesimo answered 29/3, 2017 at 13:7 Comment(6)
Works perfectly!Mismanage
Really nice ! Works perfectlyDozen
When running this on a device, sometimes I'm getting 'Trust This Computer?' alert, which prevents my app from launching.Presentable
Does this still work in latest Xcode/XCtest? And if so how/where do you initiate the deleteMyApp()?Kalat
... Works! Amazing!Kalat
Works great! I would suggest (see my edit) to wait 0.5 sec after pressing the X icon, as sometimes (in my case) the alert animation takes longer than expected.Emilemile
M
12

Solution for iOS 13.2

final class Springboard {

    private static var springboardApp = XCUIApplication(bundleIdentifier: "com.apple.springboard")

    class func deleteApp(name: String) {
        XCUIApplication().terminate()

        springboardApp.activate()

        sleep(1)

        let appIcon = springboardApp.icons.matching(identifier: name).firstMatch
        appIcon.press(forDuration: 1.3)

        sleep(1)

        springboardApp.buttons["Delete App"].tap()

        let deleteButton = springboardApp.alerts.buttons["Delete"].firstMatch
        if deleteButton.waitForExistence(timeout: 5) {
            deleteButton.tap()
        }
    }
}
Maletta answered 4/11, 2019 at 15:12 Comment(0)
C
11

You can ask your app to "clean up" itself

  • You use XCUIApplication.launchArguments to set some flag

  • In AppDelegate you check

    if NSProcessInfo.processInfo().arguments.contains("YOUR_FLAG_NAME_HERE") { // Do a clean up here }

Cheat answered 21/11, 2015 at 23:8 Comment(3)
This is a huge step towards me understanding the launchArgruments method. Thank you for that insight. It led me to nshipster.com/launch-arguments-and-environment-variables Please excuse my noobness here. If I edit the scheme and create a launch argument where and how to i set the specifics of that newly created argument? I see how to pass it as a token to the tests, but like in my case I want to run a script that reset the state of the simulator. Could you give a more detailed explanation on the creation of the actual argument?Rightist
@jermobileqa First of all, don't need to apologize. I am somewhat in similar boat to you. I started to use new UI tests literally today. And I was looking how to solve exactly this problem. I am current set XCUIApplication.launchArguments in setUp method of for my tests and check it in AppDelegate in func application. I didn't modify schema. As result, I can just run tests from XCode using Command+U and it will use this argument and my application will clean everything what it persisted.Cheat
How do i reset app permissions?Currey
M
9

I used the @ODM answer, but modified it to work for Swift 4. NB: some S/O answers don't differentiate the Swift versions, which sometimes have fairly fundamental differences. I've tested this on an iPhone 7 simulator and an iPad Air simulator in portrait orientation, and it worked for my app.

Swift 4

import XCTest
import Foundation

class Springboard {

let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let settings = XCUIApplication(bundleIdentifier: "com.apple.Preferences")


/**
 Terminate and delete the app via springboard
 */
func deleteMyApp() {
    XCUIApplication().terminate()

    // Resolve the query for the springboard rather than launching it
    springboard.activate()

    // Rotate back to Portrait, just to ensure repeatability here
    XCUIDevice.shared.orientation = UIDeviceOrientation.portrait
    // Sleep to let the device finish its rotation animation, if it needed rotating
    sleep(2)

    // Force delete the app from the springboard
    // Handle iOS 11 iPad 'duplication' of icons (one nested under "Home screen icons" and the other nested under "Multitasking Dock"
    let icon = springboard.otherElements["Home screen icons"].scrollViews.otherElements.icons["YourAppName"]
    if icon.exists {
        let iconFrame = icon.frame
        let springboardFrame = springboard.frame
        icon.press(forDuration: 2.5)

        // Tap the little "X" button at approximately where it is. The X is not exposed directly
        springboard.coordinate(withNormalizedOffset: CGVector(dx: ((iconFrame.minX + 3) / springboardFrame.maxX), dy:((iconFrame.minY + 3) / springboardFrame.maxY))).tap()
        // Wait some time for the animation end
        Thread.sleep(forTimeInterval: 0.5)

        //springboard.alerts.buttons["Delete"].firstMatch.tap()
        springboard.buttons["Delete"].firstMatch.tap()

        // Press home once make the icons stop wiggling
        XCUIDevice.shared.press(.home)
        // Press home again to go to the first page of the springboard
        XCUIDevice.shared.press(.home)
        // Wait some time for the animation end
        Thread.sleep(forTimeInterval: 0.5)

        // Handle iOS 11 iPad 'duplication' of icons (one nested under "Home screen icons" and the other nested under "Multitasking Dock"
        let settingsIcon = springboard.otherElements["Home screen icons"].scrollViews.otherElements.icons["Settings"]
        if settingsIcon.exists {
            settingsIcon.tap()
            settings.tables.staticTexts["General"].tap()
            settings.tables.staticTexts["Reset"].tap()
            settings.tables.staticTexts["Reset Location & Privacy"].tap()
            // Handle iOS 11 iPad difference in error button text
            if UIDevice.current.userInterfaceIdiom == .pad {
                settings.buttons["Reset"].tap()
            }
            else {
                settings.buttons["Reset Warnings"].tap()
            }
            settings.terminate()
        }
    }
  }
}
Mothering answered 14/11, 2017 at 1:6 Comment(3)
I had to further alter this because it doesn't work on a "Plus" model phone due to scaling changes. If you replace the constants "3" with "3 * UIScreen.main.scale" then it works properly.Pleopod
I cant seem to be able to get my iPads to hit the x button. Has anyone had any luck on an iPad?Plank
I was able to fix this as shown in my answer below.Plank
T
9

I see a lot of answers to uninstall your app in setUp or tearDown of your test.

But you can easily uninstall your app before launching your tests by adding a run script phase in your test target.

To do so :

  1. Select your application's Xcode project
  2. Select your test target
  3. Select "Build Phases"
  4. Tap on "+" and "New Run Script Phase"

Then, replace the placeholder # Type a script or drag a script file from your workspace to insert its path. by the command :

xcrun simctl boot ${TARGET_DEVICE_IDENTIFIER}
xcrun simctl uninstall ${TARGET_DEVICE_IDENTIFIER} YOUR_APP_BUNDLE
Tims answered 12/11, 2019 at 10:21 Comment(5)
Any idea how to get ID of the clone that the test will run in? Wanting to erase that clone only as other clones are still running their testsAbdicate
Hi @AlexandreG, what clone are you talking about ? Are you talking about the simulator ?Tims
Yep when using Xcode 10+ parallel testing the tests are ran on simulator clones which have their own IDs. With help of others I've found how to erase them #52660537 but do not know how to identify which one to erase before the testAbdicate
I don't know how you can get these clone IDs, however Xcode should create clones of your target simulator, so if you delete your app on the target simulator, it should be deleted on clones too.Tims
Moreover, if you really want to manage clones, you can use CLI to create yourself simulators with xcrun simctl create and then launch your tests on these simulators setting multiple destinations to xcodebuild test command. If it doesn't work, try option -only-testing: of xcodebuild test-without-building to separate UITests yourself.Tims
A
8

I used the @Chase Holland answer and updated the Springboard class following the same approach to reset the content and settings using the Settings app. This is useful when you need to reset permissions dialogs.

import XCTest

class Springboard {
    static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")
    static let settings = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.Preferences")

    /**
     Terminate and delete the app via springboard
     */
    class func deleteMyApp() {
        XCUIApplication().terminate()

        // Resolve the query for the springboard rather than launching it
        springboard.resolve()

        // Force delete the app from the springboard
        let icon = springboard.icons["MyAppName"]
        if icon.exists {
            let iconFrame = icon.frame
            let springboardFrame = springboard.frame
            icon.pressForDuration(1.3)

            // Tap the little "X" button at approximately where it is. The X is not exposed directly
            springboard.coordinateWithNormalizedOffset(CGVectorMake((iconFrame.minX + 3) / springboardFrame.maxX, (iconFrame.minY + 3) / springboardFrame.maxY)).tap()

            springboard.alerts.buttons["Delete"].tap()

            // Press home once make the icons stop wiggling
            XCUIDevice.sharedDevice().pressButton(.Home)
            // Press home again to go to the first page of the springboard
            XCUIDevice.sharedDevice().pressButton(.Home)
            // Wait some time for the animation end
            NSThread.sleepForTimeInterval(0.5)

            let settingsIcon = springboard.icons["Settings"]
            if settingsIcon.exists {
                settingsIcon.tap()
                settings.tables.staticTexts["General"].tap()
                settings.tables.staticTexts["Reset"].tap()
                settings.tables.staticTexts["Reset Location & Privacy"].tap()
                settings.buttons["Reset Warnings"].tap()
                settings.terminate()
            }
        }
    }
}
Altman answered 23/8, 2016 at 4:23 Comment(2)
XCUIApplication(privateWithPath: …) isn't exposed in Swift 3, it looks like?Tourney
@Tourney you need to create a bridging header and import private headers. Check my answer for the correct implementation.Centesimo
M
8

Starting Xcode 11.4, if all you want is to reset permissions, you can use resetAuthorizationStatus(for:) on instance of XCUIApplication, see https://developer.apple.com/documentation/xctest/xcuiapplication/3526066-resetauthorizationstatusforresou

You can also use simctl if needed, quoted from Xcode 11.4 Release Notes:

simctl now supports modifying privacy permissions. You can modify privacy permissions to create known states for testing purposes. For example, to allow an example app to access the photo library without any prompts:
xcrun simctl privacy <device> grant photos com.example.app

To reset all permissions to defaults, as if the app had never been installed before:
xcrun simctl privacy <device> reset all com.example.app.

Mayda answered 7/4, 2020 at 11:42 Comment(4)
Annoyingly this not seem to apply for notification permissions.Chaffer
@Chaffer hey, did you found any way how to reset notification permission without deleting the app?Ainslee
Annoyingly not!Chaffer
June 2022, and this still doesn't appear to work for resetAuthorizationStatus(for: .location), either.Hube
L
7

Working solution for iOS14

final class Springboard {

    private static var springboardApp = XCUIApplication(bundleIdentifier: "com.apple.springboard")

    class func deleteApp(name: String) {
        XCUIApplication().terminate()

        springboardApp.activate()

        sleep(1)

        let appIcon = springboardApp.icons.matching(identifier: name).firstMatch
        appIcon.press(forDuration: 1.3)

        sleep(1)

        springboardApp.buttons["Remove App"].tap()

        let deleteButton = springboardApp.alerts.buttons["Delete App"].firstMatch
        if deleteButton.waitForExistence(timeout: 5) {
            deleteButton.tap()
            springboardApp.alerts.buttons["Delete"].tap()
        }
    }
}
Libratory answered 8/2, 2021 at 17:40 Comment(1)
You're simulating a long-press on the app's icon, then deleting it like a user would? I like it.Hube
F
5

There are so many variations of the answer, not even sure if I should add on to that, but in case someone need a universal solution:

iOS 14.6 and 15 beta

    class func deleteApp() {
    XCUIApplication().terminate()
    
    // Force delete the app from the springboard
    let icon = springboard.icons["APP_NAME"]
    if icon.exists {
        icon.press(forDuration: 1.3)
        
        springboard.buttons["Remove App"].tap()
        springboard.alerts.buttons["Delete App"].tap()
        springboard.alerts.buttons["Delete"].tap()
        
        // Press home once to make the icons stop wiggling
        XCUIDevice.shared.press(.home)
    }
}
Faucet answered 19/7, 2021 at 13:17 Comment(1)
This doesn't seem to always work for me. It's more reliable if I add waitForExistence() calls before the tap() calls on the buttons.Valentino
F
4

For iOS 11 sims an up, I made an ever so slight modification to tap the "x" icon and where we tap per the fix @Code Monkey suggested. Fix works well on both 10.3 and 11.2 phone sims. For the record, I'm using swift 3. Thought i'd through some code out there to copy and paste to find the fix a little easier. :)

import XCTest

class Springboard {

    static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")

    class func deleteMyApp() {
        XCUIApplication().terminate()

        // Resolve the query for the springboard rather than launching it
        springboard!.resolve()

        // Force delete the app from the springboard
        let icon = springboard!.icons["My Test App"]
        if icon.exists {
            let iconFrame = icon.frame
            let springboardFrame = springboard!.frame
            icon.press(forDuration: 1.3)

            springboard!.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + 3 * UIScreen.main.scale) / springboardFrame.maxX, dy: (iconFrame.minY + 3 * UIScreen.main.scale) / springboardFrame.maxY)).tap()

            springboard!.alerts.buttons["Delete"].tap()
        }
    }
}
Fjeld answered 10/2, 2018 at 0:8 Comment(0)
G
3

This seems to work for me on iOS 12.1 & simulator

class func deleteApp(appName: String) {
    XCUIApplication().terminate()

    // Force delete the app from the springboard
    let icon = springboard.icons[appName]
    if icon.exists {
        icon.press(forDuration: 2.0)

        icon.buttons["DeleteButton"].tap()
        sleep(2)
        springboard.alerts["Delete “\(appName)”?"].buttons["Delete"].tap()
        sleep(2)

        XCUIDevice.shared.press(.home)
    }
}
Grained answered 19/3, 2019 at 9:48 Comment(0)
E
3

iOS 13.1/Swift 5.1 UI based deletion

static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")!

class func deleteApp() {
    XCUIApplication().terminate()
    XCUIDevice.shared.press(.home)
    XCUIDevice.shared.press(.home)

    let icon = springboard.icons["YourApplication"]
    if !icon.exists { return }

    springboard.swipeLeft()
    springboard.activate()
    Thread.sleep(forTimeInterval: 1.0)

    icon.press(forDuration: 1.3)
    springboard.buttons["Rearrange Apps"].eventuallyExists().tap()

    icon.buttons["DeleteButton"].eventuallyExists().tap()
    springboard.alerts.buttons["Delete"].eventuallyExists().tap()

    XCUIDevice.shared.press(.home)
    XCUIDevice.shared.press(.home)
}
Enmesh answered 27/9, 2019 at 12:41 Comment(5)
Does this work reliably for you? It intermittently has issues finding the "DeleteButton" for me.Chaffer
@Chaffer sameShiflett
you are right, it is flaky for me as well. I am thinking about erase the whole simulator until we don't find the proper way.Enmesh
I also see wrong coordinates for icon. I is x: -2, y:4. When it happens deleteButton does not exist. I tried refresh elements tree but it does not help.Highkey
I've updated the method, please try this one. Right now it is working for me. But 13.2 on the doorstep and this will be obsolete I thinkEnmesh
D
0

Updating Craig Fishers answer for Swift 4. Updated for iPad in landscape, probably only works for landscape left.

import XCTest

class Springboard {

static let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")

class func deleteMyApp(name: String) {        
    // Force delete the app from the springboard
    let icon = springboard.icons[name]
    if icon.exists {
        let iconFrame = icon.frame
        let springboardFrame = springboard.frame
        icon.press(forDuration: 2.0)

        var portaitOffset = 0.0 as CGFloat
        if XCUIDevice.shared.orientation != .portrait {
            portaitOffset = iconFrame.size.width - 2 * 3 * UIScreen.main.scale
        }

        let coord = springboard.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + portaitOffset + 3 * UIScreen.main.scale) / springboardFrame.maxX, dy: (iconFrame.minY + 3 * UIScreen.main.scale) / springboardFrame.maxY))
        coord.tap()

        let _ = springboard.alerts.buttons["Delete"].waitForExistence(timeout: 5)
        springboard.alerts.buttons["Delete"].tap()

        XCUIDevice.shared.press(.home)
    }
}

}

Deuteranope answered 31/12, 2018 at 16:58 Comment(0)
S
0

Here is an Objective C version of the above answers to delete an App and reset warnings (tested on iOS 11 & 12):

- (void)uninstallAppNamed:(NSString *)appName {

    [[[XCUIApplication alloc] init] terminate];

    XCUIApplication *springboard = [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"];
    [springboard activate];
    XCUIElement *icon = springboard.otherElements[@"Home screen icons"].scrollViews.otherElements.icons[appName];

    if (icon.exists) {
        [icon pressForDuration:2.3];
        [icon.buttons[@"DeleteButton"] tap];
        sleep(2);
        [[springboard.alerts firstMatch].buttons[@"Delete"] tap];
        sleep(2);
        [[XCUIDevice sharedDevice] pressButton:XCUIDeviceButtonHome];
        sleep(2);
    }
}

..

- (void)resetWarnings {

    XCUIApplication *settings = [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.Preferences"];
    [settings activate];
    sleep(2);
    [settings.tables.staticTexts[@"General"] tap];
    [settings.tables.staticTexts[@"Reset"] tap];
    [settings.tables.staticTexts[@"Reset Location & Privacy"] tap];

    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        [settings.buttons[@"Reset"] tap];
    } else {
        [settings.buttons[@"Reset Warnings"] tap];
    }
    sleep(2);
    [settings terminate];
}
Sambar answered 4/7, 2019 at 9:16 Comment(0)
P
0

This works for me in all OS version(iOS11,12 & 13)

static let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")

    func deleteApp() {
        XCUIApplication().terminate()
        springboard.activate()

        let icon = springboard.icons[appName]

        if icon.exists {
            icon.firstMatch.press(forDuration: 5)
            icon.buttons["DeleteButton"].tap()

            let deleteConfirmation = springboard.alerts["Delete “\(appName)”?"].buttons["Delete"]
            XCTAssertTrue(deleteConfirmation.waitForExistence(timeout: 5), "Delete confirmation not shown")
            deleteConfirmation.tap()
        }
    }
Pottery answered 1/11, 2019 at 2:30 Comment(0)
S
0

After some experiments, I've ended implementing a clearer solution that covers different iOS versions:

import XCTest

private enum Constants {
  static let springboardBundleIdentifier = "com.apple.springboard"
  static let appIconName = "Your App Name"
  static let appIconPressShortDuration: TimeInterval = 2.0
  static let appIconPressLongDuration: TimeInterval = 3.0
  static let deleteAppButton = "Delete App"
  static let removeAppButton = "Remove App"
  static let deleteButton = "Delete"
  static let deleteButtonVectorOffset: CGFloat = 3.0
}

final class SpringboardManager {

  private static let springboard = XCUIApplication(bundleIdentifier: Constants.springboardBundleIdentifier)

  static func deleteApp(_ app: XCUIApplication) {
    if app.exists && app.isHittable {
      XCUIDevice.shared.press(.home)
    }

    app.terminate()

    self.deleteAppIfNeeded(with: Constants.appIconName)
    sleep(1)
  }

  private static func deleteAppIfNeeded(with iconName: String) {
    let appIcon = self.springboard.icons[iconName]

    guard appIcon.exists else {
      return
    }

    appIcon.press(forDuration: Constants.appIconPressShortDuration)

    if let deleteListButton = self.deleteListButton() {
      deleteListButton.tap()
      self.pressDeleteAlertButtons()
    } else {
      appIcon.press(forDuration: Constants.appIconPressLongDuration)
      self.pressDeleteTopLeftButton(for: appIcon)
      self.pressDeleteAlertButtons()
    }
  }

}

private extension SpringboardManager {

  static func pressDeleteAlertButtons() {
    self.pressDeleteAlertButton(self.deleteAppAlertButton())
    self.pressDeleteAlertButton(self.deleteAlertButton())
  }

  static func pressDeleteAlertButton(_ button: XCUIElement?) {
    guard let button = button else {
      return
    }

    button.tap()
  }

  static func pressDeleteTopLeftButton(for appIcon: XCUIElement) {
    let iconFrame = appIcon.frame
    let springboardFrame = self.springboard.frame

    let deleteButtonVector = CGVector(
      dx: (iconFrame.minX + Constants.deleteButtonVectorOffset) / springboardFrame.maxX,
      dy: (iconFrame.minY + Constants.deleteButtonVectorOffset) / springboardFrame.maxY)

    let deleteButtonCoordinate = self.springboard.coordinate(withNormalizedOffset: deleteButtonVector)
    deleteButtonCoordinate.tap()
  }

}

private extension SpringboardManager {

  static func deleteListButton() -> XCUIElement? {
    sleep(1)
    
    let removeListButton = self.springboard.buttons[Constants.removeAppButton]
    let deleteListButton = self.springboard.buttons[Constants.deleteAppButton]

    if removeListButton.exists {
      return removeListButton
    } else if deleteListButton.exists {
      return deleteListButton
    }

    return nil
  }

  static func deleteAppAlertButton() -> XCUIElement? {
    sleep(1)

    let deleteAppButton = self.springboard.alerts.buttons[Constants.deleteAppButton]

    if deleteAppButton.exists {
      return deleteAppButton
    }

    return nil
  }

  static func deleteAlertButton() -> XCUIElement? {
    sleep(1)

    let deleteButton = self.springboard.alerts.buttons[Constants.deleteButton]

    if deleteButton.exists {
      return deleteButton
    }

    return nil
  }

}
Slaughterhouse answered 25/3, 2021 at 1:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.