Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-14 08:51:34 -06:00
parent a1f84c4d3e
commit 6742e63105
18 changed files with 778 additions and 21 deletions

View File

@ -14,9 +14,12 @@ This app provides interactive demos for all LocalData storage options:
| **Encrypted** | Encrypted logs (AES or ChaCha20) | Encrypted File System | | **Encrypted** | Encrypted logs (AES or ChaCha20) | Encrypted File System |
| **Sync** | Platform availability & sync policies | Multiple | | **Sync** | Platform availability & sync policies | Multiple |
The project also includes a watchOS companion app target for watch-specific demos.
## Requirements ## Requirements
- iOS 17.0+ - iOS 17.0+
- watchOS 10.0+ (companion app target)
- Xcode 15+ - Xcode 15+
## Getting Started ## Getting Started
@ -38,6 +41,11 @@ SecureStorgageSample/
├── FileSystemDemo.swift ├── FileSystemDemo.swift
├── EncryptedStorageDemo.swift ├── EncryptedStorageDemo.swift
└── PlatformSyncDemo.swift └── PlatformSyncDemo.swift
SecureStorageSample Watch App/
├── SecureStorageSampleApp.swift
├── ContentView.swift
└── Services/
└── WatchConnectivityService.swift
``` ```
## Storage Key Examples ## Storage Key Examples

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,45 @@
//
// ContentView.swift
// SecureStorageSample Watch App
//
// Created by Matt Bruce on 1/14/26.
//
import SwiftUI
struct ContentView: View {
@State private var connectivity = WatchConnectivityService.shared
var body: some View {
VStack {
if let profile = connectivity.profile {
Text(profile.name)
.bold()
Text(profile.email)
.font(.caption)
.foregroundStyle(.secondary)
Text("Age: \(profile.ageDescription)")
.font(.caption2)
.foregroundStyle(.secondary)
Text(profile.createdAt, format: .dateTime)
.font(.caption2)
.foregroundStyle(.secondary)
} else {
Text("No profile synced yet")
.font(.caption)
.foregroundStyle(.secondary)
}
if !connectivity.statusMessage.isEmpty {
Text(connectivity.statusMessage)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
.padding()
}
}
#Preview {
ContentView()
}

View File

@ -0,0 +1,15 @@
import Foundation
nonisolated(unsafe)
struct UserProfile: Codable, Sendable {
static let storageKeyName = "user_profile.json"
let name: String
let email: String
let age: Int?
let createdAt: Date
var ageDescription: String {
age.map(String.init) ?? "n/a"
}
}

View File

@ -0,0 +1,21 @@
//
// SecureStorageSampleApp.swift
// SecureStorageSample Watch App
//
// Created by Matt Bruce on 1/14/26.
//
import SwiftUI
@main
struct SecureStorageSample_Watch_AppApp: App {
init() {
_ = WatchConnectivityService.shared
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

View File

@ -0,0 +1,59 @@
import Foundation
import Observation
import WatchConnectivity
@MainActor
@Observable
final class WatchConnectivityService: NSObject, WCSessionDelegate {
static let shared = WatchConnectivityService()
private(set) var profile: UserProfile?
private(set) var statusMessage: String = ""
private override init() {
super.init()
activateIfSupported()
loadCurrentContext()
}
private func activateIfSupported() {
guard WCSession.isSupported() else {
statusMessage = "WatchConnectivity not supported"
return
}
let session = WCSession.default
session.delegate = self
session.activate()
}
private func loadCurrentContext() {
guard WCSession.isSupported() else { return }
updateProfile(from: WCSession.default.applicationContext)
}
private func updateProfile(from context: [String: Any]) {
guard let data = context[UserProfile.storageKeyName] as? Data else {
statusMessage = "No profile received"
return
}
do {
profile = try JSONDecoder().decode(UserProfile.self, from: data)
statusMessage = "Profile synced"
} catch {
statusMessage = "Failed to decode profile"
}
}
func session(
_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?
) {
loadCurrentContext()
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
updateProfile(from: applicationContext)
}
}

View File

@ -0,0 +1,17 @@
//
// SecureStorageSample_Watch_AppTests.swift
// SecureStorageSample Watch AppTests
//
// Created by Matt Bruce on 1/14/26.
//
import Testing
@testable import SecureStorageSample_Watch_App
struct SecureStorageSample_Watch_AppTests {
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
}
}

View File

@ -0,0 +1,41 @@
//
// SecureStorageSample_Watch_AppUITests.swift
// SecureStorageSample Watch AppUITests
//
// Created by Matt Bruce on 1/14/26.
//
import XCTest
final class SecureStorageSample_Watch_AppUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
@MainActor
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
@MainActor
func testLaunchPerformance() throws {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}

View File

@ -0,0 +1,33 @@
//
// SecureStorageSample_Watch_AppUITestsLaunchTests.swift
// SecureStorageSample Watch AppUITests
//
// Created by Matt Bruce on 1/14/26.
//
import XCTest
final class SecureStorageSample_Watch_AppUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
@MainActor
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}

View File

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
EA179D562F17379800B1D54A /* LocalData in Frameworks */ = {isa = PBXBuildFile; productRef = EA179D552F17379800B1D54A /* LocalData */; }; EA179D562F17379800B1D54A /* LocalData in Frameworks */ = {isa = PBXBuildFile; productRef = EA179D552F17379800B1D54A /* LocalData */; };
EA65D70D2F17DDEB00C48466 /* SecureStorageSample Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = EA65D6E52F17DD6700C48466 /* SecureStorageSample Watch App.app */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -25,12 +26,50 @@
remoteGlobalIDString = EA179D002F1722BB00B1D54A; remoteGlobalIDString = EA179D002F1722BB00B1D54A;
remoteInfo = SecureStorgageSample; remoteInfo = SecureStorgageSample;
}; };
EA65D6F22F17DD6800C48466 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = EA179CF92F1722BB00B1D54A /* Project object */;
proxyType = 1;
remoteGlobalIDString = EA65D6E42F17DD6700C48466;
remoteInfo = "SecureStorageSample Watch App";
};
EA65D6FC2F17DD6800C48466 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = EA179CF92F1722BB00B1D54A /* Project object */;
proxyType = 1;
remoteGlobalIDString = EA65D6E42F17DD6700C48466;
remoteInfo = "SecureStorageSample Watch App";
};
EA65D70E2F17DDEB00C48466 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = EA179CF92F1722BB00B1D54A /* Project object */;
proxyType = 1;
remoteGlobalIDString = EA65D6E42F17DD6700C48466;
remoteInfo = "SecureStorageSample Watch App";
};
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
EA179D312F1722BC00B1D54A /* Embed Watch Content */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
dstSubfolderSpec = 16;
files = (
EA65D70D2F17DDEB00C48466 /* SecureStorageSample Watch App.app in Embed Watch Content */,
);
name = "Embed Watch Content";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
EA179D012F1722BB00B1D54A /* SecureStorgageSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SecureStorgageSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; EA179D012F1722BB00B1D54A /* SecureStorgageSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SecureStorgageSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
EA179D0E2F1722BC00B1D54A /* SecureStorgageSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecureStorgageSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EA179D0E2F1722BC00B1D54A /* SecureStorgageSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecureStorgageSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EA179D182F1722BC00B1D54A /* SecureStorgageSampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecureStorgageSampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EA179D182F1722BC00B1D54A /* SecureStorgageSampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecureStorgageSampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EA65D6E52F17DD6700C48466 /* SecureStorageSample Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SecureStorageSample Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
EA65D6F12F17DD6800C48466 /* SecureStorageSample Watch AppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SecureStorageSample Watch AppTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
EA65D6FB2F17DD6800C48466 /* SecureStorageSample Watch AppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SecureStorageSample Watch AppUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
@ -49,6 +88,21 @@
path = SecureStorgageSampleUITests; path = SecureStorgageSampleUITests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
EA65D6E62F17DD6700C48466 /* SecureStorageSample Watch App */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = "SecureStorageSample Watch App";
sourceTree = "<group>";
};
EA65D6F42F17DD6800C48466 /* SecureStorageSample Watch AppTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = "SecureStorageSample Watch AppTests";
sourceTree = "<group>";
};
EA65D6FE2F17DD6800C48466 /* SecureStorageSample Watch AppUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = "SecureStorageSample Watch AppUITests";
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */ /* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -74,6 +128,27 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
EA65D6E22F17DD6700C48466 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EA65D6EE2F17DD6800C48466 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EA65D6F82F17DD6800C48466 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
@ -83,6 +158,10 @@
EA179D032F1722BB00B1D54A /* SecureStorgageSample */, EA179D032F1722BB00B1D54A /* SecureStorgageSample */,
EA179D112F1722BC00B1D54A /* SecureStorgageSampleTests */, EA179D112F1722BC00B1D54A /* SecureStorgageSampleTests */,
EA179D1B2F1722BC00B1D54A /* SecureStorgageSampleUITests */, EA179D1B2F1722BC00B1D54A /* SecureStorgageSampleUITests */,
EA65D6E62F17DD6700C48466 /* SecureStorageSample Watch App */,
EA65D6F42F17DD6800C48466 /* SecureStorageSample Watch AppTests */,
EA65D6FE2F17DD6800C48466 /* SecureStorageSample Watch AppUITests */,
EA65D70C2F17DDEB00C48466 /* Frameworks */,
EA179D022F1722BB00B1D54A /* Products */, EA179D022F1722BB00B1D54A /* Products */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
@ -93,10 +172,20 @@
EA179D012F1722BB00B1D54A /* SecureStorgageSample.app */, EA179D012F1722BB00B1D54A /* SecureStorgageSample.app */,
EA179D0E2F1722BC00B1D54A /* SecureStorgageSampleTests.xctest */, EA179D0E2F1722BC00B1D54A /* SecureStorgageSampleTests.xctest */,
EA179D182F1722BC00B1D54A /* SecureStorgageSampleUITests.xctest */, EA179D182F1722BC00B1D54A /* SecureStorgageSampleUITests.xctest */,
EA65D6E52F17DD6700C48466 /* SecureStorageSample Watch App.app */,
EA65D6F12F17DD6800C48466 /* SecureStorageSample Watch AppTests.xctest */,
EA65D6FB2F17DD6800C48466 /* SecureStorageSample Watch AppUITests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
EA65D70C2F17DDEB00C48466 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -107,10 +196,12 @@
EA179CFD2F1722BB00B1D54A /* Sources */, EA179CFD2F1722BB00B1D54A /* Sources */,
EA179CFE2F1722BB00B1D54A /* Frameworks */, EA179CFE2F1722BB00B1D54A /* Frameworks */,
EA179CFF2F1722BB00B1D54A /* Resources */, EA179CFF2F1722BB00B1D54A /* Resources */,
EA179D312F1722BC00B1D54A /* Embed Watch Content */,
); );
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
EA65D70F2F17DDEB00C48466 /* PBXTargetDependency */,
); );
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
EA179D032F1722BB00B1D54A /* SecureStorgageSample */, EA179D032F1722BB00B1D54A /* SecureStorgageSample */,
@ -169,6 +260,74 @@
productReference = EA179D182F1722BC00B1D54A /* SecureStorgageSampleUITests.xctest */; productReference = EA179D182F1722BC00B1D54A /* SecureStorgageSampleUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing"; productType = "com.apple.product-type.bundle.ui-testing";
}; };
EA65D6E42F17DD6700C48466 /* SecureStorageSample Watch App */ = {
isa = PBXNativeTarget;
buildConfigurationList = EA65D7032F17DD6800C48466 /* Build configuration list for PBXNativeTarget "SecureStorageSample Watch App" */;
buildPhases = (
EA65D6E12F17DD6700C48466 /* Sources */,
EA65D6E22F17DD6700C48466 /* Frameworks */,
EA65D6E32F17DD6700C48466 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
EA65D6E62F17DD6700C48466 /* SecureStorageSample Watch App */,
);
name = "SecureStorageSample Watch App";
packageProductDependencies = (
);
productName = "SecureStorageSample Watch App";
productReference = EA65D6E52F17DD6700C48466 /* SecureStorageSample Watch App.app */;
productType = "com.apple.product-type.application";
};
EA65D6F02F17DD6800C48466 /* SecureStorageSample Watch AppTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = EA65D7062F17DD6800C48466 /* Build configuration list for PBXNativeTarget "SecureStorageSample Watch AppTests" */;
buildPhases = (
EA65D6ED2F17DD6800C48466 /* Sources */,
EA65D6EE2F17DD6800C48466 /* Frameworks */,
EA65D6EF2F17DD6800C48466 /* Resources */,
);
buildRules = (
);
dependencies = (
EA65D6F32F17DD6800C48466 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EA65D6F42F17DD6800C48466 /* SecureStorageSample Watch AppTests */,
);
name = "SecureStorageSample Watch AppTests";
packageProductDependencies = (
);
productName = "SecureStorageSample Watch AppTests";
productReference = EA65D6F12F17DD6800C48466 /* SecureStorageSample Watch AppTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
EA65D6FA2F17DD6800C48466 /* SecureStorageSample Watch AppUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = EA65D7092F17DD6800C48466 /* Build configuration list for PBXNativeTarget "SecureStorageSample Watch AppUITests" */;
buildPhases = (
EA65D6F72F17DD6800C48466 /* Sources */,
EA65D6F82F17DD6800C48466 /* Frameworks */,
EA65D6F92F17DD6800C48466 /* Resources */,
);
buildRules = (
);
dependencies = (
EA65D6FD2F17DD6800C48466 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EA65D6FE2F17DD6800C48466 /* SecureStorageSample Watch AppUITests */,
);
name = "SecureStorageSample Watch AppUITests";
packageProductDependencies = (
);
productName = "SecureStorageSample Watch AppUITests";
productReference = EA65D6FB2F17DD6800C48466 /* SecureStorageSample Watch AppUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
/* Begin PBXProject section */ /* Begin PBXProject section */
@ -190,6 +349,17 @@
CreatedOnToolsVersion = 26.0; CreatedOnToolsVersion = 26.0;
TestTargetID = EA179D002F1722BB00B1D54A; TestTargetID = EA179D002F1722BB00B1D54A;
}; };
EA65D6E42F17DD6700C48466 = {
CreatedOnToolsVersion = 26.0;
};
EA65D6F02F17DD6800C48466 = {
CreatedOnToolsVersion = 26.0;
TestTargetID = EA65D6E42F17DD6700C48466;
};
EA65D6FA2F17DD6800C48466 = {
CreatedOnToolsVersion = 26.0;
TestTargetID = EA65D6E42F17DD6700C48466;
};
}; };
}; };
buildConfigurationList = EA179CFC2F1722BB00B1D54A /* Build configuration list for PBXProject "SecureStorgageSample" */; buildConfigurationList = EA179CFC2F1722BB00B1D54A /* Build configuration list for PBXProject "SecureStorgageSample" */;
@ -212,6 +382,9 @@
EA179D002F1722BB00B1D54A /* SecureStorgageSample */, EA179D002F1722BB00B1D54A /* SecureStorgageSample */,
EA179D0D2F1722BC00B1D54A /* SecureStorgageSampleTests */, EA179D0D2F1722BC00B1D54A /* SecureStorgageSampleTests */,
EA179D172F1722BC00B1D54A /* SecureStorgageSampleUITests */, EA179D172F1722BC00B1D54A /* SecureStorgageSampleUITests */,
EA65D6E42F17DD6700C48466 /* SecureStorageSample Watch App */,
EA65D6F02F17DD6800C48466 /* SecureStorageSample Watch AppTests */,
EA65D6FA2F17DD6800C48466 /* SecureStorageSample Watch AppUITests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@ -238,6 +411,27 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
EA65D6E32F17DD6700C48466 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EA65D6EF2F17DD6800C48466 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EA65D6F92F17DD6800C48466 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@ -262,6 +456,27 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
EA65D6E12F17DD6700C48466 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EA65D6ED2F17DD6800C48466 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EA65D6F72F17DD6800C48466 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
@ -275,6 +490,21 @@
target = EA179D002F1722BB00B1D54A /* SecureStorgageSample */; target = EA179D002F1722BB00B1D54A /* SecureStorgageSample */;
targetProxy = EA179D192F1722BC00B1D54A /* PBXContainerItemProxy */; targetProxy = EA179D192F1722BC00B1D54A /* PBXContainerItemProxy */;
}; };
EA65D6F32F17DD6800C48466 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = EA65D6E42F17DD6700C48466 /* SecureStorageSample Watch App */;
targetProxy = EA65D6F22F17DD6800C48466 /* PBXContainerItemProxy */;
};
EA65D6FD2F17DD6800C48466 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = EA65D6E42F17DD6700C48466 /* SecureStorageSample Watch App */;
targetProxy = EA65D6FC2F17DD6800C48466 /* PBXContainerItemProxy */;
};
EA65D70F2F17DDEB00C48466 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = EA65D6E42F17DD6700C48466 /* SecureStorageSample Watch App */;
targetProxy = EA65D70E2F17DDEB00C48466 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
@ -543,6 +773,162 @@
}; };
name = Release; name = Release;
}; };
EA65D7042F17DD6800C48466 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = SecureStorageSample;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.mbrucedogs.SecureStorgageSample;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorgageSample.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Debug;
};
EA65D7052F17DD6800C48466 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = SecureStorageSample;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.mbrucedogs.SecureStorgageSample;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorgageSample.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Release;
};
EA65D7072F17DD6800C48466 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.mbrucedogs.SecureStorageSample-Watch-AppTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SecureStorageSample Watch App.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SecureStorageSample Watch App";
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Debug;
};
EA65D7082F17DD6800C48466 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.mbrucedogs.SecureStorageSample-Watch-AppTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SecureStorageSample Watch App.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SecureStorageSample Watch App";
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Release;
};
EA65D70A2F17DD6800C48466 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.mbrucedogs.SecureStorageSample-Watch-AppUITests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
TEST_TARGET_NAME = "SecureStorageSample Watch App";
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Debug;
};
EA65D70B2F17DD6800C48466 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.mbrucedogs.SecureStorageSample-Watch-AppUITests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
TEST_TARGET_NAME = "SecureStorageSample Watch App";
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Release;
};
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
@ -582,6 +968,33 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
EA65D7032F17DD6800C48466 /* Build configuration list for PBXNativeTarget "SecureStorageSample Watch App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EA65D7042F17DD6800C48466 /* Debug */,
EA65D7052F17DD6800C48466 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
EA65D7062F17DD6800C48466 /* Build configuration list for PBXNativeTarget "SecureStorageSample Watch AppTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EA65D7072F17DD6800C48466 /* Debug */,
EA65D7082F17DD6800C48466 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
EA65D7092F17DD6800C48466 /* Build configuration list for PBXNativeTarget "SecureStorageSample Watch AppUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EA65D70A2F17DD6800C48466 /* Debug */,
EA65D70B2F17DD6800C48466 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */ /* Begin XCLocalSwiftPackageReference section */

View File

@ -4,10 +4,25 @@
<dict> <dict>
<key>SchemeUserState</key> <key>SchemeUserState</key>
<dict> <dict>
<key>SecureStorageSample Watch App.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>SecureStorageWatch Watch App.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>SecureStorgageSample.xcscheme_^#shared#^_</key> <key>SecureStorgageSample.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>1</integer>
</dict>
<key>SecureStorgageSampleWatch.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
</dict> </dict>
</dict> </dict>
</dict> </dict>

View File

@ -0,0 +1,15 @@
import Foundation
nonisolated(unsafe)
struct UserProfile: Codable, Sendable {
static let storageKeyName = "user_profile.json"
let name: String
let email: String
let age: Int?
let createdAt: Date
var ageDescription: String {
age.map(String.init) ?? "n/a"
}
}

View File

@ -9,6 +9,10 @@ import SwiftUI
@main @main
struct SecureStorgageSampleApp: App { struct SecureStorgageSampleApp: App {
init() {
_ = WatchConnectivityService.shared
}
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() ContentView()

View File

@ -0,0 +1,35 @@
import Foundation
import WatchConnectivity
@MainActor
final class WatchConnectivityService: NSObject, WCSessionDelegate {
static let shared = WatchConnectivityService()
private override init() {
super.init()
activateIfSupported()
}
private func activateIfSupported() {
guard WCSession.isSupported() else { return }
let session = WCSession.default
session.delegate = self
session.activate()
}
func session(
_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?
) {
// Intentionally empty: activation state is handled by WCSession.
}
func sessionDidBecomeInactive(_ session: WCSession) {
// No-op; required for iOS WCSessionDelegate.
}
func sessionDidDeactivate(_ session: WCSession) {
session.activate()
}
}

View File

@ -123,15 +123,19 @@ extension StorageKeys {
/// Stores user profile as JSON file in documents. /// Stores user profile as JSON file in documents.
struct UserProfileFileKey: StorageKey { struct UserProfileFileKey: StorageKey {
typealias Value = [String: AnyCodable] typealias Value = UserProfile
let name = "user_profile.json" let name = UserProfile.storageKeyName
let domain: StorageDomain = .fileSystem(directory: .documents) let domain: StorageDomain
let security: SecurityPolicy = .none let security: SecurityPolicy = .none
let serializer: Serializer<[String: AnyCodable]> = .json let serializer: Serializer<UserProfile> = .json
let owner = "SampleApp" let owner = "SampleApp"
let availability: PlatformAvailability = .phoneOnly let availability: PlatformAvailability = .phoneWithWatchSync
let syncPolicy: SyncPolicy = .never let syncPolicy: SyncPolicy = .automaticSmall
init(directory: FileDirectory = .documents) {
self.domain = .fileSystem(directory: directory)
}
} }
/// Stores cached data files. /// Stores cached data files.
@ -264,3 +268,4 @@ extension StorageKeys {
let syncPolicy: SyncPolicy = .never let syncPolicy: SyncPolicy = .never
} }
} }

View File

@ -12,7 +12,7 @@ struct FileSystemDemo: View {
@State private var profileName = "" @State private var profileName = ""
@State private var profileEmail = "" @State private var profileEmail = ""
@State private var profileAge = "" @State private var profileAge = ""
@State private var storedProfile: [String: AnyCodable]? @State private var storedProfile: UserProfile?
@State private var statusMessage = "" @State private var statusMessage = ""
@State private var isLoading = false @State private var isLoading = false
@State private var selectedDirectory: FileDirectory = .documents @State private var selectedDirectory: FileDirectory = .documents
@ -81,9 +81,10 @@ struct FileSystemDemo: View {
if let profile = storedProfile { if let profile = storedProfile {
Section("Retrieved Profile") { Section("Retrieved Profile") {
ForEach(Array(profile.keys.sorted()), id: \.self) { key in LabeledContent("Name", value: profile.name)
LabeledContent(key.capitalized, value: String(describing: profile[key]?.value ?? "nil")) LabeledContent("Email", value: profile.email)
} LabeledContent("Age", value: profile.ageDescription)
LabeledContent("Created", value: profile.createdAt.formatted(date: .abbreviated, time: .shortened))
} }
} }
@ -99,7 +100,7 @@ struct FileSystemDemo: View {
LabeledContent("Domain", value: "File System") LabeledContent("Domain", value: "File System")
LabeledContent("Security", value: "None") LabeledContent("Security", value: "None")
LabeledContent("Serializer", value: "JSON") LabeledContent("Serializer", value: "JSON")
LabeledContent("Platform", value: "Phone Only") LabeledContent("Platform", value: "Phone + Watch Sync")
} }
} }
.navigationTitle("File System") .navigationTitle("File System")
@ -118,13 +119,13 @@ struct FileSystemDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKeys.UserProfileFileKey() let key = StorageKeys.UserProfileFileKey(directory: selectedDirectory)
let profile: [String: AnyCodable] = [ let profile = UserProfile(
"name": AnyCodable(profileName), name: profileName,
"email": AnyCodable(profileEmail), email: profileEmail,
"age": AnyCodable(Int(profileAge) ?? 0), age: Int(profileAge),
"createdAt": AnyCodable(Date().ISO8601Format()) createdAt: Date()
] )
try await StorageRouter.shared.set(profile, for: key) try await StorageRouter.shared.set(profile, for: key)
statusMessage = "✓ Saved to \(selectedDirectory == .documents ? "Documents" : "Caches")" statusMessage = "✓ Saved to \(selectedDirectory == .documents ? "Documents" : "Caches")"
} catch { } catch {
@ -138,7 +139,7 @@ struct FileSystemDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKeys.UserProfileFileKey() let key = StorageKeys.UserProfileFileKey(directory: selectedDirectory)
storedProfile = try await StorageRouter.shared.get(key) storedProfile = try await StorageRouter.shared.get(key)
statusMessage = "✓ Loaded from file system" statusMessage = "✓ Loaded from file system"
} catch StorageError.notFound { } catch StorageError.notFound {
@ -155,7 +156,7 @@ struct FileSystemDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKeys.UserProfileFileKey() let key = StorageKeys.UserProfileFileKey(directory: selectedDirectory)
try await StorageRouter.shared.remove(key) try await StorageRouter.shared.remove(key)
storedProfile = nil storedProfile = nil
statusMessage = "✓ File deleted" statusMessage = "✓ File deleted"