Add AppGroupConfiguration, StorageKeys, AppGroupUserDefaultsKey, Value (+7 more); Remove AppStorageCatalog

This commit is contained in:
Matt Bruce 2026-01-14 11:54:40 -06:00
parent 3ef0bdfa34
commit 93eaaa353a
8 changed files with 256 additions and 2 deletions

View File

@ -27,6 +27,7 @@ The project also includes a watchOS companion app target for watch-specific demo
1. Open `SecureStorgageSample.xcodeproj` 1. Open `SecureStorgageSample.xcodeproj`
2. Select an iOS simulator or device 2. Select an iOS simulator or device
3. Build and run (⌘R) 3. Build and run (⌘R)
4. To use App Group demos, set your App Group identifier in `SecureStorgageSample/SecureStorgageSample/Models/AppGroupConfiguration.swift` and enable the entitlement for each target that should share data.
## Project Structure ## Project Structure
@ -49,6 +50,7 @@ SecureStorgageSample/
│ ├── Keychain/ │ ├── Keychain/
│ ├── FileSystem/ │ ├── FileSystem/
│ ├── EncryptedFileSystem/ │ ├── EncryptedFileSystem/
│ ├── AppGroup/
│ └── Platform/ │ └── Platform/
├── WatchOptimized.swift # Watch data models ├── WatchOptimized.swift # Watch data models
├── Services/ ├── Services/
@ -91,6 +93,11 @@ The app demonstrates various storage configurations:
- Caches directory (can be purged) - Caches directory (can be purged)
- JSON and PropertyList serializers - JSON and PropertyList serializers
### App Group Storage
- Shared UserDefaults via App Group identifier
- Shared files in the App Group container
- Requires App Group entitlements in all participating targets
### Encrypted Storage ### Encrypted Storage
- AES-256-GCM or ChaCha20-Poly1305 encryption - AES-256-GCM or ChaCha20-Poly1305 encryption
- PBKDF2 or HKDF key derivation - PBKDF2 or HKDF key derivation

View File

@ -0,0 +1,5 @@
import Foundation
enum AppGroupConfiguration {
static let identifier = "group.com.example.securestorage"
}

View File

@ -3,7 +3,7 @@ import LocalData
import SharedKit import SharedKit
struct AppStorageCatalog: StorageKeyCatalog { nonisolated struct AppStorageCatalog: StorageKeyCatalog {
static var allKeys: [AnyStorageKey] { static var allKeys: [AnyStorageKey] {
[ [
.key(StorageKeys.AppVersionKey()), .key(StorageKeys.AppVersionKey()),
@ -19,7 +19,9 @@ struct AppStorageCatalog: StorageKeyCatalog {
.key(StorageKeys.ExternalSessionLogsKey()), .key(StorageKeys.ExternalSessionLogsKey()),
.key(StorageKeys.WatchVibrationKey()), .key(StorageKeys.WatchVibrationKey()),
.key(StorageKeys.SyncableSettingKey()), .key(StorageKeys.SyncableSettingKey()),
.key(StorageKeys.ExternalKeyMaterialKey()) .key(StorageKeys.ExternalKeyMaterialKey()),
.key(StorageKeys.AppGroupUserDefaultsKey()),
.key(StorageKeys.AppGroupUserProfileKey())
] ]
} }
} }

View File

@ -0,0 +1,21 @@
import Foundation
import LocalData
extension StorageKeys {
/// Stores a shared setting in App Group UserDefaults.
/// - Domain: App Group UserDefaults
/// - Security: None
/// - Sync: Never
struct AppGroupUserDefaultsKey: StorageKey {
typealias Value = String
let name = "app_group_setting"
let domain: StorageDomain = .appGroupUserDefaults(identifier: AppGroupConfiguration.identifier)
let security: SecurityPolicy = .none
let serializer: Serializer<String> = .json
let owner = "SampleApp"
let description = "Stores a shared setting readable by app extensions."
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
}
}

View File

@ -0,0 +1,30 @@
import Foundation
import LocalData
import SharedKit
extension StorageKeys {
/// Stores a shared user profile in the App Group container.
/// - Domain: App Group File System
/// - Security: None
/// - Sync: Never
struct AppGroupUserProfileKey: StorageKey {
typealias Value = UserProfile
let directory: FileDirectory
let name = "app_group_user_profile.json"
let security: SecurityPolicy = .none
let serializer: Serializer<UserProfile> = .json
let owner = "SampleApp"
let description = "Stores a profile shared between the app and extensions."
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
init(directory: FileDirectory = .documents) {
self.directory = directory
}
var domain: StorageDomain {
.appGroupFileSystem(identifier: AppGroupConfiguration.identifier, directory: directory)
}
}
}

View File

@ -14,7 +14,9 @@ struct FileSystemDemo: View {
@State private var profileEmail = "" @State private var profileEmail = ""
@State private var profileAge = "" @State private var profileAge = ""
@State private var storedProfile: UserProfile? @State private var storedProfile: UserProfile?
@State private var appGroupProfile: UserProfile?
@State private var statusMessage = "" @State private var statusMessage = ""
@State private var appGroupStatusMessage = ""
@State private var isLoading = false @State private var isLoading = false
@State private var selectedDirectory: FileDirectory = .documents @State private var selectedDirectory: FileDirectory = .documents
@FocusState private var isFieldFocused: Bool @FocusState private var isFieldFocused: Bool
@ -79,6 +81,37 @@ struct FileSystemDemo: View {
.foregroundStyle(.red) .foregroundStyle(.red)
.disabled(isLoading) .disabled(isLoading)
} }
Section("App Group Storage") {
Text("Requires App Group entitlement and matching identifier in AppGroupConfiguration.")
.font(.caption)
.foregroundStyle(.secondary)
Button(action: saveAppGroupProfile) {
HStack {
Image(systemName: "doc.fill")
Text("Save to App Group")
}
}
.disabled(profileName.isEmpty || isLoading)
Button(action: loadAppGroupProfile) {
HStack {
Image(systemName: "doc.text.fill")
Text("Load from App Group")
}
}
.disabled(isLoading)
Button(action: deleteAppGroupProfile) {
HStack {
Image(systemName: "trash")
Text("Delete App Group File")
}
}
.foregroundStyle(.red)
.disabled(isLoading)
}
if let profile = storedProfile { if let profile = storedProfile {
Section("Retrieved Profile") { Section("Retrieved Profile") {
@ -88,6 +121,15 @@ struct FileSystemDemo: View {
LabeledContent("Created", value: profile.createdAt.formatted(date: .abbreviated, time: .shortened)) LabeledContent("Created", value: profile.createdAt.formatted(date: .abbreviated, time: .shortened))
} }
} }
if let profile = appGroupProfile {
Section("App Group Profile") {
LabeledContent("Name", value: profile.name)
LabeledContent("Email", value: profile.email)
LabeledContent("Age", value: profile.ageDescription)
LabeledContent("Created", value: profile.createdAt.formatted(date: .abbreviated, time: .shortened))
}
}
if !statusMessage.isEmpty { if !statusMessage.isEmpty {
Section { Section {
@ -96,6 +138,14 @@ struct FileSystemDemo: View {
.foregroundStyle(statusMessage.contains("Error") ? .red : .green) .foregroundStyle(statusMessage.contains("Error") ? .red : .green)
} }
} }
if !appGroupStatusMessage.isEmpty {
Section {
Text(appGroupStatusMessage)
.font(.caption)
.foregroundStyle(appGroupStatusMessage.contains("Error") ? .red : .green)
}
}
Section("Key Configuration") { Section("Key Configuration") {
LabeledContent("Domain", value: "File System") LabeledContent("Domain", value: "File System")
@ -167,6 +217,58 @@ struct FileSystemDemo: View {
isLoading = false isLoading = false
} }
} }
private func saveAppGroupProfile() {
isLoading = true
Task {
do {
let key = StorageKeys.AppGroupUserProfileKey(directory: selectedDirectory)
let profile = UserProfile(
name: profileName,
email: profileEmail,
age: Int(profileAge),
createdAt: Date()
)
try await StorageRouter.shared.set(profile, for: key)
appGroupStatusMessage = "✓ App Group saved to \(selectedDirectory == .documents ? "Documents" : "Caches")"
} catch {
appGroupStatusMessage = "Error: \(error.localizedDescription)"
}
isLoading = false
}
}
private func loadAppGroupProfile() {
isLoading = true
Task {
do {
let key = StorageKeys.AppGroupUserProfileKey(directory: selectedDirectory)
appGroupProfile = try await StorageRouter.shared.get(key)
appGroupStatusMessage = "✓ App Group loaded from file system"
} catch StorageError.notFound {
appGroupProfile = nil
appGroupStatusMessage = "No App Group profile file found"
} catch {
appGroupStatusMessage = "Error: \(error.localizedDescription)"
}
isLoading = false
}
}
private func deleteAppGroupProfile() {
isLoading = true
Task {
do {
let key = StorageKeys.AppGroupUserProfileKey(directory: selectedDirectory)
try await StorageRouter.shared.remove(key)
appGroupProfile = nil
appGroupStatusMessage = "✓ App Group file deleted"
} catch {
appGroupStatusMessage = "Error: \(error.localizedDescription)"
}
isLoading = false
}
}
} }
#Preview { #Preview {

View File

@ -11,6 +11,7 @@ import LocalData
struct UserDefaultsDemo: View { struct UserDefaultsDemo: View {
@State private var inputText = "" @State private var inputText = ""
@State private var storedValue = "" @State private var storedValue = ""
@State private var appGroupStoredValue = ""
@State private var statusMessage = "" @State private var statusMessage = ""
@State private var isLoading = false @State private var isLoading = false
@FocusState private var isInputFocused: Bool @FocusState private var isInputFocused: Bool
@ -66,6 +67,46 @@ struct UserDefaultsDemo: View {
.foregroundStyle(.red) .foregroundStyle(.red)
.disabled(isLoading) .disabled(isLoading)
} }
Section("App Group UserDefaults") {
Text("Requires App Group entitlement and matching identifier in AppGroupConfiguration.")
.font(.caption)
.foregroundStyle(.secondary)
Button(action: saveAppGroupValue) {
HStack {
Image(systemName: "square.and.arrow.down")
Text("Save to App Group")
}
}
.disabled(inputText.isEmpty || isLoading)
Button(action: loadAppGroupValue) {
HStack {
Image(systemName: "arrow.down.circle")
Text("Load from App Group")
}
}
.disabled(isLoading)
Button(action: removeAppGroupValue) {
HStack {
Image(systemName: "trash")
Text("Remove from App Group")
}
}
.foregroundStyle(.red)
.disabled(isLoading)
if !appGroupStoredValue.isEmpty {
HStack {
Text("App Group Stored:")
Spacer()
Text(appGroupStoredValue)
.foregroundStyle(.blue)
}
}
}
if !statusMessage.isEmpty { if !statusMessage.isEmpty {
Section { Section {
@ -140,6 +181,52 @@ struct UserDefaultsDemo: View {
isLoading = false isLoading = false
} }
} }
private func saveAppGroupValue() {
isLoading = true
Task {
do {
let key = StorageKeys.AppGroupUserDefaultsKey()
try await StorageRouter.shared.set(inputText, for: key)
statusMessage = "✓ App Group saved successfully"
} catch {
statusMessage = "Error: \(error.localizedDescription)"
}
isLoading = false
}
}
private func loadAppGroupValue() {
isLoading = true
Task {
do {
let key = StorageKeys.AppGroupUserDefaultsKey()
appGroupStoredValue = try await StorageRouter.shared.get(key)
statusMessage = "✓ App Group loaded successfully"
} catch StorageError.notFound {
appGroupStoredValue = ""
statusMessage = "No App Group value stored yet"
} catch {
statusMessage = "Error: \(error.localizedDescription)"
}
isLoading = false
}
}
private func removeAppGroupValue() {
isLoading = true
Task {
do {
let key = StorageKeys.AppGroupUserDefaultsKey()
try await StorageRouter.shared.remove(key)
appGroupStoredValue = ""
statusMessage = "✓ App Group value removed"
} catch {
statusMessage = "Error: \(error.localizedDescription)"
}
isLoading = false
}
}
} }
#Preview { #Preview {