diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..459bed7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Custom +.swiftpm/ + +## User settings +xcuserdata/ + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output diff --git a/Example/KeychainStorageExample/KeychainStorageExample.xcodeproj/project.pbxproj b/Example/KeychainStorageExample/KeychainStorageExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a765e94 --- /dev/null +++ b/Example/KeychainStorageExample/KeychainStorageExample.xcodeproj/project.pbxproj @@ -0,0 +1,362 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + FBC1310F2CE6382000E294FA /* KeychainStorageKit in Frameworks */ = {isa = PBXBuildFile; productRef = FBC1310E2CE6382000E294FA /* KeychainStorageKit */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + FB8F4CB12CE5798000254FC4 /* KeychainStorageExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KeychainStorageExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + FB8F4CB32CE5798000254FC4 /* KeychainStorageExample */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = KeychainStorageExample; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + FB8F4CAE2CE5798000254FC4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FBC1310F2CE6382000E294FA /* KeychainStorageKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + FB8F4CA82CE5798000254FC4 = { + isa = PBXGroup; + children = ( + FB8F4CB32CE5798000254FC4 /* KeychainStorageExample */, + FB8F4CB22CE5798000254FC4 /* Products */, + ); + sourceTree = ""; + }; + FB8F4CB22CE5798000254FC4 /* Products */ = { + isa = PBXGroup; + children = ( + FB8F4CB12CE5798000254FC4 /* KeychainStorageExample.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + FB8F4CB02CE5798000254FC4 /* KeychainStorageExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = FB8F4CBF2CE5798200254FC4 /* Build configuration list for PBXNativeTarget "KeychainStorageExample" */; + buildPhases = ( + FB8F4CAD2CE5798000254FC4 /* Sources */, + FB8F4CAE2CE5798000254FC4 /* Frameworks */, + FB8F4CAF2CE5798000254FC4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + FB8F4CB32CE5798000254FC4 /* KeychainStorageExample */, + ); + name = KeychainStorageExample; + packageProductDependencies = ( + FBC1310E2CE6382000E294FA /* KeychainStorageKit */, + ); + productName = KeychainStorageExample; + productReference = FB8F4CB12CE5798000254FC4 /* KeychainStorageExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + FB8F4CA92CE5798000254FC4 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1610; + LastUpgradeCheck = 1610; + TargetAttributes = { + FB8F4CB02CE5798000254FC4 = { + CreatedOnToolsVersion = 16.1; + }; + }; + }; + buildConfigurationList = FB8F4CAC2CE5798000254FC4 /* Build configuration list for PBXProject "KeychainStorageExample" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = FB8F4CA82CE5798000254FC4; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + FBC1310D2CE6382000E294FA /* XCLocalSwiftPackageReference "../../../KeychainStorageKit" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = FB8F4CB22CE5798000254FC4 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + FB8F4CB02CE5798000254FC4 /* KeychainStorageExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + FB8F4CAF2CE5798000254FC4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + FB8F4CAD2CE5798000254FC4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + FB8F4CBD2CE5798200254FC4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + FB8F4CBE2CE5798200254FC4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + FB8F4CC02CE5798200254FC4 /* 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_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = 65UTX76JJY; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ltd.bracket.KeychainStorageExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + FB8F4CC12CE5798200254FC4 /* 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_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = 65UTX76JJY; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ltd.bracket.KeychainStorageExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + FB8F4CAC2CE5798000254FC4 /* Build configuration list for PBXProject "KeychainStorageExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FB8F4CBD2CE5798200254FC4 /* Debug */, + FB8F4CBE2CE5798200254FC4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FB8F4CBF2CE5798200254FC4 /* Build configuration list for PBXNativeTarget "KeychainStorageExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FB8F4CC02CE5798200254FC4 /* Debug */, + FB8F4CC12CE5798200254FC4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + FBC1310D2CE6382000E294FA /* XCLocalSwiftPackageReference "../../../KeychainStorageKit" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../../KeychainStorageKit; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + FBC1310E2CE6382000E294FA /* KeychainStorageKit */ = { + isa = XCSwiftPackageProductDependency; + productName = KeychainStorageKit; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = FB8F4CA92CE5798000254FC4 /* Project object */; +} diff --git a/Example/KeychainStorageExample/KeychainStorageExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/KeychainStorageExample/KeychainStorageExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Example/KeychainStorageExample/KeychainStorageExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Example/KeychainStorageExample/KeychainStorageExample/KeychainStorageExampleApp.swift b/Example/KeychainStorageExample/KeychainStorageExample/KeychainStorageExampleApp.swift new file mode 100644 index 0000000..d72f6c3 --- /dev/null +++ b/Example/KeychainStorageExample/KeychainStorageExample/KeychainStorageExampleApp.swift @@ -0,0 +1,72 @@ +import KeychainStorageKit +import SwiftUI + +// MARK: - API Client +struct FetchClient { + var fetch: @Sendable () async throws -> String + + static let live = FetchClient( + fetch: { + @KeychainStorage("token") var token: String? + let result = "Result from token: [\(token ?? "missing")]" // Use token + return result + } + ) + + static let preview = FetchClient( + fetch: { + try await Task.sleep(for: .seconds(2)) + return "Result for preview [no token]" + } + ) +} + +extension EnvironmentValues { + @Entry var fetchClient: FetchClient = .live +} + +// MARK: - View +struct ContentView: View { + @Environment(\.fetchClient) var client: FetchClient + @State var result: String? + + var body: some View { + VStack { + Text(result ?? "") + Button("Fetch") { + Task { await fetch() } + } + } + .task { await tokenRefresh() } + } + + private func fetch() async { + result = try? await client.fetch() + } + + private func tokenRefresh() async { + while !Task.isCancelled { + let newToken = String((0..<5).compactMap { _ in "ABCDEF1234567890".randomElement() }) + print("New token: [\(newToken)]") + @KeychainStorage("token") var token: String? + token = newToken // Set token + try? await Task.sleep(for: .seconds(5)) + } + } +} + +// MARK: - Preview +#Preview { + ContentView() + .environment(\.fetchClient, .live) // .environment(\.fetchClient, .preview) +} + +// MARK: - Entrypoint +@main +struct KeychainStorageExampleApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Images/logo.png b/Images/logo.png new file mode 100644 index 0000000..6149bdd Binary files /dev/null and b/Images/logo.png differ diff --git a/KeychainStorageKit.xcodeproj/project.pbxproj b/KeychainStorageKit.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b110496 --- /dev/null +++ b/KeychainStorageKit.xcodeproj/project.pbxproj @@ -0,0 +1,628 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + FBDB66D42CE536DD00FD2424 /* KeychainStorageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FBDB66CB2CE536DD00FD2424 /* KeychainStorageKit.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + FBDB66D52CE536DD00FD2424 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FBDB66C22CE536DD00FD2424 /* Project object */; + proxyType = 1; + remoteGlobalIDString = FBDB66CA2CE536DD00FD2424; + remoteInfo = KeychainStorage; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + FBDB66CB2CE536DD00FD2424 /* KeychainStorageKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = KeychainStorageKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FBDB66D32CE536DD00FD2424 /* KeychainStorageKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KeychainStorageKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FBDB66EC2CE5378100FD2424 /* TestHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestHost.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + FBDB66CD2CE536DD00FD2424 /* KeychainStorageKit */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = KeychainStorageKit; + sourceTree = ""; + }; + FBDB66D72CE536DD00FD2424 /* KeychainStorageKitTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = KeychainStorageKitTests; + sourceTree = ""; + }; + FBDB66ED2CE5378100FD2424 /* TestHost */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TestHost; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + FBDB66C82CE536DD00FD2424 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FBDB66D02CE536DD00FD2424 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FBDB66D42CE536DD00FD2424 /* KeychainStorageKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FBDB66E92CE5378100FD2424 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + FBDB66C12CE536DD00FD2424 = { + isa = PBXGroup; + children = ( + FBDB66CD2CE536DD00FD2424 /* KeychainStorageKit */, + FBDB66D72CE536DD00FD2424 /* KeychainStorageKitTests */, + FBDB66ED2CE5378100FD2424 /* TestHost */, + FBDB66CC2CE536DD00FD2424 /* Products */, + ); + sourceTree = ""; + }; + FBDB66CC2CE536DD00FD2424 /* Products */ = { + isa = PBXGroup; + children = ( + FBDB66CB2CE536DD00FD2424 /* KeychainStorageKit.framework */, + FBDB66D32CE536DD00FD2424 /* KeychainStorageKitTests.xctest */, + FBDB66EC2CE5378100FD2424 /* TestHost.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + FBDB66C62CE536DD00FD2424 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + FBDB66CA2CE536DD00FD2424 /* KeychainStorageKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = FBDB66DC2CE536DD00FD2424 /* Build configuration list for PBXNativeTarget "KeychainStorageKit" */; + buildPhases = ( + FBDB66C62CE536DD00FD2424 /* Headers */, + FBDB66C72CE536DD00FD2424 /* Sources */, + FBDB66C82CE536DD00FD2424 /* Frameworks */, + FBDB66C92CE536DD00FD2424 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + FBDB66CD2CE536DD00FD2424 /* KeychainStorageKit */, + ); + name = KeychainStorageKit; + packageProductDependencies = ( + ); + productName = KeychainStorage; + productReference = FBDB66CB2CE536DD00FD2424 /* KeychainStorageKit.framework */; + productType = "com.apple.product-type.framework"; + }; + FBDB66D22CE536DD00FD2424 /* KeychainStorageKitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = FBDB66E12CE536DD00FD2424 /* Build configuration list for PBXNativeTarget "KeychainStorageKitTests" */; + buildPhases = ( + FBDB66CF2CE536DD00FD2424 /* Sources */, + FBDB66D02CE536DD00FD2424 /* Frameworks */, + FBDB66D12CE536DD00FD2424 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + FBDB66D62CE536DD00FD2424 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + FBDB66D72CE536DD00FD2424 /* KeychainStorageKitTests */, + ); + name = KeychainStorageKitTests; + packageProductDependencies = ( + ); + productName = KeychainStorageTests; + productReference = FBDB66D32CE536DD00FD2424 /* KeychainStorageKitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + FBDB66EB2CE5378100FD2424 /* TestHost */ = { + isa = PBXNativeTarget; + buildConfigurationList = FBDB670D2CE5378300FD2424 /* Build configuration list for PBXNativeTarget "TestHost" */; + buildPhases = ( + FBDB66E82CE5378100FD2424 /* Sources */, + FBDB66E92CE5378100FD2424 /* Frameworks */, + FBDB66EA2CE5378100FD2424 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + FBDB66ED2CE5378100FD2424 /* TestHost */, + ); + name = TestHost; + packageProductDependencies = ( + ); + productName = TestHost; + productReference = FBDB66EC2CE5378100FD2424 /* TestHost.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + FBDB66C22CE536DD00FD2424 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1610; + LastUpgradeCheck = 1610; + TargetAttributes = { + FBDB66CA2CE536DD00FD2424 = { + CreatedOnToolsVersion = 16.1; + LastSwiftMigration = 1610; + }; + FBDB66D22CE536DD00FD2424 = { + CreatedOnToolsVersion = 16.1; + }; + FBDB66EB2CE5378100FD2424 = { + CreatedOnToolsVersion = 16.1; + }; + }; + }; + buildConfigurationList = FBDB66C52CE536DD00FD2424 /* Build configuration list for PBXProject "KeychainStorageKit" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = FBDB66C12CE536DD00FD2424; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = FBDB66CC2CE536DD00FD2424 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + FBDB66CA2CE536DD00FD2424 /* KeychainStorageKit */, + FBDB66D22CE536DD00FD2424 /* KeychainStorageKitTests */, + FBDB66EB2CE5378100FD2424 /* TestHost */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + FBDB66C92CE536DD00FD2424 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FBDB66D12CE536DD00FD2424 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FBDB66EA2CE5378100FD2424 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + FBDB66C72CE536DD00FD2424 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FBDB66CF2CE536DD00FD2424 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FBDB66E82CE5378100FD2424 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + FBDB66D62CE536DD00FD2424 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FBDB66CA2CE536DD00FD2424 /* KeychainStorageKit */; + targetProxy = FBDB66D52CE536DD00FD2424 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + FBDB66DD2CE536DD00FD2424 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = NO; + DEVELOPMENT_TEAM = 65UTX76JJY; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = ltd.bracket.KeychainStorage; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + FBDB66DE2CE536DD00FD2424 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = NO; + DEVELOPMENT_TEAM = 65UTX76JJY; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = ltd.bracket.KeychainStorage; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + FBDB66DF2CE536DD00FD2424 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + FBDB66E02CE536DD00FD2424 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + FBDB66E22CE536DD00FD2424 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 65UTX76JJY; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ltd.bracket.KeychainStorageTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestHost.app/TestHost"; + }; + name = Debug; + }; + FBDB66E32CE536DD00FD2424 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 65UTX76JJY; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ltd.bracket.KeychainStorageTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestHost.app/TestHost"; + }; + name = Release; + }; + FBDB670E2CE5378300FD2424 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TestHost/TestHost.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = 65UTX76JJY; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ltd.bracket.KeychainStorage.TestHost; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + FBDB670F2CE5378300FD2424 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TestHost/TestHost.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = 65UTX76JJY; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ltd.bracket.KeychainStorage.TestHost; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + FBDB66C52CE536DD00FD2424 /* Build configuration list for PBXProject "KeychainStorageKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FBDB66DF2CE536DD00FD2424 /* Debug */, + FBDB66E02CE536DD00FD2424 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FBDB66DC2CE536DD00FD2424 /* Build configuration list for PBXNativeTarget "KeychainStorageKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FBDB66DD2CE536DD00FD2424 /* Debug */, + FBDB66DE2CE536DD00FD2424 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FBDB66E12CE536DD00FD2424 /* Build configuration list for PBXNativeTarget "KeychainStorageKitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FBDB66E22CE536DD00FD2424 /* Debug */, + FBDB66E32CE536DD00FD2424 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FBDB670D2CE5378300FD2424 /* Build configuration list for PBXNativeTarget "TestHost" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FBDB670E2CE5378300FD2424 /* Debug */, + FBDB670F2CE5378300FD2424 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = FBDB66C22CE536DD00FD2424 /* Project object */; +} diff --git a/KeychainStorageKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/KeychainStorageKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/KeychainStorageKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/KeychainStorageKit.xcodeproj/xcshareddata/xcschemes/TestHost.xcscheme b/KeychainStorageKit.xcodeproj/xcshareddata/xcschemes/TestHost.xcscheme new file mode 100644 index 0000000..0b2da7d --- /dev/null +++ b/KeychainStorageKit.xcodeproj/xcshareddata/xcschemes/TestHost.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KeychainStorageKit/KeychainStorage.swift b/KeychainStorageKit/KeychainStorage.swift new file mode 100644 index 0000000..d64388f --- /dev/null +++ b/KeychainStorageKit/KeychainStorage.swift @@ -0,0 +1,92 @@ +/// BSD 2-Clause License +/// +/// Copyright (c) 2024, Max Humber +/// +/// Redistribution and use in source and binary forms, with or without +/// modification, are permitted provided that the following conditions are met: +/// +/// 1. Redistributions of source code must retain the above copyright notice, this +/// list of conditions and the following disclaimer. +/// +/// 2. Redistributions in binary form must reproduce the above copyright notice, +/// this list of conditions and the following disclaimer in the documentation +/// and/or other materials provided with the distribution. +/// +/// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +/// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +/// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +/// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +/// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +/// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +/// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +/// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +/// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +/// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import Foundation +import Security + +/// A property wrapper that provides convenient storage and retrieval of `Codable` types in the Keychain. +/// +/// Usage: +/// ```swift +/// @KeychainStorage("token") var token: String? +/// ``` +@propertyWrapper +public struct KeychainStorage { + public let key: String + + /// Initializes the `KeychainStorage` property wrapper with a specific key. + /// + /// - Parameter key: A `String` representing the Keychain key. + public init(_ key: String) { + self.key = key + } + + /// The wrapped value stored in or retrieved from the Keychain. + /// + /// When setting a new value, it will be saved to the Keychain. + /// When setting `nil`, the value will be removed from the Keychain. + /// When getting, it retrieves the value from the Keychain. + public var wrappedValue: T? { + get { get(for: key) } + nonmutating set { + if let newValue { + set(newValue, for: key) + } else { + remove(for: key) + } + } + } + + private func get(for key: String) -> T? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne + ] + var item: AnyObject? + guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, let data = item as? Data else { return nil } + return try? JSONDecoder().decode(T.self, from: data) + } + + private func set(_ value: T, for key: String) { + guard let data = try? JSONEncoder().encode(value) else { return } + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key, + kSecValueData as String: data + ] + SecItemDelete(query as CFDictionary) + SecItemAdd(query as CFDictionary, nil) + } + + private func remove(for key: String) { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key + ] + SecItemDelete(query as CFDictionary) + } +} diff --git a/KeychainStorageKitTests/KeychainStorageTests.swift b/KeychainStorageKitTests/KeychainStorageTests.swift new file mode 100644 index 0000000..e157e79 --- /dev/null +++ b/KeychainStorageKitTests/KeychainStorageTests.swift @@ -0,0 +1,83 @@ +import Foundation +import Testing +@testable import KeychainStorageKit + +struct KeychainStorageTests { + @Test func testString() async throws { + let raw = "xyz" + @KeychainStorage("string") var string: String? + string = raw // set + #expect(string == raw) // get + string = nil // remove + #expect(string == nil) + } + + @Test func testBool() async throws { + let raw = true + @KeychainStorage("bool") var bool: Bool? + bool = raw // set + #expect(bool == raw) // get + bool = nil // remove + #expect(bool == nil) + } + + @Test func testInt() async throws { + let raw = 1 + @KeychainStorage("int") var int: Int? + int = raw // set + #expect(int == raw) // get + int = nil // remove + #expect(int == nil) + } + + @Test func testDouble() async throws { + let raw = 1.0 + @KeychainStorage("double") var double: Double? + double = raw // set + #expect(double == raw) // get + double = nil // remove + #expect(double == nil) + } + + @Test func testURL() async throws { + let raw = URL(string: "https://example.com")! + @KeychainStorage("url") var url: URL? + url = raw // set + #expect(url == raw) // get + url = nil // remove + #expect(url == nil) + } + + @Test func testData() async throws { + let raw = Data([0x01, 0x02, 0x03]) + @KeychainStorage("data") var data: Data? + data = raw // set + #expect(data == raw) // get + data = nil // remove + #expect(data == nil) + } + + @Test func testRawRepresentableInt() async throws { + enum IntEnum: Int, Codable { + case one = 1 + } + let raw: IntEnum = .one + @KeychainStorage("intEnum") var intEnum: IntEnum? + intEnum = raw // set + #expect(intEnum == raw) // get + intEnum = nil // remove + #expect(intEnum == nil) + } + + @Test func testRawRepresentableString() async throws { + enum StringEnum: String, Codable { + case example = "example" + } + let raw: StringEnum = .example + @KeychainStorage("stringEnum") var stringEnum: StringEnum? + stringEnum = raw // set + #expect(stringEnum == raw) // get + stringEnum = nil // remove + #expect(stringEnum == nil) + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..830a629 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +BSD 2-Clause License + +Copyright (c) 2024, Max Humber + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2819812 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +# Makefile + +SIMULATOR_DEVICE = iPhone 16 +SIMULATOR_OS = latest +SCHEME = TestHost +TEST_TARGET = KeychainStorageKitTests + +test: + xcodebuild test \ + -scheme $(SCHEME) \ + -destination 'platform=iOS Simulator,name=$(SIMULATOR_DEVICE),OS=$(SIMULATOR_OS)' \ + -only-testing:$(TEST_TARGET) diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..404af3f --- /dev/null +++ b/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 5.10 +import PackageDescription + +let package = Package( + name: "KeychainStorageKit", + platforms: [ + .iOS(.v12), + .macOS(.v10_13), + .watchOS(.v4), + .tvOS(.v12) + ], + products: [ + .library( + name: "KeychainStorageKit", + targets: ["KeychainStorageKit"] + ) + ], + targets: [ + .target( + name: "KeychainStorageKit", + path: "KeychainStorageKit" + ) + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..081ff21 --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ + + +### KeychainStorageKit + +Contains a single property wrapper, **`@KeychainStorage`**, for conviently and securely storing sensitive data in the iOS Keychain. + +#### Features + +- Simple, declarative syntax similar to `@AppStorage` +- Minimalistic implementation—just a [single file](KeychainStorageKit/KeychainStorage.swift) (should you wish to copy-and-paste!) +- Compatible with Swift 6 and low deployment targets + +#### Supported Types + +- Basic Types: `String`, `Int`, `Double`, `Bool` +- Foundation Types: `URL`, `Data` +- Custom Types: Any custom type that that conforms to `Codable` + +#### Installation + +You can integrate `KeychainStorageKit` into your project using Swift Package Manager: + +1. In Xcode, select your project in the Project Navigator +2. Go to the Package Dependencies tab +3. Click the + button to add a package dependency +4. In the search bar, enter the repository URL for: `https://github.com/maxhumber/KeychainStorageKit` + +#### Usage + +The syntax of **`@KeychainStorage`** is familiar and feels like a close cousin of `@AppStorage`: + +```swift +import KeychainStorageKit + +func setToken(_ newToken: String) { + @KeychainStorage("authToken") var token: String? + token = newToken +} + +func getToken() -> String? { + @KeychainStorage("authToken") var token: String? + return token +} + +func removeToken() { + @KeychainStorage("authToken") var token: String? + token = nil +} +``` + +#### Usage within a SwiftUI App + +Here's how you might use the **`@KeychainStorage`** wrapper in a SwiftUI app: + +```swift +import KeychainStorageKit +import SwiftUI + +// MARK: - API Client +struct FetchClient { + var fetch: @Sendable () async throws -> String + + static let live = FetchClient( + fetch: { + @KeychainStorage("token") var token: String? + let result = "Result from token: [\(token ?? "missing")]" // Use token + return result + } + ) + + static let preview = FetchClient( + fetch: { + try await Task.sleep(for: .seconds(2)) + return "Result for preview [no token]" + } + ) +} + +extension EnvironmentValues { + @Entry var fetchClient: FetchClient = .live +} + +// MARK: - View +struct ContentView: View { + @Environment(\.fetchClient) var client: FetchClient + @State var result: String? + + var body: some View { + VStack { + Text(result ?? "") + Button("Fetch") { + Task { await fetch() } + } + } + .task { await tokenRefresh() } + } + + private func fetch() async { + result = try? await client.fetch() + } + + private func tokenRefresh() async { + while !Task.isCancelled { + let newToken = String((0..<5).compactMap { _ in "ABCDEF1234567890".randomElement() }) + print("New token: [\(newToken)]") + @KeychainStorage("token") var token: String? + token = newToken // Set token + try? await Task.sleep(for: .seconds(5)) + } + } +} + +// MARK: - Preview +#Preview { + ContentView() + .environment(\.fetchClient, .live) // .environment(\.fetchClient, .preview) +} + +// MARK: - Entrypoint +@main +struct KeychainStorageExampleApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} +``` + +#### Tests + +Due to Keychain access requirements, the tests for this package must run against a "TestHost" app (following [this tutorial](https://belief-driven-design.com/testing-swift-packages-with-a-test-host-56bf1/)). To run the tests: + +```zsh +make test +``` diff --git a/TestHost/TestHost.entitlements b/TestHost/TestHost.entitlements new file mode 100644 index 0000000..a2d4d14 --- /dev/null +++ b/TestHost/TestHost.entitlements @@ -0,0 +1,10 @@ + + + + + keychain-access-groups + + $(AppIdentifierPrefix)ltd.bracket.KeychainStorage.TestHost + + + diff --git a/TestHost/TestHostApp.swift b/TestHost/TestHostApp.swift new file mode 100644 index 0000000..15ad99c --- /dev/null +++ b/TestHost/TestHostApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct TestHostApp: App { + var body: some Scene { + WindowGroup { + Text("TestHostApp") + } + } +}