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`
|
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
|
||||||
|
|||||||
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
|
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())
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 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
|
||||||
@ -80,6 +82,37 @@ struct FileSystemDemo: View {
|
|||||||
.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") {
|
||||||
LabeledContent("Name", value: profile.name)
|
LabeledContent("Name", value: profile.name)
|
||||||
@ -89,6 +122,15 @@ struct FileSystemDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
Text(statusMessage)
|
Text(statusMessage)
|
||||||
@ -97,6 +139,14 @@ struct FileSystemDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
LabeledContent("Security", value: "None")
|
LabeledContent("Security", value: "None")
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
@ -67,6 +68,46 @@ struct UserDefaultsDemo: View {
|
|||||||
.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 {
|
||||||
Text(statusMessage)
|
Text(statusMessage)
|
||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user