├── .github └── ISSUE_TEMPLATE │ ├── bugs.md │ └── new-features.md ├── .gitignore ├── LICENSE ├── Layers.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── raphaelsalaja.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── Layers ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── Dog.imageset │ │ ├── Contents.json │ │ └── Dog.jpg │ ├── Drake.imageset │ │ ├── Contents.json │ │ └── Drake.png │ ├── Raphael.imageset │ │ ├── Contents.json │ │ └── Raphael.jpg │ ├── Ronaldo.imageset │ │ ├── Contents.json │ │ └── Ronaldo.jpg │ └── Smart.imageset │ │ ├── Contents.json │ │ └── Smart.jpg ├── Example │ ├── LayerExample.swift │ └── LayerExampleComponents.swift ├── LayersApp.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── Source │ ├── Layer+Extensions.swift │ └── Layer.swift └── README.md /.github/ISSUE_TEMPLATE/bugs.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bugs 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: rafunderscore 7 | 8 | --- 9 | 10 | ### Problem 11 | > Please provide a clear and concise description of what the bug is. 12 | 13 | ### To Reproduce 14 | > Outline the steps needed to reproduce the problem 15 | 16 | ### Expected Behaviour 17 | > Provide a clear and concise description of what you expected to happen. 18 | 19 | ### Screenshots 20 | > If applicable, add screenshots to help explain your problem. 21 | 22 | ### Additional context 23 | > Add any other context about the problem here. You can also add solutions to the problem if you have them. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-features.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Features 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: rafunderscore 7 | 8 | --- 9 | 10 | ### Feature Description 11 | > A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Raphael Salaja 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 | -------------------------------------------------------------------------------- /Layers.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 111150222AC8CCDF00C0D356 /* LayersApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111150212AC8CCDF00C0D356 /* LayersApp.swift */; }; 11 | 111150262AC8CCE100C0D356 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 111150252AC8CCE100C0D356 /* Assets.xcassets */; }; 12 | 111150292AC8CCE100C0D356 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 111150282AC8CCE100C0D356 /* Preview Assets.xcassets */; }; 13 | 111150412AC8CD5A00C0D356 /* Layer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111150302AC8CD5A00C0D356 /* Layer.swift */; }; 14 | 1111504B2AC8CD5A00C0D356 /* LayerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1111503A2AC8CD5A00C0D356 /* LayerExample.swift */; }; 15 | 11C5D0F62AD0413F004A1474 /* LayerExampleComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C5D0F52AD0413F004A1474 /* LayerExampleComponents.swift */; }; 16 | 11C5D0F92AD04B34004A1474 /* Layer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C5D0F82AD04B34004A1474 /* Layer+Extensions.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 1111501E2AC8CCDF00C0D356 /* Layers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Layers.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 111150212AC8CCDF00C0D356 /* LayersApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayersApp.swift; sourceTree = ""; }; 22 | 111150252AC8CCE100C0D356 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23 | 111150282AC8CCE100C0D356 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 24 | 111150302AC8CD5A00C0D356 /* Layer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Layer.swift; sourceTree = ""; }; 25 | 1111503A2AC8CD5A00C0D356 /* LayerExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayerExample.swift; sourceTree = ""; }; 26 | 11C5D0F52AD0413F004A1474 /* LayerExampleComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayerExampleComponents.swift; sourceTree = ""; }; 27 | 11C5D0F82AD04B34004A1474 /* Layer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Layer+Extensions.swift"; sourceTree = ""; }; 28 | /* End PBXFileReference section */ 29 | 30 | /* Begin PBXFrameworksBuildPhase section */ 31 | 1111501B2AC8CCDF00C0D356 /* Frameworks */ = { 32 | isa = PBXFrameworksBuildPhase; 33 | buildActionMask = 2147483647; 34 | files = ( 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | 111150152AC8CCDF00C0D356 = { 42 | isa = PBXGroup; 43 | children = ( 44 | 111150202AC8CCDF00C0D356 /* Layers */, 45 | 1111501F2AC8CCDF00C0D356 /* Products */, 46 | ); 47 | sourceTree = ""; 48 | }; 49 | 1111501F2AC8CCDF00C0D356 /* Products */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 1111501E2AC8CCDF00C0D356 /* Layers.app */, 53 | ); 54 | name = Products; 55 | sourceTree = ""; 56 | }; 57 | 111150202AC8CCDF00C0D356 /* Layers */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 111150212AC8CCDF00C0D356 /* LayersApp.swift */, 61 | 111150252AC8CCE100C0D356 /* Assets.xcassets */, 62 | 11C5D0F72AD042AC004A1474 /* Source */, 63 | 11C5D0EE2AD03DAA004A1474 /* Example */, 64 | 111150272AC8CCE100C0D356 /* Preview Content */, 65 | ); 66 | path = Layers; 67 | sourceTree = ""; 68 | }; 69 | 111150272AC8CCE100C0D356 /* Preview Content */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 111150282AC8CCE100C0D356 /* Preview Assets.xcassets */, 73 | ); 74 | path = "Preview Content"; 75 | sourceTree = ""; 76 | }; 77 | 11C5D0EE2AD03DAA004A1474 /* Example */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | 1111503A2AC8CD5A00C0D356 /* LayerExample.swift */, 81 | 11C5D0F52AD0413F004A1474 /* LayerExampleComponents.swift */, 82 | ); 83 | path = Example; 84 | sourceTree = ""; 85 | }; 86 | 11C5D0F72AD042AC004A1474 /* Source */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 111150302AC8CD5A00C0D356 /* Layer.swift */, 90 | 11C5D0F82AD04B34004A1474 /* Layer+Extensions.swift */, 91 | ); 92 | path = Source; 93 | sourceTree = ""; 94 | }; 95 | /* End PBXGroup section */ 96 | 97 | /* Begin PBXNativeTarget section */ 98 | 1111501D2AC8CCDF00C0D356 /* Layers */ = { 99 | isa = PBXNativeTarget; 100 | buildConfigurationList = 1111502C2AC8CCE100C0D356 /* Build configuration list for PBXNativeTarget "Layers" */; 101 | buildPhases = ( 102 | 1111501A2AC8CCDF00C0D356 /* Sources */, 103 | 1111501B2AC8CCDF00C0D356 /* Frameworks */, 104 | 1111501C2AC8CCDF00C0D356 /* Resources */, 105 | ); 106 | buildRules = ( 107 | ); 108 | dependencies = ( 109 | ); 110 | name = Layers; 111 | packageProductDependencies = ( 112 | ); 113 | productName = Layers; 114 | productReference = 1111501E2AC8CCDF00C0D356 /* Layers.app */; 115 | productType = "com.apple.product-type.application"; 116 | }; 117 | /* End PBXNativeTarget section */ 118 | 119 | /* Begin PBXProject section */ 120 | 111150162AC8CCDF00C0D356 /* Project object */ = { 121 | isa = PBXProject; 122 | attributes = { 123 | BuildIndependentTargetsInParallel = 1; 124 | LastSwiftUpdateCheck = 1500; 125 | LastUpgradeCheck = 1500; 126 | TargetAttributes = { 127 | 1111501D2AC8CCDF00C0D356 = { 128 | CreatedOnToolsVersion = 15.0; 129 | }; 130 | }; 131 | }; 132 | buildConfigurationList = 111150192AC8CCDF00C0D356 /* Build configuration list for PBXProject "Layers" */; 133 | compatibilityVersion = "Xcode 14.0"; 134 | developmentRegion = en; 135 | hasScannedForEncodings = 0; 136 | knownRegions = ( 137 | en, 138 | Base, 139 | ); 140 | mainGroup = 111150152AC8CCDF00C0D356; 141 | packageReferences = ( 142 | ); 143 | productRefGroup = 1111501F2AC8CCDF00C0D356 /* Products */; 144 | projectDirPath = ""; 145 | projectRoot = ""; 146 | targets = ( 147 | 1111501D2AC8CCDF00C0D356 /* Layers */, 148 | ); 149 | }; 150 | /* End PBXProject section */ 151 | 152 | /* Begin PBXResourcesBuildPhase section */ 153 | 1111501C2AC8CCDF00C0D356 /* Resources */ = { 154 | isa = PBXResourcesBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | 111150292AC8CCE100C0D356 /* Preview Assets.xcassets in Resources */, 158 | 111150262AC8CCE100C0D356 /* Assets.xcassets in Resources */, 159 | ); 160 | runOnlyForDeploymentPostprocessing = 0; 161 | }; 162 | /* End PBXResourcesBuildPhase section */ 163 | 164 | /* Begin PBXSourcesBuildPhase section */ 165 | 1111501A2AC8CCDF00C0D356 /* Sources */ = { 166 | isa = PBXSourcesBuildPhase; 167 | buildActionMask = 2147483647; 168 | files = ( 169 | 111150412AC8CD5A00C0D356 /* Layer.swift in Sources */, 170 | 11C5D0F62AD0413F004A1474 /* LayerExampleComponents.swift in Sources */, 171 | 1111504B2AC8CD5A00C0D356 /* LayerExample.swift in Sources */, 172 | 111150222AC8CCDF00C0D356 /* LayersApp.swift in Sources */, 173 | 11C5D0F92AD04B34004A1474 /* Layer+Extensions.swift in Sources */, 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | /* End PBXSourcesBuildPhase section */ 178 | 179 | /* Begin XCBuildConfiguration section */ 180 | 1111502A2AC8CCE100C0D356 /* Debug */ = { 181 | isa = XCBuildConfiguration; 182 | buildSettings = { 183 | ALWAYS_SEARCH_USER_PATHS = NO; 184 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 185 | CLANG_ANALYZER_NONNULL = YES; 186 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 187 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 188 | CLANG_ENABLE_MODULES = YES; 189 | CLANG_ENABLE_OBJC_ARC = YES; 190 | CLANG_ENABLE_OBJC_WEAK = YES; 191 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 192 | CLANG_WARN_BOOL_CONVERSION = YES; 193 | CLANG_WARN_COMMA = YES; 194 | CLANG_WARN_CONSTANT_CONVERSION = YES; 195 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 196 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 197 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 198 | CLANG_WARN_EMPTY_BODY = YES; 199 | CLANG_WARN_ENUM_CONVERSION = YES; 200 | CLANG_WARN_INFINITE_RECURSION = YES; 201 | CLANG_WARN_INT_CONVERSION = YES; 202 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 203 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 204 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 205 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 206 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 207 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 208 | CLANG_WARN_STRICT_PROTOTYPES = YES; 209 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 210 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 211 | CLANG_WARN_UNREACHABLE_CODE = YES; 212 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 213 | COPY_PHASE_STRIP = NO; 214 | DEBUG_INFORMATION_FORMAT = dwarf; 215 | ENABLE_STRICT_OBJC_MSGSEND = YES; 216 | ENABLE_TESTABILITY = YES; 217 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 218 | GCC_C_LANGUAGE_STANDARD = gnu17; 219 | GCC_DYNAMIC_NO_PIC = NO; 220 | GCC_NO_COMMON_BLOCKS = YES; 221 | GCC_OPTIMIZATION_LEVEL = 0; 222 | GCC_PREPROCESSOR_DEFINITIONS = ( 223 | "DEBUG=1", 224 | "$(inherited)", 225 | ); 226 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 227 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 228 | GCC_WARN_UNDECLARED_SELECTOR = YES; 229 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 230 | GCC_WARN_UNUSED_FUNCTION = YES; 231 | GCC_WARN_UNUSED_VARIABLE = YES; 232 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 233 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 234 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 235 | MTL_FAST_MATH = YES; 236 | ONLY_ACTIVE_ARCH = YES; 237 | SDKROOT = iphoneos; 238 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 239 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 240 | }; 241 | name = Debug; 242 | }; 243 | 1111502B2AC8CCE100C0D356 /* Release */ = { 244 | isa = XCBuildConfiguration; 245 | buildSettings = { 246 | ALWAYS_SEARCH_USER_PATHS = NO; 247 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 248 | CLANG_ANALYZER_NONNULL = YES; 249 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 250 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 251 | CLANG_ENABLE_MODULES = YES; 252 | CLANG_ENABLE_OBJC_ARC = YES; 253 | CLANG_ENABLE_OBJC_WEAK = YES; 254 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 255 | CLANG_WARN_BOOL_CONVERSION = YES; 256 | CLANG_WARN_COMMA = YES; 257 | CLANG_WARN_CONSTANT_CONVERSION = YES; 258 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 259 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 260 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 261 | CLANG_WARN_EMPTY_BODY = YES; 262 | CLANG_WARN_ENUM_CONVERSION = YES; 263 | CLANG_WARN_INFINITE_RECURSION = YES; 264 | CLANG_WARN_INT_CONVERSION = YES; 265 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 266 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 267 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 268 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 269 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 270 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 271 | CLANG_WARN_STRICT_PROTOTYPES = YES; 272 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 273 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 274 | CLANG_WARN_UNREACHABLE_CODE = YES; 275 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 276 | COPY_PHASE_STRIP = NO; 277 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 278 | ENABLE_NS_ASSERTIONS = NO; 279 | ENABLE_STRICT_OBJC_MSGSEND = YES; 280 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 281 | GCC_C_LANGUAGE_STANDARD = gnu17; 282 | GCC_NO_COMMON_BLOCKS = YES; 283 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 284 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 285 | GCC_WARN_UNDECLARED_SELECTOR = YES; 286 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 287 | GCC_WARN_UNUSED_FUNCTION = YES; 288 | GCC_WARN_UNUSED_VARIABLE = YES; 289 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 290 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 291 | MTL_ENABLE_DEBUG_INFO = NO; 292 | MTL_FAST_MATH = YES; 293 | SDKROOT = iphoneos; 294 | SWIFT_COMPILATION_MODE = wholemodule; 295 | VALIDATE_PRODUCT = YES; 296 | }; 297 | name = Release; 298 | }; 299 | 1111502D2AC8CCE100C0D356 /* Debug */ = { 300 | isa = XCBuildConfiguration; 301 | buildSettings = { 302 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 303 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 304 | CODE_SIGN_STYLE = Automatic; 305 | CURRENT_PROJECT_VERSION = 1; 306 | DEVELOPMENT_ASSET_PATHS = "\"Layers/Preview Content\""; 307 | DEVELOPMENT_TEAM = 7VFLYD5292; 308 | ENABLE_PREVIEWS = YES; 309 | GENERATE_INFOPLIST_FILE = YES; 310 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 311 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 312 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 313 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 314 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 315 | LD_RUNPATH_SEARCH_PATHS = ( 316 | "$(inherited)", 317 | "@executable_path/Frameworks", 318 | ); 319 | MARKETING_VERSION = 1.0; 320 | PRODUCT_BUNDLE_IDENTIFIER = com.app.packages.Layers; 321 | PRODUCT_NAME = "$(TARGET_NAME)"; 322 | SWIFT_EMIT_LOC_STRINGS = YES; 323 | SWIFT_VERSION = 5.0; 324 | TARGETED_DEVICE_FAMILY = "1,2"; 325 | }; 326 | name = Debug; 327 | }; 328 | 1111502E2AC8CCE100C0D356 /* Release */ = { 329 | isa = XCBuildConfiguration; 330 | buildSettings = { 331 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 332 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 333 | CODE_SIGN_STYLE = Automatic; 334 | CURRENT_PROJECT_VERSION = 1; 335 | DEVELOPMENT_ASSET_PATHS = "\"Layers/Preview Content\""; 336 | DEVELOPMENT_TEAM = 7VFLYD5292; 337 | ENABLE_PREVIEWS = YES; 338 | GENERATE_INFOPLIST_FILE = YES; 339 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 340 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 341 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 342 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 343 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 344 | LD_RUNPATH_SEARCH_PATHS = ( 345 | "$(inherited)", 346 | "@executable_path/Frameworks", 347 | ); 348 | MARKETING_VERSION = 1.0; 349 | PRODUCT_BUNDLE_IDENTIFIER = com.app.packages.Layers; 350 | PRODUCT_NAME = "$(TARGET_NAME)"; 351 | SWIFT_EMIT_LOC_STRINGS = YES; 352 | SWIFT_VERSION = 5.0; 353 | TARGETED_DEVICE_FAMILY = "1,2"; 354 | }; 355 | name = Release; 356 | }; 357 | /* End XCBuildConfiguration section */ 358 | 359 | /* Begin XCConfigurationList section */ 360 | 111150192AC8CCDF00C0D356 /* Build configuration list for PBXProject "Layers" */ = { 361 | isa = XCConfigurationList; 362 | buildConfigurations = ( 363 | 1111502A2AC8CCE100C0D356 /* Debug */, 364 | 1111502B2AC8CCE100C0D356 /* Release */, 365 | ); 366 | defaultConfigurationIsVisible = 0; 367 | defaultConfigurationName = Release; 368 | }; 369 | 1111502C2AC8CCE100C0D356 /* Build configuration list for PBXNativeTarget "Layers" */ = { 370 | isa = XCConfigurationList; 371 | buildConfigurations = ( 372 | 1111502D2AC8CCE100C0D356 /* Debug */, 373 | 1111502E2AC8CCE100C0D356 /* Release */, 374 | ); 375 | defaultConfigurationIsVisible = 0; 376 | defaultConfigurationName = Release; 377 | }; 378 | /* End XCConfigurationList section */ 379 | }; 380 | rootObject = 111150162AC8CCDF00C0D356 /* Project object */; 381 | } 382 | -------------------------------------------------------------------------------- /Layers.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Layers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Layers.xcodeproj/xcuserdata/raphaelsalaja.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /Layers.xcodeproj/xcuserdata/raphaelsalaja.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Layers.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Layers/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Layers/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Layers/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Layers/Assets.xcassets/Dog.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Dog.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Layers/Assets.xcassets/Dog.imageset/Dog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raphaelsalaja/layers/1fecf54e0781ab5d372d47280af320c59240a901/Layers/Assets.xcassets/Dog.imageset/Dog.jpg -------------------------------------------------------------------------------- /Layers/Assets.xcassets/Drake.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Drake.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Layers/Assets.xcassets/Drake.imageset/Drake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raphaelsalaja/layers/1fecf54e0781ab5d372d47280af320c59240a901/Layers/Assets.xcassets/Drake.imageset/Drake.png -------------------------------------------------------------------------------- /Layers/Assets.xcassets/Raphael.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Raphael.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Layers/Assets.xcassets/Raphael.imageset/Raphael.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raphaelsalaja/layers/1fecf54e0781ab5d372d47280af320c59240a901/Layers/Assets.xcassets/Raphael.imageset/Raphael.jpg -------------------------------------------------------------------------------- /Layers/Assets.xcassets/Ronaldo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Ronaldo.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Layers/Assets.xcassets/Ronaldo.imageset/Ronaldo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raphaelsalaja/layers/1fecf54e0781ab5d372d47280af320c59240a901/Layers/Assets.xcassets/Ronaldo.imageset/Ronaldo.jpg -------------------------------------------------------------------------------- /Layers/Assets.xcassets/Smart.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Smart.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Layers/Assets.xcassets/Smart.imageset/Smart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raphaelsalaja/layers/1fecf54e0781ab5d372d47280af320c59240a901/Layers/Assets.xcassets/Smart.imageset/Smart.jpg -------------------------------------------------------------------------------- /Layers/Example/LayerExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayerExample.swift 3 | // Eyedee 4 | // 5 | // Created by Raphael Salaja on 21/09/2023. 6 | 7 | import SwiftUI 8 | 9 | // MARK: - Example Layer 10 | 11 | struct LayerExample: View { 12 | @Bindable var layers: LayerModel = .init( 13 | index: 0, 14 | max: 3, 15 | headers: [ 16 | 0: AnyView(ExampleHeader1()), 17 | 1: AnyView(ExampleHeader2()), 18 | 2: AnyView(ExampleHeader3()), 19 | ], 20 | contents: [ 21 | 0: AnyView(ExampleContent1()), 22 | 1: AnyView(ExampleContent2()), 23 | 2: AnyView(ExampleContent3()), 24 | ], 25 | buttons: [ 26 | 0: [["Cancel": "xmark.circle"], ["Continue": "checkmark.circle"]], 27 | 1: [["Cancel": "xmark.circle"], ["Continue": "checkmark.circle"]], 28 | 2: [["Cancel": "xmark.circle"], ["Continue": "checkmark.circle"]], 29 | ] 30 | ) 31 | 32 | var body: some View { 33 | Layer { 34 | layers.getCurrentHeader() 35 | .id("layer.stack.header.\(layers.index)") 36 | 37 | layers.getCurrentContent() 38 | .id("layer.stack.content.\(layers.index)") 39 | 40 | HStack { 41 | if !layers.getCurrentButtons()[0].isEmpty { 42 | LayerButton(text: Binding.constant(layers.getCurrentButtons()[0].keys.first ?? ""), 43 | icon: Binding.constant(layers.getCurrentButtons()[0].values.first ?? ""), 44 | background: .orange) 45 | { 46 | layers.previous() 47 | } 48 | } 49 | if !layers.getCurrentButtons()[1].isEmpty { 50 | LayerButton(text: Binding.constant(layers.getCurrentButtons()[1].keys.first ?? ""), 51 | icon: Binding.constant(layers.getCurrentButtons()[1].values.first ?? ""), 52 | background: .blue) 53 | { 54 | layers.next() 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | // MARK: - Preview 63 | 64 | struct LayerExamplePreview: View { 65 | var body: some View { 66 | ZStack { 67 | Color(.black.opacity(0.25)) 68 | .ignoresSafeArea() 69 | .zIndex(1) 70 | 71 | LayerExample() 72 | .zIndex(2) 73 | } 74 | } 75 | } 76 | 77 | #Preview() { 78 | ZStack { 79 | Color(.black.opacity(0.25)) 80 | .ignoresSafeArea() 81 | .zIndex(1) 82 | 83 | LayerExample() 84 | .zIndex(2) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Layers/Example/LayerExampleComponents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayerExampleComponents.swift 3 | // Layers 4 | // 5 | // Created by Raphael Salaja on 06/10/2023. 6 | // 7 | 8 | import MapKit 9 | import SwiftUI 10 | 11 | // MARK: - Example Text Fields 12 | 13 | struct ExampleTextField: View { 14 | enum ExampleTextFieldSize: Int { 15 | case extraSmall = 1 16 | case small = 2 17 | case medium = 3 18 | case large = 4 19 | case extraLarge = 5 20 | } 21 | 22 | @State var input: String 23 | @State var placeholder: String 24 | @State var variation: ExampleTextFieldSize 25 | 26 | internal init( 27 | input: String, 28 | placeholder: String, 29 | variation: ExampleTextFieldSize 30 | ) { 31 | self.input = input 32 | self.placeholder = placeholder 33 | self.variation = variation 34 | } 35 | 36 | var body: some View { 37 | TextField(placeholder, text: $input, axis: .vertical) 38 | .padding(20) 39 | .font(.system(.body, design: .rounded, weight: .bold)) 40 | .multilineTextAlignment(.leading) 41 | .foregroundStyle(.secondary) 42 | .background(Color(.tertiarySystemFill)) 43 | .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) 44 | .lineLimit(variation.rawValue, reservesSpace: true) 45 | } 46 | } 47 | 48 | // MARK: - Example Icons 49 | 50 | struct ExampleIcon: View { 51 | @State public var icon: String 52 | 53 | internal init(icon: String, action: (() -> Void)? = nil) { 54 | self.icon = icon 55 | } 56 | 57 | var body: some View { 58 | Button(action: {}) { 59 | Image(systemName: icon) 60 | .resizable() 61 | .scaledToFit() 62 | .frame(width: 24, height: 24) 63 | .fontWeight(.black) 64 | .fontDesign(.rounded) 65 | .scaleEffect(0.416) 66 | .foregroundColor(Color(.systemGray)) 67 | .background(Color(.systemGray6)) 68 | .clipShape(Circle()) 69 | } 70 | } 71 | } 72 | 73 | // MARK: - Example Headers 74 | 75 | struct ExampleHeader1: View { 76 | @EnvironmentObject var namespaceWrapper: NamespaceWrapper 77 | 78 | var body: some View { 79 | HStack { 80 | ExampleIcon(icon: "questionmark") 81 | .matchedGeometryEffect( 82 | id: "layer.icon.left", 83 | in: namespaceWrapper.namespace 84 | ) 85 | 86 | FullWidthText(center: true) { 87 | Text("Share Post") 88 | .fixedSize() 89 | .font(.system(.title3, design: .rounded, weight: .bold)) 90 | .multilineTextAlignment(.leading) 91 | .transition(.scale(scale: 1.0)) 92 | .matchedGeometryEffect( 93 | id: "layer.header", 94 | in: namespaceWrapper.namespace 95 | ) 96 | } 97 | 98 | ExampleIcon(icon: "xmark") 99 | .matchedGeometryEffect( 100 | id: "layer.icon.right", 101 | in: namespaceWrapper.namespace 102 | ) 103 | } 104 | } 105 | } 106 | 107 | struct ExampleHeader2: View { 108 | @EnvironmentObject var namespaceWrapper: NamespaceWrapper 109 | 110 | var body: some View { 111 | HStack { 112 | FullWidthText { 113 | Text("Attach Message") 114 | .fixedSize() 115 | .font(.system(.title3, design: .rounded, weight: .bold)) 116 | .multilineTextAlignment(.leading) 117 | .transition(.scale(scale: 1.0)) 118 | .matchedGeometryEffect( 119 | id: "layer.header", 120 | in: namespaceWrapper.namespace 121 | ) 122 | } 123 | 124 | ExampleIcon(icon: "xmark") 125 | .matchedGeometryEffect( 126 | id: "layer.icon.right", 127 | in: namespaceWrapper.namespace 128 | ) 129 | } 130 | } 131 | } 132 | 133 | struct ExampleHeader3: View { 134 | @EnvironmentObject var namespaceWrapper: NamespaceWrapper 135 | 136 | var body: some View { 137 | HStack { 138 | ExampleIcon(icon: "questionmark") 139 | .matchedGeometryEffect( 140 | id: "layer.icon.left", 141 | in: namespaceWrapper.namespace 142 | ) 143 | 144 | FullWidthText(center: true) { 145 | Text("Confirm Post") 146 | .fixedSize() 147 | .font(.system(.title3, design: .rounded, weight: .bold)) 148 | .multilineTextAlignment(.leading) 149 | .transition(.scale(scale: 1.0)) 150 | .matchedGeometryEffect( 151 | id: "layer.header", 152 | in: namespaceWrapper.namespace 153 | ) 154 | } 155 | 156 | ExampleIcon(icon: "xmark") 157 | .matchedGeometryEffect( 158 | id: "layer.icon.right", 159 | in: namespaceWrapper.namespace 160 | ) 161 | } 162 | } 163 | } 164 | 165 | // MARK: - Example Content 166 | 167 | struct ExampleContent1: View { 168 | @EnvironmentObject var namespaceWrapper: NamespaceWrapper 169 | 170 | var body: some View { 171 | VStack(spacing: 16) { 172 | HStack(alignment: .top) { 173 | VStack(alignment: .leading) { 174 | Text("Name") 175 | .font(.system(.footnote, design: .rounded, weight: .bold)) 176 | .multilineTextAlignment(.leading) 177 | .foregroundStyle(.secondary) 178 | 179 | Text("Smart Reaction") 180 | .font(.system(.callout, design: .rounded, weight: .bold)) 181 | .multilineTextAlignment(.leading) 182 | .foregroundStyle(.primary) 183 | } 184 | 185 | Spacer() 186 | 187 | VStack(alignment: .trailing) { 188 | Text("Format") 189 | .font(.system(.footnote, design: .rounded, weight: .bold)) 190 | .multilineTextAlignment(.leading) 191 | .foregroundStyle(.secondary) 192 | 193 | Text("JPEG") 194 | .font(.system(.callout, design: .rounded, weight: .bold)) 195 | .multilineTextAlignment(.leading) 196 | .foregroundStyle(.primary) 197 | } 198 | } 199 | .padding() 200 | .frame(maxWidth: 300) 201 | .background(Color(.tertiarySystemFill)) 202 | .foregroundStyle(.black) 203 | .clipShape(RoundedRectangle(cornerRadius: 20)) 204 | .matchedGeometryEffect( 205 | id: "layer.content.image.details", 206 | in: namespaceWrapper.namespace 207 | ) 208 | 209 | Image(.smart) 210 | .resizable() 211 | .frame(maxWidth: 300, maxHeight: 300) 212 | .clipShape(RoundedRectangle(cornerRadius: 20)) 213 | .transition(.scale(scale: 1.0)) 214 | .matchedGeometryEffect( 215 | id: "layer.content.image", 216 | in: namespaceWrapper.namespace 217 | ) 218 | 219 | HStack(alignment: .top) { 220 | HStack { 221 | Image(systemName: "paperplane") 222 | .font(.system(.callout, design: .rounded, weight: .bold)) 223 | .foregroundColor(Color(.systemGray)) 224 | 225 | Text("Recipient") 226 | .font(.system(.callout, design: .rounded, weight: .bold)) 227 | .multilineTextAlignment(.leading) 228 | .foregroundStyle(.secondary) 229 | } 230 | 231 | Spacer() 232 | 233 | Text("@raphaelsalaja") 234 | .font(.system(.callout, design: .rounded, weight: .bold)) 235 | .multilineTextAlignment(.leading) 236 | .foregroundStyle(.primary) 237 | } 238 | .padding() 239 | .frame(maxWidth: 300) 240 | .background(Color(.tertiarySystemFill)) 241 | .foregroundStyle(.black) 242 | .clipShape(RoundedRectangle(cornerRadius: 20)) 243 | .matchedGeometryEffect( 244 | id: "layer.content.recipient", 245 | in: namespaceWrapper.namespace 246 | ) 247 | } 248 | } 249 | } 250 | 251 | struct ExampleContent2: View { 252 | @EnvironmentObject var namespaceWrapper: NamespaceWrapper 253 | 254 | var body: some View { 255 | VStack(spacing: 16) { 256 | ExampleTextField(input: "", placeholder: "Something meaningful...", variation: .extraLarge) 257 | .matchedGeometryEffect( 258 | id: "layer.content.texfield", 259 | in: namespaceWrapper.namespace 260 | ) 261 | } 262 | } 263 | } 264 | 265 | struct ExampleContent3: View { 266 | @EnvironmentObject var namespaceWrapper: NamespaceWrapper 267 | 268 | var body: some View { 269 | VStack(spacing: 16) { 270 | HStack(alignment: .top) { 271 | HStack { 272 | Image(systemName: "paperplane") 273 | .font(.system(.callout, design: .rounded, weight: .bold)) 274 | .foregroundColor(Color(.systemGray)) 275 | 276 | Text("Recipient") 277 | .font(.system(.callout, design: .rounded, weight: .bold)) 278 | .multilineTextAlignment(.leading) 279 | .foregroundStyle(.secondary) 280 | } 281 | 282 | Spacer() 283 | 284 | Text("@raphaelsalaja") 285 | .font(.system(.callout, design: .rounded, weight: .bold)) 286 | .multilineTextAlignment(.leading) 287 | .foregroundStyle(.primary) 288 | } 289 | 290 | Divider() 291 | 292 | HStack { 293 | VStack(alignment: .leading) { 294 | Text("Name") 295 | .font(.system(.footnote, design: .rounded, weight: .bold)) 296 | .multilineTextAlignment(.leading) 297 | .foregroundStyle(.secondary) 298 | 299 | Text("Smart Reaction") 300 | .font(.system(.callout, design: .rounded, weight: .bold)) 301 | .multilineTextAlignment(.leading) 302 | .foregroundStyle(.primary) 303 | } 304 | 305 | Spacer() 306 | 307 | VStack(alignment: .trailing) { 308 | Text("Format") 309 | .font(.system(.footnote, design: .rounded, weight: .bold)) 310 | .multilineTextAlignment(.leading) 311 | .foregroundStyle(.secondary) 312 | 313 | Text("JPEG") 314 | .font(.system(.callout, design: .rounded, weight: .bold)) 315 | .multilineTextAlignment(.leading) 316 | .foregroundStyle(.primary) 317 | } 318 | } 319 | 320 | Divider() 321 | 322 | VStack(alignment: .leading) { 323 | FullWidthText { 324 | Text("Message") 325 | .font(.system(.footnote, design: .rounded, weight: .bold)) 326 | .multilineTextAlignment(.leading) 327 | .foregroundStyle(.secondary) 328 | } 329 | 330 | FullWidthText { 331 | Text("Sending this with lots of love, tell your friends I said hi.") 332 | .font(.system(.callout, design: .rounded, weight: .bold)) 333 | .multilineTextAlignment(.leading) 334 | .foregroundStyle(.primary) 335 | } 336 | } 337 | } 338 | .padding() 339 | .frame(maxWidth: 300) 340 | .background(Color(.tertiarySystemFill)) 341 | .foregroundStyle(.black) 342 | .clipShape(RoundedRectangle(cornerRadius: 20)) 343 | .matchedGeometryEffect( 344 | id: "layer.content.final", 345 | in: namespaceWrapper.namespace 346 | ) 347 | } 348 | } 349 | 350 | #Preview { 351 | VStack { 352 | ExampleContent1() 353 | ExampleContent2() 354 | ExampleContent3() 355 | } 356 | } 357 | 358 | // MARK: - Example Buttons 359 | 360 | struct LayerButton: View { 361 | @EnvironmentObject var namespaceWrapper: NamespaceWrapper 362 | 363 | @Binding var text: String 364 | @Binding var icon: String 365 | 366 | @State var id: Int 367 | @State var background: Color 368 | @State var disabled: Bool 369 | @State var foregroundColor: Color? = nil 370 | @State var pressed = false 371 | @State var action: () -> Void 372 | 373 | internal init( 374 | id: Int = Int.random(in: 1 ..< 100000000), 375 | text: Binding = .constant("Confirm"), 376 | icon: Binding = .constant("checkmark.circle"), 377 | background: Color = Color(.systemBlue), 378 | foregroundColor: Color? = nil, 379 | disabled: Bool = false, 380 | action: @escaping () -> Void = {} 381 | ) { 382 | self.id = id 383 | self._text = text 384 | self._icon = icon 385 | self.action = action 386 | self.background = background 387 | self.foregroundColor = foregroundColor 388 | self.disabled = disabled 389 | } 390 | 391 | var body: some View { 392 | ZStack { 393 | VStack(alignment: .center) { 394 | HStack(alignment: .center, spacing: 4) { 395 | if icon != "" { 396 | Image(systemName: icon) 397 | .font(.system(.body, design: .rounded, weight: .bold)) 398 | .contrastTextColor(background: background, foregroundColor: foregroundColor) 399 | .transition(.opacity) 400 | .matchedGeometryEffect( 401 | id: "layer.button.icon.\(id)", 402 | in: namespaceWrapper.namespace 403 | ) 404 | } 405 | 406 | Text(text) 407 | .contrastTextColor(background: background, foregroundColor: foregroundColor) 408 | .font(.system(.body, design: .rounded, weight: .bold)) 409 | .transition(.scale(scale: 1.0)) 410 | .matchedGeometryEffect( 411 | id: "layer.button.text.\(id)", 412 | in: namespaceWrapper.namespace 413 | ) 414 | } 415 | } 416 | .padding(16) 417 | .frame(maxWidth: .infinity) 418 | .background(disabled ? background.opacity(0.5) : background.opacity(1.0)) 419 | .clipShape(RoundedRectangle(cornerRadius: 100, style: .continuous)) 420 | .scaleEffect(pressed ? 0.85 : 1) 421 | .animation(.spring(response: 0.4, dampingFraction: 0.6), value: pressed) 422 | .gesture( 423 | DragGesture(minimumDistance: 0) 424 | .onChanged { _ in 425 | pressed = true 426 | } 427 | .onEnded { _ in 428 | pressed = false 429 | action() 430 | } 431 | ) 432 | } 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /Layers/LayersApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayersApp.swift 3 | // Layers 4 | // 5 | // Created by Raphael Salaja on 30/09/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct LayersApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | LayerExamplePreview() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Layers/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Layers/Source/Layer+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Layer+Extensions.swift 3 | // Layers 4 | // 5 | // Created by Raphael Salaja on 06/10/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // MARK: - Views 12 | 13 | /// A view that horizontally aligns its content to fill the available width. 14 | /// 15 | /// The `FullWidthText` struct is a convenient way to horizontally align text or any other content to fill the entire 16 | /// available width, optionally centering it within the space. 17 | struct FullWidthText: View { 18 | /// The text or content to be displayed and aligned within the view. 19 | @State var text: AnyView 20 | 21 | /// A boolean value that determines whether the content should be centered within the available width. 22 | @State var center: Bool 23 | 24 | /// Initializes a new instance of the `FullWidthText` view. 25 | /// 26 | /// - Parameters: 27 | /// - center: A boolean value indicating whether the content should be centered within the available width. 28 | /// - text: A closure that generates the text or content to be displayed within the view. 29 | init( 30 | center: Bool = false, 31 | @ViewBuilder text: @escaping () -> Content 32 | ) where Content: View { 33 | self.center = center 34 | self.text = AnyView(text()) 35 | } 36 | 37 | var body: some View { 38 | HStack { 39 | if center { Spacer() } 40 | 41 | text 42 | 43 | Spacer() 44 | } 45 | .frame(maxWidth: .infinity) 46 | } 47 | } 48 | 49 | // MARK: - Button Styles 50 | 51 | /// A button style that scales its content when pressed. 52 | /// 53 | /// The `ScaleButtonStyle` struct provides a button style that scales down its content when pressed, giving a visual 54 | /// feedback effect to indicate interaction. 55 | public struct ScaleButtonStyle: ButtonStyle { 56 | /// Initializes a new instance of the `ScaleButtonStyle`. 57 | public init() {} 58 | 59 | /// Creates the view for the button's body. 60 | /// 61 | /// - Parameter configuration: The button's configuration. 62 | /// - Returns: A modified view with the scaling effect applied. 63 | public func makeBody(configuration: Self.Configuration) -> some View { 64 | configuration.label 65 | .scaleEffect(configuration.isPressed ? 0.85 : 1) 66 | .transition(.scale(scale: 1.0)) 67 | } 68 | } 69 | 70 | public extension ButtonStyle where Self == ScaleButtonStyle { 71 | /// A convenience property to apply the `ScaleButtonStyle` to a button. 72 | /// 73 | /// Use this property to apply the `ScaleButtonStyle` to a button without explicitly creating an instance 74 | /// of the style. 75 | /// 76 | /// Example usage: 77 | /// ``` 78 | /// Button("Press Me") { 79 | /// // Action to perform when the button is pressed 80 | /// } 81 | /// .buttonStyle(.scale) 82 | /// ``` 83 | static var scale: ScaleButtonStyle { 84 | ScaleButtonStyle() 85 | } 86 | } 87 | 88 | // MARK: - Modifiers 89 | 90 | extension Color { 91 | private enum Luminance { 92 | static let red: CGFloat = 0.2126 93 | static let green: CGFloat = 0.7152 94 | static let blue: CGFloat = 0.0722 95 | static let threshold: CGFloat = 0.7 96 | } 97 | 98 | /// A boolean value indicating whether the color is considered dark based on its luminance. 99 | var isDark: Bool { 100 | var red: CGFloat = 0 101 | var green: CGFloat = 0 102 | var blue: CGFloat = 0 103 | 104 | guard UIColor(self).getRed(&red, green: &green, blue: &blue, alpha: nil) 105 | else { 106 | return false 107 | } 108 | 109 | let luminance = 110 | Luminance.red * red + 111 | Luminance.green * green + 112 | Luminance.blue * blue 113 | 114 | return luminance < Luminance.threshold 115 | } 116 | } 117 | 118 | /// A view modifier for setting the text color with contrast based on the background color. 119 | struct ContrastTextColor: ViewModifier { 120 | var background: Color 121 | var light: Color = .white 122 | var dark: Color = .black 123 | var foregroundColor: Color? = nil 124 | 125 | /// Applies text color contrast based on the background color. 126 | /// 127 | /// - Parameter content: The content to which the text color contrast is applied. 128 | /// - Returns: A modified view with the appropriate text color. 129 | func body(content: Content) -> some View { 130 | if let fgColor = foregroundColor { 131 | return content.foregroundColor(fgColor) 132 | } else { 133 | return content.foregroundColor(background.isDark ? light : dark) 134 | } 135 | } 136 | } 137 | 138 | extension View { 139 | /// Applies text color contrast based on the background color. 140 | /// 141 | /// - Parameters: 142 | /// - background: The background color used to determine text color contrast. 143 | /// - light: The text color to use when the background is considered dark. 144 | /// - dark: The text color to use when the background is considered light. 145 | /// - foregroundColor: An optional text color to use, which takes precedence over automatic contrast calculation. 146 | /// - Returns: A modified view with the appropriate text color contrast applied. 147 | func contrastTextColor(background: Color, light: Color = .white, dark: Color = .black, foregroundColor: Color? = nil) -> some View { 148 | modifier(ContrastTextColor(background: background, light: light, dark: dark, foregroundColor: foregroundColor)) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Layers/Source/Layer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayerStack.swift 3 | // Eyedee 4 | // 5 | // Created by Raphael Salaja on 29/09/2023. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | import SwiftData 11 | import SwiftUI 12 | 13 | // MARK: - Namespace 14 | 15 | class NamespaceWrapper: ObservableObject { 16 | var namespace: Namespace.ID 17 | 18 | init(_ namespace: Namespace.ID) { 19 | self.namespace = namespace 20 | } 21 | } 22 | 23 | // MARK: - Model 24 | 25 | /// A class representing a model for managing layered content. 26 | /// 27 | /// This class is responsible for managing the state and behavior of layered content, such as headers, contents, 28 | /// and buttons. It allows navigation between different layers of content and provides methods to access the 29 | /// current header, content, and buttons. 30 | @Observable 31 | class LayerModel { 32 | /// The current index indicating the active layer. 33 | var index: Int 34 | 35 | /// The maximum number of layers available. 36 | let max: Int 37 | 38 | /// A dictionary mapping layer indices to their corresponding header views. 39 | let headers: [Int: AnyView] 40 | 41 | /// A dictionary mapping layer indices to their corresponding content views. 42 | let contents: [Int: AnyView] 43 | 44 | /// A dictionary mapping layer indices to an array of button configurations. 45 | let buttons: [Int: [[String: String]]] 46 | 47 | /// Initializes a new instance of the `LayerModel` class. 48 | /// 49 | /// - Parameters: 50 | /// - index: The initial index of the active layer. 51 | /// - max: The maximum number of layers available. 52 | /// - headers: A dictionary of header views mapped to their respective layer indices. 53 | /// - contents: A dictionary of content views mapped to their respective layer indices. 54 | /// - buttons: A dictionary of button configurations mapped to their respective layer indices. 55 | internal init( 56 | index: Int, 57 | max: Int, 58 | headers: [Int: AnyView], 59 | contents: [Int: AnyView], 60 | buttons: [Int: [[String: String]]] 61 | ) { 62 | self.index = index 63 | self.max = max 64 | self.headers = headers 65 | self.contents = contents 66 | self.buttons = buttons 67 | } 68 | 69 | /// Moves to the next layer. 70 | func next() { 71 | withAnimation(.snappy) { 72 | index = (index + 1) % max 73 | } 74 | } 75 | 76 | /// Moves to the previous layer. 77 | func previous() { 78 | withAnimation(.snappy) { 79 | if index == 0 { 80 | index = max - 1 81 | } else { 82 | index = (index - 1) % max 83 | } 84 | } 85 | } 86 | 87 | /// Sets the active layer to the specified index. 88 | /// 89 | /// - Parameter index: The index of the layer to set as active. 90 | func set(index: Int) { 91 | withAnimation(.snappy) { 92 | self.index = index 93 | } 94 | } 95 | 96 | /// Retrieves the current header view for the active layer. 97 | /// 98 | /// - Returns: The current header view or an empty view if none is available. 99 | func getCurrentHeader() -> AnyView { 100 | return headers[index] ?? AnyView(EmptyView()) 101 | } 102 | 103 | /// Retrieves the current content view for the active layer. 104 | /// 105 | /// - Returns: The current content view or an empty view if none is available. 106 | func getCurrentContent() -> AnyView { 107 | return contents[index] ?? AnyView(EmptyView()) 108 | } 109 | 110 | /// Retrieves the button configurations for the active layer. 111 | /// 112 | /// - Returns: An array of button configurations for the active layer or an empty array if none is available. 113 | func getCurrentButtons() -> [[String: String]] { 114 | return buttons[index] ?? [["": ""]] 115 | } 116 | } 117 | 118 | // MARK: - View 119 | 120 | /// A view representing a layered content container. 121 | /// 122 | /// The `Layer` struct is used to create a layered content container. It allows you to wrap any content in a 123 | /// visually distinct layer with customizable padding and background styling. 124 | struct Layer: View { 125 | /// The namespace used for matched geometry effects within this layer. 126 | @Namespace private var namespace 127 | 128 | /// The content to be displayed within the layer. 129 | let content: AnyView 130 | 131 | /// Initializes a new instance of the `Layer` struct. 132 | /// 133 | /// - Parameters: 134 | /// - content: A closure that generates the content to be displayed within the layer. 135 | init( 136 | @ViewBuilder content: @escaping () -> Content 137 | ) where Content: View { 138 | self.content = AnyView(content()) 139 | } 140 | 141 | var body: some View { 142 | VStack { 143 | Spacer() 144 | 145 | ZStack { 146 | VStack(alignment: .leading, spacing: 24) { 147 | content 148 | } 149 | .padding(32) 150 | .frame(maxWidth: .infinity) 151 | .background( 152 | Color(.systemBackground) 153 | .matchedGeometryEffect( 154 | id: "layer.background", 155 | in: namespace 156 | ) 157 | ) 158 | .mask { 159 | RoundedRectangle(cornerRadius: 32, style: .continuous) 160 | .matchedGeometryEffect( 161 | id: "layer.mask", 162 | in: namespace 163 | ) 164 | } 165 | } 166 | } 167 | .padding( 168 | EdgeInsets( 169 | top: 0, 170 | leading: 16, 171 | bottom: 0, 172 | trailing: 16 173 | ) 174 | ) 175 | .environmentObject(NamespaceWrapper(namespace)) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | Twitter: @raphaelsalaja 6 | 7 |

8 | 9 | https://github.com/rafunderscore/Layers/assets/52125687/5ca08ccc-1989-4c90-8a96-683bdf59329c 10 | 11 | Layers is a powerful SwiftUI library designed for creating layered user interfaces, enhancing your app's user experience with smooth and performant animations between modal views. 12 | 13 | ## Vision 14 | 15 | While Layers isn't a perfect solution, it's a solid starting point. We believe in the power of open source and want the community to contribute and build upon Layers. If you have any suggestions or would like to contribute, please feel free to open an issue. 16 | 17 | ## Usage 18 | 19 | To get started with Layers: 20 | 21 | 1. Download the repository and check out the example. 22 | 2. For integration into your own projects, copy the source folder into your project. 23 | > We have plans to convert this project into a package for easier updates in the future. 24 | 25 | ### Creating Layers 26 | 27 | To create your own Layer, follow these steps: 28 | 29 | ```swift 30 | @Bindable var layers: LayerModel = .init( 31 | index: 0, 32 | max: 3, 33 | headers: [ 34 | 0: AnyView(ExampleHeader1()), 35 | 1: AnyView(ExampleHeader2()), 36 | 2: AnyView(ExampleHeader3()), 37 | ], 38 | contents: [ 39 | 0: AnyView(ExampleContent1()), 40 | 1: AnyView(ExampleContent2()), 41 | 2: AnyView(ExampleContent3()), 42 | ], 43 | buttons: [ 44 | 0: [["Cancel": "xmark.circle"], ["Continue": "checkmark.circle"]], 45 | 1: [["Cancel": "xmark.circle"], ["Continue": "checkmark.circle"]], 46 | 2: [["Cancel": "xmark.circle"], ["Continue": "checkmark.circle"]], 47 | ] 48 | ) 49 | ``` 50 | 51 | Within your view, create content wrapped within a Layer: 52 | 53 | ```swift 54 | var body: some View { 55 | Layer { 56 | layers.getCurrentHeader() 57 | .id("layer.stack.header.\(layers.index)") 58 | 59 | layers.getCurrentContent() 60 | .id("layer.stack.content.\(layers.index)") 61 | 62 | HStack { 63 | if !layers.getCurrentButtons()[0].isEmpty { 64 | LayerButton(text: Binding.constant(layers.getCurrentButtons()[0].keys.first ?? ""), 65 | icon: Binding.constant(layers.getCurrentButtons()[0].values.first ?? ""), 66 | background: .orange) 67 | { 68 | layers.previous() 69 | } 70 | } 71 | if !layers.getCurrentButtons()[1].isEmpty { 72 | LayerButton(text: Binding.constant(layers.getCurrentButtons()[1].keys.first ?? ""), 73 | icon: Binding.constant(layers.getCurrentButtons()[1].values.first ?? ""), 74 | background: .blue) 75 | { 76 | layers.next() 77 | } 78 | } 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | This setup allows you to customize your layers extensively without diving into numerous pages or extraneous files. Future iterations will further streamline this workflow. 85 | 86 | ## Contributing 87 | 88 | We welcome and encourage contributions to this project. If you have bug fixes or new features in mind, please create a new issue to discuss them. If you're interested in contributing code, fork the repository and submit a pull request. Make sure to document your changes and thoroughly test the project before submitting a pull request. Maintain consistency with the project's code style. 89 | 90 | ## Disclaimer 91 | 92 | This project is open source under the MIT license, granting you full access to the source code for modifications to suit your needs. Please note that this project is in beta, so there may be bugs or areas for improvement. If you discover any issues or have suggestions, please report them by creating a new issue. 93 | --------------------------------------------------------------------------------