SwiftUI #AppStorage not always persisting value - ios

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

Related

SwiftUI: how do I initialize a #StateObject variable before the app launches?

Basically in the AppDelegate method of my app I connect to Firebase. I then need to initialize my UserProfileService before the app launches. Inside UserProfileService init I connect to Firebase to get the authenticated user data and store it.
The problem arises because if I don't initialize UserProfileService before the app launches, as you can see in the code below, when I run the function .hasData() to check if the UserProfileService has inside it the data I need, it should either end up in the phoneRegistrationView or the mainTabBarView. If the user is indeed already authenticated, at app launch, since the initialization of UserProfileService needs to wait for Firebase to retrieve the data, the function .hasData() returns false and shows phoneRegistrationView() for a few milliseconds, then it returns to true and goes to mainTabBarView.
#main
struct myApp: App {
#StateObject private var userProfileService = UserProfileService()
var body: some Scene {
WindowGroup {
// user not registered or authenticated
if !userProfileService.hasData() {
NavigationView {
phoneRegistrationView()
}
.environmentObject(userProfileService)
}
// user registered and profile completed
else {
mainTabBarView()
.environmentObject(userProfileService)
}
}
}
Is there any way to solve this?
I tried await functions to initialize the data in UserProfileService but nothing seemed to work.
The
#main
struct myApp: App {
is an entry point to the app. Nothing should or will run before that. So you need to change your requirements and show some progress / loading screen while the data is being loaded:
if userProfileService.isLoading {
ProgressView {
Text("Please wait...")
}
} else if !userProfileService.hasData() {
// ... same as before
While you could set userProfileService.isLoading to true in the init of the UserProfileService, and set it back to false when you get a response from Firabase.
This is of course the most primitive way, just to show the state. Many other ways are possible, but the point is: if you depend on some network operation, have a state in your app that "entertains" the user while that operation completes.
#StateObject is for when you need a reference type in an #State and both of these are for when you want a source of truth for view data tied to the lifetime of something on screen. I.e. created on appear and destroyed on disappear. Since you wouldn't want your service to ever be destroyed #StateObject is not the right tool for the job.
Some other options are to use a singleton, see PersistenceController.shared in an Xcode template with core data checked. Or you could use the #UIApplicationDelegateAdaptor that gives you a place you can create services and respond to the usual app lifecycle delegate events.

CloudKit App to handle different iCloud accounts

I have an app that keeps user data in a private database using CloudKit and iCloud. I have written code to handle when the user logs out of iCloud and back in as a different user via Settings -> iCloud on their device. I am currently in Beta Testing mode and when I try this as an internal tester, the app crashes when I change accounts. When I test this in development mode using either the Xcode simulator or a real device, my code works great. My question is: should CloudKit apps be able to handle when the iCloud account changes on the device? If so, is this case able to be tested with internal testing via TestFlight. My code to handle account changes is below...
func isCloudAccountAvailableASync() {
container.accountStatusWithCompletionHandler { (accountStatus, error) in
switch accountStatus {
case .Available:
print("INFO: iCloud Available!")
// begin sync process by finding the current iCloud user
self.fetchUserID()
return
case .NoAccount:
print("INFO: No iCloud account")
case .Restricted:
print("WARNING: iCloud restricted")
case .CouldNotDetermine:
print("WARNING: Unable to determine iCloud status")
}
// if you get here, no sync happened so make sure to exec the callback
self.syncEnded(false)
}
}
func fetchUserID(numTries: Int = 0) {
container.fetchUserRecordIDWithCompletionHandler( { recordID, error in
if let error = error {
// error handling code
// reach here then fetchUser was a failure
self.syncEnded(false)
}
else {
// fetchUserID success
if self.lastiCloudUser == nil {
print("INFO: our first iCloud user! \(recordID)")
self.saveUserID()
}
else if !recordID!.isEqual(self.lastiCloudUser) {
// User has changed!!!!!!!!!!!!!
self.saveUserID()
// delete all local data
// reset the saved server token
self.saveServerToken(nil)
// try again with reset fields
}
// else user never changed, then just create a zone
// continue the sync process
self.initZone()
}
})
}
---EDIT/UPDATE:---
Here is a screenshot of my crash log
---EDIT/UPDATE 2:---
I was able to generate another crash with a different crash log. This crash log still doesn't point to my code, but at least describes a function...
I kept making it crash and somehow got a crash log that included a line of code I could link to a line in my Xcode project. My issue was with NSOrderedSet and iOS 9. That issue can be found here. I do not know why I only got hex in my crash log before, if anyone knows how to deal with hex crash logs I would love to hear it.
Here are the answers to my original question for anyone out there:
Should CloudKit apps be able to handle when the iCloud account changes on the device?
Answer: Yes
If so, is this case able to be tested with internal testing via TestFlight?
Answer: Yes
It's hard to say were your app crashes exactly.
You have to be aware that after an account change you should reset the state of your app (clearing local user data and navigating away from the screen that has user specific data)
On startup of your app you should call a function like this:
func reactToiCloudloginChanges() {
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSUbiquityIdentityDidChange, object: nil, queue: nil) { _ in
// The user’s iCloud login changed: should refresh all local user data here.
Async.main {
self.viewController?.removeFromParentViewController()
// Or some other code to go back to your start screen.
}
return
}
}

Xcode UI Testing: how to test app's first launching

I'm writing UITests in xCode 7.1 and have a function that tests app during first launching. The question is: how to check does the app launched for the first time or not.
I need something like this:
func testFirstLaunching() {
if ("app is launched for the first time") {
//test scenario of first launching
}
else {
//invoke func that will test other scenario
}
}
Does anybody know any trick how to check this?
App is using NSUserDefaults, maybe Xcode tests have a super-powerful feature to access it?
Any suggestions would be valuable!)
Here is a manual solution to your problem:
Create a global variable in your first presented ViewController, make it false
In your viewDidLoad, change its value to true
Save it
In testFirstLaunching(), read the value of your variable
Use the if-statement for doing whatever you want to do
Hope this helps :)

CloudKit method call hung up

When app starts some preliminary process take place. Sometimes it is done quickly in some second, and sometimes It does not end, but without any error it hung up.
I.e. at launch client always fetch the last serverChangedToken. and sometimes it just hung up it does not complete. I am talking about production environment, developer works well. So this route get called, but some times it does not finishes. Any idea why? I do not get any error, timeout.
let fnco = CKFetchNotificationChangesOperation(previousServerChangeToken: nil)
fnco.fetchNotificationChangesCompletionBlock = {newServerChangeToken, error in
if error == nil {
serverChangeToken = newServerChangeToken
dispatch_sync(dispatch_get_main_queue(), {
(colorCodesInUtility.subviews[10] ).hidden = false
})
} else {
Utility.writeMessageToLog("error 4559: \(error!.localizedDescription)")
}
dispatch_semaphore_signal(sema)
}
defaultContainer.addOperation(fnco)
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER)
I know it is not recommended to use semaphores to control flow of the CloudKit method calls.
Do you think the last two line can be swapped? First dispatch_semaphore_wait and then addOperation be called?
Strange that app worked for iOS 8, this bug arise only in iOS 9.
Adding the following line of code will probably solve your problem:
queryOperation.qualityOfService = .UserInteractive
In iOS 9 Apple changed the behavior of that setting. Especially when using cellular data you could get the behaviour you described.
The documentation states for the .qualityOfService this:
The default value of this property is NSOperationQualityOfServiceBackground and you should leave that value in place whenever possible.
In my opinion this is more a CloudKit bug than a changed feature. More people have the same issue. I did already report it at https://bugreport.apple.com

Xcode 7 UI Testing: how to dismiss a series of system alerts in code

I am writing UI test cases using the new Xcode 7 UI Testing feature. At some point of my app, I ask the user for permission of camera access and push notification. So two iOS popups will show up: "MyApp Would Like to Access the Camera" popup and "MyApp Would Like to Send You Notifications" popup. I'd like my test to dismiss both popups.
UI recording generated the following code for me:
[app.alerts[#"cameraAccessTitle"].collectionViews.buttons[#"OK"] tap];
However, [app.alerts[#"cameraAccessTitle"] exists] resolves to false, and the code above generates an error: Assertion Failure: UI Testing Failure - Failure getting refresh snapshot Error Domain=XCTestManagerErrorDomain Code=13 "Error copying attributes -25202".
So what's the best way of dismissing a stack of system alerts in test? The system popups interrupt my app flow and fail my normal UI test cases immediately. In fact, any recommendations regarding how I can bypass the system alerts so I can resume testing the usual flow are appreciated.
This question might be related to this SO post which also doesn't have an answer: Xcode7 | Xcode UI Tests | How to handle location service alert?
Thanks in advance.
Xcode 7.1
Xcode 7.1 has finally fixed the issue with system alerts. There are, however, two small gotchas.
First, you need to set up a "UI Interuption Handler" before presenting the alert. This is our way of telling the framework how to handle an alert when it appears.
Second, after presenting the alert you must interact with the interface. Simply tapping the app works just fine, but is required.
addUIInterruptionMonitorWithDescription("Location Dialog") { (alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
app.buttons["Request Location"].tap()
app.tap() // need to interact with the app for the handler to fire
The "Location Dialog" is just a string to help the developer identify which handler was accessed, it is not specific to the type of alert.
I believe that returning true from the handler marks it as "complete", which means it won't be called again. For your situation I would try returning false so the second alert will trigger the handler again.
Xcode 7.0
The following will dismiss a single "system alert" in Xcode 7 Beta 6:
let app = XCUIApplication()
app.launch()
// trigger location permission dialog
app.alerts.element.collectionViews.buttons["Allow"].tap()
Beta 6 introduced a slew of fixes for UI Testing and I believe this was one of them.
Also note that I am calling -element directly on -alerts. Calling -element on an XCUIElementQuery forces the framework to choose the "one and only" matching element on the screen. This works great for alerts where you can only have one visible at a time. However, if you try this for a label and have two labels the framework will raise an exception.
Objective - C
-(void) registerHandlerforDescription: (NSString*) description {
[self addUIInterruptionMonitorWithDescription:description handler:^BOOL(XCUIElement * _Nonnull interruptingElement) {
XCUIElement *element = interruptingElement;
XCUIElement *allow = element.buttons[#"Allow"];
XCUIElement *ok = element.buttons[#"OK"];
if ([ok exists]) {
[ok tap];
return YES;
}
if ([allow exists]) {
[allow tap];
return YES;
}
return NO;
}];
}
-(void)setUp {
[super setUp];
self.continueAfterFailure = NO;
self.app = [[XCUIApplication alloc] init];
[self.app launch];
[self registerHandlerforDescription:#"“MyApp” would like to make data available to nearby Bluetooth devices even when you're not using app."];
[self registerHandlerforDescription:#"“MyApp” Would Like to Access Your Photos"];
[self registerHandlerforDescription:#"“MyApp” Would Like to Access the Camera"];
}
Swift
addUIInterruptionMonitorWithDescription("Description") { (alert) -> Bool in
alert.buttons["Allow"].tap()
alert.buttons["OK"].tap()
return true
}
Gosh.
It always taps on "Don't Allow" even though I deliberately say tap on "Allow"
At least
if app.alerts.element.collectionViews.buttons["Allow"].exists {
app.tap()
}
allows me to move on and do other tests.
For the ones who are looking for specific descriptions for specific system dialogs (like i did) there is none :) the string is just for testers tracking purposes. Related apple document link : https://developer.apple.com/documentation/xctest/xctestcase/1496273-adduiinterruptionmonitor
Update : xcode 9.2
The method is sometimes triggered sometimes not. Best workaround for me is when i know there will be a system alert, i add :
sleep(2)
app.tap()
and system alert is gone
God! I hate how XCTest has the worst time dealing with UIView Alerts. I have an app where I get 2 alerts the first one wants me to select "Allow" to enable locations services for App permissions, then on a splash page the user has to press a UIButton called "Turn on location" and finally there is a notification sms alert in a UIViewAlert and the user has to select "OK". The problem we were having was not being able to interact with the system Alerts, but also a race condition where behavior and its appearance on screen was untimely. It seems that if you use the alert.element.buttons["whateverText"].tap the logic of XCTest is to keep pressing until the time of the test runs out. So basically keep pressing anything on the screen until all the system alerts are clear of view.
This is a hack but this is what worked for me.
func testGetPastTheStupidAlerts() {
let app = XCUIApplication()
app.launch()
if app.alerts.element.collectionViews.buttons["Allow"].exists {
app.tap()
}
app.buttons["TURN ON MY LOCATION"].tap()
}
The string "Allow" is completely ignored and the logic to app.tap() is called evreytime an alert is in view and finally the button I wanted to reach ["Turn On Location"] is accessible and the test pass
~Totally confused, thanks Apple.
The only thing I found that reliably fixed this was to set up two separate tests to handle the alerts. In the first test, I call app.tap() and do nothing else. In the second test, I call app.tap() again and then do the real work.
On xcode 9.1, alerts are only being handled if the test device has iOS 11. Doesn't work on older iOS versions e.g 10.3 etc. Reference: https://forums.developer.apple.com/thread/86989
To handle alerts use this:
//Use this before the alerts appear. I am doing it before app.launch()
let allowButtonPredicate = NSPredicate(format: "label == 'Always Allow' || label == 'Allow'")
//1st alert
_ = addUIInterruptionMonitor(withDescription: "Allow to access your location?") { (alert) -> Bool in
let alwaysAllowButton = alert.buttons.matching(allowButtonPredicate).element.firstMatch
if alwaysAllowButton.exists {
alwaysAllowButton.tap()
return true
}
return false
}
//Copy paste if there are more than one alerts to handle in the app
#Joe Masilotti's answer is correct and thanks for that, it helped me a lot :)
I would just like to point out the one thing, and that is the UIInterruptionMonitor catches all system alerts presented in series TOGETHER, so that the action you apply in the completion handler gets applied to every alert ("Don't allow" or "OK"). If you want to handle alert actions differently, you have to check, inside the completion handler, which alert is currently presented e.g. by checking its static text, and then the action will be applied only on that alert.
Here's small code snippet for applying the "Don't allow" action on the second alert, in series of three alerts, and "OK" action on the remaining two:
addUIInterruptionMonitor(withDescription: "Access to sound recording") { (alert) -> Bool in
if alert.staticTexts["MyApp would like to use your microphone for recording your sound."].exists {
alert.buttons["Don’t Allow"].tap()
} else {
alert.buttons["OK"].tap()
}
return true
}
app.tap()
This is an old question but there is now another way to handle these alerts.
The system alert isn't accessibly from the app context of the app you are launched in, however you can access the app context anyway. Look at this simple example:
func testLoginHappyPath() {
let app = XCUIApplication()
app.textFields["Username"].typeText["Billy"]
app.secureTextFields["Password"].typeText["hunter2"]
app.buttons["Log In"].tap()
}
In a vacuum with a simulator already launched and permissions already granted or denied, this will work. But if we put it in a CI pipeline where it gets a brand new simulator, all of the sudden it won't be able to find that Username field because there's a notification alert popping up.
So now there's 3 choices on how to handle that:
Implicitly
There's already a default system alert interrupt handler. So in theory, simply trying to typeText on that first field should check for an interrupting event and handle it in the affirmative.
If everything works as designed, you won't have to write any code but you'll see an interruption logged and handled in the log, and your test will take a couple seconds more.
Explicitly via interruptionmonitor
I won't rewrite the previous work on this, but this is where you explicitly set up an interruptionmonitor to handle the specific alert being popped up - or whatever alerts you expect to happen.
This is useful if the built-in handler doesn't do what you want - or doesn't work at all.
Explicitly via XCUITest framework
In xCode 9.0 and above, you can switch between app contexts fluidly by simply defining multiple XCUIApplication() instances. Then you can locate the field you need via familiar methods. So to do this explicitly would look like the following:
func testLoginHappyPath() {
let app = XCUIApplication()
let springboardApp = XCUIApplication(bundleidentifier: "com.apple.springboard")
if springboardApp.alerts[""FunHappyApp" would like permission to own your soul."].exists {
springboardApp.alerts.buttons["Allow"].tap()
}
app.textFields["Username"].typeText["Billy"]
app.secureTextFields["Password"].typeText["hunter2"]
app.buttons["Log In"].tap()
}
Sounds like the approach to implementing camera access and notifications are threaded as you say, but not physically managed and left to chance when and how they are displayed.
I suspect one is triggered by the other and when it is programatically clicked it wipes out the other one as well (which Apple would probably never allow)
Think of it you're asking for a users permission then making the decision on their behalf? Why? Because you can't get your code to work maybe.
How to fix - trace where these two components are triggering the pop up dialogues - where are they being called?, rewrite to trigger just one, send an NSNotification when one dialogue has been completed to trigger and display the remaining one.
I would seriously discourage the approach of programatically clicking dialogue buttons meant for the user.

Resources