Skip to content

Commit d371913

Browse files
committed
Add custom UserDefaults store support for app groups and shared defaults
1 parent 2f66952 commit d371913

6 files changed

Lines changed: 84 additions & 70 deletions

File tree

.github/workflows/ci.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
build:
11+
runs-on: macos-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- name: Build
15+
run: swift build
16+
- name: Test
17+
run: swift test

.gitignore

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -47,44 +47,4 @@ playground.xcworkspace
4747
.swiftpm
4848

4949
.build/
50-
51-
# CocoaPods
52-
#
53-
# We recommend against adding the Pods directory to your .gitignore. However
54-
# you should judge for yourself, the pros and cons are mentioned at:
55-
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56-
#
57-
# Pods/
58-
#
59-
# Add this line if you want to avoid checking in source code from the Xcode workspace
60-
# *.xcworkspace
61-
62-
# Carthage
63-
#
64-
# Add this line if you want to avoid checking in source code from Carthage dependencies.
65-
# Carthage/Checkouts
66-
67-
Carthage/Build/
68-
69-
# Accio dependency management
70-
Dependencies/
71-
.accio/
72-
73-
# fastlane
74-
#
75-
# It is recommended to not store the screenshots in the git repo.
76-
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
77-
# For more information about the recommended setup visit:
78-
# https://docs.fastlane.tools/best-practices/source-control/#source-control
79-
80-
fastlane/report.xml
81-
fastlane/Preview.html
82-
fastlane/screenshots/**/*.png
83-
fastlane/test_output
84-
85-
# Code Injection
86-
#
87-
# After new code Injection tools there's a generated folder /iOSInjectionProject
88-
# https://github.com/johnno1962/injectionforxcode
89-
90-
iOSInjectionProject/
50+
.DS_Store

.spi.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
version: 1
2+
builder:
3+
configs:
4+
- documentation_targets: [CloudUserDefaults]

README.md

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
# About
2+
Just drop one file in your project and add a few lines of code and your settings are now visible to all your user's devices.
23

34
CloudUserDefaults automatically syncs `UserDefaults` **values** that use a **key** with a specified prefix to the cloud. Silently listening to system events it detects when a `UserDefaults` **key** with a given prefix is changed and automatically syncs the **value**. For example, if you choose ***cloud_*** as the prefix `UserDefaults.standard.set(1, forKey: "cloud_count")` is set on all the user's devices.
45

56
This is based on Mugunth Kumar's elegant solution `MKiCloudSync` an Objective-C [GitHub repository](https://github.com/MugunthKumar/MKiCloudSync) that is now archived. Thanks to [Paul Hudson for the introduction to `MKiCloudSync`](https://www.hackingwithswift.com/example-code/system/how-to-store-userdefaults-options-in-icloud).
67

7-
## Installation
8+
![CI](https://github.com/nbasham/CloudUserDefaults/actions/workflows/ci.yml/badge.svg)
89

10+
## Installation
911
### Swift Package Manager
10-
If you are using Xcode 11 or later:
1112
1. Click `File`
12-
2. `Swift Packages`
13-
3. `Add Package Dependency...`
14-
4. Specify the git URL for CloudUserDefaults.
15-
13+
2. `Add Package Dependencies...`
14+
3. Specify the git URL for CloudUserDefaults.
1615
```swift
1716
https://github.com/nbasham/CloudUserDefaults.git
1817
```
19-
2018
### Manual
2119
Copy `CloudUserDefaults.swift` to your project
2220

@@ -26,13 +24,30 @@ In Xcode, click your project, click your target, click `Signing & Capabilities`,
2624
**NOTE** iCloud events are not sent to the simulator.
2725

2826
## Usage
29-
Create an instance of `CloudUserDefaults` some place it will stay in scope (e.g. in your `AppDelegate`) and call `start` with a prefix of your choosing e.g.
27+
Create an instance of `CloudUserDefaults` somewhere it will stay in scope and call `start` with a prefix of your choosing.
28+
29+
**AppDelegate**
3030
```swift
3131
import CloudUserDefaults
3232
...
3333
let cloudUserDefaults = CloudUserDefaults()
3434
cloudUserDefaults.start(prefix: "cloud_")
3535
```
36+
37+
**SwiftUI**
38+
```swift
39+
import CloudUserDefaults
40+
41+
@main
42+
struct MyApp: App {
43+
let cloudUserDefaults = CloudUserDefaults()
44+
45+
init() {
46+
cloudUserDefaults.start(prefix: "cloud_")
47+
}
48+
}
49+
```
50+
3651
That's it, whenever a `UserDefaults` **key** starts with `cloud_` it is automatically synced to all the user's devices e.g.
3752
```swift
3853
UserDefaults.standard.set(42, forKey: "cloud_answer") // synced to cloud
@@ -42,17 +57,23 @@ UserDefaults.standard.set(42, forKey: "answer") // local
4257
Subscribe to `CloudUserDefaults.cloudSyncNotification` if you want to be notified when user defaults from another device are delivered e.g.
4358
```swift
4459
NotificationCenter.default.addObserver(self,
45-
selector: #selector(cloudUpdate(notification:)),
46-
name: CloudUserDefaults.cloudSyncNotification,
47-
object: nil)
60+
selector: #selector(cloudUpdate(notification:)),
61+
name: CloudUserDefaults.cloudSyncNotification,
62+
object: nil)
63+
```
64+
65+
## Advanced Usage
66+
If you use app groups (e.g. to share defaults with a widget), pass your suite instead:
67+
```swift
68+
let suite = UserDefaults(suiteName: "group.com.mycompany.myapp")!
69+
let cloudUserDefaults = CloudUserDefaults(defaults: suite)
70+
cloudUserDefaults.start(prefix: "cloud_")
4871
```
72+
4973
## Contributing
5074
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
5175

5276
Please make sure to update tests as appropriate.
5377

5478
## License
5579
[MIT](https://choosealicense.com/licenses/mit/)
56-
57-
58-

Sources/CloudUserDefaults/CloudUserDefaults.swift

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,56 @@
11
import Foundation
22

3+
/// Automatically syncs `UserDefaults` values to iCloud across all of a user's devices.
4+
/// Values are synced when their key matches a specified prefix.
5+
///
6+
/// Create an instance somewhere it will stay in scope, then call ``start(prefix:)``.
7+
///
8+
/// ```swift
9+
/// // AppDelegate
10+
/// let cloudUserDefaults = CloudUserDefaults()
11+
/// cloudUserDefaults.start(prefix: "cloud_")
12+
/// ```
313
open class CloudUserDefaults {
414

15+
/// Posted when values have been received from iCloud.
16+
/// The notification object contains the full iCloud key-value dictionary.
517
public static let cloudSyncNotification = Notification.Name("CloudSyncNotification")
18+
619
internal var prefix: String!
20+
private let defaults: UserDefaults
721

8-
public init() {}
22+
/// Creates a new instance using the specified `UserDefaults` store.
23+
/// - Parameter defaults: The `UserDefaults` instance to sync. Defaults to `UserDefaults.standard`.
24+
/// Pass a suite-based instance for app groups and widgets.
25+
public init(defaults: UserDefaults = .standard) {
26+
self.defaults = defaults
27+
}
928

29+
/// Begins listening for changes to `UserDefaults` and iCloud, syncing any key that starts with the given prefix.
30+
/// - Parameter prefix: The key prefix that identifies values to sync, e.g. `"cloud_"`.
1031
public func start(prefix: String) {
1132
self.prefix = prefix
1233
NotificationCenter.default.addObserver(self, selector: #selector(notificationFromCloud(notification:)), name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil)
13-
NotificationCenter.default.addObserver(self, selector: #selector(notifyCloud(notification:)), name: UserDefaults.didChangeNotification, object: nil)
34+
NotificationCenter.default.addObserver(self, selector: #selector(notifyCloud(notification:)), name: UserDefaults.didChangeNotification, object: defaults)
1435
}
1536

1637
@objc internal func notificationFromCloud(notification: NSNotification) {
1738
let dict = NSUbiquitousKeyValueStore.default.dictionaryRepresentation
18-
1939
// Disable notifications to cloud while we set local values from cloud
20-
NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil)
21-
40+
NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: defaults)
2241
for (key, value) in dict {
2342
if key.hasPrefix(prefix) {
24-
UserDefaults.standard.set(value, forKey: key)
43+
defaults.set(value, forKey: key)
2544
}
2645
}
27-
2846
// Resume notifications to cloud
29-
NotificationCenter.default.addObserver(self, selector: #selector(notifyCloud(notification:)), name: UserDefaults.didChangeNotification, object: nil)
30-
47+
NotificationCenter.default.addObserver(self, selector: #selector(notifyCloud(notification:)), name: UserDefaults.didChangeNotification, object: defaults)
3148
// Send a message with cloud payload that app can listen to
3249
NotificationCenter.default.post(name: CloudUserDefaults.cloudSyncNotification, object: dict)
3350
}
3451

3552
@objc internal func notifyCloud(notification: NSNotification) {
36-
let dict = UserDefaults.standard.dictionaryRepresentation()
53+
let dict = defaults.dictionaryRepresentation()
3754
for (key, value) in dict {
3855
if key.hasPrefix(prefix) {
3956
NSUbiquitousKeyValueStore.default.set(value, forKey: key)

Tests/CloudUserDefaultsTests/CloudUserDefaultsTests.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import XCTest
22
@testable import CloudUserDefaults
33

44
final class CloudUserDefaultsTests: XCTestCase {
5-
65
private let prefix = "cloud_"
76
private let testKey = "test_key"
87
private let testCloudKey = "cloud_test_key"
@@ -23,8 +22,6 @@ final class CloudUserDefaultsTests: XCTestCase {
2322
func testNotificationFromCloud() {
2423
let cloudUserDefaults = CloudUserDefaults()
2524
cloudUserDefaults.start(prefix: prefix)
26-
27-
// Check that CloudUserDefaults.cloudSyncNotification is sent
2825
let _ = expectation(forNotification: CloudUserDefaults.cloudSyncNotification, object: nil, handler: nil)
2926
cloudUserDefaults.notificationFromCloud(notification: unusedNotification)
3027
waitForExpectations(timeout: 1, handler: nil)
@@ -35,8 +32,6 @@ final class CloudUserDefaultsTests: XCTestCase {
3532
cloudUserDefaults.start(prefix: prefix)
3633
UserDefaults.standard.set(testValue, forKey: testKey)
3734
UserDefaults.standard.set(testValue, forKey: testCloudKey)
38-
39-
// Check that values are set in expected stores
4035
XCTAssertNil(NSUbiquitousKeyValueStore.default.object(forKey: testKey))
4136
XCTAssertNotNil(UserDefaults.standard.object(forKey: testKey))
4237
XCTAssertNotNil(NSUbiquitousKeyValueStore.default.object(forKey: testCloudKey))

0 commit comments

Comments
 (0)