codeapp icon indicating copy to clipboard operation
codeapp copied to clipboard

Feature Request: Settings Sync via iCloud

Open sfkmk opened this issue 2 months ago • 1 comments

A sync feature would be great so we can keep multiple iCloud devices in sync without redoing settings on each device. It would also be beneficial if we can sync saved remote connections.

sfkmk avatar Oct 26 '25 11:10 sfkmk

From Copilot: To implement the iCloud settings sync feature (including saved remote connections) using NSUbiquitousKeyValueStore, you'll need to replace or augment the current local UserDefaults usage with Apple's iCloud-backed key-value store. This allows small amounts of data (up to 1 MB total per app) to sync across devices signed into the same iCloud account. Based on the app's current code (e.g., UserDefaults+Hosts.swift and related files), settings like remoteHosts, gitCredentialsLookupEntries, and other preferences are stored locally—migrating to NSUbiquitousKeyValueStore will enable cross-device syncing.

Here's a step-by-step explanation of how to implement this. Note that NSUbiquitousKeyValueStore is suitable for the simple key-value data currently stored in UserDefaults, but for larger or more complex data (e.g., if remote connections grow significantly), consider CloudKit as an alternative.

1. Enable iCloud Key-Value Storage in Xcode

  • In your Xcode project, select the target (CodeApp).
  • Go to Signing & Capabilities > + Capability > iCloud.
  • Check Key-Value Storage (leave other options unchecked unless needed for CloudKit).
  • This adds the necessary entitlements and frameworks (Foundation is already imported).

2. Update Data Storage Logic

  • NSUbiquitousKeyValueStore works similarly to UserDefaults but syncs via iCloud. Store encoded data (e.g., as Data) for complex types like arrays of structs.

  • Replace or wrap UserDefaults usage for syncable keys. Use a helper to prioritize iCloud data on first load.

  • Example migration for remoteHosts (from UserDefaults+Hosts.swift):

    import Foundation
    
    extension NSUbiquitousKeyValueStore {
        // Use ubiquitous store for synced data
        var remoteHosts: [RemoteHost] {
            get {
                // Check if iCloud has data; fall back to local UserDefaults if not
                if let data = self.data(forKey: "remote.hosts") ?? UserDefaults.standard.data(forKey: "remote.hosts"),
                    let array = try? PropertyListDecoder().decode([RemoteHost].self, from: data) {
                    return array
                }
                return []
            }
            set {
                if let data = try? PropertyListEncoder().encode(newValue) {
                    self.set(data, forKey: "remote.hosts")
                    // Optionally mirror to local UserDefaults for offline fallback
                    UserDefaults.standard.set(data, forKey: "remote.hosts")
                    self.synchronize()  // Force sync
                }
            }
        }
    
        // Similarly for gitCredentialsLookupEntries
        var gitCredentialsLookupEntries: [GitCredentials] {
            get {
                if let data = self.data(forKey: "git.credentials.entries") ?? UserDefaults.standard.data(forKey: "git.credentials.entries"),
                    let array = try? PropertyListDecoder().decode([GitCredentials].self, from: data) {
                    return array
                }
                return []
            }
            set {
                if let data = try? PropertyListEncoder().encode(newValue) {
                    self.set(data, forKey: "git.credentials.entries")
                    UserDefaults.standard.set(data, forKey: "git.credentials.entries")
                    self.synchronize()
                }
            }
        }
    }
    
  • In files like RemoteContainer.swift, update calls from UserDefaults.standard.remoteHosts to NSUbiquitousKeyValueStore.default.remoteHosts.

  • Do the same for other syncable settings (e.g., gitCredentialsLookupEntries in LocalGitCredentialsHelper.swift, terminal options in SettingsView.swift, etc.). Non-sensitive settings like UI preferences can be synced this way.

3. Handle Initial Sync and Conflicts

  • On app launch (e.g., in CodeApp.swift's setup()), check for iCloud data and migrate if needed:
    private func setup() {
        // ... existing code ...
        migrateToUbiquitousStore()
        // ... rest of setup ...
    }
    
    private func migrateToUbiquitousStore() {
        let ubiquitous = NSUbiquitousKeyValueStore.default
        // If no iCloud data, copy from local UserDefaults
        if ubiquitous.data(forKey: "remote.hosts") == nil,
           let localData = UserDefaults.standard.data(forKey: "remote.hosts") {
            ubiquitous.set(localData, forKey: "remote.hosts")
        }
        // Repeat for other keys
        ubiquitous.synchronize()
    }
    
  • For conflicts (e.g., data changed on another device), NSUbiquitousKeyValueStore merges automatically, but you can observe changes to resolve manually if needed.

4. Observe Changes for Real-Time Sync

  • Register for notifications when iCloud data changes (e.g., on another device). Update the UI or local state accordingly.
  • In a manager class (e.g., add to MainApp.swift or create a new SyncManager):
    class SyncManager {
        static let shared = SyncManager()
    
        private init() {
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(ubiquitousStoreDidChange),
                name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
                object: NSUbiquitousKeyValueStore.default
            )
        }
    
        @objc private func ubiquitousStoreDidChange(notification: Notification) {
            // Reload data or notify UI to refresh
            // E.g., reload remote hosts list in RemoteContainer
            DispatchQueue.main.async {
                // Trigger UI updates, e.g., via @Published properties
            }
        }
    }
    
  • Call SyncManager.shared in setup() to start observing.

5. Testing and Considerations

  • Testing: Use multiple devices signed into the same iCloud account. Changes should sync within minutes (or faster with synchronize()).
  • Limitations: Data is limited to ~1 MB; if remote connections or settings grow, use CloudKit. iCloud must be enabled on devices.
  • Security: NSUbiquitousKeyValueStore is not encrypted like Keychain—avoid storing sensitive secrets here (e.g., keep passwords in Keychain, which can sync separately via iCloud Keychain).
  • Fallback: If iCloud is unavailable, fall back to local UserDefaults.
  • Edge Cases: Handle app deletion/reinstall (iCloud data persists) and user sign-out from iCloud.

This approach directly addresses the issue by syncing settings and remote connections. For a full implementation, update all relevant files (e.g., RemoteContainer.swift, LocalGitCredentialsHelper.swift). If you need code for other settings or a pull request, let me know!

bummoblizard avatar Nov 04 '25 04:11 bummoblizard