Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capacitor Preferences plugin does not guarantee immediate synchronization with the UserDefaults system #2261

Open
shawnweiland opened this issue Nov 24, 2024 · 0 comments
Labels

Comments

@shawnweiland
Copy link

shawnweiland commented Nov 24, 2024

Core Issue: JS side of the code updates key value to true but the native UserDefaults key value returns false (or nil). Any fix to the issue would be much appreciated.

JavaScript (Angular Side):

The Preferences.set() function successfully sets the key isSyncing to "true".
A subsequent Preferences.get() confirms the value as "true".

Native (iOS Side):

The UserDefaults.didChangeNotification is triggered, but the native code reads the isSyncing value as "false".
This suggests that the native UserDefaults is not properly synchronized with the value set by the Capacitor Preferences plugin.

Root Cause
The issue arises because the Capacitor Preferences plugin does not guarantee immediate synchronization with the UserDefaults system. By default, updates to UserDefaults are asynchronous, and changes made by the plugin might not propagate in time for the native code to detect.

Code:
JS:
private async setSyncingState(value: boolean): Promise {
try {
// Set the syncing state
await Preferences.set({ key: SYNC_KEY, value: value.toString() });
console.log(
[Angular] Preferences.set completed. Key: ${SYNC_KEY}, Value: ${value},
);

  // Verify the stored value
  const { value: storedValue } = await Preferences.get({ key: SYNC_KEY });
  console.log(
    `[Angular] Verified Preferences.get. Key: ${SYNC_KEY}, Value: ${storedValue}`,
  );
} catch (error) {
  console.error('Error updating sync state in Preferences:', error);
}

}

async enableSync(): Promise {
console.log('[Angular] Enabling sync...');
await this.setSyncingState(true);
}

async disableSync(): Promise {
console.log('[Angular] Disabling sync...');
await this.setSyncingState(false);
}

async checkSyncStatus(): Promise {
try {
const { value } = await Preferences.get({ key: SYNC_KEY });
const isSyncing = value === 'true';
console.log([Angular] Current sync status: ${isSyncing});
} catch (error) {
console.error('Error reading sync status from Preferences:', error);
}
}

App Delegate:

import UIKit
import Capacitor
import FirebaseCore
import FirebaseAuth

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?
private var photoSyncService: PhotoSyncService?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Initialize Firebase
    FirebaseApp.configure()

    // Check current user authentication
    if let currentUser = Auth.auth().currentUser {
        print("User is authenticated in Native Code with UID: \(currentUser.uid)")
    } else {
        print("No user in Native Code currently authenticated.")
    }

    // Initialize the PhotoSyncService
    photoSyncService = PhotoSyncService()
    print("⚡️ PhotoSyncService initialized in AppDelegate.")

    // Add observer for UserDefaults changes
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(handleSyncStateChange),
        name: UserDefaults.didChangeNotification,
        object: nil
    )

    return true
}

// Extract sync handling logic into a separate method

// Handle UserDefaults changes
@objc private func handleSyncStateChange() {
    // Access the standard UserDefaults
    let userDefaults = UserDefaults.standard

    // Read the "isSyncing" key
    let isSyncingString = userDefaults.string(forKey: "isSyncing") ?? "false"
    let isSyncing = (isSyncingString.lowercased() == "true")

    print("⚡️ [AppDelegate] Detected UserDefaults change for isSyncing.")
    print("⚡️ [AppDelegate] isSyncing raw value: \(isSyncingString)")
    print("⚡️ [AppDelegate] Parsed isSyncing state: \(isSyncing)")

    // Update the PhotoSyncService based on isSyncing value
    if isSyncing {
        print("⚡️ [AppDelegate] Syncing is enabled. Starting PhotoSyncService...")
        self.photoSyncService?.startServiceIfNeeded()
    } else {
        print("⚡️ [AppDelegate] Syncing is disabled. Stopping PhotoSyncService...")
        self.photoSyncService?.stopService()
    }
}

func applicationWillResignActive(_ application: UIApplication) {
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}

func applicationDidEnterBackground(_ application: UIApplication) {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}

func applicationWillEnterForeground(_ application: UIApplication) {
    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}

func applicationDidBecomeActive(_ application: UIApplication) {
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}

func applicationWillTerminate(_ application: UIApplication) {
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    // See https://github.com/capawesome-team/capacitor-firebase/tree/main/packages/authentication#installation
    if Auth.auth().canHandle(url) {
        return true
    }
    // Called when the app was launched with a url. Feel free to add additional processing here,
    // but if you want the App API to support tracking app url opens, make sure to keep this call
    return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    // Called when the app was launched with an activity, including Universal Links.
    // Feel free to add additional processing here, but if you want the App API to support
    // tracking app url opens, make sure to keep this call
    return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
    NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
}

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    NotificationCenter.default.post(name: Notification.Name.init("didReceiveRemoteNotification"), object: completionHandler, userInfo: userInfo)
}

deinit {
    // Remove observer to prevent memory leaks
    NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil)
}

}

I have been through the docs and nothing seems to fix this issue. Any help would be great.

@ionitron-bot ionitron-bot bot added the triage label Nov 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant