How to clear NSUserDefaults programmatically in XCUITest using Simulator - ios

I've read several answers related to this and they suggest doing one of the following, but these options are not working for me. I have an XCUITest and I'm trying to clear the standard user defaults before before running the rest of my XCUITest. Currently my test app has a button that calls this code. I've also tried calling this code directly from within the XCUITest (I'm not sure if this is expected to work or if it needs to be run from within the app).
NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier];
[[NSUserDefaults standardUserDefaults] removePersistentDomainForName:appDomain];
I've also tried removing each individually:
[[NSUserDefaults standardUserDefaults] removeObjectForKey:#"MyKey1"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:#"MyKey2"];
I also tried each of the above methods followed by a synchronize call:
[[NSUserDefaults standardUserDefaults] synchronize];
The next time I read #"MyKey1" from the NSUserDefaults its still has the old value and has not been deleted.
Is there any way to remove an object from the NSUserDefaults programmatically when running an XCUITest in the simulator? These are automated tests, so I can't always manually click on "Reset Contents and Settings" in xcode.
Thanks!

There are couple of ways to solve your issues by setting the UserDefaults values before your tests run the app (not after).
Solution #1
Mock UserDefaults values for certain keys using launchArguments:
func testExample() {
let app = XCUIApplication()
app.launchArguments += ["-keepScreenOnKey", "YES"]
app.launch()
}
Note the minus sign before the keepScreenOnKey key. The minus sign indicates that it should take the next launch argument value as a value for that UserDefaults key.
Solution #2 (if solution #1 doesn’t work)
The previous solution might not work if you’re using the SwiftyUserDefaults library. In this case, there is one elegant solution: if the application is running under UI tests, erase all UserDefaults data from the app. How does the app know if it is being run under UI tests? By passing the launch arguments message, like this:
override func setUp() {
super.setUp()
app.launchArguments += ["UI-Testing"]
}
Then, in AppDelegate.swift check if the app is running under UI-Testing and remove all UserDefaults data (like you do):
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
setStateForUITesting()
return true
}
static var isUITestingEnabled: Bool {
get {
return ProcessInfo.processInfo.arguments.contains("UI-Testing")
}
}
private func setStateForUITesting() {
if AppDelegate.isUITestingEnabled {
UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
}
}
}
Solution #3 (if solution #2 is not enough)
But what if the UI test expects states other than the default state set by solution #2? Bools default to false, Integers to 0 and Strings to "", but what if you need true for the Bool key?
Do something like this:
func testExample() {
app.launchEnvironment["UI-TestingKey_keepScreenOn"] = "YES"
app.launch()
}
And then in AppDelegate.swift file:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
static let uiTestingKeyPrefix = "UI-TestingKey_"
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if AppDelegate.isUITestingEnabled {
setUserDefaults()
}
return true
}
static var isUITestingEnabled: Bool {
get {
return ProcessInfo.processInfo.arguments.contains("UI-Testing")
}
}
private func setUserDefaults() {
for (key, value)
in ProcessInfo.processInfo.environment
where key.hasPrefix(AppDelegate.uiTestingKeyPrefix) {
// Truncate "UI-TestingKey_" part
let userDefaultsKey = key.truncateUITestingKey()
switch value {
case "YES":
UserDefaults.standard.set(true, forKey: userDefaultsKey)
case "NO":
UserDefaults.standard.set(false, forKey: userDefaultsKey)
default:
UserDefaults.standard.set(value, forKey: userDefaultsKey)
}
}
}
}
extension String {
func truncateUITestingKey() -> String {
if let range = self.range(of: AppDelegate.uiTestingKeyPrefix) {
let userDefaultsKey = self[range.upperBound...]
return String(userDefaultsKey)
}
return self
}
}
Please note that this example only works for Bool and String keys. If you need more scalability, the switch command should be modified to somehow check if the value is Integer or Double or Any other value, but the general idea is here.
EDIT: It looks like the reason for using solutions #2 and #3 is not valid anymore as of SwiftyUserDefaults version 4.0.0-beta.1 as they've added support for setting values through launch arguments. But, I have to admit that I have not tested SwiftyUserDefaults library from this version onward, so I'll keep both solutions here.

With UI tests, the app runs as a separate process. You would need to call the methods to clear NSUserDefaults from within the app itself.
We have our UI Test pass a resetNSUserDefaults flag to the app when it launches the app. The app then clears the NSUserDefaults early in the launch process.

Related

iOS - Intents eligible for in-app handling not work

I have an application with a deployment target on iOS 14 and built entirely with swiftui.
I have added an intentdefinition file and added a couple of intents with their respective handlers, and I have added to my app delegate the function to handle them:
struct ...App: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
...
}
extension AppDelegate {
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? {
switch intent {
case is NewTaskIntent:
return NewTaskHandler()
case is TodayTasksIntent:
return TodayTasksHandler()
default:
return nil
}
}
}
The problem is that this function is never called and when I run the shortcut from its app it always launches my app.
The info Plist contains the keys correctly:
<key>INIntentsSupported</key>
<array>
<string>NewTaskIntent</string>
<string>TodayTasksIntent</string>
</array>
Any idea what is happening?
If you are using SwiftUI, try onContinueUserActivity(_:perform:).
https://developer.apple.com/documentation/swiftui/view/oncontinueuseractivity(_:perform:)
Note: its first argument activityType is defined in your main app's Custom iOS Target Properties with NSUserActivityTypes key. This property seems to be generated by Xcode auto-ly.
Inspired by the "Step Six" in https://toolboxpro.app/blog/adding-shortcuts-to-an-app-part-four
However, it's quite wired that my func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? triggered yesterday, but makes no sense right now. Is it a 'feature' 😂

Is it good idea to store value in userDefaults on sign in and check user status every time when App open?

When user Sign In, I'm storing some value into userDefaults and checking user at main screen is it already logged in or not .
Is it good idea to use userDefaults or there any other Method to check user logged in status.
Yes, you can save access token or user id in Userdefaults and check whether the user logged in or not.It is used to navigate to Home page or anyother if the user has already logged in.You can write a code for navigation in your Appdelegate.
If a piece of data is not sensitive (e.g., default font size), store it in NSUserDefaults.
If it needs to be secure from casual snooping (e.g., user's password), store it in the Keychain.
If it needs to be secure from the user (e.g., registration code), you will need to roll your own encryption, then store the data wherever you like.
In normal iPhones and iPads it's secure to store in UserDeafaults but in jailbroken devices one can get those data easily. That's why it's preferred to store sensitive data into Keychain.
Yes, UserDefaults is the best way to store information that isn't large.
You can store your users preferences in UserDefaults.
Generally, in you didFinishLaunching method in AppDelegate, you can do something like this:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if UserDefaults.standard.value(forKey: "userId") != nil {
/// etc.
}
return true
}
I would rather suggest Keychain instead of UserDefaults.
A keychain is an encrypted container that holds passwords for
multiple applications and secure services. Apple Inc. uses keychains
as password management system in Mac OS and iOS.
Having said that, all the sensitive information should go to the Keychain.
As per your point on checking user status each time, it is the right way to go. Mobile apps don't generally show login screen each time unless it is a critical app like that of banking.
var isUserLoggedIn: Bool {
return UserDefaults.standard.value(forKey: "userDetail") != nil
}
var currentUser : [String:Any] {
set{
guard let unwrappedKey = newValue else{
return
}
UserDefaults.standard.set(unwrappedKey, forKey: "userDetail")
UserDefaults.standard.synchronize()
} get {
guard let userDict = UserDefaults.standard.object(forKey:"userDetail") else {
return nil
}
return userDict
}
}
func removeCurrentUser(){
if UserDefaults.standard.value(forKey: "userDetail") != nil {
UserDefaults.standard.removeObject(forKey: "userDetail")
UserDefaults.standard.synchronize()
}
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if isUserLoggedIn {
//etc..
}
return true
}
Now you can check user login or not using isUserLoggedIn property.
You can also create user model class.

Inconsistent value from NSUserDefaults at app startup

Under the iOS simulator, I'm having trouble getting a consistent value back from NSUserDefault on app startup. I've set the value in a previous run of the app (and I put some prints in there to make sure there was no accidental changes), so I don't think there's a problem with the way I'm calling synchronize(). The reads are happening on the main thread also, and after I've set the default values. After a couple of seconds, if I retry the fetch from NSUserDefaults, I get the correct value (I put a timer in there to verify that the value gets corrected and seems to stay the same after startup).
The value represents where the user will store the data (on iCloud or just on the local device) and is represented by an integer in the user defaults. Here's the enum representing the values:
class MovingDocument: UIDocument {
enum StorageSettingsState: Int {
case NoChoice = 0
case Local = 1
case ICloud = 2
}
}
Here's an excerpt of the class used to access NSUserDefaults:
class Settings {
static let global = Settings()
private enum LocalStoreKeys : String {
case IsHorizontal = "IsHorizontal"
case ItemInPortrait = "ItemInPortrait"
case RotateTitle = "RotateTitle"
case StorageLocation = "StorageLocation"
}
// Other computed variables here for the other settings.
// Computed variable for the storage setting.
var storageLocation: MovingDocument.StorageSettingsState {
get {
print("Storage state fetch: \(NSUserDefaults.standardUserDefaults().integerForKey(LocalStoreKeys.StorageLocation.rawValue))")
return MovingDocument.StorageSettingsState(rawValue: NSUserDefaults.standardUserDefaults().integerForKey(LocalStoreKeys.StorageLocation.rawValue)) ?? .NoChoice
}
set {
print("Setting storage location to \(newValue) (\(newValue.rawValue))")
NSUserDefaults.standardUserDefaults().setInteger(newValue.rawValue, forKey: LocalStoreKeys.StorageLocation.rawValue)
synchronize()
CollectionDocument.settingsChanged()
}
}
func prepDefaultValues() {
NSUserDefaults.standardUserDefaults().registerDefaults([
LocalStoreKeys.IsHorizontal.rawValue: true,
LocalStoreKeys.ItemInPortrait.rawValue: true,
LocalStoreKeys.RotateTitle.rawValue: true,
LocalStoreKeys.StorageLocation.rawValue: MovingDocument.StorageSettingsState.NoChoice.rawValue
])
}
func synchronize() {
NSUserDefaults.standardUserDefaults().synchronize()
}
}
The value that I should be reading (and which I always seem to get after startup) is the value '1' or .Local. The value that I am sometimes getting instead is '0' or .NoChoice (which just happens to be the default value). So, it looks like it's using the default value until some later point in the application startup.
For completeness sake, here's the relevant app delegate code:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
Settings.global.prepDefaultValues() // Must be called before doing CollectionDocument.start() because we need the default value for storage location.
CollectionDocument.start()
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
browserController = ViewController()
navController = UINavigationController(rootViewController: browserController!)
navController!.navigationBar.translucent = false
self.window!.rootViewController = navController
self.window!.makeKeyAndVisible()
checkStorageLocation()
return true
}
The getter is called within CollectionDocument.start() (CollectionDocument is a subclass of MovingDocument, where the location enum is defined). The call at the end to checkStorageLocation() is my periodic debugging check for the value of the storage location (it calls itself back via dispatch_after()).
As a side note, I also tried putting most of the code in the application(didFinishLaunchingWithOptions...) into a dispatch_async call (on the main thread) to see if that made a difference (just the call to set up the default values and the return statement were done immediately, the rest of the calls were put into the asynchronous call block), and it still got the wrong value (sometimes). I guess I could try using dispatch_after instead (and wait a second or two), but I can't really start the app until this is complete (because I can't read the user data from the doc until I know where it is) and a loading screen for just this one problem seems ridiculous.
Any ideas about what might be going wrong? I'm unaware of any limitations on NSUserDefaults as far as when you are allowed to read from it...
Rebooting my mac fixed the problem (whatever it was). iOS 10, NSUserDefaults Does Not Work has a similar problem and the same solution (though the circumstances are quite different, since I have tried neither Xcode 8 nor iOS 10).

How to speed up UI test cases in Xcode?

Since Xcode 7 we have a nice API for UI testing.
Mostly I'm satisfied with it. The only concern is related to the speed.
In the beginning an ordinary UI test case (about 15 actions) ran approximately 25 seconds. Then I mocked networking completely. Now it takes 20 seconds. Considering the fact that the time is taken only by animations and a launch time (1 second or even less), I assume, there must be a way to speed it up.
Try setting this property when your UI tests run:
UIApplication.shared.keyWindow?.layer.speed = 100
Here's how I set it:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if ProcessInfo.processInfo.arguments.contains("UITests") {
UIApplication.shared.keyWindow?.layer.speed = 100
}
}
And in my UI tests:
class MyAppUITests: XCTestCase {
// MARK: - SetUp / TearDown
override func setUp() {
super.setUp()
let app = XCUIApplication()
app.launchArguments = ["UITests"]
app.launch()
}
}
There's a few more handy tips in this blog post.
Another possibility is to disable animations at all:
[UIView setAnimationsEnabled:NO];
Swift 3:
UIView.setAnimationsEnabled(false)
Following #Mark answer, the Swift 3 version:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if ProcessInfo.processInfo.arguments.contains("UITests") {
UIApplication.shared.keyWindow?.layer.speed = 200
}
}
On you ui test file:
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
let app = XCUIApplication()
app.launchArguments = ["UITests"]
app.launch()
Add it in didFinishLaunch
[UIApplication sharedApplication].keyWindow.layer.speed = 2;
The default value is 1, make it 2 to double its speed.
Run them in parallel.
EDIT: This may be outdated since my original answer in 2019, since Xcode allows now testing on multiple simulators within one machine:
If you have only 1 build machine, you can use Bluepill: https://github.com/linkedin/bluepill
EDIT: However, I don't use either of these (bluepill / Xcode), so I'll keep mention of the bluepill in this answer, maybe it has some uses.
If you have multiple machines, you can use Emcee: https://github.com/avito-tech/Emcee (it also works for a single machine setup)
We have 50 hours of UI tests, and Emcee allows us to run them in 1 hour. There are several tricks to make UI tests faster, but it is pointless if you are not running them in parallel. E.g. you can't make your 25 seconds tests run in 0.5 second.
Tricks:
Run XCUIApplication without reinstalling it:
XCUIApplication(
privateWithPath: nil,
bundleID: "your.bundle.id"
)
Note: you should launch app with XCUIApplication().launch() at least once per launching XCUI test runner to install the app. This requires usage of private API.
You can move something to background thread. E.g.
let someDataFromApi = getSomeData() // start request asynchronously and immediately return
launchApp() // this can be few seconds
useInTest(someDataFromApi) // access to data will wait request to finish
You can make the code look like there is no asynchronous things, it would be easier for QA. We want to implement this, but we didn't, so it is just an idea.
Switch to EarlGrey and make tests that lasts few seconds (but they will not be black box).
Open screens via deep links if you have deep links.
Use a lot of private API and other hacks. E.g.: instead of going via UI to Settings app and then reset privacy settings you can call some private API.
Never use long sleeps. Use polling.
Speed up animations. Do not disable them! (we use layer.speed = 100 on every view and we got severe problems with the value of 10000, full code: https://pastebin.com/AnsZmzuQ)
Pass some flags from UI tests to app to skip initial alerts/tutorials/popups. Can save a lot of time. Be sure to have at least 1 test that checks that those alerts work.
Advertisement: most of these is implemented in https://github.com/avito-tech/Mixbox. Disclaimer: I'm the main contributor in this test framework. Its tedious to set up, but at least you can reuse some code if you don't want to reuse whole framework.
I wanted to disable ALL animations during Snapshot testing. I was able to able to achieve this by disabling both Core Animation and UIView animations as below.
Note because my app used storyboards UIApplication.shared.keyWindow was nil at launch so I access the UIWindow by referring to the window property directly.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if ProcessInfo.processInfo.arguments.contains("SnapshotTests") {
// Disable Core Animations
window?.layer.speed = 0
// Disable UIView animations
UIView.setAnimationsEnabled(false)
}
return true
}
I decrease my UITests time in 30%, follow all steps:
When you run your app, add the argument:
let app = XCUIApplication()
override func setUp() {
super.setUp()
continueAfterFailure = false
app.launchArguments += ["--Reset"]
app.launch()
}
Now, in your AppDelegate add:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
setStateForUITesting()
}
static var isUITestingEnabled: Bool {
get {
return ProcessInfo.processInfo.arguments.contains("--Reset")
}
}
private func setStateForUITesting() {
if AppDelegate.isUITestingEnabled {
// If you need reset your app to clear state
UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
// To speed up your tests
UIApplication.shared.keyWindow?.layer.speed = 2
UIView.setAnimationsEnabled(false)
}
}
In your code, to verify if is in test mode, you can use:
if AppDelegate.isUITestingEnabled {
print("Test Mode")
}
Additionally, to can wait while the element load I created this extension:
import XCTest
extension XCUIElement {
func tap(wait: Int, test: XCTestCase) {
if !isHittable {
test.expectation(for: NSPredicate(format: "hittable == true"), evaluatedWith: self, handler: nil);
test.waitForExpectations(timeout: TimeInterval(wait), handler: nil)
}
tap()
}
}
Use like this:
app.buttons["start"].tap(wait: 20, test: self)
Note that keyWindow is deprecated as of iOS 13. Specifically,
UIApplication.sharedApplication.keyWindow and self.window.keyWindow within the AppDelegate both return nil. This answer says to loop through UIApplication.shared.windows and find the one with isKeyWindow true, but in my test using ios14, even that return false for my only window. In the end, setting self.window.layer.speed worked for me.

Can I disable custom keyboards (iOS8) for my app?

EDIT: tl;dr - it is possible, see accepted answer below.
Is there any (not only programatic) way of preventing custom keyboards (iOS8) from being used for my application? I am mainly interested in "per-app" setting, so just my app is not allowed to use custom keyboards, but disabling custom keyboards system-wide is last resort.
So far I know that custom keyboards are system-wide and can be used by any application. The OS will fallback to stock keyboard only for secure text entry (text fields with secureTextEntry set to YES). Not much hope here.
I got an impression from App Extension Programming Guide that MDM (Mobile Device Management) can restrict device from using custom keyboards at all, but I didn't find that option in the new beta version of Apple Configurator.app for OS X Yosemite. Is 'Configurator' just missing that option?
Any ideas here? Should I file a radar to suggest that Apple should introduce such functionality?
Looks like you got what you wanted in beta seed 3. Line 440 of UIApplication.h:
// Applications may reject specific types of extensions based on the extension point identifier.
// Constants representing common extension point identifiers are provided further down.
// If unimplemented, the default behavior is to allow the extension point identifier.
- (BOOL)application:(UIApplication *)application shouldAllowExtensionPointIdentifier:(NSString *)extensionPointIdentifier NS_AVAILABLE_IOS(8_0);
It's not currently included in the docs, but sound like it will do exactly what you asked here.
I'm guessing these "extension point identifiers" are not unique identifiers of extensions, but of their types, as there is also this on line 545:
// Extension point identifier constants
UIKIT_EXTERN NSString *const UIApplicationKeyboardExtensionPointIdentifier NS_AVAILABLE_IOS(8_0);
TLDR: to disable custom keyboards you would include something like this in your app delegate:
- (BOOL)application:(UIApplication *)application shouldAllowExtensionPointIdentifier:(NSString *)extensionPointIdentifier {
if ([extensionPointIdentifier isEqualToString: UIApplicationKeyboardExtensionPointIdentifier]) {
return NO;
}
return YES;
}
Swift 3 :
func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplicationExtensionPointIdentifier) -> Bool {
if extensionPointIdentifier == UIApplicationExtensionPointIdentifier.keyboard {
return false
}
return true
}
I just want to add this for those developers who want to implement this method in Xamarin iOS. The idea is to override theShouldAllowExtensionPointIdentifier method in your AppDelegate:
public override bool ShouldAllowExtensionPointIdentifier(UIApplication application, NSString extensionPointIdentifier)
{
if (extensionPointIdentifier == UIExtensionPointIdentifier.Keyboard)
{
return false;
}
return true;
}
In Swift 5, UIApplicationExtensionPointIdentifier was changed to UIApplication.ExtensionPointIdentifier.
func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier) -> Bool {
if extensionPointIdentifier == UIApplication.ExtensionPointIdentifier.keyboard {
return false
}
return true
}
In Swift 5 using a switch:
func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier) -> Bool {
switch extensionPointIdentifier {
case .keyboard:
return false
default:
return true
}
}

Resources