I have been developing an app that stores user data (username, etc.) between launches of the app; I stored this data in UserDefaults. However, I have recently noticed a problem: Sometimes, when I run the app, I will get some value back from UserDefaults.standard.object(forKey:), but other times I will get nil when I know for a fact that there is something there.
This makes no sense to me.
I have searched for the answer to this question on SO but only found this, which did not help.
In order to test this, I made an empty app and put the following in viewDidAppear, and then I ran the app once:
UserDefaults.standard.setValue("aValue", forKey: "aKey")
The above line is just to ensure that there is in fact a value stored. I then deleted the above line from viewDidAppear, and then I put this in didFinishLaunchingWithOptions:
print(UserDefaults.standard.object(forKey: "aKey") as? String)
I then ran the app 20 times. Here is my data for if the print(...) printed nil or "aValue":
✓ 𐄂 𐄂 𐄂 ✓ 𐄂 ✓ ✓ ✓ ✓ ✓ ✓ 𐄂 ✓ ✓ ✓ 𐄂 ✓ ✓ 𐄂
It printed nil 35% of the time and it seems to me fairly random too.
I have two questions:
Why would this happen, and how can I fix/prevent it?
It is crazy to put the set in viewDidAppear but the get in didFinishLaunching, because they are unrelated and you don't know anything about the order in which they will happen or even whether viewDidAppear will ever happen (not every view controller ever appears, after all).
Put them in the same place to run your test. For example, let's put them both in didFinishLaunching:
let val = UserDefaults.standard.object(forKey: "aKey") as? String
if val != nil {
print("got it")
} else {
print("didn't get it, setting it")
UserDefaults.standard.set("aValue", forKey: "aKey")
}
You'll find that this works just as you would expect.
EDIT Okay, thanks to the movie you posted, we see that there's a complication: you're testing this on the device. You are testing by repeatedly hitting the Run button in Xcode without stopping the app in a coherent way, and this possibly confuses Xcode so that it does a complete replacement, or so that it never gets a chance to write the defaults to disk in the first place. Instead, Run, switch to the device and hit the Home button, now switch back to Xcode and Stop. Now Run again. I think you'll find that you'll get a more consistent result.
It could happen if didFinishLaunchingWithOptions in AppDelegate is called after viewDidAppear in the view controller.
The recommended way (by Apple) is to register all key / value pairs as soon as possible (awakeFromNib or applicationWillFinishLaunching)
let defaultValues = ["aKey" : "aValue"]
UserDefaults.standard.register(defaults: defaultValues)
The default value "aValue" is considered until it is overwritten the first time.
If you reset the data of the app, the default value is read again.
This is the most reliable way.
PS: Never use valueForKey: and setValue:forKey: with UserDefaults
Related
I'm using #AppStorage with a String property. When changing the value of the property, the view automatically updates to reflect the change as expected. However, the majority of the time it hasn't persisted in UserDefaults. I'm feeling daft with the idea i've missed something here. Is anyone else seeing this?
Steps:
Launch app
Change value
Kill and re-launch app
Change value to something else
Kill and re-launch app
Environment:
Xcode 13.4 (13F17a)
iOS 15.5 - iPhone 13 Pro Simulator
Very simple example:
struct ContentView: View {
#AppStorage("token") var token: String = ""
var body: some View {
Form {
Section("Token") {
Text(token)
}
Section("Debug") {
Button("Update 01") {
token = "token-01"
}
Button("Update 02") {
token = "token-02"
}
}
}
}
}
This typical scenario can happen in development phase but not in production phase because there user doesn't have stop button like Xcode and UserDefault class write changes to disk before the app terminated by system.
So we know that user's default database is written to disk asynchronously, so may be the changes we made to default doesn't written to database when we stop the app from stop button on Xcode, you can try it own on your own by stopping app by swiping, your data will be stored when launch next time but when you stop from Xcode your data will not be saved
The answers above are correct, but I don't see the solution anywhere so I'll add it here:
Just always background your app before you terminate it from Xcode
PS: the answer is not, nor has never been, to call .synchronize on user defaults
I have a universal link, say: https://universallink.com/account/settings
When my application is fully closed, this works perfectly. However, if app is open in background and the link is clicked from safari or from slack, or something like that, this is non functional.
What I have observed:
application(\_:willContinueUserActivityWithType:) is of type "NSUserActivityTypeBrowsingWeb"
application.userActivity is nil OR application.userActivity not nil but application.userActivity.webpageURL is nil during application(\_:willContinueUserActivityWithType:)
application(\_:continue:restorationHandler:) is never called.
application(_:didFinishLaunchingWithOptions:) and application(_:willFinishLaunchingWithOptions:) are returning true.
Checking for let url = launchOptions?[.url] as? URL in application(_:didFinishLaunchingWithOptions:) and application(_:willFinishLaunchingWithOptions:) finds nothing.
I am not exactly sure how to proceed in fixing this scenario. The user activity clearly knows that is was opened with a link, but the link is never preserved so as to be handled.
Does anyone know something else that may be causing this issue?
I am in process in adding CloudKit to my app to enable iCloud sync. But I ran into problem with my method, that executes query with perform method on private database.
My method worked fine, I then changed a few related methods (just with check if iCloud is available) and suddenly my perform method does nothing. By nothing I mean that nothing in perform(query: ) closure gets executed. I have breakpoint on the first line and others on the next lines but never manage to hit them.
private static func getAppDetailsFromCloud(completion: #escaping (_ appDetails: [CloudAppDetails]?) -> Void) {
var cloudAppDetails = [CloudAppDetails]()
let privateDatabase = CKContainer.default().privateCloudDatabase
let query = CKQuery(recordType: APPID_Type, predicate: NSPredicate(format: "TRUEPREDICATE"))
privateDatabase.perform(query, inZoneWith: nil) { (records, error) in
if let error = error {
print(error)
completion(nil)
} else {
if let records = records {
for record in records {
let appId = record.object(forKey: APPID_ID_Property) as? Int
let isDeleted = record.object(forKey: APPID_ISDELETED_Property) as? Int
if let appId = appId, let isDeleted = isDeleted {
cloudAppDetails.append(CloudAppDetails(id: appId, isDeleted: isDeleted == 1))
}
}
completion(cloudAppDetails)
return
}
}
completion(nil)
}
}
My problem starts at privateDatabase.perform line, after that no breakpoints are hit and my execution moves to function which called this one getAppDetailsFromCloud. There is no error...
This is my first time implementing CloudKit and I have no idea why nothing happens in the closure above.
Thanks for help.
EDIT: Forgot to mention that this metod used to work fine and I was able to get records from iCloud. I have not made any edits to it and now it does not work as described :/
EDIT 2: When I run the app without debugger attached then everything works flawlessly. I can sync all data between devices as expected. When I try to debug the code, then I once again get no records from iCloud.
In the completion handler shown here, if there's no error and no results are found, execution will fall through and quietly exit. So, there are two possible conditions happening here: the query isn't running or the query isn't finding any results. I'd perform the following investigative steps, in order:
Check your .entitlements file for the key com.apple.dev.icloud-container-environment. If this key isn't present, then builds from xcode will utilize the development environment. If this key is set, then builds from xcode will access the environment pointed to by this key. (Users that installed this app from Testflight or the app store will always use the production environment).
Open the cloudkit dashboard in the web browser and validate that the records you expect are indeed present in the environment indicated by step 1 and the container you expect. If the records aren't there, then you've found your problem.
If the records appear as expected in the dashboard, then place the breakpoint on the .perform line. If the query is not being called when you expected, then you need to look earlier in the call stack... who was expected to call this function?
If the .perform is being called as expected, then add an else to the if let record statement. Put a breakpoint in the else block. If that fires, then the query ran but found no records.
If, after the above steps, you find that the completion handler absolutely isn't executed, this suggests a malformed query. Try running the query by hand using the cloudkit dashboard and observing the results.
The closure executes asynchronously and usually you need to wait few seconds.
Take into account you can't debug many threads in same way as single. Bcs debugger will not hit breakpoint in closure while you staying in your main thread.
2019, I encountered this issue while working on my CloudKit tasks. Thunk's selected answer didn't help me, so I guess I'm gonna share here my magic. I got the idea of removing the breakpoints and print the results instead. And it worked. But I still need to use breakpoints inside the closure. Well, what I had to do is restart the Xcode. You know the drill in iOS development, if something's not right, restart the Xcode, reconnect the device, and whatnot.
I have a section of code that runs if the user needs to re-auth after logging in. During UI tests, this popover is sometimes displayed, so I have a check for it existing
if (XCUIApplication().staticText["authLabel"].exists) {
completeAuthDialog()
}
When this runs locally, it is fine, completes and the framework finds the element no problem. But when the nightly job on the CI is ran, it fails the first time, but once the same build is set to be rebuilt, the test passes. authLabel is the UILabel's accessibility identifier(btw), so I have been trying to figure out what is causing the flickering.
Yesterday I spent time on the issue, and it seems that the framework just doesn't find the elements sometimes? I have used the accessibility inspector in ensure I am query for the same time it sees.
I even expanded that if check with 4 or 5 additional || to check for any element inside of the popover. The Elements all have accessibility identifiers, I have also used the record feature to ensure that it passes back the same element "names" I am using.
I am kind of stuck, I don't know what else to try/could be causing this issue. The worst part is it ran fine for couple of months, but it seems to fail every night now, and as I said when the tests are ran locally inside xcode they pass fine. Could this be a issue with building from command line?
It is often slower when your tests execute on a different machine, this problem seems particularly prevelant with CI machines as they tend to be under-powered.
If you just do a single check for an element existing, the test only has one point in time to get it right and if the app was slow to present the element then the test will fail.
You can defend against having a flaky test by using a waiter to check a few times over a few seconds to ensure that you've given the app enough time to show the authentication dialog before continuing.
let authElement = XCUIApplication().staticText["authLabel"]
let existsPredicate = NSPredicate(format: "exists == true")
let expectation = XCTNSPredicateExpectation(predicate: existsPredicate, object: authElement)
let result = XCTWaiter().wait(for: [expectation], timeout: 5)
if (result == .completed) {
completeAuthDialog()
}
You can adjust the timeout to suit your needs - a longer timeout will result in the test waiting a longer time to continue if the auth dialog doesn't appear, but will give the dialog more time to appear if the machine is slow. Try it out and see how flaky the tests are with different timeouts to optimise.
my firebase data structure looks like the following
user
|__{user_id}
|__userMatch
|__{userMatchId}
|__createdAt: <UNIX time in milliseconds>
I'm trying to listen for the child added event under userMatch since a particular given time. Here's my swift code:
func listenForNewUserMatches(since: NSDate) -> UInt? {
NSLog("listenForNewUserMatches since: \(since)")
var handle:UInt?
let userMatchRef = usersRef.childByAppendingPath("\(user.objectId!)/userMatch")
var query = userMatchRef.queryOrderedByChild("createdAt");
query = query.queryStartingAtValue(since.timeIntervalSince1970 * 1000)
handle = query.observeEventType(FEventType.ChildAdded, withBlock: { snapshot in
let userMatchId = snapshot.key
NSLog("New firebase UserMatch created \(userMatchId)")
}, withCancelBlock: { error in
NSLog("Error listening for new userMatches: \(error)")
})
return handle
}
What's happening is that the event call back is called only once. Subsequent data insertion under userMatch didn't trigger the call. Sort of behaves like observeSingleEventOfType
I have the following data inserted into firebase under user/{some-id}/userMatch:
QGgmQnDLUB
createdAt: 1448934387867
bMfJH1bzNs
createdAt: 1448934354943
Here are the logs:
2015-11-30 17:32:38.632 listenForNewUserMatches since:2015-12-01 01:32:37 +0000
2015-11-30 17:45:55.163 New firebase UserMatch created bMfJH1bzNs
The call back was fired for bMfJH1bzNs but not for QGgmQnDLUB which was added at a later time. It's very consistent: after opening the app, it only fires for the first event. Not sure what I'm doing wrong here.
Update: Actually the behavior is not very consistent. Sometimes the call back is not fired at all, not even once. But since I persist the since time I should use when calling listenForNewUserMatches function. If I kill the app and restart the app, the callback will get fired (listenForNewUserMatches is called upon app start), for the childAdded event before I killed the app. This happens very consistently (callback always called upon kill-restart the app for events that happened prior to killing the app).
Update 2: Don't know why, but if I add queryLimitedToLast to the query, it works all the time now. I mean, by changing userMatchRef.queryOrderedByChild("createdAt") to userMatchRef.queryOrderedByChild("createdAt").queryLimitedToLast(10), it's working now. 10 is just an arbitrary number I chose.
I think the issue comes from the nature of time based data.
You created a query that says: "Get me all the matches that happened after now." This should work when the app is running and new data comes in like bMfJH1bzNs. But older data like QGgmQnDLUB won't show up.
Then when you run again, the since.timeIntervalSince1970 has changed to a later date. Now neither of the objects before will show up in your query.
When you changed your query to use queryLimitedToLast you avoided this issue because you're no longer querying based on time. Now your query says: "Get me the last ten children at this location."
As long as there is data at that location you'll always receive data in the callback.
So you either need to ensure that since.timeIntervalSince1970 is always earlier than the data you expect to come back, or use queryLimitedToLast.