diff --git a/0304-sqlite-pt4/README.md b/0304-sqlite-pt4/README.md new file mode 100644 index 00000000..37c69f28 --- /dev/null +++ b/0304-sqlite-pt4/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [SQLite: Observation](https://www.pointfree.co/episodes/ep304-sqlite-observation) +> +> We conclude our introductory series on SQLite by showing how to live update SwiftUI views powered by database using GRDB’s “value observation.” Along the way we will tackle quite a few Swift 6 concurrency issues, and we will tie things in a bow by showing how the SwiftUI environment can vastly simplify how our application is powered by SQLite. diff --git a/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations.xcodeproj/project.pbxproj b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ff7f95a0 --- /dev/null +++ b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations.xcodeproj/project.pbxproj @@ -0,0 +1,488 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 2AA9B3BD2CDAB9BD00BB83C0 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 2AA9B3BC2CDAB9BD00BB83C0 /* GRDB */; }; + 4BBE003F2CDBFB1800EBD6F0 /* IssueReporting in Frameworks */ = {isa = PBXBuildFile; productRef = 4BBE003E2CDBFB1800EBD6F0 /* IssueReporting */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2A2D2A8A2CD2BAD00071022C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2A2D2A712CD2BACC0071022C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A2D2A782CD2BACC0071022C; + remoteInfo = SQLiteExplorations; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2A2D2A792CD2BACC0071022C /* SQLiteExplorations.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SQLiteExplorations.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A2D2A892CD2BAD00071022C /* SQLiteExplorationsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SQLiteExplorationsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 2A2D2A7B2CD2BACC0071022C /* SQLiteExplorations */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = SQLiteExplorations; + sourceTree = ""; + }; + 2A2D2A8C2CD2BAD00071022C /* SQLiteExplorationsTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = SQLiteExplorationsTests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2A2D2A762CD2BACC0071022C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2AA9B3BD2CDAB9BD00BB83C0 /* GRDB in Frameworks */, + 4BBE003F2CDBFB1800EBD6F0 /* IssueReporting in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A2D2A862CD2BAD00071022C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2A2D2A702CD2BACC0071022C = { + isa = PBXGroup; + children = ( + 2A2D2A7B2CD2BACC0071022C /* SQLiteExplorations */, + 2A2D2A8C2CD2BAD00071022C /* SQLiteExplorationsTests */, + 2A2D2A7A2CD2BACC0071022C /* Products */, + ); + sourceTree = ""; + }; + 2A2D2A7A2CD2BACC0071022C /* Products */ = { + isa = PBXGroup; + children = ( + 2A2D2A792CD2BACC0071022C /* SQLiteExplorations.app */, + 2A2D2A892CD2BAD00071022C /* SQLiteExplorationsTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2A2D2A782CD2BACC0071022C /* SQLiteExplorations */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A2D2A9D2CD2BAD00071022C /* Build configuration list for PBXNativeTarget "SQLiteExplorations" */; + buildPhases = ( + 2A2D2A752CD2BACC0071022C /* Sources */, + 2A2D2A762CD2BACC0071022C /* Frameworks */, + 2A2D2A772CD2BACC0071022C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 2A2D2A7B2CD2BACC0071022C /* SQLiteExplorations */, + ); + name = SQLiteExplorations; + packageProductDependencies = ( + 2AA9B3BC2CDAB9BD00BB83C0 /* GRDB */, + 4BBE003E2CDBFB1800EBD6F0 /* IssueReporting */, + ); + productName = SQLiteExplorations; + productReference = 2A2D2A792CD2BACC0071022C /* SQLiteExplorations.app */; + productType = "com.apple.product-type.application"; + }; + 2A2D2A882CD2BAD00071022C /* SQLiteExplorationsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A2D2AA02CD2BAD00071022C /* Build configuration list for PBXNativeTarget "SQLiteExplorationsTests" */; + buildPhases = ( + 2A2D2A852CD2BAD00071022C /* Sources */, + 2A2D2A862CD2BAD00071022C /* Frameworks */, + 2A2D2A872CD2BAD00071022C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2A2D2A8B2CD2BAD00071022C /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 2A2D2A8C2CD2BAD00071022C /* SQLiteExplorationsTests */, + ); + name = SQLiteExplorationsTests; + packageProductDependencies = ( + ); + productName = SQLiteExplorationsTests; + productReference = 2A2D2A892CD2BAD00071022C /* SQLiteExplorationsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2A2D2A712CD2BACC0071022C /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1610; + LastUpgradeCheck = 1610; + TargetAttributes = { + 2A2D2A782CD2BACC0071022C = { + CreatedOnToolsVersion = 16.1; + }; + 2A2D2A882CD2BAD00071022C = { + CreatedOnToolsVersion = 16.1; + TestTargetID = 2A2D2A782CD2BACC0071022C; + }; + }; + }; + buildConfigurationList = 2A2D2A742CD2BACC0071022C /* Build configuration list for PBXProject "SQLiteExplorations" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2A2D2A702CD2BACC0071022C; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 2AA9B3BB2CDAB9BD00BB83C0 /* XCRemoteSwiftPackageReference "GRDB" */, + 4BBE003D2CDBFB1800EBD6F0 /* XCRemoteSwiftPackageReference "xctest-dynamic-overlay" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 2A2D2A7A2CD2BACC0071022C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2A2D2A782CD2BACC0071022C /* SQLiteExplorations */, + 2A2D2A882CD2BAD00071022C /* SQLiteExplorationsTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2A2D2A772CD2BACC0071022C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A2D2A872CD2BAD00071022C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2A2D2A752CD2BACC0071022C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A2D2A852CD2BAD00071022C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 2A2D2A8B2CD2BAD00071022C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A2D2A782CD2BACC0071022C /* SQLiteExplorations */; + targetProxy = 2A2D2A8A2CD2BAD00071022C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 2A2D2A9B2CD2BAD00071022C /* 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"; + SWIFT_VERSION = 6.0; + }; + name = Debug; + }; + 2A2D2A9C2CD2BAD00071022C /* 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; + SWIFT_VERSION = 6.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 2A2D2A9E2CD2BAD00071022C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + 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"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SQLiteExplorations; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2A2D2A9F2CD2BAD00071022C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + 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"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SQLiteExplorations; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2A2D2AA12CD2BAD00071022C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SQLiteExplorationsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SQLiteExplorations.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SQLiteExplorations"; + }; + name = Debug; + }; + 2A2D2AA22CD2BAD00071022C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SQLiteExplorationsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SQLiteExplorations.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SQLiteExplorations"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2A2D2A742CD2BACC0071022C /* Build configuration list for PBXProject "SQLiteExplorations" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A2D2A9B2CD2BAD00071022C /* Debug */, + 2A2D2A9C2CD2BAD00071022C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A2D2A9D2CD2BAD00071022C /* Build configuration list for PBXNativeTarget "SQLiteExplorations" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A2D2A9E2CD2BAD00071022C /* Debug */, + 2A2D2A9F2CD2BAD00071022C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A2D2AA02CD2BAD00071022C /* Build configuration list for PBXNativeTarget "SQLiteExplorationsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A2D2AA12CD2BAD00071022C /* Debug */, + 2A2D2AA22CD2BAD00071022C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2AA9B3BB2CDAB9BD00BB83C0 /* XCRemoteSwiftPackageReference "GRDB" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "http://github.com/groue/GRDB.swift"; + requirement = { + branch = "v7.0.0-beta.6"; + kind = branch; + }; + }; + 4BBE003D2CDBFB1800EBD6F0 /* XCRemoteSwiftPackageReference "xctest-dynamic-overlay" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/xctest-dynamic-overlay.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.4.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2AA9B3BC2CDAB9BD00BB83C0 /* GRDB */ = { + isa = XCSwiftPackageProductDependency; + package = 2AA9B3BB2CDAB9BD00BB83C0 /* XCRemoteSwiftPackageReference "GRDB" */; + productName = GRDB; + }; + 4BBE003E2CDBFB1800EBD6F0 /* IssueReporting */ = { + isa = XCSwiftPackageProductDependency; + package = 4BBE003D2CDBFB1800EBD6F0 /* XCRemoteSwiftPackageReference "xctest-dynamic-overlay" */; + productName = IssueReporting; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 2A2D2A712CD2BACC0071022C /* Project object */; +} diff --git a/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/Assets.xcassets/AccentColor.colorset/Contents.json b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/Assets.xcassets/AppIcon.appiconset/Contents.json b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..23058801 --- /dev/null +++ b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/Assets.xcassets/Contents.json b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/ContentView.swift b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/ContentView.swift new file mode 100644 index 00000000..4711b1b7 --- /dev/null +++ b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/ContentView.swift @@ -0,0 +1,199 @@ +import GRDB +import IssueReporting +import SwiftUI + +struct ContentView: View { + @Environment(\.databaseQueue) var databaseQueue + @State var players: [Player] = [] + @State var playerDetail: Player? + @State var order: Order = .created + + enum Order: String, CaseIterable { + case created = "Created" + case isInjured = "Injured?" + case name = "Name" + var orderingTerm: any SQLOrderingTerm & Sendable { + switch self { + case .created: + Column("createdAt") + case .isInjured: + Column("isInjured").desc + case .name: + Column("Name") + } + } + } + + var body: some View { + Form { + ForEach(players) { player in + Button { + playerDetail = player + } label: { + HStack { + Text(player.name) + if player.isInjured { + Spacer() + Image(systemName: "stethoscope") + .foregroundStyle(.red) + } + } + } + } + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Picker(order.rawValue, selection: $order) { + Section { + ForEach(Order.allCases, id: \.self) { order in + Text(order.rawValue) + .tag(order) + } + } header: { + Text("Sort by") + } + } + } + } + .sheet(item: $playerDetail) { player in + NavigationStack { + PlayerDetailView( + player: player + ) + } + .presentationDetents([.medium]) + } + .task(id: order) { + let values = ValueObservation.tracking { [orderingTerm = order.orderingTerm] db in + try Player + .order(orderingTerm) + .fetchAll(db) + } + .values(in: databaseQueue) + do { + for try await players in values { + withAnimation { + self.players = players + } + } + } catch { + reportIssue(error) + } + } + } +} + +struct PlayerDetailView: View { + @Environment(\.databaseQueue) var databaseQueue + @State var player: Player + @State var team: Team? + @Environment(\.dismiss) var dismiss + + var body: some View { + Form { + Section { + TextField("Name", text: $player.name) + Button { + player.isInjured.toggle() + } label: { + if player.isInjured { + Text("\(Image(systemName: "stethoscope")) Injured") + .foregroundStyle(.red) + } else { + Text("Not injured") + } + } + } header: { + Text("Details") + } + + Section { + if let team { + Text(team.name) + } else { + Text("No team") + .italic() + } + } header: { + Text("Team") + } + } + .navigationTitle("Player") + .onAppear { + do { + guard let teamID = player.teamID + else { return } + team = try databaseQueue.read { db in + try Team.fetchOne(db, id: teamID) + } + } catch { + reportIssue(error) + } + } + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + dismiss() + } + } + ToolbarItem(placement: .primaryAction) { + Button("Save") { + do { + try databaseQueue.write { db in + try player.save(db) + } + dismiss() + } catch { + reportIssue(error) + } + } + } + } +// .onChange(of: player) { +// do { +// try databaseQueue.write { db in +// try player.save(db) +// } +// } catch { +// reportIssue(error) +// } +// } + } +} + +#Preview { + @Previewable @Environment(\.databaseQueue) var databaseQueue + try! databaseQueue.write { db in + for index in 1...10 { + _ = try! Player( + name: "Blob \(index)", + createdAt: Date(), + isInjured: index.isMultiple(of: 3) + ).inserted(db) + } + } + return NavigationStack { + ContentView() + } + .environment(\.databaseQueue, databaseQueue) +} + +#Preview("Player detail") { + @Previewable @State var isPresented = true + + Button("Present") { + isPresented = true + } + .sheet(isPresented: $isPresented) { + NavigationStack { + PlayerDetailView( + player: Player( + name: "Blob", + createdAt: Date(), + teamID: 3 + ) + ) + } + .presentationDetents([.medium]) + } +} diff --git a/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/Models.swift b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/Models.swift new file mode 100644 index 00000000..4ea13daa --- /dev/null +++ b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/Models.swift @@ -0,0 +1,98 @@ +import Foundation +import GRDB + +struct Player: MutablePersistableRecord, FetchableRecord, Codable, Identifiable, Equatable { + static let databaseTableName = "players" + + var id: Int64? + var name = "" + var createdAt: Date + var isInjured = false + var teamID: Int64? + + mutating func didInsert(_ inserted: InsertionSuccess) { + id = inserted.rowID + } +} + +struct Team: MutablePersistableRecord, FetchableRecord, Codable, Identifiable { + static let databaseTableName = "teams" + + var id: Int64? + var name = "" + + mutating func didInsert(_ inserted: InsertionSuccess) { + id = inserted.rowID + } +} + +extension DatabaseQueue { + static func appDatabase() throws -> DatabaseQueue { + let databasePath = URL.documentsDirectory.appending(path: "db.sqlite") + .path() + print("open", databasePath) + var config = Configuration() + config.foreignKeysEnabled = true + config.prepareDatabase { + $0.trace { print($0) } + } + let databaseQueue: DatabaseQueue + if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == nil + { + databaseQueue = try DatabaseQueue( + path: databasePath, + configuration: config + ) + } else { + databaseQueue = try DatabaseQueue(configuration: config) + } + var migrator = DatabaseMigrator() + #if DEBUG + migrator.eraseDatabaseOnSchemaChange = true + #endif + migrator.registerMigration("Create 'players' table") { db in + try db.create(table: Player.databaseTableName) { table in + table.autoIncrementedPrimaryKey("id") + table.column("name", .text).notNull() + table.column("createdAt", .datetime).notNull() + } + } + migrator.registerMigration("Add 'isInjured' to 'players'") { db in + try db.alter(table: Player.databaseTableName) { table in + table.add(column: "isInjured", .boolean).defaults(to: false) + } + } + migrator.registerMigration("Create 'teams' table") { db in + try db.create(table: Team.databaseTableName) { table in + table.autoIncrementedPrimaryKey("id") + table.column("name", .text).notNull() + } + try db.alter(table: Player.databaseTableName) { table in + table.add(column: "teamID", .integer) + .references("teams") + } + } + #if DEBUG && targetEnvironment(simulator) + migrator.registerMigration("Seed simulator data") { db in + let lions = try Team(name: "Lions").inserted(db) + let tigers = try Team(name: "Tigers").inserted(db) + let bears = try Team(name: "Bears").inserted(db) + _ = try Player(name: "Blob", createdAt: Date(), isInjured: false, teamID: lions.id) + .inserted(db) + _ = try Player(name: "Blob Jr", createdAt: Date(), isInjured: false, teamID: tigers.id) + .inserted(db) + _ = try Player(name: "Blob Sr", createdAt: Date(), isInjured: true, teamID: bears.id) + .inserted(db) + } + #endif + try migrator.migrate(databaseQueue) + + return databaseQueue + } +} + +import SwiftUI + +extension EnvironmentValues { + @Entry var databaseQueue: DatabaseQueue = try! .appDatabase() +} diff --git a/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/SQLiteExplorationsApp.swift b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/SQLiteExplorationsApp.swift new file mode 100644 index 00000000..2b73bb82 --- /dev/null +++ b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorations/SQLiteExplorationsApp.swift @@ -0,0 +1,114 @@ +import GRDB +import SQLite3 +import SwiftUI +import Synchronization + +@main +struct SQLiteExplorationsApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} + +func sqlite3() { + let databasePath = URL.documentsDirectory.appending(path: "db.sqlite") + .path() + print("open", databasePath) + var db: OpaquePointer? + guard + sqlite3_open_v2( + databasePath, + &db, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, + nil + ) == SQLITE_OK + else { + fatalError("Could not open database at \(databasePath)") + } + print( + "Create table", + sqlite3_exec( + db, + """ + CREATE TABLE IF NOT EXISTS "players" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE, + "name" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL + ) + """, + nil, + nil, + nil + ) == SQLITE_OK + ) + let name = "Blob" + let createdAt = Date() + var statement: OpaquePointer? + sqlite3_prepare_v2( + db, + """ + INSERT INTO "players" + ("name", "createdAt") + VALUES + (?, ?) + """, + -1, + &statement, + nil + ) + let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + print( + "Binding name", + sqlite3_bind_text( + statement, + 1, + name, + -1, + SQLITE_TRANSIENT + ) == SQLITE_OK + ) + print( + "Binding createdAt", + sqlite3_bind_int64( + statement, + 2, + Int64(createdAt.timeIntervalSince1970) + ) == SQLITE_OK + ) + print( + "Insert Blob", + sqlite3_step(statement) == SQLITE_DONE + ) + print( + "Finalize insert", + sqlite3_finalize(statement) == SQLITE_OK + ) + statement = nil + sqlite3_prepare_v2( + db, + """ + SELECT * FROM "players" WHERE "createdAt" > ? + """, + -1, + &statement, + nil + ) + sqlite3_bind_int64( + statement, 1, Int64(Date().addingTimeInterval(-10).timeIntervalSince1970)) + while sqlite3_step(statement) == SQLITE_ROW { + struct Player { + let id: Int64 + let name: String + let createdAt: Date + } + let id: Int64 = sqlite3_column_int64(statement, 0) + let name = String(cString: sqlite3_column_text(statement, 1)) + let createdAt = Date( + timeIntervalSince1970: Double(sqlite3_column_int64(statement, 2))) + let player = Player(id: id, name: name, createdAt: createdAt) + print(player) + } + sqlite3_finalize(statement) +} diff --git a/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorationsTests/SQLiteExplorationsTests.swift b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorationsTests/SQLiteExplorationsTests.swift new file mode 100644 index 00000000..eca431bc --- /dev/null +++ b/0304-sqlite-pt4/SQLiteExplorations/SQLiteExplorationsTests/SQLiteExplorationsTests.swift @@ -0,0 +1,17 @@ +// +// SQLiteExplorationsTests.swift +// SQLiteExplorationsTests +// +// Created by Point-Free on 10/30/24. +// + +import Testing +@testable import SQLiteExplorations + +struct SQLiteExplorationsTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/0305-sharing-pt1/README.md b/0305-sharing-pt1/README.md new file mode 100644 index 00000000..1438ee06 --- /dev/null +++ b/0305-sharing-pt1/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Tour of Sharing: App Storage, Part 1](https://www.pointfree.co/episodes/ep305-tour-of-sharing-app-storage-part-1) +> +> “Sharing” is a brand new library for sharing state throughout your application and to external systems like user defaults, the file system, and more. We start our tour of the library by comparing it to a tool that inspired its design: SwiftUI’s `@AppStorage`. diff --git a/0305-sharing-pt1/TourOfSharing/TourOfSharing.xcodeproj/project.pbxproj b/0305-sharing-pt1/TourOfSharing/TourOfSharing.xcodeproj/project.pbxproj new file mode 100644 index 00000000..3667a29d --- /dev/null +++ b/0305-sharing-pt1/TourOfSharing/TourOfSharing.xcodeproj/project.pbxproj @@ -0,0 +1,579 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 2A6E8B342CE7E25E00319151 /* Sharing in Frameworks */ = {isa = PBXBuildFile; productRef = 2A6E8B332CE7E25E00319151 /* Sharing */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2A5622DB2CE7D2E600FC29DB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2A5622C22CE7D2E400FC29DB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A5622C92CE7D2E500FC29DB; + remoteInfo = TourOfSharing; + }; + 2A5622E52CE7D2E600FC29DB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2A5622C22CE7D2E400FC29DB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A5622C92CE7D2E500FC29DB; + remoteInfo = TourOfSharing; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2A5622CA2CE7D2E500FC29DB /* TourOfSharing.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TourOfSharing.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A5622DA2CE7D2E600FC29DB /* TourOfSharingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TourOfSharingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A5622E42CE7D2E600FC29DB /* TourOfSharingUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TourOfSharingUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 2A5622CC2CE7D2E500FC29DB /* TourOfSharing */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TourOfSharing; + sourceTree = ""; + }; + 2A5622DD2CE7D2E600FC29DB /* TourOfSharingTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TourOfSharingTests; + sourceTree = ""; + }; + 2A5622E72CE7D2E600FC29DB /* TourOfSharingUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TourOfSharingUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2A5622C72CE7D2E500FC29DB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A6E8B342CE7E25E00319151 /* Sharing in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A5622D72CE7D2E600FC29DB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A5622E12CE7D2E600FC29DB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2A5622C12CE7D2E400FC29DB = { + isa = PBXGroup; + children = ( + 2A5622CC2CE7D2E500FC29DB /* TourOfSharing */, + 2A5622DD2CE7D2E600FC29DB /* TourOfSharingTests */, + 2A5622E72CE7D2E600FC29DB /* TourOfSharingUITests */, + 2A5622CB2CE7D2E500FC29DB /* Products */, + ); + sourceTree = ""; + }; + 2A5622CB2CE7D2E500FC29DB /* Products */ = { + isa = PBXGroup; + children = ( + 2A5622CA2CE7D2E500FC29DB /* TourOfSharing.app */, + 2A5622DA2CE7D2E600FC29DB /* TourOfSharingTests.xctest */, + 2A5622E42CE7D2E600FC29DB /* TourOfSharingUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2A5622C92CE7D2E500FC29DB /* TourOfSharing */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A5622EE2CE7D2E600FC29DB /* Build configuration list for PBXNativeTarget "TourOfSharing" */; + buildPhases = ( + 2A5622C62CE7D2E500FC29DB /* Sources */, + 2A5622C72CE7D2E500FC29DB /* Frameworks */, + 2A5622C82CE7D2E500FC29DB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 2A5622CC2CE7D2E500FC29DB /* TourOfSharing */, + ); + name = TourOfSharing; + packageProductDependencies = ( + 2A6E8B332CE7E25E00319151 /* Sharing */, + ); + productName = TourOfSharing; + productReference = 2A5622CA2CE7D2E500FC29DB /* TourOfSharing.app */; + productType = "com.apple.product-type.application"; + }; + 2A5622D92CE7D2E600FC29DB /* TourOfSharingTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A5622F12CE7D2E600FC29DB /* Build configuration list for PBXNativeTarget "TourOfSharingTests" */; + buildPhases = ( + 2A5622D62CE7D2E600FC29DB /* Sources */, + 2A5622D72CE7D2E600FC29DB /* Frameworks */, + 2A5622D82CE7D2E600FC29DB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2A5622DC2CE7D2E600FC29DB /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 2A5622DD2CE7D2E600FC29DB /* TourOfSharingTests */, + ); + name = TourOfSharingTests; + packageProductDependencies = ( + ); + productName = TourOfSharingTests; + productReference = 2A5622DA2CE7D2E600FC29DB /* TourOfSharingTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 2A5622E32CE7D2E600FC29DB /* TourOfSharingUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A5622F42CE7D2E600FC29DB /* Build configuration list for PBXNativeTarget "TourOfSharingUITests" */; + buildPhases = ( + 2A5622E02CE7D2E600FC29DB /* Sources */, + 2A5622E12CE7D2E600FC29DB /* Frameworks */, + 2A5622E22CE7D2E600FC29DB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2A5622E62CE7D2E600FC29DB /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 2A5622E72CE7D2E600FC29DB /* TourOfSharingUITests */, + ); + name = TourOfSharingUITests; + packageProductDependencies = ( + ); + productName = TourOfSharingUITests; + productReference = 2A5622E42CE7D2E600FC29DB /* TourOfSharingUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2A5622C22CE7D2E400FC29DB /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1610; + LastUpgradeCheck = 1610; + TargetAttributes = { + 2A5622C92CE7D2E500FC29DB = { + CreatedOnToolsVersion = 16.1; + }; + 2A5622D92CE7D2E600FC29DB = { + CreatedOnToolsVersion = 16.1; + TestTargetID = 2A5622C92CE7D2E500FC29DB; + }; + 2A5622E32CE7D2E600FC29DB = { + CreatedOnToolsVersion = 16.1; + TestTargetID = 2A5622C92CE7D2E500FC29DB; + }; + }; + }; + buildConfigurationList = 2A5622C52CE7D2E400FC29DB /* Build configuration list for PBXProject "TourOfSharing" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2A5622C12CE7D2E400FC29DB; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 2A6E8B322CE7E25E00319151 /* XCRemoteSwiftPackageReference "swift-sharing" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 2A5622CB2CE7D2E500FC29DB /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2A5622C92CE7D2E500FC29DB /* TourOfSharing */, + 2A5622D92CE7D2E600FC29DB /* TourOfSharingTests */, + 2A5622E32CE7D2E600FC29DB /* TourOfSharingUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2A5622C82CE7D2E500FC29DB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A5622D82CE7D2E600FC29DB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A5622E22CE7D2E600FC29DB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2A5622C62CE7D2E500FC29DB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A5622D62CE7D2E600FC29DB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A5622E02CE7D2E600FC29DB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 2A5622DC2CE7D2E600FC29DB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A5622C92CE7D2E500FC29DB /* TourOfSharing */; + targetProxy = 2A5622DB2CE7D2E600FC29DB /* PBXContainerItemProxy */; + }; + 2A5622E62CE7D2E600FC29DB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A5622C92CE7D2E500FC29DB /* TourOfSharing */; + targetProxy = 2A5622E52CE7D2E600FC29DB /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 2A5622EC2CE7D2E600FC29DB /* 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"; + SWIFT_VERSION = 6.0; + }; + name = Debug; + }; + 2A5622ED2CE7D2E600FC29DB /* 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; + SWIFT_VERSION = 6.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 2A5622EF2CE7D2E600FC29DB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + 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"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.TourOfSharing; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2A5622F02CE7D2E600FC29DB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + 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"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.TourOfSharing; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2A5622F22CE7D2E600FC29DB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.TourOfSharingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TourOfSharing.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TourOfSharing"; + }; + name = Debug; + }; + 2A5622F32CE7D2E600FC29DB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.TourOfSharingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TourOfSharing.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TourOfSharing"; + }; + name = Release; + }; + 2A5622F52CE7D2E600FC29DB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.TourOfSharingUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = TourOfSharing; + }; + name = Debug; + }; + 2A5622F62CE7D2E600FC29DB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.TourOfSharingUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = TourOfSharing; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2A5622C52CE7D2E400FC29DB /* Build configuration list for PBXProject "TourOfSharing" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A5622EC2CE7D2E600FC29DB /* Debug */, + 2A5622ED2CE7D2E600FC29DB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A5622EE2CE7D2E600FC29DB /* Build configuration list for PBXNativeTarget "TourOfSharing" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A5622EF2CE7D2E600FC29DB /* Debug */, + 2A5622F02CE7D2E600FC29DB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A5622F12CE7D2E600FC29DB /* Build configuration list for PBXNativeTarget "TourOfSharingTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A5622F22CE7D2E600FC29DB /* Debug */, + 2A5622F32CE7D2E600FC29DB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A5622F42CE7D2E600FC29DB /* Build configuration list for PBXNativeTarget "TourOfSharingUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A5622F52CE7D2E600FC29DB /* Debug */, + 2A5622F62CE7D2E600FC29DB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2A6E8B322CE7E25E00319151 /* XCRemoteSwiftPackageReference "swift-sharing" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-sharing"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2A6E8B332CE7E25E00319151 /* Sharing */ = { + isa = XCSwiftPackageProductDependency; + package = 2A6E8B322CE7E25E00319151 /* XCRemoteSwiftPackageReference "swift-sharing" */; + productName = Sharing; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 2A5622C22CE7D2E400FC29DB /* Project object */; +} diff --git a/0305-sharing-pt1/TourOfSharing/TourOfSharing.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0305-sharing-pt1/TourOfSharing/TourOfSharing.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0305-sharing-pt1/TourOfSharing/TourOfSharing.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0305-sharing-pt1/TourOfSharing/TourOfSharing/Assets.xcassets/AccentColor.colorset/Contents.json b/0305-sharing-pt1/TourOfSharing/TourOfSharing/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0305-sharing-pt1/TourOfSharing/TourOfSharing/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0305-sharing-pt1/TourOfSharing/TourOfSharing/Assets.xcassets/AppIcon.appiconset/Contents.json b/0305-sharing-pt1/TourOfSharing/TourOfSharing/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..23058801 --- /dev/null +++ b/0305-sharing-pt1/TourOfSharing/TourOfSharing/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0305-sharing-pt1/TourOfSharing/TourOfSharing/Assets.xcassets/Contents.json b/0305-sharing-pt1/TourOfSharing/TourOfSharing/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0305-sharing-pt1/TourOfSharing/TourOfSharing/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0305-sharing-pt1/TourOfSharing/TourOfSharing/ContentView.swift b/0305-sharing-pt1/TourOfSharing/TourOfSharing/ContentView.swift new file mode 100644 index 00000000..5ccd10e5 --- /dev/null +++ b/0305-sharing-pt1/TourOfSharing/TourOfSharing/ContentView.swift @@ -0,0 +1,100 @@ +import Sharing +import SwiftUI + +struct ManyCountersView: View { + @Shared(.appStorage("count")) var count = 0 + + var body: some View { + Form { + Section { + CounterView() + } + + Section { + CounterView() + } + + Button(#"UserDefaults.set(0, "count")"#) { + withAnimation { + $count.withLock { $0 = 0 } +// UserDefaults.standard.set( +// 0, +// forKey: "count" +// ) + } + } + } + } +} + +@Observable +class CounterModel { + @ObservationIgnored + @Shared(.appStorage("count")) var count = 0 + //@AppStorage("co.pointfree.countermodel.count") var count = 0 +} + +struct CounterView: View { + @State var model = CounterModel() + + var body: some View { + Text("\(model.count)") + .font(.largeTitle) + Button("Decrement") { + //model.count -= 1 + model.$count.withLock { $0 -= 1 } + } + Button("Increment") { + //model.count += 1 + model.$count.withLock { $0 += 1 } + } +// Button("Race!") { +// Task { +// await withTaskGroup(of: Void.self) { [sharedCount = model.$count] group in +// for _ in 1...1_000 { +// group.addTask { +// sharedCount.withLock { $0 += 1 } +// } +// } +// } +// } +// } + } +} + +#Preview("CounterView") { + CounterView() +} + +#Preview("ManyCountersView") { + ManyCountersView() +} + + + +struct AppStorageRaceCondition: View { + @AppStorage("racey-count") var count = 0 + var body: some View { + Form { + Text("\(count)") + Button("Race!") { + Task { + await withTaskGroup(of: Void.self) { group in + for _ in 1...1_000 { + group.addTask { + await MainActor.run { + count += 1 + } + //_count.wrappedValue += 1 + } + } + } + } + } + } + } +} + +#Preview("AppStorage race condition") { + AppStorageRaceCondition() +} diff --git a/0305-sharing-pt1/TourOfSharing/TourOfSharing/TourOfSharingApp.swift b/0305-sharing-pt1/TourOfSharing/TourOfSharing/TourOfSharingApp.swift new file mode 100644 index 00000000..df07b9ae --- /dev/null +++ b/0305-sharing-pt1/TourOfSharing/TourOfSharing/TourOfSharingApp.swift @@ -0,0 +1,11 @@ +import SwiftUI + +@main +struct TourOfSharingApp: App { + var body: some Scene { + WindowGroup { + ManyCountersView() + //CounterView() + } + } +} diff --git a/0305-sharing-pt1/TourOfSharing/TourOfSharingTests/TourOfSharingTests.swift b/0305-sharing-pt1/TourOfSharing/TourOfSharingTests/TourOfSharingTests.swift new file mode 100644 index 00000000..7087a6c9 --- /dev/null +++ b/0305-sharing-pt1/TourOfSharing/TourOfSharingTests/TourOfSharingTests.swift @@ -0,0 +1,17 @@ +// +// TourOfSharingTests.swift +// TourOfSharingTests +// +// Created by Point-Free on 11/15/24. +// + +import Testing +@testable import TourOfSharing + +struct TourOfSharingTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/0305-sharing-pt1/TourOfSharing/TourOfSharingUITests/TourOfSharingUITests.swift b/0305-sharing-pt1/TourOfSharing/TourOfSharingUITests/TourOfSharingUITests.swift new file mode 100644 index 00000000..5fac01f5 --- /dev/null +++ b/0305-sharing-pt1/TourOfSharing/TourOfSharingUITests/TourOfSharingUITests.swift @@ -0,0 +1,43 @@ +// +// TourOfSharingUITests.swift +// TourOfSharingUITests +// +// Created by Point-Free on 11/15/24. +// + +import XCTest + +final class TourOfSharingUITests: 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 it’s 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 { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/0305-sharing-pt1/TourOfSharing/TourOfSharingUITests/TourOfSharingUITestsLaunchTests.swift b/0305-sharing-pt1/TourOfSharing/TourOfSharingUITests/TourOfSharingUITestsLaunchTests.swift new file mode 100644 index 00000000..30a26050 --- /dev/null +++ b/0305-sharing-pt1/TourOfSharing/TourOfSharingUITests/TourOfSharingUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// TourOfSharingUITestsLaunchTests.swift +// TourOfSharingUITests +// +// Created by Point-Free on 11/15/24. +// + +import XCTest + +final class TourOfSharingUITestsLaunchTests: 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) + } +} diff --git a/0306-sharing-pt2/README.md b/0306-sharing-pt2/README.md new file mode 100644 index 00000000..d9d5fbd2 --- /dev/null +++ b/0306-sharing-pt2/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Tour of Sharing: App Storage, Part 2](https://www.pointfree.co/episodes/ep306-tour-of-sharing-app-storage-part-2) +> +> We show how the `@Shared` property wrapper, unlike `@AppStorage`, can be used anywhere, not just SwiftUI views. And we show how `@Shared` has some extra bells and whistles that make it easier to write maintainable Xcode previews and avoid potential bugs around “string-ly” typed keys and default values. diff --git a/0306-sharing-pt2/TourOfSharing/TourOfSharing.xcodeproj/project.pbxproj b/0306-sharing-pt2/TourOfSharing/TourOfSharing.xcodeproj/project.pbxproj new file mode 100644 index 00000000..3667a29d --- /dev/null +++ b/0306-sharing-pt2/TourOfSharing/TourOfSharing.xcodeproj/project.pbxproj @@ -0,0 +1,579 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 2A6E8B342CE7E25E00319151 /* Sharing in Frameworks */ = {isa = PBXBuildFile; productRef = 2A6E8B332CE7E25E00319151 /* Sharing */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2A5622DB2CE7D2E600FC29DB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2A5622C22CE7D2E400FC29DB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A5622C92CE7D2E500FC29DB; + remoteInfo = TourOfSharing; + }; + 2A5622E52CE7D2E600FC29DB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2A5622C22CE7D2E400FC29DB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A5622C92CE7D2E500FC29DB; + remoteInfo = TourOfSharing; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2A5622CA2CE7D2E500FC29DB /* TourOfSharing.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TourOfSharing.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A5622DA2CE7D2E600FC29DB /* TourOfSharingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TourOfSharingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A5622E42CE7D2E600FC29DB /* TourOfSharingUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TourOfSharingUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 2A5622CC2CE7D2E500FC29DB /* TourOfSharing */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TourOfSharing; + sourceTree = ""; + }; + 2A5622DD2CE7D2E600FC29DB /* TourOfSharingTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TourOfSharingTests; + sourceTree = ""; + }; + 2A5622E72CE7D2E600FC29DB /* TourOfSharingUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TourOfSharingUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2A5622C72CE7D2E500FC29DB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A6E8B342CE7E25E00319151 /* Sharing in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A5622D72CE7D2E600FC29DB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A5622E12CE7D2E600FC29DB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2A5622C12CE7D2E400FC29DB = { + isa = PBXGroup; + children = ( + 2A5622CC2CE7D2E500FC29DB /* TourOfSharing */, + 2A5622DD2CE7D2E600FC29DB /* TourOfSharingTests */, + 2A5622E72CE7D2E600FC29DB /* TourOfSharingUITests */, + 2A5622CB2CE7D2E500FC29DB /* Products */, + ); + sourceTree = ""; + }; + 2A5622CB2CE7D2E500FC29DB /* Products */ = { + isa = PBXGroup; + children = ( + 2A5622CA2CE7D2E500FC29DB /* TourOfSharing.app */, + 2A5622DA2CE7D2E600FC29DB /* TourOfSharingTests.xctest */, + 2A5622E42CE7D2E600FC29DB /* TourOfSharingUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2A5622C92CE7D2E500FC29DB /* TourOfSharing */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A5622EE2CE7D2E600FC29DB /* Build configuration list for PBXNativeTarget "TourOfSharing" */; + buildPhases = ( + 2A5622C62CE7D2E500FC29DB /* Sources */, + 2A5622C72CE7D2E500FC29DB /* Frameworks */, + 2A5622C82CE7D2E500FC29DB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 2A5622CC2CE7D2E500FC29DB /* TourOfSharing */, + ); + name = TourOfSharing; + packageProductDependencies = ( + 2A6E8B332CE7E25E00319151 /* Sharing */, + ); + productName = TourOfSharing; + productReference = 2A5622CA2CE7D2E500FC29DB /* TourOfSharing.app */; + productType = "com.apple.product-type.application"; + }; + 2A5622D92CE7D2E600FC29DB /* TourOfSharingTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A5622F12CE7D2E600FC29DB /* Build configuration list for PBXNativeTarget "TourOfSharingTests" */; + buildPhases = ( + 2A5622D62CE7D2E600FC29DB /* Sources */, + 2A5622D72CE7D2E600FC29DB /* Frameworks */, + 2A5622D82CE7D2E600FC29DB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2A5622DC2CE7D2E600FC29DB /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 2A5622DD2CE7D2E600FC29DB /* TourOfSharingTests */, + ); + name = TourOfSharingTests; + packageProductDependencies = ( + ); + productName = TourOfSharingTests; + productReference = 2A5622DA2CE7D2E600FC29DB /* TourOfSharingTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 2A5622E32CE7D2E600FC29DB /* TourOfSharingUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A5622F42CE7D2E600FC29DB /* Build configuration list for PBXNativeTarget "TourOfSharingUITests" */; + buildPhases = ( + 2A5622E02CE7D2E600FC29DB /* Sources */, + 2A5622E12CE7D2E600FC29DB /* Frameworks */, + 2A5622E22CE7D2E600FC29DB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2A5622E62CE7D2E600FC29DB /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 2A5622E72CE7D2E600FC29DB /* TourOfSharingUITests */, + ); + name = TourOfSharingUITests; + packageProductDependencies = ( + ); + productName = TourOfSharingUITests; + productReference = 2A5622E42CE7D2E600FC29DB /* TourOfSharingUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2A5622C22CE7D2E400FC29DB /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1610; + LastUpgradeCheck = 1610; + TargetAttributes = { + 2A5622C92CE7D2E500FC29DB = { + CreatedOnToolsVersion = 16.1; + }; + 2A5622D92CE7D2E600FC29DB = { + CreatedOnToolsVersion = 16.1; + TestTargetID = 2A5622C92CE7D2E500FC29DB; + }; + 2A5622E32CE7D2E600FC29DB = { + CreatedOnToolsVersion = 16.1; + TestTargetID = 2A5622C92CE7D2E500FC29DB; + }; + }; + }; + buildConfigurationList = 2A5622C52CE7D2E400FC29DB /* Build configuration list for PBXProject "TourOfSharing" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2A5622C12CE7D2E400FC29DB; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 2A6E8B322CE7E25E00319151 /* XCRemoteSwiftPackageReference "swift-sharing" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 2A5622CB2CE7D2E500FC29DB /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2A5622C92CE7D2E500FC29DB /* TourOfSharing */, + 2A5622D92CE7D2E600FC29DB /* TourOfSharingTests */, + 2A5622E32CE7D2E600FC29DB /* TourOfSharingUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2A5622C82CE7D2E500FC29DB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A5622D82CE7D2E600FC29DB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A5622E22CE7D2E600FC29DB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2A5622C62CE7D2E500FC29DB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A5622D62CE7D2E600FC29DB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A5622E02CE7D2E600FC29DB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 2A5622DC2CE7D2E600FC29DB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A5622C92CE7D2E500FC29DB /* TourOfSharing */; + targetProxy = 2A5622DB2CE7D2E600FC29DB /* PBXContainerItemProxy */; + }; + 2A5622E62CE7D2E600FC29DB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A5622C92CE7D2E500FC29DB /* TourOfSharing */; + targetProxy = 2A5622E52CE7D2E600FC29DB /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 2A5622EC2CE7D2E600FC29DB /* 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"; + SWIFT_VERSION = 6.0; + }; + name = Debug; + }; + 2A5622ED2CE7D2E600FC29DB /* 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; + SWIFT_VERSION = 6.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 2A5622EF2CE7D2E600FC29DB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + 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"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.TourOfSharing; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2A5622F02CE7D2E600FC29DB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + 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"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.TourOfSharing; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2A5622F22CE7D2E600FC29DB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.TourOfSharingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TourOfSharing.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TourOfSharing"; + }; + name = Debug; + }; + 2A5622F32CE7D2E600FC29DB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.TourOfSharingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TourOfSharing.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TourOfSharing"; + }; + name = Release; + }; + 2A5622F52CE7D2E600FC29DB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.TourOfSharingUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = TourOfSharing; + }; + name = Debug; + }; + 2A5622F62CE7D2E600FC29DB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.TourOfSharingUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = TourOfSharing; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2A5622C52CE7D2E400FC29DB /* Build configuration list for PBXProject "TourOfSharing" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A5622EC2CE7D2E600FC29DB /* Debug */, + 2A5622ED2CE7D2E600FC29DB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A5622EE2CE7D2E600FC29DB /* Build configuration list for PBXNativeTarget "TourOfSharing" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A5622EF2CE7D2E600FC29DB /* Debug */, + 2A5622F02CE7D2E600FC29DB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A5622F12CE7D2E600FC29DB /* Build configuration list for PBXNativeTarget "TourOfSharingTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A5622F22CE7D2E600FC29DB /* Debug */, + 2A5622F32CE7D2E600FC29DB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A5622F42CE7D2E600FC29DB /* Build configuration list for PBXNativeTarget "TourOfSharingUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A5622F52CE7D2E600FC29DB /* Debug */, + 2A5622F62CE7D2E600FC29DB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2A6E8B322CE7E25E00319151 /* XCRemoteSwiftPackageReference "swift-sharing" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-sharing"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2A6E8B332CE7E25E00319151 /* Sharing */ = { + isa = XCSwiftPackageProductDependency; + package = 2A6E8B322CE7E25E00319151 /* XCRemoteSwiftPackageReference "swift-sharing" */; + productName = Sharing; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 2A5622C22CE7D2E400FC29DB /* Project object */; +} diff --git a/0306-sharing-pt2/TourOfSharing/TourOfSharing.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0306-sharing-pt2/TourOfSharing/TourOfSharing.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0306-sharing-pt2/TourOfSharing/TourOfSharing.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0306-sharing-pt2/TourOfSharing/TourOfSharing/Assets.xcassets/AccentColor.colorset/Contents.json b/0306-sharing-pt2/TourOfSharing/TourOfSharing/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0306-sharing-pt2/TourOfSharing/TourOfSharing/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0306-sharing-pt2/TourOfSharing/TourOfSharing/Assets.xcassets/AppIcon.appiconset/Contents.json b/0306-sharing-pt2/TourOfSharing/TourOfSharing/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..23058801 --- /dev/null +++ b/0306-sharing-pt2/TourOfSharing/TourOfSharing/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0306-sharing-pt2/TourOfSharing/TourOfSharing/Assets.xcassets/Contents.json b/0306-sharing-pt2/TourOfSharing/TourOfSharing/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0306-sharing-pt2/TourOfSharing/TourOfSharing/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0306-sharing-pt2/TourOfSharing/TourOfSharing/ContentView.swift b/0306-sharing-pt2/TourOfSharing/TourOfSharing/ContentView.swift new file mode 100644 index 00000000..3f2c2266 --- /dev/null +++ b/0306-sharing-pt2/TourOfSharing/TourOfSharing/ContentView.swift @@ -0,0 +1,254 @@ +import Sharing +import SwiftUI + +struct ManyCountersView: View { + @Dependency(\.defaultAppStorage) var store + + var body: some View { + Form { + Section { + CounterView() + } header: { + Text("@Shared in an observable model") + } + + Section { + AnotherCounterView() + } header: { + Text("@Shared in a view") + } + + Section { + AppStorageCounterView() + } header: { + Text("@AppStorage") + } + + Section { + CounterViewController.Representable() + } header: { + Text("@Shared in a view controller") + } + + Button(#"UserDefaults.set(0, "count")"#) { + UIView.animate(withDuration: 0.35) { + withAnimation { + store.set(0, forKey: "count") + } + } + } + } + } +} + +@Observable +class CounterModel { + @ObservationIgnored + @Shared(.count) var count + //@AppStorage("co.pointfree.countermodel.count") var count = 0 +} + +struct CounterView: View { + @State var model = CounterModel() + + var body: some View { + HStack { + Text("\(model.count)") + .font(.largeTitle) + Button("Decrement") { + model.$count.withLock { $0 -= 1 } + } + Button("Increment") { + model.$count.withLock { $0 += 1 } + } + } + .buttonStyle(.borderless) + } +} + +struct AnotherCounterView: View { + @Shared(.count) var count + + var body: some View { + HStack { + Text("\(count)") + .font(.largeTitle) + Button("Decrement") { + $count.withLock { $0 -= 1 } + } + Button("Increment") { + $count.withLock { $0 += 1 } + } + } + .buttonStyle(.borderless) + } +} + +struct ToggleView: View { + @AppStorage(.count) var count = false + var body: some View { + HStack { + Text("\(count)") + Button("Toggle") { count.toggle() } + } + } +} +struct OtherView: View { + @AppStorage(.count) var count = 100 + var body: some View { EmptyView() } +} +extension String { static var count: String { "count" } } + +struct AppStorageCounterView: View { + @AppStorage(.count) var count = 0 + + var body: some View { + HStack { + Text("\(count)") + .font(.largeTitle) + Button("Decrement") { + count -= 1 + } + Button("Increment") { + count += 1 + } + } + .buttonStyle(.borderless) + + ToggleView() + } +} + +extension SharedKey where Self == AppStorageKey.Default { + static var count: Self { + Self[.appStorage("count"), default: 0] + } +} + +#Preview("CounterView") { + CounterView() +} + +import Dependencies +#Preview( + "ManyCountersView" + //, traits: .dependency(\.defaultAppStorage, .standard) +) { + @Dependency(\.defaultAppStorage) var store + ManyCountersView() + .defaultAppStorage(store) +} + +import Combine + +final class CounterViewController: UIViewController { + @Shared(.count) var count + var cancellables: Set = [] + + struct Representable: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> CounterViewController { + CounterViewController() + } + func updateUIViewController(_ uiViewController: CounterViewController, context: Context) { + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.distribution = .fillProportionally + stackView.spacing = 8 + view.addSubview(stackView) + + let countLabel = UILabel() + countLabel.textColor = .black + countLabel.font = .preferredFont(forTextStyle: .largeTitle) + stackView.addArrangedSubview(countLabel) + + let decrementButton = UIButton(type: .system) + decrementButton.setTitle("Decrement", for: .normal) + decrementButton.titleLabel?.font = .preferredFont(forTextStyle: .body) + decrementButton.addAction( + UIAction { [$count] _ in + $count.withLock { $0 -= 1 } + }, + for: .touchUpInside + ) + stackView.addArrangedSubview(decrementButton) + + let incrementButton = UIButton(type: .system) + incrementButton.setTitle("Increment", for: .normal) + incrementButton.titleLabel?.font = .preferredFont(forTextStyle: .body) + incrementButton.addAction( + UIAction { [$count] _ in + $count.withLock { $0 += 1 } + }, + for: .touchUpInside + ) + stackView.addArrangedSubview(incrementButton) + + NSLayoutConstraint.activate([ + stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + stackView.topAnchor.constraint(equalTo: view.topAnchor), + stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + stackView.heightAnchor.constraint(equalToConstant: 300) + ]) + + $count.publisher + .sink { + countLabel.text = $0.description + } + .store(in: &cancellables) + +// observe { [weak self] in +// guard let self else { return } +// countLabel.text = count.description +// } + } +} + +struct AppStorageRaceCondition: View { + @AppStorage("racey-count") var count = 0 + var body: some View { + Form { + Text("\(count)") + Button("Race!") { + Task { + await withTaskGroup(of: Void.self) { group in + for _ in 1...1_000 { + group.addTask { + await MainActor.run { + count += 1 + } + //_count.wrappedValue += 1 + } + } + } + } + } + } + } +} + +#Preview("AppStorage race condition") { + AppStorageRaceCondition() +} + +#Preview("AppStorageCounterView: Large count") { + let _ = UserDefaults.standard.set(10_000, forKey: "count") + AppStorageCounterView() +} +#Preview("AppStorageCounterView: Version 2") { + let _ = UserDefaults.standard.set(0, forKey: "count") + AppStorageCounterView() +} + +#Preview("CounterView: Large count") { + @Shared(.count) var count = 1_000_000 + CounterView() +} +#Preview("CounterView: Version 2") { + CounterView() +} diff --git a/0306-sharing-pt2/TourOfSharing/TourOfSharing/TourOfSharingApp.swift b/0306-sharing-pt2/TourOfSharing/TourOfSharing/TourOfSharingApp.swift new file mode 100644 index 00000000..df07b9ae --- /dev/null +++ b/0306-sharing-pt2/TourOfSharing/TourOfSharing/TourOfSharingApp.swift @@ -0,0 +1,11 @@ +import SwiftUI + +@main +struct TourOfSharingApp: App { + var body: some Scene { + WindowGroup { + ManyCountersView() + //CounterView() + } + } +} diff --git a/0306-sharing-pt2/TourOfSharing/TourOfSharingTests/TourOfSharingTests.swift b/0306-sharing-pt2/TourOfSharing/TourOfSharingTests/TourOfSharingTests.swift new file mode 100644 index 00000000..7087a6c9 --- /dev/null +++ b/0306-sharing-pt2/TourOfSharing/TourOfSharingTests/TourOfSharingTests.swift @@ -0,0 +1,17 @@ +// +// TourOfSharingTests.swift +// TourOfSharingTests +// +// Created by Point-Free on 11/15/24. +// + +import Testing +@testable import TourOfSharing + +struct TourOfSharingTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/0306-sharing-pt2/TourOfSharing/TourOfSharingUITests/TourOfSharingUITests.swift b/0306-sharing-pt2/TourOfSharing/TourOfSharingUITests/TourOfSharingUITests.swift new file mode 100644 index 00000000..5fac01f5 --- /dev/null +++ b/0306-sharing-pt2/TourOfSharing/TourOfSharingUITests/TourOfSharingUITests.swift @@ -0,0 +1,43 @@ +// +// TourOfSharingUITests.swift +// TourOfSharingUITests +// +// Created by Point-Free on 11/15/24. +// + +import XCTest + +final class TourOfSharingUITests: 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 it’s 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 { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/0306-sharing-pt2/TourOfSharing/TourOfSharingUITests/TourOfSharingUITestsLaunchTests.swift b/0306-sharing-pt2/TourOfSharing/TourOfSharingUITests/TourOfSharingUITestsLaunchTests.swift new file mode 100644 index 00000000..30a26050 --- /dev/null +++ b/0306-sharing-pt2/TourOfSharing/TourOfSharingUITests/TourOfSharingUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// TourOfSharingUITestsLaunchTests.swift +// TourOfSharingUITests +// +// Created by Point-Free on 11/15/24. +// + +import XCTest + +final class TourOfSharingUITestsLaunchTests: 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) + } +} diff --git a/README.md b/README.md index 49fb8a2c..59a15f37 100644 --- a/README.md +++ b/README.md @@ -305,3 +305,6 @@ This repository is the home of code written on episodes of [Point-Free](https:// 1. [SQLite: The C Library](0301-sqlite-pt1) 1. [SQLite: GRDB](0302-sqlite-pt2) 1. [SQLite: SwiftUI](0303-sqlite-pt3) +1. [SQLite: Observation](0304-sqlite-pt4) +1. [Tour of Sharing: App Storate, Part 1](0305-sharing-pt1) +1. [Tour of Sharing: App Storate, Part 2](0306-sharing-pt2)