Add AppGroupConfiguration, StorageKeys, AppGroupUserDefaultsKey, Value (+7 more); Remove AppStorageCatalog
This commit is contained in:
parent
3ef0bdfa34
commit
93eaaa353a
@ -27,6 +27,7 @@ The project also includes a watchOS companion app target for watch-specific demo
|
||||
1. Open `SecureStorgageSample.xcodeproj`
|
||||
2. Select an iOS simulator or device
|
||||
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
|
||||
|
||||
@ -49,6 +50,7 @@ SecureStorgageSample/
|
||||
│ ├── Keychain/
|
||||
│ ├── FileSystem/
|
||||
│ ├── EncryptedFileSystem/
|
||||
│ ├── AppGroup/
|
||||
│ └── Platform/
|
||||
├── WatchOptimized.swift # Watch data models
|
||||
├── Services/
|
||||
@ -91,6 +93,11 @@ The app demonstrates various storage configurations:
|
||||
- Caches directory (can be purged)
|
||||
- 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
|
||||
- AES-256-GCM or ChaCha20-Poly1305 encryption
|
||||
- PBKDF2 or HKDF key derivation
|
||||
|
||||
5
SecureStorgageSample/Models/AppGroupConfiguration.swift
Normal file
5
SecureStorgageSample/Models/AppGroupConfiguration.swift
Normal file
@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
enum AppGroupConfiguration {
|
||||
static let identifier = "group.com.example.securestorage"
|
||||
}
|
||||
@ -3,7 +3,7 @@ import LocalData
|
||||
import SharedKit
|
||||
|
||||
|
||||
struct AppStorageCatalog: StorageKeyCatalog {
|
||||
nonisolated struct AppStorageCatalog: StorageKeyCatalog {
|
||||
static var allKeys: [AnyStorageKey] {
|
||||
[
|
||||
.key(StorageKeys.AppVersionKey()),
|
||||
@ -19,7 +19,9 @@ struct AppStorageCatalog: StorageKeyCatalog {
|
||||
.key(StorageKeys.ExternalSessionLogsKey()),
|
||||
.key(StorageKeys.WatchVibrationKey()),
|
||||
.key(StorageKeys.SyncableSettingKey()),
|
||||
.key(StorageKeys.ExternalKeyMaterialKey())
|
||||
.key(StorageKeys.ExternalKeyMaterialKey()),
|
||||
.key(StorageKeys.AppGroupUserDefaultsKey()),
|
||||
.key(StorageKeys.AppGroupUserProfileKey())
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,9 @@ struct FileSystemDemo: View {
|
||||
@State private var profileEmail = ""
|
||||
@State private var profileAge = ""
|
||||
@State private var storedProfile: UserProfile?
|
||||
@State private var appGroupProfile: UserProfile?
|
||||
@State private var statusMessage = ""
|
||||
@State private var appGroupStatusMessage = ""
|
||||
@State private var isLoading = false
|
||||
@State private var selectedDirectory: FileDirectory = .documents
|
||||
@FocusState private var isFieldFocused: Bool
|
||||
@ -79,6 +81,37 @@ struct FileSystemDemo: View {
|
||||
.foregroundStyle(.red)
|
||||
.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 {
|
||||
Section("Retrieved Profile") {
|
||||
@ -88,6 +121,15 @@ struct FileSystemDemo: View {
|
||||
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 {
|
||||
Section {
|
||||
@ -96,6 +138,14 @@ struct FileSystemDemo: View {
|
||||
.foregroundStyle(statusMessage.contains("Error") ? .red : .green)
|
||||
}
|
||||
}
|
||||
|
||||
if !appGroupStatusMessage.isEmpty {
|
||||
Section {
|
||||
Text(appGroupStatusMessage)
|
||||
.font(.caption)
|
||||
.foregroundStyle(appGroupStatusMessage.contains("Error") ? .red : .green)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Key Configuration") {
|
||||
LabeledContent("Domain", value: "File System")
|
||||
@ -167,6 +217,58 @@ struct FileSystemDemo: View {
|
||||
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 {
|
||||
|
||||
@ -11,6 +11,7 @@ import LocalData
|
||||
struct UserDefaultsDemo: View {
|
||||
@State private var inputText = ""
|
||||
@State private var storedValue = ""
|
||||
@State private var appGroupStoredValue = ""
|
||||
@State private var statusMessage = ""
|
||||
@State private var isLoading = false
|
||||
@FocusState private var isInputFocused: Bool
|
||||
@ -66,6 +67,46 @@ struct UserDefaultsDemo: View {
|
||||
.foregroundStyle(.red)
|
||||
.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 {
|
||||
Section {
|
||||
@ -140,6 +181,52 @@ struct UserDefaultsDemo: View {
|
||||
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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user