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:
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.
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>]
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
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.
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()
}
}
}
}
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()
}
}
}
}
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()
}
}
}
}
I used the @ODManswer, 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()
}
}
}
}
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()
}
}
}
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)
}
}
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.
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)
}
}