├── .gitignore ├── LICENSE ├── README.md ├── Testo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── Testo.xcscheme └── Testo ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── Base.lproj └── LaunchScreen.storyboard ├── BindingExampleOne.swift ├── BindingExampleTwo.swift ├── ContentView.swift ├── DistinctObjectsExample.swift ├── EnvironmentExampleOne.swift ├── EnvironmentExampleTwo.swift ├── EnvironmentObjectExample.swift ├── Info.plist ├── ObservedObjectExampleOne.swift ├── ObservedObjectExampleTwo.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json ├── SceneDelegate.swift ├── StateExampleOne.swift └── StateExampleTwo.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | # Ugggghhh 93 | *.DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jared Sinclair 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swiftui-property-wrappers 2 | Examples of appropriate usage of SwiftUI property wrappers. 3 | -------------------------------------------------------------------------------- /Testo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7F0D2D00246187CD007D5B3F /* ObservedObjectExampleOne.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0D2CFF246187CC007D5B3F /* ObservedObjectExampleOne.swift */; }; 11 | 7F0D2D0424619CDD007D5B3F /* EnvironmentObjectExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0D2D0324619CDD007D5B3F /* EnvironmentObjectExample.swift */; }; 12 | 7F0D2D052461A202007D5B3F /* ObservedObjectExampleTwo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0D2D0124618966007D5B3F /* ObservedObjectExampleTwo.swift */; }; 13 | 7F2D1F4B2464BDA50017BA67 /* EnvironmentExampleOne.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2D1F4A2464BDA50017BA67 /* EnvironmentExampleOne.swift */; }; 14 | 7F2D1F4D2464C2070017BA67 /* EnvironmentExampleTwo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2D1F4C2464C2070017BA67 /* EnvironmentExampleTwo.swift */; }; 15 | 7F2D1F4F2464CE2F0017BA67 /* DistinctObjectsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2D1F4E2464CE2F0017BA67 /* DistinctObjectsExample.swift */; }; 16 | 7FB3EC85244F30430089854F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB3EC84244F30430089854F /* AppDelegate.swift */; }; 17 | 7FB3EC87244F30430089854F /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB3EC86244F30430089854F /* SceneDelegate.swift */; }; 18 | 7FB3EC89244F30430089854F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB3EC88244F30430089854F /* ContentView.swift */; }; 19 | 7FB3EC8B244F30470089854F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7FB3EC8A244F30470089854F /* Assets.xcassets */; }; 20 | 7FB3EC8E244F30470089854F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7FB3EC8D244F30470089854F /* Preview Assets.xcassets */; }; 21 | 7FB3EC91244F30470089854F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7FB3EC8F244F30470089854F /* LaunchScreen.storyboard */; }; 22 | 7FB3EC99244F37CA0089854F /* StateExampleOne.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB3EC98244F37CA0089854F /* StateExampleOne.swift */; }; 23 | 7FB3EC9B244F38910089854F /* StateExampleTwo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB3EC9A244F38910089854F /* StateExampleTwo.swift */; }; 24 | 7FC811ED24508EB4002B2487 /* BindingExampleOne.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FC811EC24508EB4002B2487 /* BindingExampleOne.swift */; }; 25 | 7FC811F32451E853002B2487 /* BindingExampleTwo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FC811F22451E853002B2487 /* BindingExampleTwo.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | 7F0D2CFF246187CC007D5B3F /* ObservedObjectExampleOne.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservedObjectExampleOne.swift; sourceTree = ""; }; 30 | 7F0D2D0124618966007D5B3F /* ObservedObjectExampleTwo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservedObjectExampleTwo.swift; sourceTree = ""; }; 31 | 7F0D2D0324619CDD007D5B3F /* EnvironmentObjectExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentObjectExample.swift; sourceTree = ""; }; 32 | 7F2D1F4A2464BDA50017BA67 /* EnvironmentExampleOne.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentExampleOne.swift; sourceTree = ""; }; 33 | 7F2D1F4C2464C2070017BA67 /* EnvironmentExampleTwo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentExampleTwo.swift; sourceTree = ""; }; 34 | 7F2D1F4E2464CE2F0017BA67 /* DistinctObjectsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistinctObjectsExample.swift; sourceTree = ""; }; 35 | 7FB3EC81244F30430089854F /* Testo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Testo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 7FB3EC84244F30430089854F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7FB3EC86244F30430089854F /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 38 | 7FB3EC88244F30430089854F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 39 | 7FB3EC8A244F30470089854F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 40 | 7FB3EC8D244F30470089854F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 41 | 7FB3EC90244F30470089854F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 42 | 7FB3EC92244F30470089854F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43 | 7FB3EC98244F37CA0089854F /* StateExampleOne.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateExampleOne.swift; sourceTree = ""; }; 44 | 7FB3EC9A244F38910089854F /* StateExampleTwo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateExampleTwo.swift; sourceTree = ""; }; 45 | 7FC811EC24508EB4002B2487 /* BindingExampleOne.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindingExampleOne.swift; sourceTree = ""; }; 46 | 7FC811F22451E853002B2487 /* BindingExampleTwo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindingExampleTwo.swift; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | 7FB3EC7E244F30430089854F /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | /* End PBXFrameworksBuildPhase section */ 58 | 59 | /* Begin PBXGroup section */ 60 | 7FB3EC78244F30430089854F = { 61 | isa = PBXGroup; 62 | children = ( 63 | 7FB3EC83244F30430089854F /* Testo */, 64 | 7FB3EC82244F30430089854F /* Products */, 65 | ); 66 | sourceTree = ""; 67 | }; 68 | 7FB3EC82244F30430089854F /* Products */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 7FB3EC81244F30430089854F /* Testo.app */, 72 | ); 73 | name = Products; 74 | sourceTree = ""; 75 | }; 76 | 7FB3EC83244F30430089854F /* Testo */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 7FB3EC84244F30430089854F /* AppDelegate.swift */, 80 | 7FB3EC86244F30430089854F /* SceneDelegate.swift */, 81 | 7FB3EC88244F30430089854F /* ContentView.swift */, 82 | 7FB3EC98244F37CA0089854F /* StateExampleOne.swift */, 83 | 7FB3EC9A244F38910089854F /* StateExampleTwo.swift */, 84 | 7FC811EC24508EB4002B2487 /* BindingExampleOne.swift */, 85 | 7FC811F22451E853002B2487 /* BindingExampleTwo.swift */, 86 | 7F0D2CFF246187CC007D5B3F /* ObservedObjectExampleOne.swift */, 87 | 7F0D2D0124618966007D5B3F /* ObservedObjectExampleTwo.swift */, 88 | 7F0D2D0324619CDD007D5B3F /* EnvironmentObjectExample.swift */, 89 | 7F2D1F4A2464BDA50017BA67 /* EnvironmentExampleOne.swift */, 90 | 7F2D1F4C2464C2070017BA67 /* EnvironmentExampleTwo.swift */, 91 | 7F2D1F4E2464CE2F0017BA67 /* DistinctObjectsExample.swift */, 92 | 7FB3EC8A244F30470089854F /* Assets.xcassets */, 93 | 7FB3EC8F244F30470089854F /* LaunchScreen.storyboard */, 94 | 7FB3EC92244F30470089854F /* Info.plist */, 95 | 7FB3EC8C244F30470089854F /* Preview Content */, 96 | ); 97 | path = Testo; 98 | sourceTree = ""; 99 | }; 100 | 7FB3EC8C244F30470089854F /* Preview Content */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 7FB3EC8D244F30470089854F /* Preview Assets.xcassets */, 104 | ); 105 | path = "Preview Content"; 106 | sourceTree = ""; 107 | }; 108 | /* End PBXGroup section */ 109 | 110 | /* Begin PBXNativeTarget section */ 111 | 7FB3EC80244F30430089854F /* Testo */ = { 112 | isa = PBXNativeTarget; 113 | buildConfigurationList = 7FB3EC95244F30470089854F /* Build configuration list for PBXNativeTarget "Testo" */; 114 | buildPhases = ( 115 | 7FB3EC7D244F30430089854F /* Sources */, 116 | 7FB3EC7E244F30430089854F /* Frameworks */, 117 | 7FB3EC7F244F30430089854F /* Resources */, 118 | ); 119 | buildRules = ( 120 | ); 121 | dependencies = ( 122 | ); 123 | name = Testo; 124 | productName = Testo; 125 | productReference = 7FB3EC81244F30430089854F /* Testo.app */; 126 | productType = "com.apple.product-type.application"; 127 | }; 128 | /* End PBXNativeTarget section */ 129 | 130 | /* Begin PBXProject section */ 131 | 7FB3EC79244F30430089854F /* Project object */ = { 132 | isa = PBXProject; 133 | attributes = { 134 | LastSwiftUpdateCheck = 1130; 135 | LastUpgradeCheck = 1130; 136 | ORGANIZATIONNAME = "Big Nerd Ranch LLC"; 137 | TargetAttributes = { 138 | 7FB3EC80244F30430089854F = { 139 | CreatedOnToolsVersion = 11.3.1; 140 | }; 141 | }; 142 | }; 143 | buildConfigurationList = 7FB3EC7C244F30430089854F /* Build configuration list for PBXProject "Testo" */; 144 | compatibilityVersion = "Xcode 9.3"; 145 | developmentRegion = en; 146 | hasScannedForEncodings = 0; 147 | knownRegions = ( 148 | en, 149 | Base, 150 | ); 151 | mainGroup = 7FB3EC78244F30430089854F; 152 | productRefGroup = 7FB3EC82244F30430089854F /* Products */; 153 | projectDirPath = ""; 154 | projectRoot = ""; 155 | targets = ( 156 | 7FB3EC80244F30430089854F /* Testo */, 157 | ); 158 | }; 159 | /* End PBXProject section */ 160 | 161 | /* Begin PBXResourcesBuildPhase section */ 162 | 7FB3EC7F244F30430089854F /* Resources */ = { 163 | isa = PBXResourcesBuildPhase; 164 | buildActionMask = 2147483647; 165 | files = ( 166 | 7FB3EC91244F30470089854F /* LaunchScreen.storyboard in Resources */, 167 | 7FB3EC8E244F30470089854F /* Preview Assets.xcassets in Resources */, 168 | 7FB3EC8B244F30470089854F /* Assets.xcassets in Resources */, 169 | ); 170 | runOnlyForDeploymentPostprocessing = 0; 171 | }; 172 | /* End PBXResourcesBuildPhase section */ 173 | 174 | /* Begin PBXSourcesBuildPhase section */ 175 | 7FB3EC7D244F30430089854F /* Sources */ = { 176 | isa = PBXSourcesBuildPhase; 177 | buildActionMask = 2147483647; 178 | files = ( 179 | 7F0D2D00246187CD007D5B3F /* ObservedObjectExampleOne.swift in Sources */, 180 | 7FC811ED24508EB4002B2487 /* BindingExampleOne.swift in Sources */, 181 | 7F2D1F4B2464BDA50017BA67 /* EnvironmentExampleOne.swift in Sources */, 182 | 7F0D2D052461A202007D5B3F /* ObservedObjectExampleTwo.swift in Sources */, 183 | 7FB3EC85244F30430089854F /* AppDelegate.swift in Sources */, 184 | 7FB3EC87244F30430089854F /* SceneDelegate.swift in Sources */, 185 | 7FB3EC99244F37CA0089854F /* StateExampleOne.swift in Sources */, 186 | 7F2D1F4F2464CE2F0017BA67 /* DistinctObjectsExample.swift in Sources */, 187 | 7F0D2D0424619CDD007D5B3F /* EnvironmentObjectExample.swift in Sources */, 188 | 7FB3EC89244F30430089854F /* ContentView.swift in Sources */, 189 | 7F2D1F4D2464C2070017BA67 /* EnvironmentExampleTwo.swift in Sources */, 190 | 7FB3EC9B244F38910089854F /* StateExampleTwo.swift in Sources */, 191 | 7FC811F32451E853002B2487 /* BindingExampleTwo.swift in Sources */, 192 | ); 193 | runOnlyForDeploymentPostprocessing = 0; 194 | }; 195 | /* End PBXSourcesBuildPhase section */ 196 | 197 | /* Begin PBXVariantGroup section */ 198 | 7FB3EC8F244F30470089854F /* LaunchScreen.storyboard */ = { 199 | isa = PBXVariantGroup; 200 | children = ( 201 | 7FB3EC90244F30470089854F /* Base */, 202 | ); 203 | name = LaunchScreen.storyboard; 204 | sourceTree = ""; 205 | }; 206 | /* End PBXVariantGroup section */ 207 | 208 | /* Begin XCBuildConfiguration section */ 209 | 7FB3EC93244F30470089854F /* Debug */ = { 210 | isa = XCBuildConfiguration; 211 | buildSettings = { 212 | ALWAYS_SEARCH_USER_PATHS = NO; 213 | CLANG_ANALYZER_NONNULL = YES; 214 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 215 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 216 | CLANG_CXX_LIBRARY = "libc++"; 217 | CLANG_ENABLE_MODULES = YES; 218 | CLANG_ENABLE_OBJC_ARC = YES; 219 | CLANG_ENABLE_OBJC_WEAK = YES; 220 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 221 | CLANG_WARN_BOOL_CONVERSION = YES; 222 | CLANG_WARN_COMMA = YES; 223 | CLANG_WARN_CONSTANT_CONVERSION = YES; 224 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 225 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 226 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 227 | CLANG_WARN_EMPTY_BODY = YES; 228 | CLANG_WARN_ENUM_CONVERSION = YES; 229 | CLANG_WARN_INFINITE_RECURSION = YES; 230 | CLANG_WARN_INT_CONVERSION = YES; 231 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 232 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 233 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 234 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 235 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 236 | CLANG_WARN_STRICT_PROTOTYPES = YES; 237 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 238 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 239 | CLANG_WARN_UNREACHABLE_CODE = YES; 240 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 241 | COPY_PHASE_STRIP = NO; 242 | DEBUG_INFORMATION_FORMAT = dwarf; 243 | ENABLE_STRICT_OBJC_MSGSEND = YES; 244 | ENABLE_TESTABILITY = YES; 245 | GCC_C_LANGUAGE_STANDARD = gnu11; 246 | GCC_DYNAMIC_NO_PIC = NO; 247 | GCC_NO_COMMON_BLOCKS = YES; 248 | GCC_OPTIMIZATION_LEVEL = 0; 249 | GCC_PREPROCESSOR_DEFINITIONS = ( 250 | "DEBUG=1", 251 | "$(inherited)", 252 | ); 253 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 254 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 255 | GCC_WARN_UNDECLARED_SELECTOR = YES; 256 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 257 | GCC_WARN_UNUSED_FUNCTION = YES; 258 | GCC_WARN_UNUSED_VARIABLE = YES; 259 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 260 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 261 | MTL_FAST_MATH = YES; 262 | ONLY_ACTIVE_ARCH = YES; 263 | SDKROOT = iphoneos; 264 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 265 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 266 | }; 267 | name = Debug; 268 | }; 269 | 7FB3EC94244F30470089854F /* Release */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | ALWAYS_SEARCH_USER_PATHS = NO; 273 | CLANG_ANALYZER_NONNULL = YES; 274 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 275 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 276 | CLANG_CXX_LIBRARY = "libc++"; 277 | CLANG_ENABLE_MODULES = YES; 278 | CLANG_ENABLE_OBJC_ARC = YES; 279 | CLANG_ENABLE_OBJC_WEAK = YES; 280 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 281 | CLANG_WARN_BOOL_CONVERSION = YES; 282 | CLANG_WARN_COMMA = YES; 283 | CLANG_WARN_CONSTANT_CONVERSION = YES; 284 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 285 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 286 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 287 | CLANG_WARN_EMPTY_BODY = YES; 288 | CLANG_WARN_ENUM_CONVERSION = YES; 289 | CLANG_WARN_INFINITE_RECURSION = YES; 290 | CLANG_WARN_INT_CONVERSION = YES; 291 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 292 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 293 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 294 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 295 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 296 | CLANG_WARN_STRICT_PROTOTYPES = YES; 297 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 298 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 299 | CLANG_WARN_UNREACHABLE_CODE = YES; 300 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 301 | COPY_PHASE_STRIP = NO; 302 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 303 | ENABLE_NS_ASSERTIONS = NO; 304 | ENABLE_STRICT_OBJC_MSGSEND = YES; 305 | GCC_C_LANGUAGE_STANDARD = gnu11; 306 | GCC_NO_COMMON_BLOCKS = YES; 307 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 308 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 309 | GCC_WARN_UNDECLARED_SELECTOR = YES; 310 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 311 | GCC_WARN_UNUSED_FUNCTION = YES; 312 | GCC_WARN_UNUSED_VARIABLE = YES; 313 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 314 | MTL_ENABLE_DEBUG_INFO = NO; 315 | MTL_FAST_MATH = YES; 316 | SDKROOT = iphoneos; 317 | SWIFT_COMPILATION_MODE = wholemodule; 318 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 319 | VALIDATE_PRODUCT = YES; 320 | }; 321 | name = Release; 322 | }; 323 | 7FB3EC96244F30470089854F /* Debug */ = { 324 | isa = XCBuildConfiguration; 325 | buildSettings = { 326 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 327 | CODE_SIGN_STYLE = Automatic; 328 | DEVELOPMENT_ASSET_PATHS = "\"Testo/Preview Content\""; 329 | DEVELOPMENT_TEAM = 442RNUGV2T; 330 | ENABLE_PREVIEWS = YES; 331 | INFOPLIST_FILE = Testo/Info.plist; 332 | LD_RUNPATH_SEARCH_PATHS = ( 333 | "$(inherited)", 334 | "@executable_path/Frameworks", 335 | ); 336 | PRODUCT_BUNDLE_IDENTIFIER = com.niceboy.Testo; 337 | PRODUCT_NAME = "$(TARGET_NAME)"; 338 | SWIFT_VERSION = 5.0; 339 | TARGETED_DEVICE_FAMILY = "1,2"; 340 | }; 341 | name = Debug; 342 | }; 343 | 7FB3EC97244F30470089854F /* Release */ = { 344 | isa = XCBuildConfiguration; 345 | buildSettings = { 346 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 347 | CODE_SIGN_STYLE = Automatic; 348 | DEVELOPMENT_ASSET_PATHS = "\"Testo/Preview Content\""; 349 | DEVELOPMENT_TEAM = 442RNUGV2T; 350 | ENABLE_PREVIEWS = YES; 351 | INFOPLIST_FILE = Testo/Info.plist; 352 | LD_RUNPATH_SEARCH_PATHS = ( 353 | "$(inherited)", 354 | "@executable_path/Frameworks", 355 | ); 356 | PRODUCT_BUNDLE_IDENTIFIER = com.niceboy.Testo; 357 | PRODUCT_NAME = "$(TARGET_NAME)"; 358 | SWIFT_VERSION = 5.0; 359 | TARGETED_DEVICE_FAMILY = "1,2"; 360 | }; 361 | name = Release; 362 | }; 363 | /* End XCBuildConfiguration section */ 364 | 365 | /* Begin XCConfigurationList section */ 366 | 7FB3EC7C244F30430089854F /* Build configuration list for PBXProject "Testo" */ = { 367 | isa = XCConfigurationList; 368 | buildConfigurations = ( 369 | 7FB3EC93244F30470089854F /* Debug */, 370 | 7FB3EC94244F30470089854F /* Release */, 371 | ); 372 | defaultConfigurationIsVisible = 0; 373 | defaultConfigurationName = Release; 374 | }; 375 | 7FB3EC95244F30470089854F /* Build configuration list for PBXNativeTarget "Testo" */ = { 376 | isa = XCConfigurationList; 377 | buildConfigurations = ( 378 | 7FB3EC96244F30470089854F /* Debug */, 379 | 7FB3EC97244F30470089854F /* Release */, 380 | ); 381 | defaultConfigurationIsVisible = 0; 382 | defaultConfigurationName = Release; 383 | }; 384 | /* End XCConfigurationList section */ 385 | }; 386 | rootObject = 7FB3EC79244F30430089854F /* Project object */; 387 | } 388 | -------------------------------------------------------------------------------- /Testo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Testo.xcodeproj/xcshareddata/xcschemes/Testo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Testo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 7 | return true 8 | } 9 | 10 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 11 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Testo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Testo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Testo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Testo/BindingExampleOne.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct BindingExampleOne: View { 4 | @State var isPresentingAlert = false 5 | 6 | var body: some View { 7 | CustomButton(action: { 8 | self.isPresentingAlert = true 9 | }, label: { 10 | Text("Present Custom Alert") 11 | }) 12 | .customAlert(isBeingPresented: $isPresentingAlert) { 13 | CustomAlert(title: Text("Oh no, it has happened."), dismissButtonTitle: Text("Dismiss")) 14 | } 15 | } 16 | } 17 | 18 | struct CustomAlert { 19 | let title: Text 20 | let dismissButtonTitle: Text 21 | } 22 | 23 | extension View { 24 | func customAlert(isBeingPresented: Binding, alert: () -> CustomAlert) -> some View { 25 | CustomAlertView( 26 | isBeingPresented: Binding( 27 | get: { isBeingPresented.wrappedValue }, 28 | set: { newValue in 29 | withAnimation(.easeInOut) { 30 | isBeingPresented.wrappedValue = newValue 31 | } 32 | }), 33 | model: alert(), 34 | presentingContent: AnyView(self) 35 | ) 36 | } 37 | } 38 | 39 | private struct CustomAlertView: View { 40 | 41 | @Binding var isBeingPresented: Bool 42 | let model: CustomAlert 43 | let presentingContent: AnyView 44 | 45 | var body: some View { 46 | ZStack { 47 | presentingContent 48 | GeometryReader { proxy in 49 | ZStack { 50 | Rectangle() 51 | .foregroundColor(.black) 52 | .opacity(0.25) 53 | .aspectRatio(nil, contentMode: .fill) 54 | .edgesIgnoringSafeArea(.all) 55 | .onTapGesture { self.isBeingPresented = false } 56 | VStack(spacing: 10) { 57 | self.model.title 58 | .font(.headline) 59 | .fontWeight(.black) 60 | CustomButton( 61 | action: { 62 | self.isBeingPresented = false 63 | }, label: { 64 | self.model.dismissButtonTitle 65 | .foregroundColor(.blue) 66 | .fontWeight(.bold) 67 | }).aspectRatio(contentMode: .fill) 68 | }.padding(.top, 40) 69 | .padding(.bottom, 20) 70 | .frame(width: proxy.size.width - 40) 71 | .background(Capsule().foregroundColor(.white)) 72 | } 73 | }.opacity(isBeingPresented ? 1 : 0) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Testo/BindingExampleTwo.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | final class Person: ObservableObject { 4 | @Published var name: String = "Bart" 5 | } 6 | 7 | struct BindingExampleTwo: View { 8 | @ObservedObject var person = Person() 9 | 10 | var body: some View { 11 | NamePicker(name: $person.name) 12 | } 13 | } 14 | 15 | struct NamePicker: View { 16 | @Binding var name: String 17 | 18 | var body: some View { 19 | VStack(spacing: 20) { 20 | Text("Tap to change:") 21 | CustomButton(action: { 22 | self.name = names.randomElement()! 23 | }, label: { 24 | Text(self.name) 25 | }) 26 | } 27 | } 28 | } 29 | 30 | private let names = [ 31 | "Homer Simpson", 32 | "Marge Simpson", 33 | "Bart Simpson", 34 | "Lisa Simpson", 35 | "Maggie Simpson", 36 | "Abraham Simpson", 37 | "Santa's Little Helper", 38 | "Snowball II/V", 39 | "Apu Nahasapeemapetilon", 40 | "Barney Gumble", 41 | "Bleeding Gums Murphy[B]", 42 | "Chief Clancy Wiggum", 43 | "Dewey Largo", 44 | "Eddie", 45 | "Edna Krabappel", 46 | "Itchy", 47 | "Janey Powell", 48 | "Jasper Beardsley", 49 | "Kent Brockman", 50 | "Krusty The Clown", 51 | "Lenny Leonard", 52 | "Lou", 53 | "Martin Prince", 54 | "Marvin Monroe[C]", 55 | "Milhouse Van Houten", 56 | "Moe Szyslak", 57 | "Mr. Burns", 58 | "Ned Flanders", 59 | "Otto Mann", 60 | "Patty Bouvier", 61 | "Ralph Wiggum", 62 | "Reverend Timothy Lovejoy", 63 | "Scratchy", 64 | "Selma Bouvier", 65 | "Seymour Skinner", 66 | "Sherri", 67 | "Sideshow Bob", 68 | "Terri", 69 | "Todd Flanders", 70 | "Waylon Smithers", 71 | "Wendell Borton", 72 | "Bernice Hibbert", 73 | "Surly Duff" 74 | ] 75 | -------------------------------------------------------------------------------- /Testo/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | @State var isPresentingAlert = false 5 | 6 | var body: some View { 7 | List(Example.allCases) { example in 8 | NavigationLink( 9 | destination: example.view.navigationBarTitle(example.title), 10 | label: { Text(example.title) } 11 | ) 12 | }.navigationBarTitle(Text("Property Wrappers"), displayMode: .large) 13 | } 14 | 15 | } 16 | 17 | enum Example: String, CaseIterable, Identifiable { 18 | case stateOne, stateTwo 19 | case bindingOne, bindingTwo 20 | case observedObjectOne, observedObjectTwo 21 | case environmentObject 22 | case environmentOne, environmentTwo 23 | case distinctObjects 24 | 25 | var id: String { rawValue } 26 | 27 | var title: String { 28 | switch self { 29 | case .stateOne: return "State 1" 30 | case .stateTwo: return "State 2" 31 | case .bindingOne: return "Binding 1" 32 | case .bindingTwo: return "Binding 2" 33 | case .observedObjectOne: return "ObservedObject 1" 34 | case .observedObjectTwo: return "ObservedObject 2" 35 | case .environmentObject: return "EnvironmentObject" 36 | case .environmentOne: return "Environment 1" 37 | case .environmentTwo: return "Environment 2" 38 | case .distinctObjects: return "Distinct Objects" 39 | } 40 | } 41 | 42 | var view: AnyView { 43 | switch self { 44 | 45 | case .stateOne: 46 | return StateExampleOne().erased 47 | 48 | case .stateTwo: 49 | return StateExampleTwo().erased 50 | 51 | case .bindingOne: 52 | return BindingExampleOne().erased 53 | 54 | case .bindingTwo: 55 | return BindingExampleTwo().erased 56 | 57 | case .observedObjectOne: 58 | return ObservedObjectExampleOne().erased 59 | 60 | case .observedObjectTwo: 61 | return ObservedObjectExampleTwo( 62 | dessertFetcher: DessertFetcher( 63 | preferences: DessertFetcher.UserPreferences( 64 | toleratesMint: false 65 | ) 66 | ) 67 | ).erased 68 | 69 | case .environmentObject: 70 | return EnvironmentObjectExample() 71 | .environmentObject(VegetableFetcher()) 72 | .erased 73 | 74 | case .environmentOne: 75 | return EnvironmentExampleOne() 76 | .environment(\.theme, PinkTheme()) 77 | .erased 78 | 79 | case .environmentTwo: 80 | return EnvironmentExampleTwo() 81 | .environment(\.positiveTheme, PositiveTheme()) 82 | .environment(\.negativeTheme, NegativeTheme()) 83 | .erased 84 | 85 | case .distinctObjects: 86 | return DistinctObjectsExample() 87 | .environment(\.posts, Microservice.posts) 88 | .environment(\.users, Microservice.users) 89 | .environment(\.channels, Microservice.channels) 90 | .erased 91 | } 92 | } 93 | } 94 | 95 | extension View { 96 | var erased: AnyView { 97 | return AnyView(self) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Testo/DistinctObjectsExample.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct DistinctObjectsExample: View { 4 | 5 | @DistinctEnvironmentObject(\.posts) var postsService: Microservice 6 | @DistinctEnvironmentObject(\.users) var usersService: Microservice 7 | @DistinctEnvironmentObject(\.channels) var channelsService: Microservice 8 | 9 | var body: some View { 10 | Form { 11 | Section(header: Text("Posts")) { 12 | List(postsService.content, id: \.self) { 13 | Text($0) 14 | } 15 | } 16 | 17 | Section(header: Text("Users")) { 18 | List(usersService.content, id: \.self) { 19 | Text($0) 20 | } 21 | } 22 | 23 | Section(header: Text("Channels")) { 24 | List(channelsService.content, id: \.self) { 25 | Text($0) 26 | } 27 | } 28 | }.onAppear(perform: fetchContent) 29 | } 30 | 31 | func fetchContent() { 32 | postsService.fetchContent() 33 | usersService.fetchContent() 34 | channelsService.fetchContent() 35 | } 36 | } 37 | 38 | // MARK: - Property Wrapper To Make This All Work 39 | 40 | @propertyWrapper 41 | struct DistinctEnvironmentObject: DynamicProperty where Wrapped : ObservableObject { 42 | var wrappedValue: Wrapped { 43 | _wrapped 44 | } 45 | 46 | @ObservedObject private var _wrapped: Wrapped 47 | 48 | init(_ keypath: KeyPath) { 49 | _wrapped = Environment(keypath).wrappedValue 50 | } 51 | } 52 | 53 | // MARK: - Dependencies 54 | 55 | class Microservice: ObservableObject { 56 | @Published private(set) var content: [String] = [] 57 | 58 | func fetchContent() { 59 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 60 | self.content = self.fakeContentForDemo 61 | } 62 | } 63 | 64 | private let fakeContentForDemo: [String] 65 | 66 | init(fakeContentForDemo: [String]) { 67 | self.fakeContentForDemo = fakeContentForDemo 68 | } 69 | } 70 | 71 | extension Microservice { 72 | static let posts = Microservice( 73 | fakeContentForDemo: ["What about Mesa Verde?", "S'all good, man."] 74 | ) 75 | } 76 | 77 | extension Microservice { 78 | static let users = Microservice( 79 | fakeContentForDemo: ["Jimmy", "Kim", "Mike"] 80 | ) 81 | } 82 | 83 | extension Microservice { 84 | static let channels = Microservice( 85 | fakeContentForDemo: ["#watercooler", "#gifs", "#drugtrafficking"] 86 | ) 87 | } 88 | 89 | // MARK: - Environment Boilerplate 90 | 91 | struct PostsServiceKey: EnvironmentKey { 92 | static var defaultValue: Microservice { 93 | return Microservice.posts 94 | } 95 | } 96 | 97 | struct UsersServiceKey: EnvironmentKey { 98 | static var defaultValue: Microservice { 99 | return Microservice.users 100 | } 101 | } 102 | 103 | struct ChannelsServiceKey: EnvironmentKey { 104 | static var defaultValue: Microservice { 105 | return Microservice.channels 106 | } 107 | } 108 | 109 | extension EnvironmentValues { 110 | var posts: Microservice { 111 | get { return self[PostsServiceKey.self] } 112 | set { self[PostsServiceKey.self] = newValue } 113 | } 114 | 115 | var users: Microservice { 116 | get { return self[UsersServiceKey.self] } 117 | set { self[UsersServiceKey.self] = newValue } 118 | } 119 | 120 | var channels: Microservice { 121 | get { return self[ChannelsServiceKey.self] } 122 | set { self[ChannelsServiceKey.self] = newValue } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Testo/EnvironmentExampleOne.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct EnvironmentExampleOne: View { 4 | @Environment(\.theme) var theme: Theme 5 | 6 | var body: some View { 7 | Text("CLIPPER SHIPS\n\nMe and my dad make models of clipper ships. I like clipper ships because they are fast. Clipper ships sail on the ocean. Clipper ships never sail on rivers or lakes. Clipper ships have lots of sails and are made of wood.\n\n~ Matt Montini") 8 | .foregroundColor(theme.foregroundColor) 9 | .padding(32) 10 | .background(theme.backgroundColor) 11 | .cornerRadius(32) 12 | .padding(32) 13 | } 14 | } 15 | 16 | // MARK: - Dependencies 17 | 18 | protocol Theme { 19 | var foregroundColor: Color { get } 20 | var backgroundColor: Color { get } 21 | } 22 | 23 | struct PinkTheme: Theme { 24 | var foregroundColor: Color { .white } 25 | var backgroundColor: Color { .pink } 26 | } 27 | 28 | struct GreenTheme: Theme { 29 | var foregroundColor: Color { .yellow } 30 | var backgroundColor: Color { .green } 31 | } 32 | 33 | struct BlueTheme: Theme { 34 | var foregroundColor: Color { .white } 35 | var backgroundColor: Color { .blue } 36 | } 37 | 38 | // MARK: - Environment Boilerplate 39 | 40 | struct ThemeKey: EnvironmentKey { 41 | static var defaultValue: Theme { 42 | return BlueTheme() 43 | } 44 | } 45 | 46 | extension EnvironmentValues { 47 | var theme: Theme { 48 | get { return self[ThemeKey.self] } 49 | set { self[ThemeKey.self] = newValue } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Testo/EnvironmentExampleTwo.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct EnvironmentExampleTwo: View { 4 | @Environment(\.positiveTheme) var positiveTheme: Theme 5 | @Environment(\.negativeTheme) var negativeTheme: Theme 6 | 7 | var body: some View { 8 | VStack(spacing: 40) { 9 | Text("Positive") 10 | .fontWeight(.black) 11 | .font(.largeTitle) 12 | .foregroundColor(positiveTheme.foregroundColor) 13 | .padding(32) 14 | .background( 15 | Capsule().foregroundColor(positiveTheme.backgroundColor)) 16 | Text("Negative") 17 | .fontWeight(.black) 18 | .font(.largeTitle) 19 | .foregroundColor(negativeTheme.foregroundColor) 20 | .padding(32) 21 | .background( 22 | Capsule().foregroundColor(negativeTheme.backgroundColor)) 23 | } 24 | } 25 | } 26 | 27 | // MARK: - Dependencies 28 | 29 | struct PositiveTheme: Theme { 30 | var foregroundColor: Color { .white } 31 | var backgroundColor: Color { .green } 32 | } 33 | 34 | struct NegativeTheme: Theme { 35 | var foregroundColor: Color { .white } 36 | var backgroundColor: Color { .red } 37 | } 38 | 39 | // MARK: - Environment Boilerplate 40 | 41 | struct PositiveThemeKey: EnvironmentKey { 42 | static var defaultValue: Theme { 43 | return PositiveTheme() 44 | } 45 | } 46 | 47 | struct NegativeThemeKey: EnvironmentKey { 48 | static var defaultValue: Theme { 49 | return NegativeTheme() 50 | } 51 | } 52 | 53 | extension EnvironmentValues { 54 | var positiveTheme: Theme { 55 | get { return self[PositiveThemeKey.self] } 56 | set { self[PositiveThemeKey.self] = newValue } 57 | } 58 | 59 | var negativeTheme: Theme { 60 | get { return self[NegativeThemeKey.self] } 61 | set { self[NegativeThemeKey.self] = newValue } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Testo/EnvironmentObjectExample.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct EnvironmentObjectExample: View { 4 | @EnvironmentObject var veggieFetcher: VegetableFetcher 5 | 6 | var body: some View { 7 | List(veggieFetcher.veggies) { 8 | Text($0.name) 9 | }.onAppear { 10 | self.veggieFetcher.fetch() 11 | } 12 | } 13 | } 14 | 15 | struct SomeParentView: View { 16 | var body: some View { 17 | EnvironmentObjectExample() 18 | } 19 | } 20 | 21 | struct SomeGrandparentView: View { 22 | var body: some View { 23 | SomeParentView() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Testo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Testo/ObservedObjectExampleOne.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ObservedObjectExampleOne: View { 4 | @ObservedObject var veggieFetcher = VegetableFetcher() 5 | 6 | var body: some View { 7 | List(veggieFetcher.veggies) { 8 | Text($0.name) 9 | }.onAppear { 10 | self.veggieFetcher.fetch() 11 | } 12 | } 13 | } 14 | 15 | class VegetableFetcher: ObservableObject { 16 | @Published private(set) var veggies: [Veggie] = [] 17 | 18 | struct Veggie: Identifiable { 19 | let id: String 20 | let name: String 21 | let isActuallyAFruit: Bool 22 | } 23 | 24 | func fetch() { 25 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 26 | self.veggies = [ 27 | Veggie(id: "a", name: "Carrots", isActuallyAFruit: false), 28 | Veggie(id: "b", name: "Peas", isActuallyAFruit: false), 29 | Veggie(id: "c", name: "Tomatoes", isActuallyAFruit: true), 30 | Veggie(id: "d", name: "Squash", isActuallyAFruit: true), 31 | Veggie(id: "e", name: "Asparagus", isActuallyAFruit: false) 32 | ] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Testo/ObservedObjectExampleTwo.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ObservedObjectExampleTwo: View { 4 | @ObservedObject var dessertFetcher: DessertFetcher 5 | 6 | var body: some View { 7 | List(dessertFetcher.desserts) { 8 | Text($0.name) 9 | }.onAppear { 10 | self.dessertFetcher.fetch() 11 | } 12 | } 13 | } 14 | 15 | extension UIViewController { 16 | 17 | func observedObjectExampleTwo() -> UIViewController { 18 | let fetcher = DessertFetcher(preferences: .init(toleratesMint: false)) 19 | let view = ObservedObjectExampleTwo(dessertFetcher: fetcher) 20 | let host = UIHostingController(rootView: view) 21 | return host 22 | } 23 | 24 | } 25 | 26 | class DessertFetcher: ObservableObject { 27 | @Published private(set) var desserts: [Dessert] = [] 28 | 29 | let preferences: UserPreferences 30 | 31 | init(preferences: UserPreferences) { 32 | self.preferences = preferences 33 | } 34 | 35 | struct UserPreferences { 36 | let toleratesMint: Bool 37 | } 38 | 39 | struct Dessert: Identifiable { 40 | let id: String 41 | let name: String 42 | let isMinty: Bool 43 | } 44 | 45 | func fetch() { 46 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 47 | self.desserts = [ 48 | Dessert(id: "a", name: "Strawberry Ice Cream", isMinty: false), 49 | Dessert(id: "c", name: "Creme Bruleé", isMinty: false), 50 | Dessert(id: "d", name: "Tiramisu", isMinty: false), 51 | ] 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Testo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Testo/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | 4 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 5 | var window: UIWindow? 6 | 7 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 8 | if let windowScene = scene as? UIWindowScene { 9 | let window = UIWindow(windowScene: windowScene) 10 | let nav = NavigationView { ContentView() } 11 | window.rootViewController = UIHostingController(rootView: nav) 12 | self.window = window 13 | window.makeKeyAndVisible() 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Testo/StateExampleOne.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct StateExampleOne: View { 4 | var body: some View { 5 | CustomButton( 6 | action: { 7 | print("You pressed me!") 8 | }, label: { 9 | Text("Press Me") 10 | }) 11 | } 12 | } 13 | 14 | struct CustomButton