├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── ExampleApp ├── OpenSwiftUIViewsExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── OpenSwiftUIViewsExample │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Example Views │ ├── NativeScrollViewExample.swift │ ├── OpenAlignOffsetExample.swift │ ├── OpenAlignViewExample.swift │ ├── OpenDragAndDropAnyExample.swift │ ├── OpenDragAndDropAnyToTypeExample.swift │ ├── OpenDragAndDropExample.swift │ ├── OpenRelativeOffsetExample.swift │ ├── OpenRelativePositionExample.swift │ └── OpenScrollViewExample.swift │ ├── Info.plist │ ├── Main │ └── OpenSwiftUIViewsExample.swift │ └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── OpenSwiftUIViews │ ├── Location.swift │ ├── OpenAlignView │ │ ├── OpenAlignOffset_Modifier.swift │ │ ├── OpenAlignSize_Modifier.swift │ │ ├── OpenAlignView.swift │ │ └── OpenAlign_getOffset.swift │ ├── OpenDragAndDrop │ │ ├── CGSize+Volume.swift │ │ ├── OnOpenDrag.swift │ │ ├── OnOpenDrop.swift │ │ ├── OpenDragAndDropView.swift │ │ ├── OpenDragPreferenceKey.swift │ │ ├── _DragAndDropProxy+Methods.swift │ │ └── _DragAndDropProxy.swift │ ├── OpenRelativeOffset │ │ └── OpenRelativeOffset.swift │ ├── OpenRelativePosition │ │ └── OpenRelativePosition.swift │ ├── OpenScrollView │ │ ├── OpenScrollView.swift │ │ ├── OpenScrollViewBlocking.swift │ │ ├── OpenScrollViewProxy.swift │ │ └── OpenScrollViewReader.swift │ └── OpenViews │ │ ├── HelpText.swift │ │ └── PickerStride.swift └── tryingStuff │ ├── ContentView1.swift │ ├── ContentView2.swift │ ├── ContentView3.swift │ ├── ContentView4.swift │ ├── ContentView5.swift │ ├── ContentView6.swift │ ├── ContentView7.swift │ ├── ContentView8.swift │ └── ContentView9.swift └── Tests └── OpenSwiftUIViewsTests └── SwiftUI_AnimationTestTests.swift /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 20044F3D2975D5EC00023D58 /* NativeScrollViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20044F3C2975D5EC00023D58 /* NativeScrollViewExample.swift */; }; 11 | 200EF68928341833001E60FF /* OpenDragAndDropAnyExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200EF68828341833001E60FF /* OpenDragAndDropAnyExample.swift */; }; 12 | 200EF68B283420E8001E60FF /* OpenDragAndDropAnyToTypeExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200EF68A283420E8001E60FF /* OpenDragAndDropAnyToTypeExample.swift */; }; 13 | 201ADE6527CFFA5800B15902 /* OpenScrollViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201ADE6427CFFA5800B15902 /* OpenScrollViewExample.swift */; }; 14 | 208CDB7727F7550B0025CD94 /* OpenSwiftUIViews in Frameworks */ = {isa = PBXBuildFile; productRef = 208CDB7627F7550B0025CD94 /* OpenSwiftUIViews */; }; 15 | 208CDB7A27F7563B0025CD94 /* OpenRelativePositionExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 208CDB7827F7563B0025CD94 /* OpenRelativePositionExample.swift */; }; 16 | 208CDB7B27F7563B0025CD94 /* OpenRelativeOffsetExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 208CDB7927F7563B0025CD94 /* OpenRelativeOffsetExample.swift */; }; 17 | 208CDB7F27F770B80025CD94 /* OpenAlignViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 208CDB7E27F770B80025CD94 /* OpenAlignViewExample.swift */; }; 18 | 208CDB8127F789EC0025CD94 /* OpenAlignOffsetExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 208CDB8027F789EC0025CD94 /* OpenAlignOffsetExample.swift */; }; 19 | 20F9E91125489BC200DF13DA /* OpenSwiftUIViewsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20F9E91025489BC200DF13DA /* OpenSwiftUIViewsExample.swift */; }; 20 | 20F9E91525489BC400DF13DA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 20F9E91425489BC400DF13DA /* Assets.xcassets */; }; 21 | 20F9E91825489BC400DF13DA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 20F9E91725489BC400DF13DA /* Preview Assets.xcassets */; }; 22 | 20FB18E32825D12500F7CC66 /* OpenDragAndDropExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20FB18E22825D12500F7CC66 /* OpenDragAndDropExample.swift */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 20044F3C2975D5EC00023D58 /* NativeScrollViewExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativeScrollViewExample.swift; sourceTree = ""; }; 27 | 200EF68828341833001E60FF /* OpenDragAndDropAnyExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenDragAndDropAnyExample.swift; sourceTree = ""; }; 28 | 200EF68A283420E8001E60FF /* OpenDragAndDropAnyToTypeExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenDragAndDropAnyToTypeExample.swift; sourceTree = ""; }; 29 | 201ADE6427CFFA5800B15902 /* OpenScrollViewExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenScrollViewExample.swift; sourceTree = ""; }; 30 | 208CDB7527F754FA0025CD94 /* OpenSwiftUIViews */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = OpenSwiftUIViews; path = ..; sourceTree = ""; }; 31 | 208CDB7827F7563B0025CD94 /* OpenRelativePositionExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenRelativePositionExample.swift; sourceTree = ""; }; 32 | 208CDB7927F7563B0025CD94 /* OpenRelativeOffsetExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenRelativeOffsetExample.swift; sourceTree = ""; }; 33 | 208CDB7E27F770B80025CD94 /* OpenAlignViewExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAlignViewExample.swift; sourceTree = ""; }; 34 | 208CDB8027F789EC0025CD94 /* OpenAlignOffsetExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenAlignOffsetExample.swift; sourceTree = ""; }; 35 | 20F9E90D25489BC200DF13DA /* OpenSwiftUIViewsExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenSwiftUIViewsExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 20F9E91025489BC200DF13DA /* OpenSwiftUIViewsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSwiftUIViewsExample.swift; sourceTree = ""; }; 37 | 20F9E91425489BC400DF13DA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 38 | 20F9E91725489BC400DF13DA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 39 | 20F9E91925489BC400DF13DA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 20FB18E22825D12500F7CC66 /* OpenDragAndDropExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenDragAndDropExample.swift; sourceTree = ""; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | 20F9E90A25489BC200DF13DA /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | 208CDB7727F7550B0025CD94 /* OpenSwiftUIViews in Frameworks */, 49 | ); 50 | runOnlyForDeploymentPostprocessing = 0; 51 | }; 52 | /* End PBXFrameworksBuildPhase section */ 53 | 54 | /* Begin PBXGroup section */ 55 | 2085CB0427D38B3C0090ED19 /* Packages */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | 208CDB7527F754FA0025CD94 /* OpenSwiftUIViews */, 59 | ); 60 | name = Packages; 61 | sourceTree = ""; 62 | }; 63 | 2085CB0627D38B690090ED19 /* Frameworks */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | ); 67 | name = Frameworks; 68 | sourceTree = ""; 69 | }; 70 | 208CDB7C27F757580025CD94 /* Main */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 20F9E91025489BC200DF13DA /* OpenSwiftUIViewsExample.swift */, 74 | ); 75 | path = Main; 76 | sourceTree = ""; 77 | }; 78 | 208CDB7D27F757720025CD94 /* Example Views */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 20044F3C2975D5EC00023D58 /* NativeScrollViewExample.swift */, 82 | 208CDB7927F7563B0025CD94 /* OpenRelativeOffsetExample.swift */, 83 | 208CDB7827F7563B0025CD94 /* OpenRelativePositionExample.swift */, 84 | 201ADE6427CFFA5800B15902 /* OpenScrollViewExample.swift */, 85 | 20FB18E22825D12500F7CC66 /* OpenDragAndDropExample.swift */, 86 | 200EF68828341833001E60FF /* OpenDragAndDropAnyExample.swift */, 87 | 200EF68A283420E8001E60FF /* OpenDragAndDropAnyToTypeExample.swift */, 88 | 208CDB7E27F770B80025CD94 /* OpenAlignViewExample.swift */, 89 | 208CDB8027F789EC0025CD94 /* OpenAlignOffsetExample.swift */, 90 | ); 91 | path = "Example Views"; 92 | sourceTree = ""; 93 | }; 94 | 20F9E90425489BC200DF13DA = { 95 | isa = PBXGroup; 96 | children = ( 97 | 2085CB0427D38B3C0090ED19 /* Packages */, 98 | 20F9E90F25489BC200DF13DA /* OpenSwiftUIViewsExample */, 99 | 20F9E90E25489BC200DF13DA /* Products */, 100 | 2085CB0627D38B690090ED19 /* Frameworks */, 101 | ); 102 | sourceTree = ""; 103 | }; 104 | 20F9E90E25489BC200DF13DA /* Products */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 20F9E90D25489BC200DF13DA /* OpenSwiftUIViewsExample.app */, 108 | ); 109 | name = Products; 110 | sourceTree = ""; 111 | }; 112 | 20F9E90F25489BC200DF13DA /* OpenSwiftUIViewsExample */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 208CDB7D27F757720025CD94 /* Example Views */, 116 | 208CDB7C27F757580025CD94 /* Main */, 117 | 20F9E91425489BC400DF13DA /* Assets.xcassets */, 118 | 20F9E91925489BC400DF13DA /* Info.plist */, 119 | 20F9E91625489BC400DF13DA /* Preview Content */, 120 | ); 121 | path = OpenSwiftUIViewsExample; 122 | sourceTree = ""; 123 | }; 124 | 20F9E91625489BC400DF13DA /* Preview Content */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 20F9E91725489BC400DF13DA /* Preview Assets.xcassets */, 128 | ); 129 | path = "Preview Content"; 130 | sourceTree = ""; 131 | }; 132 | /* End PBXGroup section */ 133 | 134 | /* Begin PBXNativeTarget section */ 135 | 20F9E90C25489BC200DF13DA /* OpenSwiftUIViewsExample */ = { 136 | isa = PBXNativeTarget; 137 | buildConfigurationList = 20F9E91C25489BC400DF13DA /* Build configuration list for PBXNativeTarget "OpenSwiftUIViewsExample" */; 138 | buildPhases = ( 139 | 20F9E90925489BC200DF13DA /* Sources */, 140 | 20F9E90A25489BC200DF13DA /* Frameworks */, 141 | 20F9E90B25489BC200DF13DA /* Resources */, 142 | ); 143 | buildRules = ( 144 | ); 145 | dependencies = ( 146 | ); 147 | name = OpenSwiftUIViewsExample; 148 | packageProductDependencies = ( 149 | 208CDB7627F7550B0025CD94 /* OpenSwiftUIViews */, 150 | ); 151 | productName = SwiftUI_AnimationTest; 152 | productReference = 20F9E90D25489BC200DF13DA /* OpenSwiftUIViewsExample.app */; 153 | productType = "com.apple.product-type.application"; 154 | }; 155 | /* End PBXNativeTarget section */ 156 | 157 | /* Begin PBXProject section */ 158 | 20F9E90525489BC200DF13DA /* Project object */ = { 159 | isa = PBXProject; 160 | attributes = { 161 | LastSwiftUpdateCheck = 1210; 162 | LastUpgradeCheck = 1210; 163 | TargetAttributes = { 164 | 20F9E90C25489BC200DF13DA = { 165 | CreatedOnToolsVersion = 12.1; 166 | }; 167 | }; 168 | }; 169 | buildConfigurationList = 20F9E90825489BC200DF13DA /* Build configuration list for PBXProject "OpenSwiftUIViewsExample" */; 170 | compatibilityVersion = "Xcode 9.3"; 171 | developmentRegion = en; 172 | hasScannedForEncodings = 0; 173 | knownRegions = ( 174 | en, 175 | Base, 176 | ); 177 | mainGroup = 20F9E90425489BC200DF13DA; 178 | productRefGroup = 20F9E90E25489BC200DF13DA /* Products */; 179 | projectDirPath = ""; 180 | projectRoot = ""; 181 | targets = ( 182 | 20F9E90C25489BC200DF13DA /* OpenSwiftUIViewsExample */, 183 | ); 184 | }; 185 | /* End PBXProject section */ 186 | 187 | /* Begin PBXResourcesBuildPhase section */ 188 | 20F9E90B25489BC200DF13DA /* Resources */ = { 189 | isa = PBXResourcesBuildPhase; 190 | buildActionMask = 2147483647; 191 | files = ( 192 | 20F9E91825489BC400DF13DA /* Preview Assets.xcassets in Resources */, 193 | 20F9E91525489BC400DF13DA /* Assets.xcassets in Resources */, 194 | ); 195 | runOnlyForDeploymentPostprocessing = 0; 196 | }; 197 | /* End PBXResourcesBuildPhase section */ 198 | 199 | /* Begin PBXSourcesBuildPhase section */ 200 | 20F9E90925489BC200DF13DA /* Sources */ = { 201 | isa = PBXSourcesBuildPhase; 202 | buildActionMask = 2147483647; 203 | files = ( 204 | 208CDB7F27F770B80025CD94 /* OpenAlignViewExample.swift in Sources */, 205 | 208CDB8127F789EC0025CD94 /* OpenAlignOffsetExample.swift in Sources */, 206 | 201ADE6527CFFA5800B15902 /* OpenScrollViewExample.swift in Sources */, 207 | 208CDB7A27F7563B0025CD94 /* OpenRelativePositionExample.swift in Sources */, 208 | 20F9E91125489BC200DF13DA /* OpenSwiftUIViewsExample.swift in Sources */, 209 | 208CDB7B27F7563B0025CD94 /* OpenRelativeOffsetExample.swift in Sources */, 210 | 200EF68928341833001E60FF /* OpenDragAndDropAnyExample.swift in Sources */, 211 | 200EF68B283420E8001E60FF /* OpenDragAndDropAnyToTypeExample.swift in Sources */, 212 | 20FB18E32825D12500F7CC66 /* OpenDragAndDropExample.swift in Sources */, 213 | 20044F3D2975D5EC00023D58 /* NativeScrollViewExample.swift in Sources */, 214 | ); 215 | runOnlyForDeploymentPostprocessing = 0; 216 | }; 217 | /* End PBXSourcesBuildPhase section */ 218 | 219 | /* Begin XCBuildConfiguration section */ 220 | 20F9E91A25489BC400DF13DA /* Debug */ = { 221 | isa = XCBuildConfiguration; 222 | buildSettings = { 223 | ALWAYS_SEARCH_USER_PATHS = NO; 224 | CLANG_ANALYZER_NONNULL = YES; 225 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 226 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 227 | CLANG_CXX_LIBRARY = "libc++"; 228 | CLANG_ENABLE_MODULES = YES; 229 | CLANG_ENABLE_OBJC_ARC = YES; 230 | CLANG_ENABLE_OBJC_WEAK = YES; 231 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 232 | CLANG_WARN_BOOL_CONVERSION = YES; 233 | CLANG_WARN_COMMA = YES; 234 | CLANG_WARN_CONSTANT_CONVERSION = YES; 235 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 236 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 237 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 238 | CLANG_WARN_EMPTY_BODY = YES; 239 | CLANG_WARN_ENUM_CONVERSION = YES; 240 | CLANG_WARN_INFINITE_RECURSION = YES; 241 | CLANG_WARN_INT_CONVERSION = YES; 242 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 243 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 244 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 245 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 246 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 247 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 248 | CLANG_WARN_STRICT_PROTOTYPES = YES; 249 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 250 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 251 | CLANG_WARN_UNREACHABLE_CODE = YES; 252 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 253 | COPY_PHASE_STRIP = NO; 254 | DEBUG_INFORMATION_FORMAT = dwarf; 255 | ENABLE_STRICT_OBJC_MSGSEND = YES; 256 | ENABLE_TESTABILITY = YES; 257 | GCC_C_LANGUAGE_STANDARD = gnu11; 258 | GCC_DYNAMIC_NO_PIC = NO; 259 | GCC_NO_COMMON_BLOCKS = YES; 260 | GCC_OPTIMIZATION_LEVEL = 0; 261 | GCC_PREPROCESSOR_DEFINITIONS = ( 262 | "DEBUG=1", 263 | "$(inherited)", 264 | ); 265 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 266 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 267 | GCC_WARN_UNDECLARED_SELECTOR = YES; 268 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 269 | GCC_WARN_UNUSED_FUNCTION = YES; 270 | GCC_WARN_UNUSED_VARIABLE = YES; 271 | IPHONEOS_DEPLOYMENT_TARGET = 15.4; 272 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 273 | MTL_FAST_MATH = YES; 274 | ONLY_ACTIVE_ARCH = YES; 275 | SDKROOT = iphoneos; 276 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 277 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 278 | }; 279 | name = Debug; 280 | }; 281 | 20F9E91B25489BC400DF13DA /* Release */ = { 282 | isa = XCBuildConfiguration; 283 | buildSettings = { 284 | ALWAYS_SEARCH_USER_PATHS = NO; 285 | CLANG_ANALYZER_NONNULL = YES; 286 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 287 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 288 | CLANG_CXX_LIBRARY = "libc++"; 289 | CLANG_ENABLE_MODULES = YES; 290 | CLANG_ENABLE_OBJC_ARC = YES; 291 | CLANG_ENABLE_OBJC_WEAK = YES; 292 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 293 | CLANG_WARN_BOOL_CONVERSION = YES; 294 | CLANG_WARN_COMMA = YES; 295 | CLANG_WARN_CONSTANT_CONVERSION = YES; 296 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 298 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 299 | CLANG_WARN_EMPTY_BODY = YES; 300 | CLANG_WARN_ENUM_CONVERSION = YES; 301 | CLANG_WARN_INFINITE_RECURSION = YES; 302 | CLANG_WARN_INT_CONVERSION = YES; 303 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 304 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 305 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 306 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 307 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 308 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 309 | CLANG_WARN_STRICT_PROTOTYPES = YES; 310 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 311 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 312 | CLANG_WARN_UNREACHABLE_CODE = YES; 313 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 314 | COPY_PHASE_STRIP = NO; 315 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 316 | ENABLE_NS_ASSERTIONS = NO; 317 | ENABLE_STRICT_OBJC_MSGSEND = YES; 318 | GCC_C_LANGUAGE_STANDARD = gnu11; 319 | GCC_NO_COMMON_BLOCKS = YES; 320 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 321 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 322 | GCC_WARN_UNDECLARED_SELECTOR = YES; 323 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 324 | GCC_WARN_UNUSED_FUNCTION = YES; 325 | GCC_WARN_UNUSED_VARIABLE = YES; 326 | IPHONEOS_DEPLOYMENT_TARGET = 15.4; 327 | MTL_ENABLE_DEBUG_INFO = NO; 328 | MTL_FAST_MATH = YES; 329 | SDKROOT = iphoneos; 330 | SWIFT_COMPILATION_MODE = wholemodule; 331 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 332 | VALIDATE_PRODUCT = YES; 333 | }; 334 | name = Release; 335 | }; 336 | 20F9E91D25489BC400DF13DA /* Debug */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 340 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 341 | CODE_SIGN_STYLE = Automatic; 342 | DEVELOPMENT_TEAM = MZYRW3QS38; 343 | ENABLE_PREVIEWS = YES; 344 | INFOPLIST_FILE = OpenSwiftUIViewsExample/Info.plist; 345 | LD_RUNPATH_SEARCH_PATHS = ( 346 | "$(inherited)", 347 | "@executable_path/Frameworks", 348 | ); 349 | PRODUCT_BUNDLE_IDENTIFIER = blue.same.OpenSwiftUIViewsExample; 350 | PRODUCT_NAME = "$(TARGET_NAME)"; 351 | SWIFT_VERSION = 5.0; 352 | TARGETED_DEVICE_FAMILY = "1,2"; 353 | }; 354 | name = Debug; 355 | }; 356 | 20F9E91E25489BC400DF13DA /* Release */ = { 357 | isa = XCBuildConfiguration; 358 | buildSettings = { 359 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 360 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 361 | CODE_SIGN_STYLE = Automatic; 362 | DEVELOPMENT_TEAM = MZYRW3QS38; 363 | ENABLE_PREVIEWS = YES; 364 | INFOPLIST_FILE = OpenSwiftUIViewsExample/Info.plist; 365 | LD_RUNPATH_SEARCH_PATHS = ( 366 | "$(inherited)", 367 | "@executable_path/Frameworks", 368 | ); 369 | PRODUCT_BUNDLE_IDENTIFIER = blue.same.OpenSwiftUIViewsExample; 370 | PRODUCT_NAME = "$(TARGET_NAME)"; 371 | SWIFT_VERSION = 5.0; 372 | TARGETED_DEVICE_FAMILY = "1,2"; 373 | }; 374 | name = Release; 375 | }; 376 | /* End XCBuildConfiguration section */ 377 | 378 | /* Begin XCConfigurationList section */ 379 | 20F9E90825489BC200DF13DA /* Build configuration list for PBXProject "OpenSwiftUIViewsExample" */ = { 380 | isa = XCConfigurationList; 381 | buildConfigurations = ( 382 | 20F9E91A25489BC400DF13DA /* Debug */, 383 | 20F9E91B25489BC400DF13DA /* Release */, 384 | ); 385 | defaultConfigurationIsVisible = 0; 386 | defaultConfigurationName = Release; 387 | }; 388 | 20F9E91C25489BC400DF13DA /* Build configuration list for PBXNativeTarget "OpenSwiftUIViewsExample" */ = { 389 | isa = XCConfigurationList; 390 | buildConfigurations = ( 391 | 20F9E91D25489BC400DF13DA /* Debug */, 392 | 20F9E91E25489BC400DF13DA /* Release */, 393 | ); 394 | defaultConfigurationIsVisible = 0; 395 | defaultConfigurationName = Release; 396 | }; 397 | /* End XCConfigurationList section */ 398 | 399 | /* Begin XCSwiftPackageProductDependency section */ 400 | 208CDB7627F7550B0025CD94 /* OpenSwiftUIViews */ = { 401 | isa = XCSwiftPackageProductDependency; 402 | productName = OpenSwiftUIViews; 403 | }; 404 | /* End XCSwiftPackageProductDependency section */ 405 | }; 406 | rootObject = 20F9E90525489BC200DF13DA /* Project object */; 407 | } 408 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-collections", 6 | "repositoryURL": "https://github.com/apple/swift-collections.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", 10 | "version": "1.0.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/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 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/Example Views/NativeScrollViewExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenScrollViewExample.swift 3 | // 4 | // Created by Marco Boerner on 27.02.22. 5 | // 6 | 7 | import SwiftUI 8 | import OpenSwiftUIViews 9 | 10 | import UIKit 11 | 12 | extension UIScrollView { 13 | open override var clipsToBounds: Bool { 14 | get { false } 15 | set { } 16 | } 17 | } 18 | 19 | struct NativeScrollViewExample: View { 20 | 21 | var body: some View { 22 | ScrollViewReader { proxy in 23 | ScrollView(.vertical) { 24 | Button { 25 | proxy.scrollTo(30, anchor: .top) 26 | } label: { 27 | Text("Go to 30, .top") 28 | } 29 | .padding(10) 30 | ForEach(0..<40, id: \.self) { id in 31 | JustSomeElement(stringNumber: "\(id)") 32 | .id(id) 33 | .frame(maxWidth: .infinity) 34 | } 35 | } 36 | } 37 | } 38 | } 39 | 40 | // MARK: - Preview 41 | 42 | struct OpenGoToViewExample_Previews: PreviewProvider { 43 | static var previews: some View { 44 | NativeScrollViewExample() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/Example Views/OpenAlignOffsetExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenAlignViewExample.swift 3 | // OpenSwiftUIViewsExample 4 | // 5 | // Created by Marco Boerner on 01.04.22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import OpenSwiftUIViews 11 | 12 | struct OpenAlignOffsetExample: View { 13 | 14 | var body: some View { 15 | OpenAlignView { 16 | VStack(alignment: .leading) { 17 | Spacer() 18 | HStack() { 19 | Text("XXX") 20 | .background(Color.yellow) 21 | Text("Laboris nisi aute\nenim sunt\n qui aute ut lorem") 22 | 23 | .openAlignSize(column: 10, row: 77, .leading) 24 | .background(Color.orange) 25 | .openAlignOffset(column: 10, row: 77, .leading) 26 | 27 | 28 | 29 | 30 | Text("sit occaecat") 31 | 32 | .openAlignSize(column: 11, row: 77, .leading) 33 | .background(Color.green) 34 | .openAlignOffset(column: 11, row: 77, .leading) 35 | 36 | 37 | 38 | Text("laborum amet elit") 39 | .background(Color.orange) 40 | // .openAlignOffset(column: 12, row: 10, .leading) 41 | } 42 | Spacer() 43 | HStack() { 44 | Text("lorem aliqua pariatur\nexcepteur aliquip labore\ndo eiusmod ipsum qui non\nfugiat fugiat") 45 | .openAlignSize(column: 10, row: 88, .leading) 46 | .background(Color.orange) 47 | .openAlignOffset(column: 10, row: 88, .leading) 48 | 49 | 50 | 51 | Text("sed dolor qui") 52 | .openAlignOffset(column: 11, row: 88, .leading) 53 | .background(Color.green) 54 | .openAlignSize(column: 11, row: 88, .leading) 55 | 56 | 57 | 58 | Text("sint consectetur consectetur sit excepteur quis lorem") 59 | .background(Color.orange) 60 | // .openAlignOffset(column: 12, row: 11, .leading) 61 | } 62 | Spacer() 63 | } 64 | } 65 | } 66 | } 67 | 68 | // FIXME: - I think right now it's possible by using offset that column 2 won't always be column two, it could even be after three of one is using trailing and the other leading. hmm 69 | // FIXME: - Currently when I add also the align view, it gets pretty big and over the edge of the screen. Maybe there is a way of clipping it or keeping the size contained. Try with custom coordinate space maybe 70 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/Example Views/OpenAlignViewExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenAlignViewExample.swift 3 | // OpenSwiftUIViewsExample 4 | // 5 | // Created by Marco Boerner on 01.04.22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import OpenSwiftUIViews 11 | 12 | struct OpenAlignViewExample: View { 13 | 14 | var body: some View { 15 | OpenAlignView { 16 | VStack(alignment: .leading) { 17 | Spacer() 18 | HStack(alignment: .top) { 19 | Text("Laboris nisi aute\nenim sunt qui aute ut lorem") 20 | .lineLimit(3) 21 | .openAlignSize(column: 1, row: 1, .topLeading) 22 | .background(Color.orange) 23 | Text("XXX") 24 | Text("sit occaecat") 25 | .openAlignSize(column: 2, row: 1, .topLeading) 26 | .background(Color.orange) 27 | Text("laborum amet elit") 28 | .openAlignSize(column: 3, row: 1, .topLeading) 29 | .background(Color.orange) 30 | } 31 | HStack(alignment: .top) { 32 | Text("lorem aliqua pariatur\nexcepteur aliquip labore\ndo eiusmod ipsum qui non\nfugiat fugiat") 33 | .openAlignSize(column: 1, row: 1, .topLeading) 34 | .background(Color.orange) 35 | Text("sed dolor qui sint consectetur") 36 | .openAlignSize(column: 2, row: 1, .topLeading) 37 | .background(Color.orange) 38 | Text("consectetur sit excepteur quis lorem") 39 | .openAlignSize(column: 3, row: 1, .topLeading) 40 | .background(Color.orange) 41 | } 42 | Spacer() 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/Example Views/OpenDragAndDropAnyExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenScrollViewExample.swift 3 | // 4 | // Created by Marco Boerner on 27.02.22. 5 | // 6 | 7 | import SwiftUI 8 | import OpenSwiftUIViews 9 | 10 | struct OpenDragAndDropAnyExample: View { 11 | 12 | @State private var targeted: Bool = false 13 | 14 | var body: some View { 15 | OpenDragAndDropView { 16 | HStack { 17 | VStack { 18 | Spacer() 19 | VStack(spacing: 10) { 20 | ForEach(0..<3, id: \.self) { id in 21 | AnyTargetElement(label: "Drop destination #\(id)") 22 | .frame(maxWidth: .infinity) 23 | } 24 | } 25 | Spacer() 26 | } 27 | 28 | VStack { 29 | Spacer() 30 | LazyVStack(spacing: 10) { 31 | ForEach(0..<5, id: \.self) { id in 32 | JustAnyAnotherElement(stringNumber: "\(id)") 33 | .frame(maxWidth: .infinity) 34 | } 35 | } 36 | Spacer() 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | // MARK: - List Element 44 | 45 | struct JustAnyAnotherElement: View { 46 | 47 | internal init(stringNumber: String) { 48 | self.stringNumber = stringNumber 49 | self.someValue = SomeValue(id: stringNumber) 50 | } 51 | 52 | let stringNumber: String 53 | private var someValue: SomeValue 54 | 55 | var body: some View { 56 | Circle() 57 | .overlay( 58 | Text(stringNumber) 59 | .foregroundColor(.green) 60 | ) 61 | .frame(width: 100, height: 100) 62 | .onOpenDrag(dragIdentifier: "MyValue") { 63 | return [someValue] 64 | } 65 | } 66 | } 67 | 68 | struct AnyTargetElement: View { 69 | 70 | var label: String 71 | @State private var hoover: Bool = false 72 | 73 | var body: some View { 74 | Rectangle() 75 | .overlay( 76 | Text(label) 77 | .foregroundColor(.green) 78 | ) 79 | .foregroundColor(hoover ? Color.red : Color.black) // listening to the isDragging state. 80 | .onOpenDrop(dragIdentifier: "MyValue", isTargeted: $hoover) { draggedItems in 81 | guard let draggedItems = draggedItems as? [SomeValue] else { return } 82 | print("dropped Any (SomeValue): \(draggedItems.first?.id ?? "nothing")") 83 | } 84 | } 85 | } 86 | 87 | 88 | // MARK: - Preview 89 | 90 | struct OpenDragAndDropAnyExample_Previews: PreviewProvider { 91 | static var previews: some View { 92 | OpenDragAndDropAnyExample() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/Example Views/OpenDragAndDropAnyToTypeExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenScrollViewExample.swift 3 | // 4 | // Created by Marco Boerner on 27.02.22. 5 | // 6 | 7 | import SwiftUI 8 | import OpenSwiftUIViews 9 | 10 | struct OpenDragAndDropAnyToTypeExample: View { 11 | 12 | @State private var targeted: Bool = false 13 | 14 | var body: some View { 15 | OpenDragAndDropView { 16 | HStack { 17 | VStack { 18 | Spacer() 19 | VStack(spacing: 10) { 20 | ForEach(0..<3, id: \.self) { id in 21 | AnyToTypeTargetElement(label: "Drop destination #\(id)") 22 | .frame(maxWidth: .infinity) 23 | } 24 | } 25 | Spacer() 26 | } 27 | 28 | VStack { 29 | Spacer() 30 | LazyVStack(spacing: 10) { 31 | ForEach(0..<5, id: \.self) { id in 32 | JustAnyToTypeAnotherElement(stringNumber: "\(id)") 33 | .frame(maxWidth: .infinity) 34 | } 35 | } 36 | Spacer() 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | // MARK: - List Element 44 | 45 | struct JustAnyToTypeAnotherElement: View { 46 | 47 | internal init(stringNumber: String) { 48 | self.stringNumber = stringNumber 49 | self.someValue = SomeValue(id: stringNumber) 50 | } 51 | 52 | let stringNumber: String 53 | private var someValue: SomeValue 54 | 55 | var body: some View { 56 | Circle() 57 | .overlay( 58 | Text(stringNumber) 59 | .foregroundColor(.green) 60 | ) 61 | .frame(width: 100, height: 100) 62 | .onOpenDrag(dragIdentifier: "MyValue") { 63 | return [someValue] 64 | } 65 | } 66 | } 67 | 68 | struct AnyToTypeTargetElement: View { 69 | 70 | var label: String 71 | @State private var hoover: Bool = false 72 | 73 | var body: some View { 74 | Rectangle() 75 | .overlay( 76 | Text(label) 77 | .foregroundColor(.green) 78 | ) 79 | .foregroundColor(hoover ? Color.red : Color.black) // listening to the isDragging state. 80 | .onOpenDrop(of: SomeValue.self, dragIdentifier: "MyValue", isTargeted: $hoover) { draggedItems in 81 | print("dropped SomeValue (from Any): \(draggedItems.first?.id ?? "nothing")") 82 | } 83 | } 84 | } 85 | 86 | 87 | // MARK: - Preview 88 | 89 | struct OpenDragAndDropAnyToTypeExample_Previews: PreviewProvider { 90 | static var previews: some View { 91 | OpenDragAndDropAnyToTypeExample() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/Example Views/OpenDragAndDropExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenScrollViewExample.swift 3 | // 4 | // Created by Marco Boerner on 27.02.22. 5 | // 6 | 7 | import SwiftUI 8 | import OpenSwiftUIViews 9 | 10 | struct OpenDragAndDropExample: View { 11 | 12 | @State private var targeted: Bool = false 13 | 14 | var body: some View { 15 | OpenDragAndDropView { 16 | HStack { 17 | VStack { 18 | Spacer() 19 | VStack(spacing: 10) { 20 | ForEach(0..<3, id: \.self) { id in 21 | TargetElement(label: "Drop destination #\(id)") 22 | .frame(maxWidth: .infinity) 23 | } 24 | } 25 | Spacer() 26 | } 27 | 28 | VStack { 29 | Spacer() 30 | LazyVStack(spacing: 10) { 31 | ForEach(0..<5, id: \.self) { id in 32 | JustAnotherElement(stringNumber: "\(id)") 33 | .frame(maxWidth: .infinity) 34 | } 35 | } 36 | Spacer() 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | class SomeValue: Hashable { 44 | func hash(into hasher: inout Hasher) { 45 | hasher.combine(id) 46 | } 47 | internal init(id: String) { 48 | self.id = id 49 | } 50 | static func == (lhs: SomeValue, rhs: SomeValue) -> Bool { 51 | lhs.id == rhs.id 52 | } 53 | 54 | let id: String 55 | } 56 | 57 | // MARK: - List Element 58 | 59 | struct JustAnotherElement: View { 60 | 61 | internal init(stringNumber: String) { 62 | self.stringNumber = stringNumber 63 | self.someValue = SomeValue(id: stringNumber) 64 | } 65 | 66 | let stringNumber: String 67 | private var someValue: SomeValue 68 | 69 | var body: some View { 70 | Circle() 71 | .overlay( 72 | Text(stringNumber) 73 | .foregroundColor(.green) 74 | ) 75 | .frame(width: 100, height: 100) 76 | .onOpenDrag { 77 | return [someValue] 78 | } 79 | } 80 | } 81 | 82 | struct TargetElement: View { 83 | 84 | var label: String 85 | @State private var hoover: Bool = false 86 | 87 | var body: some View { 88 | Rectangle() 89 | .overlay( 90 | Text(label) 91 | .foregroundColor(.green) 92 | ) 93 | .foregroundColor(hoover ? Color.red : Color.black) // listening to the isDragging state. 94 | .onOpenDrop(of: SomeValue.self , isTargeted: $hoover) { draggedItems in 95 | print("dropped SomeValue: \(draggedItems.first?.id ?? "nothing")") 96 | } 97 | } 98 | } 99 | 100 | 101 | // MARK: - Preview 102 | 103 | struct OpenDragAndDropExample_Previews: PreviewProvider { 104 | static var previews: some View { 105 | OpenDragAndDropExample() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/Example Views/OpenRelativeOffsetExample.swift: -------------------------------------------------------------------------------- 1 | // ContentView.swift 2 | // SameSame SwiftUI Tests 3 | // 4 | // Created by Marco Boerner on 10.11.20. 5 | 6 | import SwiftUI 7 | import OpenSwiftUIViews 8 | 9 | struct OpenRelativeOffsetExample: View { 10 | 11 | @State private var targetFrame: CGRect = .zero 12 | @State private var tapped: Bool = false 13 | 14 | var body: some View { 15 | ZStack { 16 | VStack { 17 | 18 | Spacer() 19 | HStack { 20 | Circle() 21 | .frame(width: 100, height: 100) 22 | .foregroundColor(.blue) 23 | .openRelativeOffset(tapped ? CGPoint(x: targetFrame.midX, y: targetFrame.midY) : nil, in: .named("TARGET")) 24 | .onTapGesture { 25 | tapped.toggle() 26 | } 27 | } 28 | .background(.brown) 29 | .zIndex(999) 30 | Spacer() 31 | HStack { 32 | Circle() 33 | .frame(width: 100, height: 100) 34 | .foregroundColor(Color.green.opacity(0.5)) 35 | .coordinateSpace(name: "TARGET") 36 | .background( 37 | GeometryReader { geometry in 38 | Color.clear 39 | .onAppear { 40 | targetFrame = geometry.frame(in: .named("TARGET")) 41 | } 42 | .onChange(of: geometry.frame(in: .named("TARGET"))) { newValue in 43 | targetFrame = newValue 44 | } 45 | } 46 | ) 47 | 48 | } 49 | .background(Color.orange.opacity(0.5)) 50 | Spacer() 51 | } 52 | } 53 | } 54 | } 55 | 56 | struct OpenRelativeOffsetExample_Previews: PreviewProvider { 57 | static var previews: some View { 58 | OpenRelativeOffsetExample() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/Example Views/OpenRelativePositionExample.swift: -------------------------------------------------------------------------------- 1 | // ContentView.swift 2 | // SameSame SwiftUI Tests 3 | // 4 | // Created by Marco Boerner on 10.11.20. 5 | 6 | import SwiftUI 7 | import OpenSwiftUIViews 8 | 9 | struct OpenRelativePositionExample: View { 10 | 11 | @State private var targetFrame: CGRect = .zero 12 | @State private var tapped: Bool = false 13 | 14 | var body: some View { 15 | VStack { 16 | Spacer() 17 | HStack { 18 | Circle() 19 | .frame(width: 100, height: 100) 20 | .foregroundColor(.green) 21 | .coordinateSpace(name: "TARGET") 22 | .background( 23 | GeometryReader { geometry in 24 | Color.clear 25 | .onAppear { 26 | targetFrame = geometry.frame(in: .named("TARGET")) 27 | }.onChange(of: geometry.frame(in: .named("TARGET"))) { newValue in 28 | targetFrame = newValue 29 | } 30 | } 31 | ) 32 | .position(CGPoint(x: 100, y: 120)) 33 | } 34 | .background(Color.orange.opacity(0.5)) 35 | Spacer() 36 | HStack { 37 | Circle() 38 | .frame(width: 100, height: 100) 39 | .foregroundColor(.blue) 40 | .openRelativePosition(tapped ? CGPoint(x: targetFrame.midX, y: targetFrame.midY) : nil, in: .named("TARGET")) 41 | .onTapGesture { 42 | tapped.toggle() 43 | } 44 | } 45 | .background(.brown) 46 | Spacer() 47 | } 48 | } 49 | } 50 | 51 | struct OpenRelativePositionExample_Previews: PreviewProvider { 52 | static var previews: some View { 53 | OpenRelativePositionExample() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/Example Views/OpenScrollViewExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenScrollViewExample.swift 3 | // 4 | // Created by Marco Boerner on 27.02.22. 5 | // 6 | 7 | import SwiftUI 8 | import OpenSwiftUIViews 9 | 10 | struct OpenScrollViewExample: View { 11 | 12 | @State private var goTo: Int = 50 13 | 14 | var body: some View { 15 | VStack { 16 | Text("Hello there") 17 | Spacer() 18 | OpenScrollViewReader { proxy in 19 | OpenScrollView() { 20 | LazyVStack(spacing: 10) { 21 | ForEach(0..<99, id: \.self) { id in 22 | JustSomeElement(stringNumber: "\(id)") 23 | .openScrollID(id) 24 | .frame(maxWidth: .infinity) 25 | } 26 | } 27 | } 28 | .clipped() 29 | .onChange(of: goTo) { newValue in 30 | proxy.scrollTo(newValue) 31 | } 32 | } 33 | Spacer() 34 | Button { 35 | goTo -= 1 36 | } label: { 37 | Text("GoTo") 38 | } 39 | } 40 | 41 | } 42 | } 43 | 44 | // MARK: - List Element 45 | 46 | struct JustSomeElement: View { 47 | 48 | let stringNumber: String 49 | 50 | @State private var offset = CGSize.zero 51 | @State private var isDragging = false 52 | @GestureState private var isTapping = false 53 | 54 | var body: some View { 55 | 56 | // Gets triggered immediately because a drag of 0 distance starts already when touching down. 57 | let tapGesture = DragGesture(minimumDistance: 0) 58 | .updating($isTapping) {_, isTapping, _ in 59 | isTapping = true 60 | } 61 | 62 | // minimumDistance here is mainly relevant to change to red before the drag 63 | let dragGesture = DragGesture(minimumDistance: 0) 64 | .onChanged { offset = $0.translation } 65 | .onEnded { _ in 66 | withAnimation { 67 | offset = .zero 68 | isDragging = false 69 | } 70 | } 71 | 72 | let pressGesture = LongPressGesture(minimumDuration: 1.0) 73 | .onEnded { value in 74 | withAnimation { 75 | isDragging = true 76 | } 77 | } 78 | 79 | // The dragGesture will wait until the pressGesture has triggered after minimumDuration 1.0 seconds. 80 | let combined = pressGesture.sequenced(before: dragGesture) 81 | 82 | // The new combined gesture is set to run together with the tapGesture. 83 | let simultaneously = tapGesture.simultaneously(with: combined) 84 | 85 | return Circle() 86 | .overlay( 87 | Text(stringNumber) 88 | .foregroundColor(.green) 89 | ) 90 | .overlay(isTapping ? Circle().stroke(Color.red, lineWidth: 5) : nil) //listening to the isTapping state 91 | .frame(width: 100, height: 100) 92 | .foregroundColor(isDragging ? Color.red : Color.black) // listening to the isDragging state. 93 | .offset(offset) 94 | .blockScrolling(isDragging) 95 | .gesture(simultaneously) 96 | } 97 | } 98 | 99 | // MARK: - Preview 100 | 101 | struct OpenScrollViewExample_Previews: PreviewProvider { 102 | static var previews: some View { 103 | OpenScrollViewExample() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/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 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/Main/OpenSwiftUIViewsExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUI_AnimationTestApp.swift 3 | // SwiftUI_AnimationTest 4 | // 5 | // Created by Marco Boerner on 27.10.20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct OpenSwiftUIViewsExample: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | NavigationView { 15 | VStack(spacing: 20) { 16 | NavigationLink { 17 | NativeScrollViewExample() 18 | } label: { 19 | Text("Native ScrollView Example") 20 | } 21 | NavigationLink { 22 | OpenRelativeOffsetExample() 23 | } label: { 24 | Text("OpenRelativeOffset Example") 25 | } 26 | NavigationLink { 27 | OpenRelativePositionExample() 28 | } label: { 29 | Text("OpenRelativePosition Example") 30 | } 31 | NavigationLink { 32 | OpenScrollViewExample() 33 | } label: { 34 | Text("OpenScrollView Example") 35 | } 36 | NavigationLink { 37 | OpenDragAndDropExample() 38 | } label: { 39 | Text("OpenDragAndDrop Example") 40 | } 41 | NavigationLink { 42 | OpenDragAndDropAnyExample() 43 | } label: { 44 | Text("OpenDragAndDrop (Any) Example") 45 | } 46 | NavigationLink { 47 | OpenDragAndDropAnyToTypeExample() 48 | } label: { 49 | Text("OpenDragAndDrop (Any with Type) Example") 50 | } 51 | NavigationLink { 52 | OpenAlignOffsetExample() 53 | } label: { 54 | Text("OpenAlignOffset Example") 55 | } 56 | NavigationLink { 57 | OpenAlignViewExample() 58 | } label: { 59 | VStack { 60 | Text("OpenAlignView Example") 61 | Text("Recommended landscape mode and large devices for better view") 62 | .font(.footnote) 63 | } 64 | } 65 | } 66 | .navigationTitle("OpenSwiftUIViews") 67 | .navigationBarTitleDisplayMode(.inline) 68 | 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ExampleApp/OpenSwiftUIViewsExample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Marco Boerner 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 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-collections", 6 | "repositoryURL": "https://github.com/apple/swift-collections.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", 10 | "version": "1.0.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "OpenSwiftUIViews", 8 | platforms: [ 9 | .iOS(.v17), 10 | .macOS(.v14) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "OpenSwiftUIViews", 16 | targets: ["OpenSwiftUIViews"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.0")) 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "OpenSwiftUIViews", 27 | dependencies: [ 28 | .product(name: "Collections", package: "swift-collections") 29 | ]), 30 | .testTarget( 31 | name: "OpenSwiftUIViewsTests", 32 | dependencies: ["OpenSwiftUIViews"]) 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenSwiftUIViews 2 | 3 | ## This repository is a work in progress and should not be used directly yet. 4 | 5 | ### You can clone, fork, or copy parts of the code and make it work for your projects. Feedback, bug reports or pull requests, any sort of collaboration efforts are welcome. Once again: 6 | 7 | * I do not recommend using it as a direct dependency 8 | * The code will have bugs 9 | * Using it as it is might cause unexpected behaviors 10 | * I'm constantly changing it while developing another project 11 | * Every change might be a breaking change, even bug fixes 12 | * There are no tests 13 | * The example app might not have example for all and is not always updated when the package changes 14 | * This help file might not be up to date either 15 | 16 | ## Other than that I'm developing this package to implement some of the features I'm missing in SwiftUI the most or that are not implemented the way I find them useful. 17 | 18 | 19 | ### OpenScrollView 20 | 21 | #### As of early 2023 I recommend in most cases the native scroll view and scroll view reader with the UIKit scroll view override. See the example app for a basic implementation. Using simultaneous gestures, it seems other gestures (Not currently in the example app) or the scrolling are also no longer blocked if you use the right combination of gestures in recent iOS versions. 22 | 23 | A _non gesture blocking_, _non clipping by default_ custom scroll view implementation with example code. 24 | 25 | This is an attempt for a custom scroll view in SwiftUI that will not block other gestures or be blocked by other gestures unless specified with a view modifier. I tried to implement some of the other scroll view features like a scroll view reader and proxy as well. 26 | 27 | Ideally I'd like to find a way to use this view together with the regular ScrollView reader but I wasn't able to figure out the internals of it to make it work. 28 | 29 | It does work for my purposes but could probably be extended and made more generic and simplified. 30 | 31 | [Related questions and answer on Stack Overflow](https://stackoverflow.com/a/64592385/12764795) 32 | 33 | ### OpenDragAndDrop 34 | 35 | Similar to the drag and drop capabilities that SwiftUI provides but only meant for within the app. Allowing views to be dragged and dropped with drop detection and an attached dragged object. 36 | 37 | ### OpenAlignView 38 | 39 | #### Introduced at WWDC 2022, there is now also a native version for that. 40 | 41 | Allows to align the size of the frames of _multiple_ views from multiple stacks in rows and cols. 42 | 43 | ### OpenAlignOffset 44 | 45 | #### Introduced at WWDC 2022, there is now also a native version for that. 46 | 47 | Aligns the offset of _multiple_ views organized in rows and cols, aligning both axes. 48 | 49 | ### OpenRelativePosition and OpenRelativeOffset 50 | 51 | Both allow the positioning of a view in another coordinate system. Relative position is using an internal position modifier, relative offset using an internal offset modifier. 52 | 53 | [Related questions and answer on Stack Overflow](https://stackoverflow.com/a/65584150/12764795) 54 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Location.swift 3 | // 4 | // 5 | // Created by Marco Boerner on 06.03.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Observable Location Objects 11 | 12 | public class ScrollDestination: ObservableObject, Equatable { 13 | 14 | public static func == (lhs: ScrollDestination, rhs: ScrollDestination) -> Bool { 15 | lhs.anchorPoint == rhs.anchorPoint 16 | } 17 | 18 | public func setAnchorPoint(frame: CGRect, anchor: UnitPoint) { 19 | let x = frame.minX + (frame.maxX - frame.minX) * anchor.x 20 | let y = frame.minY + (frame.maxY - frame.minY) * anchor.y 21 | self.anchorPoint = CGPoint(x: x, y: y) 22 | } 23 | 24 | @Published public private(set) var anchorPoint: CGPoint = .zero 25 | } 26 | 27 | public class Location: ObservableObject, Equatable { 28 | 29 | public init(frame: CGRect = .zero, anchor: UnitPoint = .zero) { 30 | self.frame = frame 31 | self.anchor = anchor 32 | } 33 | 34 | public static func == (lhs: Location, rhs: Location) -> Bool { 35 | lhs.frame == rhs.frame && 36 | lhs.anchor == rhs.anchor 37 | } 38 | 39 | @Published public var frame: CGRect 40 | @Published public var anchor: UnitPoint 41 | } 42 | 43 | public class IdentifiableLocation: Location { 44 | public static func == (lhs: IdentifiableLocation, rhs: IdentifiableLocation) -> Bool { 45 | lhs.frame == rhs.frame && 46 | lhs.anchor == rhs.anchor && 47 | lhs.id == rhs.id 48 | } 49 | 50 | public init(id: AnyHashable = Double.infinity, frame: CGRect = .zero, anchor: UnitPoint = .zero) { 51 | self.id = id 52 | super.init(frame: frame, anchor: anchor) 53 | } 54 | 55 | @Published public var id: AnyHashable 56 | } 57 | 58 | public class DoubleIdentifiableLocation: Location { 59 | public static func == (lhs: DoubleIdentifiableLocation, rhs: DoubleIdentifiableLocation) -> Bool { 60 | lhs.frame == rhs.frame && 61 | lhs.anchor == rhs.anchor && 62 | lhs.id1 == rhs.id1 && 63 | lhs.id2 == rhs.id2 64 | } 65 | 66 | public init(id1: AnyHashable = Double.infinity, id2: AnyHashable = Double.infinity, frame: CGRect = .zero, anchor: UnitPoint = .zero) { 67 | self.id1 = id1 68 | self.id2 = id2 69 | super.init(frame: frame, anchor: anchor) 70 | } 71 | 72 | @Published public var id1: AnyHashable 73 | @Published public var id2: AnyHashable 74 | } 75 | 76 | public class IdentifiableMaxX: Equatable { 77 | public static func == (lhs: IdentifiableMaxX, rhs: IdentifiableMaxX) -> Bool { 78 | lhs.id == rhs.id && 79 | lhs.maxX == rhs.maxX 80 | } 81 | 82 | public init(id: AnyHashable, maxX: CGFloat) { 83 | self.id = id 84 | self.maxX = maxX 85 | } 86 | 87 | public var id: AnyHashable 88 | public var maxX: CGFloat 89 | } 90 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenAlignView/OpenAlignOffset_Modifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Marco Boerner on 06.04.22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // MARK: - Open Align Offset 12 | 13 | public extension View { 14 | func openAlignOffset(column: T? = nil, minColumnSpacing: CGFloat? = 10, row: T? = nil, minRowSpacing: CGFloat? = 10, _ alignment: Alignment = .center, in coordinateSpace: CoordinateSpace = .global) -> some View { 15 | modifier(OpenAlignOffset(column: column, minColumnSpacing: minColumnSpacing, row: row, minRowSpacing: minRowSpacing, alignment: alignment, coordinateSpace: coordinateSpace)) 16 | } 17 | } 18 | 19 | struct OpenAlignOffset: ViewModifier { 20 | 21 | internal init(column: T?, minColumnSpacing: CGFloat?, row: T?, minRowSpacing: CGFloat?, alignment: Alignment, coordinateSpace: CoordinateSpace) { 22 | self.alignment = alignment 23 | self.column = column 24 | self.row = row 25 | self.minColumnSpacing = minColumnSpacing 26 | self.minRowSpacing = minRowSpacing 27 | self.coordinateSpace = coordinateSpace 28 | } 29 | 30 | let coordinateSpace: CoordinateSpace 31 | 32 | private let alignment: Alignment 33 | private let column: T? 34 | private let row: T? 35 | private let minColumnSpacing: CGFloat? 36 | private let minRowSpacing: CGFloat? 37 | @State private var offset: CGSize = .zero 38 | private var offsetMaxX: CGFloat = .zero 39 | @EnvironmentObject var openAlignState: OpenAlignState 40 | 41 | func body(content: Content) -> some View { 42 | content 43 | // .background( 44 | // GeometryReader { geometry in 45 | // Color.blue.opacity(0.2) 46 | // .preference( 47 | // key: OpenAlignMaxXOffsetsPreferenceKeys.self, 48 | // value: IdentifiableMaxX(id: column, maxX: geometry.frame(in: coordinateSpace).maxX) 49 | // ) 50 | // } 51 | // ) 52 | .offset(offset) 53 | .background( 54 | GeometryReader { geometry in 55 | Color.clear 56 | .preference(key: OpenAlignPreferenceKey.self, value: [DoubleIdentifiableLocation(id1: column, id2: row, frame: geometry.frame(in: coordinateSpace))]) 57 | // .preference( 58 | // key: OpenAlignMaxXOffsetsPreferenceKeys.self, 59 | // value: IdentifiableMaxX(id: column, maxX: geometry.frame(in: coordinateSpace).maxX) 60 | // ) 61 | .onReceive(openAlignState.$alignLocations) { alignLocations in 62 | offset = getOffset(for: alignLocations, with: geometry, alignment: alignment, column: column, row: row) 63 | } 64 | // .onReceive(openAlignState.$alignedXOffsets) { alignedXOffsets in 65 | // 66 | // } 67 | } 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenAlignView/OpenAlignSize_Modifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Marco Boerner on 01.04.22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // MARK: - Open Align Size 12 | 13 | public extension View { 14 | func openAlignSize(column: T? = nil, row: T? = nil, _ alignment: Alignment = .center) -> some View { 15 | modifier(OpenAlignSize(column: column, row: row, alignment: alignment)) 16 | } 17 | } 18 | 19 | struct OpenAlignSize: ViewModifier { 20 | 21 | internal init(column: T?, row: T?, alignment: Alignment) { 22 | self.alignment = alignment 23 | self.column = column 24 | self.row = row 25 | } 26 | 27 | private let alignment: Alignment 28 | private let column: T? 29 | private let row: T? 30 | @State private var width: CGFloat? 31 | @State private var height: CGFloat? 32 | @EnvironmentObject var openAlignState: OpenAlignState 33 | 34 | func body(content: Content) -> some View { 35 | 36 | content 37 | .frame(width: width, height: height, alignment: alignment) 38 | .background( 39 | GeometryReader { geometry in 40 | Color.clear 41 | .preference(key: OpenAlignPreferenceKey.self, value: [DoubleIdentifiableLocation(id1: column, id2: row, frame: geometry.frame(in: .global))]) 42 | .onReceive(openAlignState.$alignLocations) { alignLocations in 43 | 44 | let filteredVerticalLocations = alignLocations.filter({ column != nil && $0.id1 == column as AnyHashable }) 45 | let filteredHorizontalLocations = alignLocations.filter({ row != nil && $0.id2 == row as AnyHashable }) 46 | 47 | width = filteredVerticalLocations.max(by: { $0.frame.width < $1.frame.width })?.frame.width 48 | height = filteredHorizontalLocations.max(by: { $0.frame.height < $1.frame.height })?.frame.height 49 | } 50 | } 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenAlignView/OpenAlignView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Marco Boerner on 01.04.22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import OrderedCollections 11 | 12 | 13 | // MARK: - Align View 14 | 15 | public struct OpenAlignView: View where Content: View { 16 | @inlinable public init(@ViewBuilder content: @escaping () -> Content) { 17 | self.content = content 18 | } 19 | 20 | @State private var openAlignState: OpenAlignState = OpenAlignState() 21 | 22 | public var content: () -> Content 23 | 24 | public var body: some View { 25 | self.content() 26 | .onPreferenceChange(OpenAlignPreferenceKey.self) { alignLocations in 27 | self.openAlignState.alignLocations = alignLocations 28 | } 29 | .onPreferenceChange(OpenAlignMaxXOffsetsPreferenceKeys.self) { identifiableMaxX in 30 | // => I might be able to use this here to make sure only the relevant values are published, somehow. 31 | self.openAlignState.alignedXOffsets[identifiableMaxX.id] = identifiableMaxX.maxX 32 | } 33 | .environmentObject(openAlignState) 34 | } 35 | } 36 | 37 | // MARK: - Align State 38 | 39 | class OpenAlignState: ObservableObject, Equatable { 40 | static func == (lhs: OpenAlignState, rhs: OpenAlignState) -> Bool { 41 | lhs.alignLocations == rhs.alignLocations && 42 | lhs.alignedYOffsets == rhs.alignedYOffsets && 43 | lhs.alignedXOffsets == rhs.alignedXOffsets 44 | } 45 | 46 | @Published var alignLocations: [DoubleIdentifiableLocation] = [] 47 | @Published var alignedYOffsets: OrderedDictionary = [:] 48 | @Published var alignedXOffsets: OrderedDictionary = [:] 49 | } 50 | 51 | // MARK: - Preference key 52 | 53 | struct OpenAlignPreferenceKey: PreferenceKey, Equatable { 54 | static func reduce(value: inout [DoubleIdentifiableLocation], nextValue: () -> [DoubleIdentifiableLocation]) { 55 | value.append(contentsOf: nextValue()) 56 | } 57 | static var defaultValue: [DoubleIdentifiableLocation] = [] 58 | } 59 | 60 | struct OpenAlignMaxXOffsetsPreferenceKeys: PreferenceKey, Equatable { 61 | static func reduce(value: inout IdentifiableMaxX, nextValue: () -> IdentifiableMaxX) { 62 | 63 | guard nextValue().maxX > 0 else { return value = defaultValue } 64 | 65 | guard nextValue().id == value.id else { return value = nextValue() } 66 | 67 | value = value.maxX > nextValue().maxX ? value : nextValue() 68 | 69 | 70 | // guard let nextValue = nextValue().first else { return } 71 | // value[nextValue.key] = max(nextValue.value, value[nextValue.key] ?? 0) 72 | } 73 | static var defaultValue: IdentifiableMaxX = IdentifiableMaxX(id: 999, maxX: 0.0) 74 | } 75 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenAlignView/OpenAlign_getOffset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Marco Boerner on 07.04.22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import OrderedCollections 11 | 12 | extension OpenAlignOffset { 13 | 14 | internal func getOffset(for alignLocations: [DoubleIdentifiableLocation], with geometry: GeometryProxy, alignment: Alignment, column: T?, row: T?) -> CGSize { 15 | 16 | var valueX: KeyPath = \.midX 17 | var valueY: KeyPath = \.midY 18 | 19 | // Assigning the CGRect KeyPath determined by the alignment. 20 | switch alignment { 21 | case .topLeading: 22 | valueX = \.minX 23 | valueY = \.minY 24 | case .leading: 25 | valueX = \.minX 26 | case .bottomLeading: 27 | valueX = \.minX 28 | valueY = \.maxY 29 | case .top: 30 | valueY = \.minY 31 | case .bottom: 32 | valueY = \.maxY 33 | case .topTrailing: 34 | valueX = \.maxX 35 | valueY = \.minY 36 | case .trailing: 37 | valueX = \.maxX 38 | case .bottomTrailing: 39 | valueX = \.maxX 40 | valueY = \.maxY 41 | default: 42 | break 43 | } 44 | 45 | // Creating two collections of frames filtered by the id of the rows and columns. 46 | let xFrames = alignLocations.filter({ column != nil && $0.id1 == column as AnyHashable }).map { $0.frame } 47 | let yFrames = alignLocations.filter({ row != nil && $0.id2 == row as AnyHashable }).map { $0.frame } 48 | 49 | // Creating the predicate to be used to be used later to get the min or max values of the columns 50 | let xPredicate: (CGRect, CGRect) -> Bool = { $0[keyPath: valueX] < $1[keyPath: valueX] } 51 | let yPredicate: (CGRect, CGRect) -> Bool = { $0[keyPath: valueY] < $1[keyPath: valueY] } 52 | 53 | // the either min or max location (frames) of x and y 54 | var xFrame: CGRect? 55 | var yFrame: CGRect? 56 | 57 | // using the above predicate to get the individual min or max x or y frame 58 | switch valueX { 59 | case \.minX: 60 | xFrame = xFrames.min(by: xPredicate) 61 | case \.maxX: 62 | xFrame = xFrames.max(by: xPredicate) 63 | default: //midX 64 | break 65 | } 66 | 67 | switch valueY { 68 | case \.minY: 69 | yFrame = yFrames.min(by: yPredicate) 70 | case \.maxY: 71 | yFrame = yFrames.max(by: yPredicate) 72 | default: //midY 73 | break 74 | } 75 | 76 | // from the min or max frame we get the min or max x or y value 77 | let x = xFrame?[keyPath: valueX] 78 | let y = yFrame?[keyPath: valueY] 79 | 80 | let localFrame = geometry.frame(in: .local) 81 | let globalFrame = geometry.frame(in: coordinateSpace) 82 | 83 | let localPosition = CGPoint(x: localFrame[keyPath: valueX], y: localFrame[keyPath: valueY]) 84 | let globalPosition = CGPoint(x: globalFrame[keyPath: valueX], y: globalFrame[keyPath: valueY]) 85 | 86 | // getting the new x and y position in the other coordinate space relative to the local coordinate space. 87 | let newX = localPosition.x - globalPosition.x + (x ?? globalPosition.x) 88 | let newY = localPosition.y - globalPosition.y + (y ?? globalPosition.y) 89 | 90 | let newPosition = CGPoint(x: newX, y: newY) 91 | 92 | // Calculating the relative offset 93 | let offset = CGSize(width: newPosition.x - abs(localPosition.x), height: newPosition.y - abs(localPosition.y)) 94 | 95 | // Returning the final offset 96 | return offset 97 | } 98 | 99 | } 100 | 101 | extension OpenAlignOffset { 102 | 103 | internal func offsetAlignment(xSpacing: CGFloat? = nil, ySpacing: CGFloat? = nil) { 104 | 105 | // > 106 | // let minXPredicate: (CGRect, CGRect) -> Bool = { $0.minX < $1.minX } 107 | // 108 | // // > 109 | // var minXFrame: CGRect? 110 | // 111 | // // > 112 | // minXFrame = xFrames.min(by: minXPredicate) 113 | // // > 114 | // let minX = minXFrame?.minX 115 | // 116 | // let previousColumnMaxX: CGFloat 117 | // 118 | // if let xIndex = alignedXOffsets.index(forKey: column), xIndex > 0 { 119 | // previousColumnMaxX = alignedXOffsets.elements[xIndex - 1].value 120 | // } else if alignedXOffsets.elements.indices.contains(0) { 121 | // previousColumnMaxX = alignedXOffsets.elements[0].value 122 | // } else { 123 | // previousColumnMaxX = .zero 124 | // } 125 | // 126 | // if let minX = minX { 127 | // if previousColumnMaxX > minX { 128 | // x? += previousColumnMaxX - minX 129 | // } 130 | // } 131 | 132 | } 133 | } 134 | 135 | // TODO: - Implement some warnings when the same modifier is implemented on the same view multiple times. Like on a custom view and also in the subview of the custom view. 136 | 137 | // TODO: - Allow the alignment to be set by the OpenAlignView and be inherited, but can be overwritten by the individual modifiers 138 | // TODO: - When having both open align offset and open align size, make it possible that only one of the two need to assign the id (when no id is given. 139 | // TODO: - Can there be a way to auto assign an id if the views are in a 2d grid? 140 | // TODO: - Figure out if I even need a VStack or HStack, could I also use it without as some sort if view builder? I guess I'd need to lay them out first. Could I create a view that internally has a v and h stack, but looks more like: 141 | 142 | /* 143 | 144 | OpenAlignView(.topLeading) { 145 | OpenRow { 146 | Text1() 147 | .trailing 148 | Text2() 149 | .bottomLeading 150 | Text3() 151 | } 152 | OpenRow { 153 | TextA() 154 | TextB() 155 | .bottomLeading 156 | TextC() 157 | } 158 | */ 159 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenDragAndDrop/CGSize+Volume.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Marco Boerner on 17.03.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension CGSize { 11 | /// Calculates the absolute volume 12 | func volume() -> CGFloat { 13 | abs(self.width) * abs(self.height) 14 | } 15 | } 16 | 17 | public extension CGRect { 18 | 19 | /// Calculates how much the rect is intersecting the other rect 20 | func intersecting(_ frame: CGRect) -> CGFloat { 21 | intersection(frame).size.volume() / size.volume() 22 | } 23 | 24 | /// Checks if the intersection is over a certain amount 25 | func intersecting(_ frame: CGRect, by rate: CGFloat) -> Bool { 26 | intersecting(frame) >= rate 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenDragAndDrop/OnOpenDrag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnOpenDrag.swift 3 | // Doomsday Trainer 4 | // 5 | // Created by Marco Boerner on 19.03.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | func onOpenDrag(removalOffset: CGSize = CGSize(width: -1, height: -1), onRemove: (([T]) -> Void)? = nil, didStartDragging: @escaping () -> [T]) -> some View { 12 | modifier(OnOpenDrag(removalOffset: removalOffset, didStartDragging: didStartDragging, onRemove: onRemove)) 13 | } 14 | 15 | func onOpenDrag(dragIdentifier: AnyHashable, removalOffset: CGSize = CGSize(width: -1, height: -1), onRemove: (([A]) -> Void)? = nil, didStartDragging: @escaping () -> [A]) -> some View { 16 | modifier(OnOpenDrag(removalOffset: removalOffset, dragIdentifierForAny: dragIdentifier, didStartDraggingAny: didStartDragging, onRemoveAny: onRemove)) 17 | } 18 | } 19 | 20 | // MARK: - Gesture as ViewModifier 21 | 22 | struct OnOpenDrag: ViewModifier { 23 | 24 | private let internalID: UUID = UUID() 25 | @State private var gestureValue: GestureValue = GestureValue() 26 | @EnvironmentObject var openDragAndDropState: OpenDragAndDropState 27 | var removalOffset: CGSize 28 | var didStartDragging: (() -> [T])? 29 | var onRemove: (([T]) -> Void)? 30 | var dragIdentifierForAny: T? 31 | var didStartDraggingAny: (() -> [A])? 32 | var onRemoveAny: (([A]) -> Void)? 33 | 34 | @State private var scale: CGFloat = 1.0 35 | 36 | func body(content: Content) -> some View { 37 | 38 | // Gets triggered immediately because a drag of 0 distance starts already when touching down. 39 | let tapGesture = DragGesture(minimumDistance: 0) 40 | .onChanged { _ in 41 | gestureValue.isTapping = true 42 | } 43 | .onEnded { _ in 44 | gestureValue.isTapping = false 45 | 46 | } 47 | 48 | let pressGesture = LongPressGesture(minimumDuration: 0.3) 49 | .onEnded { _ in 50 | gestureValue.zIndex += 100 51 | withAnimation { 52 | gestureValue.isDragging = true 53 | } 54 | } 55 | 56 | // minimumDistance here is mainly relevant to change to red before the drag 57 | let dragGesture = DragGesture(minimumDistance: 0) 58 | .onChanged { gestureValue.offset = $0.translation } 59 | .onEnded { _ in 60 | gestureValue.isDragging = false 61 | } 62 | 63 | // The dragGesture will wait until the pressGesture has triggered after minimumDuration 1.0 seconds. 64 | let pressDragGesture = pressGesture.sequenced(before: dragGesture) 65 | 66 | // The new combined gesture is set to run together with the tapGesture. 67 | let tapPressDragGesture = tapGesture.simultaneously(with: pressDragGesture) 68 | 69 | content 70 | .background( 71 | GeometryReader { geometry in 72 | if gestureValue.isDragging { 73 | Color.clear 74 | .preference(key: OpenDragPreferenceKey.self, value: IdentifiableLocation(id: internalID, frame: geometry.frame(in: .global))) 75 | } else { 76 | Color.clear 77 | } 78 | } 79 | ) 80 | .scaleEffect(scale) 81 | .zIndex(gestureValue.zIndex) 82 | .offset(gestureValue.offset) 83 | .blockScrolling(gestureValue.isDragging) 84 | .gesture(tapPressDragGesture) 85 | .onChange(of: gestureValue.isDragging) { isDragging in 86 | if isDragging { 87 | if let didStartDragging = didStartDragging { 88 | openDragAndDropState.items = didStartDragging() 89 | } else if let didStartDraggingAny = didStartDraggingAny, let dragIdentifierForAny = dragIdentifierForAny { 90 | openDragAndDropState.anyItems = (dragIdentifierForAny, didStartDraggingAny()) 91 | } 92 | openDragAndDropState.dragResult = nil 93 | } else if !isDragging && openDragAndDropState.dragResult == nil { 94 | if shouldRemove() { 95 | openDragAndDropState.dragResult = .removed(internalID) 96 | } else { 97 | openDragAndDropState.dragResult = .cancelled(internalID) 98 | } 99 | } 100 | } 101 | .onReceive(openDragAndDropState.$dragResult) { dragResult in 102 | guard let dragResult = dragResult else { return } 103 | 104 | switch dragResult { 105 | case .success(let id): 106 | guard id as? UUID == internalID else { return } 107 | scale = 0.01 // <- it is important to start with a value > 0. 108 | withAnimation { 109 | gestureValue.offset = .zero 110 | scale = 1.0 111 | } 112 | case .cancelled(let id): 113 | guard id as? UUID == internalID else { return } 114 | 115 | withAnimation { 116 | scale = 1.0 117 | gestureValue.offset = .zero 118 | } 119 | case .removed(let id): 120 | guard id as? UUID == internalID else { return } 121 | withAnimation { 122 | scale = 0.1 123 | } 124 | removedAction() 125 | } 126 | } 127 | } 128 | 129 | // MARK: - Helper 130 | 131 | private func removedAction() { 132 | // All this only happens when no drop has been detected anywhere and only if onRemove is set. 133 | if let onRemove = onRemove, didStartDragging != nil, !openDragAndDropState.items.isEmpty { 134 | onRemove(openDragAndDropState.items.compactMap({ $0 as? T })) 135 | openDragAndDropState.items.removeAll() 136 | } else if let onRemoveAny = onRemoveAny, didStartDraggingAny != nil || dragIdentifierForAny != nil { 137 | onRemoveAny(openDragAndDropState.anyItems.items.compactMap({ $0 as? A })) 138 | openDragAndDropState.anyItems = (AnyHashable(Int.zero), []) 139 | } 140 | } 141 | 142 | private func shouldRemove() -> Bool { 143 | (abs(gestureValue.offset.height) >= removalOffset.height || abs(gestureValue.offset.width) >= removalOffset.width) && 144 | (onRemove != nil || onRemoveAny != nil) 145 | } 146 | private func shouldNotRemove() -> Bool { 147 | !shouldRemove() 148 | } 149 | } 150 | 151 | // MARK: - GestureValue 152 | 153 | public struct GestureValue: Equatable { 154 | public static func == (lhs: GestureValue, rhs: GestureValue) -> Bool { 155 | lhs.isTapping == rhs.isTapping && 156 | lhs.isDragging == rhs.isDragging && 157 | lhs.offset == rhs.offset && 158 | lhs.zIndex == rhs.zIndex 159 | } 160 | 161 | var isTapping: Bool = false 162 | var isDragging = false 163 | var offset: CGSize = .zero 164 | var zIndex: Double = 0.0 165 | } 166 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenDragAndDrop/OnOpenDrop.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnDrop.swift 3 | // Doomsday Trainer 4 | // 5 | // Created by Marco Boerner on 19.03.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Gesture as ViewModifier with Bindings 11 | 12 | public extension View { 13 | /// A drop destination that checks for a specific type and returns an array of that type 14 | func onOpenDrop(of supportedType: T.Type, isTargeted: Binding?, perform action: @escaping (_ draggedItems: [T]) -> Void) -> some View { 15 | modifier(OnOpenDrop(of: supportedType, isTargeted: isTargeted, didDropCompletion: action)) 16 | } 17 | /// A drop destination that only checks for the dragIdentifier and returns an array of Any 18 | /// The array of any can be type cast in the completion closure 19 | func onOpenDrop(dragIdentifier: AnyHashable, isTargeted: Binding?, perform action: @escaping (_ draggedItems: [Any]) -> Void) -> some View { 20 | modifier(OnOpenDrop(dragIdentifier: dragIdentifier, isTargeted: isTargeted, didDropAnyCompletion: action)) 21 | } 22 | /// A drop destination that checks for the dragIdentifier and conveniently type casts the array of Any to the given type. 23 | func onOpenDrop(of supportedType: T.Type, dragIdentifier: AnyHashable, isTargeted: Binding?, perform action: @escaping (_ draggedItems: [T]) -> Void) -> some View { 24 | modifier(OnOpenDrop(of: supportedType, dragIdentifier: dragIdentifier, isTargeted: isTargeted, didDropCompletion: action)) 25 | } 26 | } 27 | 28 | struct OnOpenDrop: ViewModifier { 29 | 30 | internal init(of supportedType: T.Type, isTargeted: Binding?, didDropCompletion: @escaping ([T]) -> Void) { 31 | _isTargeted = isTargeted ?? .constant(false) 32 | self.supportedType = supportedType 33 | self.didDropCompletion = didDropCompletion 34 | } 35 | internal init(dragIdentifier: T, isTargeted: Binding?, didDropAnyCompletion: @escaping ([Any]) -> Void) { 36 | _isTargeted = isTargeted ?? .constant(false) 37 | self.dragIdentifier = dragIdentifier 38 | self.didDropAnyCompletion = didDropAnyCompletion 39 | } 40 | internal init(of supportedType: T.Type, dragIdentifier: AnyHashable, isTargeted: Binding?, didDropCompletion: @escaping ([T]) -> Void) { 41 | _isTargeted = isTargeted ?? .constant(false) 42 | self.supportedType = supportedType 43 | self.dragIdentifier = dragIdentifier 44 | self.didDropCompletion = didDropCompletion 45 | } 46 | 47 | @State private var internalChildID: UUID = UUID() 48 | 49 | @EnvironmentObject var openDragAndDropState: OpenDragAndDropState 50 | 51 | var supportedType: T.Type? 52 | var dragIdentifier: AnyHashable? 53 | @Binding var isTargeted: Bool 54 | var didDropCompletion: (([T]) -> Void)? 55 | var didDropAnyCompletion: (([Any]) -> Void)? 56 | 57 | func body(content: Content) -> some View { 58 | 59 | content 60 | .onPreferenceChange(OpenDragPreferenceKey.self) { 61 | // If a child of the drop location is also the dragged item, choosing to ignore that item. 62 | guard let internalChildID = $0.id as? UUID else { return } 63 | self.internalChildID = internalChildID 64 | } 65 | .background( 66 | GeometryReader { geometry in 67 | Color.clear 68 | .onReceive(openDragAndDropState.$dragLocation) { [dragLocation = openDragAndDropState.dragLocation] newDragLocation in 69 | 70 | // Checking is not itself as being dragged through a drag modifier 71 | guard internalChildID != newDragLocation.id as? UUID else { return } 72 | 73 | // Checking if the currently dragged items match the expected type or the drag Identifiers match 74 | guard openDragAndDropState.items.contains(where: { $0 as? T != nil }) || openDragAndDropState.anyItems.dragIdentifier == dragIdentifier else { return } 75 | 76 | // If the dragged item changes, which could also due to a drop (new id = .inf), potentially dropping the item 77 | if dragLocation.id != newDragLocation.id, isTargeted { 78 | 79 | if dragIdentifier != nil, let didDropCompletion = didDropCompletion { 80 | // returning and type casting the anyItems 81 | openDragAndDropState.dragResult = .success(dragLocation.id) 82 | didDropCompletion(openDragAndDropState.anyItems.items.compactMap({ $0 as? T })) 83 | openDragAndDropState.anyItems = (AnyHashable(Int.zero), []) 84 | } else if let didDropCompletion = didDropCompletion { 85 | // returning and type casting the dropped items... 86 | openDragAndDropState.dragResult = .success(dragLocation.id) 87 | didDropCompletion(openDragAndDropState.items.compactMap({ $0 as? T })) 88 | openDragAndDropState.items.removeAll() 89 | } else if let didDropAnyCompletion = didDropAnyCompletion { 90 | // ... or returning anyItems as an array of any 91 | openDragAndDropState.dragResult = .success(dragLocation.id) 92 | didDropAnyCompletion(openDragAndDropState.anyItems.items) 93 | openDragAndDropState.anyItems = (AnyHashable(Int.zero), []) 94 | } 95 | } 96 | 97 | // Activating the binding if a dragged item is over the drop area. 98 | withAnimation { 99 | isTargeted = newDragLocation.frame.intersecting(geometry.frame(in: .global), by: 0.6) 100 | } 101 | } 102 | } 103 | ) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenDragAndDrop/OpenDragAndDropView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenDragAndDropView.swift 3 | // Doomsday Trainer 4 | // 5 | // Created by Marco Boerner on 19.03.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class OpenDragAndDropState: ObservableObject, Equatable { 11 | static func == (lhs: OpenDragAndDropState, rhs: OpenDragAndDropState) -> Bool { 12 | lhs.dragLocation == rhs.dragLocation && 13 | lhs.items == rhs.items && 14 | lhs.anyItems.dragIdentifier == rhs.anyItems.dragIdentifier && 15 | lhs.dragResult == rhs.dragResult && 16 | lhs.hasDropTarget == rhs.hasDropTarget 17 | } 18 | 19 | @Published var dragLocation: IdentifiableLocation = IdentifiableLocation() 20 | @Published var items: [AnyHashable] = [] 21 | @Published var anyItems: (dragIdentifier: AnyHashable, items: [Any]) = (AnyHashable(Int.zero), []) 22 | @Published var dragResult: OpenDragResult? = nil 23 | @Published var hasDropTarget: Bool = false 24 | } 25 | 26 | enum OpenDragResult: Equatable { 27 | case success(AnyHashable) 28 | case cancelled(AnyHashable) 29 | case removed(AnyHashable) 30 | } 31 | 32 | // MARK: - Drag and Drop Reading 33 | 34 | public struct OpenDragAndDropView: View where Content: View { 35 | public init(content: @escaping () -> Content) { 36 | self.content = content 37 | } 38 | 39 | @State private var openDragAndDropState: OpenDragAndDropState = OpenDragAndDropState() 40 | 41 | var content: () -> Content 42 | 43 | public var body: some View { 44 | self.content() 45 | .onPreferenceChange(OpenDragPreferenceKey.self) { dragLocation in 46 | self.openDragAndDropState.dragLocation = dragLocation 47 | } 48 | .environmentObject(openDragAndDropState) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenDragAndDrop/OpenDragPreferenceKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenDragPreferenceKey.swift 3 | // Doomsday Trainer 4 | // 5 | // Created by Marco Boerner on 19.03.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OpenDragPreferenceKey: PreferenceKey, Equatable { 11 | static func reduce(value: inout IdentifiableLocation, nextValue: () -> IdentifiableLocation) { 12 | if nextValue().frame != .zero { 13 | value = nextValue() 14 | } 15 | } 16 | static var defaultValue: IdentifiableLocation = IdentifiableLocation() 17 | } 18 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenDragAndDrop/_DragAndDropProxy+Methods.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Marco Boerner on 17.03.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Intersection with drop destinations 11 | 12 | //extension OpenDragAndDropProxy { 13 | // 14 | // public var intersectingDestinations: [IdentifiableLocation] { 15 | // dropDestinations 16 | // .filter { $1.intersects(draggedItem.frame) } 17 | // .sorted { 18 | // $0.value.intersection(draggedItem.frame).size.volume() > $1.value.intersection(draggedItem.frame).size.volume() 19 | // } 20 | // .map { IdentifiableLocation(id: $0, frame: $1) } 21 | // } 22 | // 23 | // public var mostIntersectingDestination: IdentifiableLocation { 24 | // dropDestinations 25 | // .filter { $1.intersects(draggedItem.frame) } 26 | // .reduce(IdentifiableLocation()) { 27 | // $0.frame.size.volume() > $1.value.intersection(draggedItem.frame).size.volume() ? 28 | // $0 : IdentifiableLocation(id: $1.key, frame: $1.value) 29 | // } 30 | // } 31 | // 32 | // public var topIntersectingDestination: IdentifiableLocation { 33 | // dropDestinations 34 | // .filter { 35 | // $1.intersects(draggedItem.frame) && 36 | // $1.maxY < draggedItem.frame.maxY && 37 | // $1.minY < draggedItem.frame.minY 38 | // } 39 | // .reduce(IdentifiableLocation()) { 40 | // $0.frame.size.volume() > $1.value.intersection(draggedItem.frame).size.volume() ? 41 | // $0 : IdentifiableLocation(id: $1.key, frame: $1.value) 42 | // } 43 | // } 44 | // 45 | // public var bottomIntersectingDestination: IdentifiableLocation { 46 | // dropDestinations 47 | // .filter { 48 | // $1.intersects(draggedItem.frame) && 49 | // $1.maxY > draggedItem.frame.maxY && 50 | // $1.minY > draggedItem.frame.minY 51 | // } 52 | // .reduce(IdentifiableLocation()) { 53 | // $0.frame.size.volume() > $1.value.intersection(draggedItem.frame).size.volume() ? 54 | // $0 : IdentifiableLocation(id: $1.key, frame: $1.value) 55 | // } 56 | // } 57 | // 58 | // public var leadingIntersectingDestination: IdentifiableLocation { 59 | // dropDestinations 60 | // .filter { 61 | // $1.intersects(draggedItem.frame) && 62 | // $1.maxX < draggedItem.frame.maxX && 63 | // $1.minX < draggedItem.frame.minX 64 | // } 65 | // .reduce(IdentifiableLocation()) { 66 | // $0.frame.size.volume() > $1.value.intersection(draggedItem.frame).size.volume() ? 67 | // $0 : IdentifiableLocation(id: $1.key, frame: $1.value) 68 | // } 69 | // } 70 | // 71 | // public var trailingIntersectingDestination: IdentifiableLocation { 72 | // dropDestinations 73 | // .filter { 74 | // $1.intersects(draggedItem.frame) && 75 | // $1.maxX > draggedItem.frame.maxX && 76 | // $1.minX > draggedItem.frame.minX 77 | // } 78 | // .reduce(IdentifiableLocation()) { 79 | // $0.frame.size.volume() > $1.value.intersection(draggedItem.frame).size.volume() ? 80 | // $0 : IdentifiableLocation(id: $1.key, frame: $1.value) 81 | // } 82 | // } 83 | //} 84 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenDragAndDrop/_DragAndDropProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenDragAndDropProxy.swift 3 | // 4 | // 5 | // Created by Marco Boerner on 06.03.22. 6 | // 7 | // 8 | //import SwiftUI 9 | // 10 | //// MARK: - Drag and Drop proxy 11 | // 12 | ///// Use the proxy to scroll to a specific location. 13 | ///// - Note: use .onChange() with individual computed properties and not the whole proxy to only receive updates when the result of the properties changes. 14 | //public class OpenDragAndDropProxy: Equatable, ObservableObject { 15 | // public static func == (lhs: OpenDragAndDropProxy, rhs: OpenDragAndDropProxy) -> Bool { 16 | // lhs.dropDestinations == rhs.dropDestinations && 17 | // lhs.draggedItem == rhs.draggedItem 18 | // } 19 | // 20 | // init(dropDestinations: [AnyHashable: CGRect] = [:], draggedItem: IdentifiableLocation = IdentifiableLocation()) { 21 | // self.dropDestinations = dropDestinations 22 | // self.draggedItem = draggedItem 23 | // } 24 | // 25 | // static let openDragAndDropCoordinateSpaceName = "OpenDragAndDrop" 26 | // 27 | // @State public var dropDestinations: [AnyHashable: CGRect] 28 | // @State public var draggedItemStart: Location = Location() 29 | // @State public var draggedItem: IdentifiableLocation 30 | // 31 | // // TODO: - Need to somehow get the iniital locaion stored and only changed when I select another item, then I need to find the right offset so that I can have the drop hover snap 32 | // 33 | // 34 | //} 35 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenRelativeOffset/OpenRelativeOffset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewExtensions.swift 3 | // SameSame SwiftUI Tests 4 | // 5 | // Created by Marco Boerner on 06.01.21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 12 | public extension View { 13 | /// Positions the center of this view at the specified point in the specified 14 | /// coordinate space using offset. 15 | /// 16 | /// Use the `openRelativeOffset(_ position:in:)` modifier to place the center of a view at a 17 | /// specific coordinate in the specified coordinate space using a 18 | /// CGPoint to specify the `x` 19 | /// and `y` position of the target CoordinateSpace defined by the Enum `coordinateSpace` 20 | /// This is not changing the position of the view by internally using an offset, other views using auto layout should not be affected. 21 | /// 22 | /// Text("Position by passing a CGPoint() and CoordinateSpace") 23 | /// .openRelativeOffset(CGPoint(x: 175, y: 100), in: .global) 24 | /// .border(Color.gray) 25 | /// 26 | /// - Parameters 27 | /// - position: The point in the target CoordinateSpace at which to place the center of this. Uses auto layout if nil. 28 | /// view. 29 | /// - in coordinateSpace: The target CoordinateSpace at which to place the center of this view. 30 | /// 31 | /// - Returns: A view that fixes the center of this view at `position` in `coordinateSpace` . 32 | func openRelativeOffset(_ position: CGPoint?, in coordinateSpace: CoordinateSpace) -> some View { 33 | modifier(OpenRelativeOffset(position: position, coordinateSpace: coordinateSpace)) 34 | } 35 | } 36 | 37 | private struct OpenRelativeOffset: ViewModifier { 38 | 39 | var position: CGPoint? 40 | @State private var newOffset: CGSize = .zero 41 | 42 | let coordinateSpace: CoordinateSpace 43 | 44 | func body(content: Content) -> some View { 45 | 46 | if let position = position { 47 | return AnyView( 48 | content 49 | .offset(newOffset) 50 | .background( 51 | GeometryReader { geometry in 52 | Color.clear 53 | .onAppear { 54 | let localFrame = geometry.frame(in: .local) 55 | let otherFrame = geometry.frame(in: coordinateSpace) 56 | 57 | let localPosition = CGPoint(x: localFrame.midX, y: localFrame.midY) 58 | let targetPosition = CGPoint(x: otherFrame.midX, y: otherFrame.midY) 59 | 60 | let newPosition = CGPoint( 61 | x: localPosition.x - targetPosition.x + position.x, 62 | y: localPosition.y - targetPosition.y + position.y 63 | ) 64 | 65 | newOffset = CGSize(width: newPosition.x - abs(localPosition.x), height: newPosition.y - abs(localPosition.y)) 66 | } 67 | } 68 | ) 69 | ) 70 | } else { 71 | return AnyView( 72 | content 73 | ) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenRelativePosition/OpenRelativePosition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewExtensions.swift 3 | // SameSame SwiftUI Tests 4 | // 5 | // Created by Marco Boerner on 06.01.21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 12 | public extension View { 13 | /// Positions the center of this view at the specified point in the specified 14 | /// coordinate space. 15 | /// 16 | /// Use the `openRelativePosition(_ position:in:)` modifier to place the center of a view at a 17 | /// specific coordinate in the specified coordinate space using a 18 | /// CGPoint to specify the `x` 19 | /// and `y` position of the target CoordinateSpace defined by the Enum `coordinateSpace` 20 | /// As this is changing the position of the view other views using auto layout might be changed. 21 | /// Use any other `.position()` or `openRelativePosition` modifier to avoid that. 22 | /// 23 | /// Text("Position by passing a CGPoint() and CoordinateSpace") 24 | /// .openRelativePosition(CGPoint(x: 175, y: 100), in: .global) 25 | /// .border(Color.gray) 26 | /// 27 | /// - Parameters 28 | /// - position: The point in the target CoordinateSpace at which to place the center of this. Uses auto layout if nil. 29 | /// view. 30 | /// - in coordinateSpace: The target CoordinateSpace at which to place the center of this view. 31 | /// 32 | /// - Returns: A view that fixes the center of this view at `position` in `coordinateSpace` . 33 | func openRelativePosition(_ position: CGPoint?, in coordinateSpace: CoordinateSpace) -> some View { 34 | modifier(OpenRelativePosition(position: position, coordinateSpace: coordinateSpace)) 35 | } 36 | } 37 | 38 | private struct OpenRelativePosition: ViewModifier { 39 | 40 | var position: CGPoint? 41 | @State private var newPosition: CGPoint = .zero 42 | 43 | let coordinateSpace: CoordinateSpace 44 | 45 | @State private var localPosition: CGPoint = .zero 46 | @State private var targetPosition: CGPoint = .zero 47 | 48 | func body(content: Content) -> some View { 49 | 50 | if let position = position { 51 | return AnyView( 52 | content 53 | .position(newPosition) 54 | .background( 55 | GeometryReader { geometry in 56 | Color.clear 57 | .onAppear { 58 | let localFrame = geometry.frame(in: .local) 59 | let otherFrame = geometry.frame(in: coordinateSpace) 60 | 61 | localPosition = CGPoint(x: localFrame.midX, y: localFrame.midY) 62 | targetPosition = CGPoint(x: otherFrame.midX, y: otherFrame.midY) 63 | newPosition.x = localPosition.x - targetPosition.x + position.x 64 | newPosition.y = localPosition.y - targetPosition.y + position.y 65 | } 66 | } 67 | ) 68 | ) 69 | } else { 70 | return AnyView( 71 | content 72 | ) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenScrollView/OpenScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenScrollView.swift 3 | // SwiftUI_AnimationTest 4 | // 5 | // Created by Marco Boerner on 03.03.22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | internal struct K { 12 | static let openScrollViewCoordinateSpaceName: String = "OpenScrollView" 13 | } 14 | 15 | struct DestinationPreferenceKey: PreferenceKey { 16 | static func reduce(value: inout ScrollDestination, nextValue: () -> ScrollDestination) { 17 | value = nextValue() 18 | } 19 | static var defaultValue: ScrollDestination = ScrollDestination() 20 | } 21 | 22 | // MARK: - Initialization and variables 23 | 24 | /// Use this ScrolLView to prevent other gestures from blocking or being blocked by default. 25 | /// Clipping is deactivated by default and not all ScrollView features are implemented or will behave the same. 26 | public struct OpenScrollView: View where Content: View { 27 | 28 | public init(_ axes: Axis.Set = .vertical, content: @escaping () -> Content) { 29 | self.axes = axes 30 | self.content = content 31 | } 32 | 33 | /// The scroll direction, supported are vertical and horizontal or both. 34 | private var axes: Axis.Set 35 | 36 | var content: () -> Content 37 | 38 | /// The current offset of the view content 39 | @State private var heightOffset: CGFloat = .zero 40 | @State private var widthOffset: CGFloat = .zero 41 | /// The accumulated offset, set when the scrolling ends. 42 | @State private var accumulatedHeightOffset: CGFloat = .zero 43 | @State private var accumulatedWidthOffset: CGFloat = .zero 44 | 45 | /// Set to true when another gesture contained in the scroll view needs to block this views gestures. 46 | @State private var scrollingIsBlocked = false 47 | 48 | @State private var innerGeometrySize: CGSize = .zero 49 | @State private var outerGeometrySize: CGSize = .zero 50 | 51 | /// This parameter is changed through the proxy and the goTo method. 52 | @State private var scrollDestination = ScrollDestination() 53 | 54 | } 55 | 56 | 57 | extension OpenScrollView { 58 | 59 | public var body: some View { 60 | 61 | // FIXME: - I need to find a way to not have the geometry reader change the view. Otherwise the outer reader is gonna make the view use all available space. 62 | 63 | // An outer geometry ... 64 | GeometryReader { outerGeometry in 65 | self.content() 66 | .onChange(of: outerGeometry.size) { outerGeometrySize = $0 } 67 | .contentShape(Rectangle()) 68 | // ... and inner geometry is used to calculate the final extreme positions of the view ... 69 | .background(GeometryReader { innerGeometry in 70 | Color.clear 71 | .onChange(of: innerGeometry.size) { innerGeometrySize = $0 } 72 | }) 73 | // The actual offset of the view is applied here, depending on the axis. 74 | .offset(x: axes.contains(.horizontal) ? accumulatedWidthOffset + widthOffset : 0, y: axes.contains(.vertical) ? accumulatedHeightOffset + heightOffset : 0) 75 | // Scrolling is triggered by the proxy's goTo method 76 | .onReceive(scrollDestination.$anchorPoint) { anchorPoint in 77 | 78 | print(accumulatedHeightOffset, anchorPoint.y) 79 | 80 | accumulatedHeightOffset -= anchorPoint.y 81 | accumulatedHeightOffset -= anchorPoint.x 82 | 83 | print(accumulatedHeightOffset) 84 | } 85 | .preference(key: DestinationPreferenceKey.self, value: scrollDestination) 86 | // Blocking is set by the blocking modifier 87 | .onPreferenceChange(BlockScrollingPreferenceKey.self) { blockScrolling in 88 | self.scrollingIsBlocked = blockScrolling 89 | } 90 | // The scrolling of this view needs to be simultaneous of other gestures to not block other gestures by default. 91 | .simultaneousGesture( 92 | // The minimum distance can be changed depending on the needs. 10 worked good so far. 93 | DragGesture(minimumDistance: 10.0) 94 | .onChanged { gesture in 95 | heightOffset = scrollingIsBlocked ? .zero : gesture.translation.height 96 | widthOffset = scrollingIsBlocked ? .zero : gesture.translation.width 97 | } 98 | .onEnded { gesture in 99 | // Animating the end of scrolling, keep scrolling if velocity was high 100 | withAnimation(.easeOut(duration: 0.6)) { 101 | heightOffset = scrollingIsBlocked ? .zero : gesture.predictedEndTranslation.height 102 | widthOffset = scrollingIsBlocked ? .zero : gesture.predictedEndTranslation.width 103 | } 104 | // this doesn't need to be animated as this is just setting the same values as above for the next scrolling gesture 105 | accumulatedHeightOffset += heightOffset 106 | accumulatedWidthOffset += widthOffset 107 | heightOffset = .zero 108 | widthOffset = .zero 109 | bounceBackHeight() 110 | bounceBackWidth() 111 | } 112 | ) 113 | } 114 | .coordinateSpace(name: K.openScrollViewCoordinateSpaceName) 115 | } 116 | } 117 | 118 | // MARK: - End of view bouncing 119 | 120 | extension OpenScrollView { 121 | 122 | private func bounceBackHeight() { 123 | // ... in order to bounce back to the most outside view here ... 124 | // bounce back to the top, also if the view does not have enough items to fill the scroll view. 125 | if accumulatedHeightOffset > 0 || innerGeometrySize.height <= outerGeometrySize.height { 126 | withAnimation(.spring()) { 127 | accumulatedHeightOffset = 0 128 | } 129 | // bounce to the bottom 130 | } else if abs(accumulatedHeightOffset) > innerGeometrySize.height - outerGeometrySize.height { 131 | withAnimation(.spring()) { 132 | accumulatedHeightOffset = -1*(innerGeometrySize.height - outerGeometrySize.height) 133 | } 134 | } 135 | } 136 | 137 | private func bounceBackWidth() { 138 | // ... and here. 139 | if accumulatedWidthOffset > 0 { 140 | withAnimation(.spring()) { 141 | accumulatedWidthOffset = 0 142 | } 143 | } else if abs(accumulatedWidthOffset) > innerGeometrySize.width - outerGeometrySize.width { 144 | withAnimation(.spring()) { 145 | accumulatedWidthOffset = -1*(innerGeometrySize.width - outerGeometrySize.width) 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenScrollView/OpenScrollViewBlocking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenScrollViewBlocking.swift 3 | // SwiftUI_AnimationTest 4 | // 5 | // Created by Marco Boerner on 03.03.22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // MARK: - Blocking 12 | 13 | public extension View { 14 | /// Pass in a state that is supposed to block the OpenScrollView scrolling. 15 | func blockScrolling(_ value: Bool) -> some View { 16 | preference(key: BlockScrollingPreferenceKey.self, value: value) 17 | } 18 | } 19 | 20 | struct BlockScrollingPreferenceKey: PreferenceKey { 21 | static var defaultValue: Bool = false 22 | 23 | static func reduce(value: inout Bool, nextValue: () -> Bool) { 24 | value = value || nextValue() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenScrollView/OpenScrollViewProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenScrollViewProxy.swift 3 | // 4 | // 5 | // Created by Marco Boerner on 06.03.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Open Scroll View Proxy 11 | 12 | /// Use the proxy to scroll to a specific location. 13 | public struct OpenScrollViewProxy { 14 | 15 | var frames: [AnyHashable: CGRect] 16 | @Binding var scrollDestination: ScrollDestination 17 | @State private var lastFrame: CGRect? 18 | 19 | /// Scrolls to the location of the ID 20 | /// Set the ID on the views inside the OpenScrollView with the customID modifier. 21 | public func scrollTo(_ id: ID?, anchor: UnitPoint = .zero) where ID: Hashable { 22 | 23 | if id == nil, var lastFrame { 24 | lastFrame.size.width = -lastFrame.width 25 | lastFrame.size.height = -lastFrame.height 26 | scrollDestination.setAnchorPoint(frame: lastFrame, anchor: anchor) 27 | } else if let frame = frames[id] { 28 | lastFrame = frame 29 | scrollDestination.setAnchorPoint(frame: frame, anchor: anchor) 30 | } 31 | } 32 | } 33 | 34 | // MARK: - Location Preference Key 35 | 36 | /// Collecting the current frames (locations) from every ID'ed scroll item. Updating while scrolling 37 | struct LocationPreferenceKey: PreferenceKey { 38 | static func reduce(value: inout [AnyHashable: CGRect], nextValue: () -> [AnyHashable: CGRect]) { 39 | 40 | value.merge(nextValue(), uniquingKeysWith: { (oldValue, newValue) in 41 | return newValue 42 | }) 43 | } 44 | static var defaultValue: [AnyHashable: CGRect] = [:] 45 | } 46 | 47 | // MARK: - Open Scroll ID 48 | 49 | public extension View { 50 | /// Assign a custom ID to the elements to be used by the OpenScrollViewReader 51 | func openScrollID(_ value: ID) -> some View where ID: Hashable { 52 | modifier(OpenScrollID(value: value)) 53 | } 54 | } 55 | 56 | /// Assign a custom ID to the elements to be used by the OpenScrollViewReader 57 | public struct OpenScrollID: ViewModifier { 58 | public init(value: ID) { 59 | self.value = value 60 | } 61 | 62 | var value: ID 63 | 64 | public func body(content: Content) -> some View { 65 | content 66 | .background( 67 | GeometryReader { geometry in 68 | Color.clear 69 | .preference(key: LocationPreferenceKey.self, value: [value: geometry.frame(in: .named(K.openScrollViewCoordinateSpaceName))]) 70 | } 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenScrollView/OpenScrollViewReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenScrollViewReader.swift 3 | // SwiftUI_AnimationTest 4 | // 5 | // Created by Marco Boerner on 03.03.22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // MARK: - OpenScrollViewReader 12 | 13 | /// Use with OpenScrollView to programmatically scroll to a specific location identified and set with the customID modifier. 14 | public struct OpenScrollViewReader: View where Content: View { 15 | 16 | public init(content: @escaping (OpenScrollViewProxy) -> Content) { 17 | self.content = content 18 | } 19 | 20 | @State private var frames: [AnyHashable: CGRect] = [:] 21 | @State private var scrollDestination = ScrollDestination() 22 | 23 | var content: (OpenScrollViewProxy) -> Content 24 | 25 | public var body: some View { 26 | 27 | let openScrollViewProxy = OpenScrollViewProxy(frames: frames, scrollDestination: $scrollDestination) 28 | 29 | return self.content(openScrollViewProxy) 30 | .onPreferenceChange(DestinationPreferenceKey.self) { destination in 31 | self.scrollDestination = destination 32 | } 33 | .onPreferenceChange(LocationPreferenceKey.self) { newFrames in 34 | self.frames = newFrames 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenViews/HelpText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelpText.swift 3 | // Doomsday Raider 4 | // 5 | // Created by Marco Boerner on 24.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | func helpText(_ showHelp: Bool, _ text: String) -> some View { 12 | self.modifier(HelpText(show: showHelp, text: text)) 13 | } 14 | } 15 | 16 | public struct HelpText: ViewModifier { 17 | public init(show showHelp: Bool = false, text: String) { 18 | self.showHelp = showHelp 19 | self.text = text 20 | } 21 | 22 | var showHelp: Bool = false 23 | var text: String 24 | 25 | public func body(content: Content) -> some View { 26 | VStack(alignment: .leading) { 27 | content 28 | if showHelp { 29 | Text(text) 30 | .font(.caption) 31 | .foregroundStyle(.secondary) 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/OpenSwiftUIViews/OpenViews/PickerStride.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PickerStride.swift 3 | // 4 | // 5 | // Created by Marco Boerner on 19.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct PickerStride: View where T: Strideable, T: Hashable, Label: View, Selection: View, Content: View { 11 | public init( 12 | selection: Binding, 13 | from start: T, 14 | through: T? = nil, 15 | by steps: T.Stride, 16 | content: @escaping (T) -> Content, 17 | emptySelection: @escaping () -> Selection, 18 | label: @escaping () -> Label 19 | ) { 20 | self._selection = selection 21 | self.start = start 22 | self.through = through 23 | self.steps = steps 24 | self.content = content 25 | self.emptySelection = emptySelection 26 | self.label = label 27 | } 28 | 29 | public init( 30 | selection: Binding, 31 | from start: T, 32 | to: T? = nil, 33 | by steps: T.Stride, 34 | content: @escaping (T) -> Content, 35 | emptySelection: @escaping () -> Selection, 36 | label: @escaping () -> Label 37 | ) { 38 | self._selection = selection 39 | self.start = start 40 | self.to = to 41 | self.steps = steps 42 | self.content = content 43 | self.emptySelection = emptySelection 44 | self.label = label 45 | } 46 | 47 | @Binding private var selection: T? 48 | private var start: T 49 | private var through: T? 50 | private var to: T? 51 | private var steps: T.Stride 52 | @ViewBuilder private var content: (T) -> Content 53 | @ViewBuilder private var emptySelection: () -> Selection 54 | @ViewBuilder private var label: () -> Label 55 | 56 | public var body: some View { 57 | Picker(selection: $selection) { 58 | if let end = through { 59 | ForEach(Array(stride(from: start, through: end, by: steps)), id: \.self) { 60 | content($0) 61 | .tag($0 as T?) 62 | } 63 | } else if let end = to { 64 | ForEach(Array(stride(from: start, to: end, by: steps)), id: \.self) { 65 | content($0) 66 | .tag($0 as T?) 67 | } 68 | } 69 | emptySelection() 70 | .tag(nil as T?) 71 | } label: { 72 | label() 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/tryingStuff/ContentView1.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// ContentView7.swift 3 | //// SwiftUI_AnimationTest 4 | //// 5 | //// Created by Marco Boerner on 10.01.21. 6 | //// 7 | // 8 | //import SwiftUI 9 | // 10 | //struct ContentView: View { 11 | // 12 | // @State private var offset = CGSize.zero 13 | // 14 | // var body: some View { 15 | // ScrollViewReader { scrollView in 16 | // ScrollView() { 17 | // ForEach(0..<5, id: \.self) { i in 18 | // ListElem(offset: $offset) 19 | // .frame(maxWidth: .infinity) 20 | // .offset(offset) 21 | // } 22 | // } 23 | // } 24 | // } 25 | //} 26 | // 27 | //struct ListElem: View { 28 | // 29 | // @Binding var offset: CGSize 30 | // 31 | // //@State private var offset = CGSize.zero 32 | // @State private var translation = CGSize.zero 33 | // @State private var isDragging = false 34 | // @GestureState var isTapping = false 35 | // @GestureState var offsetState = CGSize.zero 36 | // 37 | // var body: some View { 38 | // 39 | // // Gets triggered immediately because a drag of 0 distance starts already when touching down. 40 | // let tapGesture = DragGesture(minimumDistance: 0) 41 | // .updating($isTapping) {_, isTapping, _ in 42 | // isTapping = true 43 | // } 44 | // .onChanged { 45 | // offset.height = offset.height + $0.translation.height 46 | // //offset.width = offset.width + $0.translation.width 47 | // } 48 | // .onEnded { value in 49 | // withAnimation { 50 | // offset = .zero 51 | // isDragging = false 52 | // } 53 | // } 54 | // 55 | // 56 | // // minimumDistance here is mainly relevant to change to red before the drag 57 | // let dragGesture = DragGesture(minimumDistance: 0) 58 | // .onChanged { offset = $0.translation } 59 | // .onEnded { _ in 60 | // withAnimation { 61 | // offset = .zero 62 | // isDragging = false 63 | // } 64 | // } 65 | // 66 | // let pressGesture = LongPressGesture(minimumDuration: 1.0) 67 | // .onEnded { value in 68 | // withAnimation { 69 | // isDragging = true 70 | // } 71 | // } 72 | // 73 | // // The dragGesture will wait until the pressGesture has triggered after minimumDuration 1.0 seconds. 74 | // let combined = pressGesture.sequenced(before: dragGesture) 75 | // 76 | // // The new combined gesture is set to run together with the tapGesture. 77 | // let simultaneously = tapGesture.simultaneously(with: combined) 78 | // 79 | // return Circle() 80 | // .overlay(isTapping ? Circle().stroke(Color.red, lineWidth: 5) : nil) //listening to the isTapping state 81 | // .frame(width: 100, height: 100) 82 | // .foregroundColor(isDragging ? Color.red : Color.green) // listening to the isDragging state. 83 | // //.offset(offset) 84 | // .gesture(simultaneously) 85 | // } 86 | //} 87 | // 88 | // 89 | //struct ContentView_Previews: PreviewProvider { 90 | // static var previews: some View { 91 | // ContentView() 92 | // } 93 | //} 94 | // 95 | -------------------------------------------------------------------------------- /Sources/tryingStuff/ContentView2.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// ContentView.swift 3 | //// SwiftUI_AnimationTest 4 | //// 5 | //// Created by Marco Boerner on 27.10.20. 6 | //// 7 | // 8 | //import SwiftUI 9 | // 10 | //struct ContentView2: View { 11 | // 12 | // @State private var animationAmount = 0.0 13 | // @State private var enabled = false 14 | // 15 | // var body: some View { 16 | // Button("Tap Me") { 17 | // self.enabled.toggle() //togling the enabled variable and changing the color and rotation. 18 | // withAnimation(.interpolatingSpring(stiffness: 5, damping: 1.0)) { 19 | // self.animationAmount += 360 //each button press we turn 360 degree more 20 | // } 21 | // } 22 | // .padding(50) 23 | // .background(enabled ? Color.red : Color.blue) 24 | // .animation(.default) //this animates everything above. The animation for the rotation is set in the 25 | // .foregroundColor(.white) 26 | // .clipShape(Circle()) 27 | // .rotation3DEffect( 28 | // .degrees(animationAmount), 29 | // axis: enabled ? (x: 0, y: 1, z: 0) : (x: 1, y: 0, z: 0)) 30 | // //.animation(.interpolatingSpring(stiffness: 5, damping: 1.0)) 31 | // } 32 | //} 33 | // 34 | //// very interesting how if I use the animation below the rotation effect it's acting different than when I use it in the button part. 35 | // 36 | //struct ContentView2_Previews: PreviewProvider { 37 | // static var previews: some View { 38 | // ContentView2() 39 | // } 40 | //} 41 | -------------------------------------------------------------------------------- /Sources/tryingStuff/ContentView3.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// ContentView.swift 3 | //// SwiftUI_AnimationTest 4 | //// 5 | //// Created by Marco Boerner on 27.10.20. 6 | //// 7 | // 8 | //import SwiftUI 9 | // 10 | //struct ContentView3: View { 11 | // 12 | // @State private var dragAmount = CGSize.zero 13 | // 14 | // var body: some View { 15 | // LinearGradient(gradient: Gradient(colors: [.yellow, .red]), startPoint: .topLeading, endPoint: .bottomTrailing) 16 | // .frame(width: 300, height: 200) 17 | // .clipShape(RoundedRectangle(cornerRadius: 10)) 18 | // .offset(dragAmount) 19 | // .gesture( 20 | // DragGesture() 21 | // .onChanged { self.dragAmount = $0.translation} 22 | // .onEnded { _ in 23 | // withAnimation(.spring()) { 24 | // self.dragAmount = .zero 25 | // } 26 | // } 27 | // ) 28 | // //.animation(.spring()) 29 | // } 30 | //} 31 | // 32 | // 33 | //struct ContentView3_Previews: PreviewProvider { 34 | // static var previews: some View { 35 | // ContentView3() 36 | // } 37 | //} 38 | -------------------------------------------------------------------------------- /Sources/tryingStuff/ContentView4.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// ContentView.swift 3 | //// SwiftUI_AnimationTest 4 | //// 5 | //// Created by Marco Boerner on 27.10.20. 6 | //// 7 | // 8 | //import SwiftUI 9 | // 10 | //struct ContentView4: View { 11 | // let letters = Array("Hello SwiftUI") 12 | // @State private var enabled = false 13 | // @State private var dragAmount = CGSize.zero 14 | // 15 | // var body: some View { 16 | // HStack(spacing: 0) { 17 | // ForEach(0..: View where Content: View { 27 | // 28 | // @State private var heightOffset: CGFloat = .zero 29 | // @State private var accumulatedHeightOffset: CGFloat = .zero 30 | // @State private var scrollingIsBlocked = false 31 | // 32 | // var content: () -> Content 33 | // 34 | // var body: some View { 35 | // GeometryReader { outerGeometry in 36 | // self.content() 37 | // .contentShape(Rectangle()) 38 | // .frame(height: outerGeometry.size.height) 39 | // .offset(x: 0, y: accumulatedHeightOffset + heightOffset) 40 | // .onPreferenceChange(BlockScrollingPreferenceKey.self) { blockScrolling in 41 | // self.scrollingIsBlocked = blockScrolling 42 | // } 43 | // .simultaneousGesture( 44 | // DragGesture(minimumDistance: 10.0) 45 | // .onChanged { gesture in 46 | // heightOffset = scrollingIsBlocked ? .zero : gesture.translation.height 47 | // } 48 | // .onEnded { gesture in 49 | // accumulatedHeightOffset += heightOffset 50 | // heightOffset = .zero 51 | // } 52 | // ) 53 | // } 54 | // } 55 | //} 56 | // 57 | //// MARK: - Blocking 58 | // 59 | ////see file 10 for following implementation 60 | // 61 | ////extension View { 62 | //// func blockScrolling(_ value: Bool) -> some View { 63 | //// preference(key: BlockScrollingPreferenceKey.self, value: value) 64 | //// } 65 | ////} 66 | //// 67 | ////struct BlockScrollingPreferenceKey: PreferenceKey { 68 | //// static var defaultValue: Bool = false 69 | //// 70 | //// static func reduce(value: inout Bool, nextValue: () -> Bool) { 71 | //// value = value || nextValue() 72 | //// } 73 | ////} 74 | // 75 | //// MARK: - List Element 76 | // 77 | //struct ListElem8: View { 78 | // 79 | // @State private var offset = CGSize.zero 80 | // @State private var isDragging = false 81 | // @GestureState private var isTapping = false 82 | // 83 | // var body: some View { 84 | // 85 | // // Gets triggered immediately because a drag of 0 distance starts already when touching down. 86 | // let tapGesture = DragGesture(minimumDistance: 0) 87 | // .updating($isTapping) {_, isTapping, _ in 88 | // isTapping = true 89 | // } 90 | // 91 | // // minimumDistance here is mainly relevant to change to red before the drag 92 | // let dragGesture = DragGesture(minimumDistance: 0) 93 | // .onChanged { offset = $0.translation } 94 | // .onEnded { _ in 95 | // withAnimation { 96 | // offset = .zero 97 | // isDragging = false 98 | // } 99 | // } 100 | // 101 | // let pressGesture = LongPressGesture(minimumDuration: 1.0) 102 | // .onEnded { value in 103 | // withAnimation { 104 | // isDragging = true 105 | // } 106 | // } 107 | // 108 | // // The dragGesture will wait until the pressGesture has triggered after minimumDuration 1.0 seconds. 109 | // let combined = pressGesture.sequenced(before: dragGesture) 110 | // 111 | // // The new combined gesture is set to run together with the tapGesture. 112 | // let simultaneously = tapGesture.simultaneously(with: combined) 113 | // 114 | // return Circle() 115 | // .overlay(isTapping ? Circle().stroke(Color.red, lineWidth: 5) : nil) //listening to the isTapping state 116 | // .frame(width: 100, height: 100) 117 | // .foregroundColor(isDragging ? Color.red : Color.black) // listening to the isDragging state. 118 | // .offset(offset) 119 | // .blockScrolling(isDragging) 120 | // .gesture(simultaneously) 121 | // } 122 | //} 123 | // 124 | //// MARK: - Preview 125 | // 126 | //struct ContentView8_Previews: PreviewProvider { 127 | // static var previews: some View { 128 | // ContentView8() 129 | // } 130 | //} 131 | -------------------------------------------------------------------------------- /Sources/tryingStuff/ContentView9.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// ContentView8.swift 3 | //// SwiftUI_AnimationTest 4 | //// 5 | //// Created by Marco Boerner on 27.02.22. 6 | //// 7 | // 8 | //import SwiftUI 9 | // 10 | //struct ContentView9: View { 11 | // 12 | // @State private var goTo: Int = 25 13 | // 14 | // var body: some View { 15 | // VStack { 16 | // Text("Hello there") 17 | // Spacer() 18 | // CustomScrollViewReader { proxy in 19 | // CustomScrollView() { 20 | // LazyVStack { 21 | // ForEach(0..<99, id: \.self) { id in 22 | // ListElement8(stringNumber: "\(id)") 23 | // .customID(id) 24 | // .frame(maxWidth: .infinity) 25 | // } 26 | // } 27 | // } 28 | // .onChange(of: goTo) { newValue in 29 | // proxy.scrollTo(newValue) 30 | // } 31 | // } 32 | // Spacer() 33 | // Button { 34 | // goTo -= 1 35 | // } label: { 36 | // Text("GoTo") 37 | // } 38 | // } 39 | // .frame(maxWidth: .infinity, maxHeight: .infinity) 40 | // 41 | // } 42 | //} 43 | // 44 | //// MARK: - Reader and proxy 45 | // 46 | //class ScrollDestination: ObservableObject { 47 | // @Published var frame: CGRect = .zero 48 | // @Published var point: CGPoint = .zero 49 | //} 50 | // 51 | //struct CustomScrollViewReader: View where Content: View { 52 | // 53 | // @State private var frames: [AnyHashable: CGRect] = [:] 54 | // @StateObject var scrollDestination = ScrollDestination() 55 | // 56 | // var content: (CustomScrollViewProxy) -> Content 57 | // 58 | // var body: some View { 59 | // 60 | // let customScrollViewProxy = CustomScrollViewProxy(positions: frames, scrollDestination: scrollDestination) 61 | // 62 | // return self.content(customScrollViewProxy) 63 | // .environmentObject(scrollDestination) 64 | // .onPreferenceChange(LocationPreferenceKey.self) { frames in 65 | // self.frames = frames 66 | // } 67 | // } 68 | //} 69 | // 70 | //struct CustomScrollViewProxy { 71 | // 72 | // static let customScrollViewCoordinateSpaceName = "CustomScrollView" 73 | // 74 | // @State private var positions: [AnyHashable: CGRect] 75 | // @ObservedObject var scrollDestination: ScrollDestination 76 | // 77 | // public func scrollTo(_ id: ID, anchor: UnitPoint = .zero) where ID : Hashable { 78 | // guard let position = positions[id] else { return } 79 | // 80 | // let x = position.minX + (position.maxX - position.minX) * anchor.x 81 | // let y = position.minY + (position.maxY - position.minY) * anchor.y 82 | // 83 | // scrollDestination.frame = position 84 | // scrollDestination.point = CGPoint(x: x, y: y) 85 | // } 86 | //} 87 | // 88 | //struct LocationPreferenceKey: PreferenceKey { 89 | // static func reduce(value: inout [AnyHashable : CGRect], nextValue: () -> [AnyHashable : CGRect]) { 90 | // 91 | // value.merge(nextValue(), uniquingKeysWith: { (oldValue, newValue) in 92 | // return newValue 93 | // }) 94 | // } 95 | // static var defaultValue: [AnyHashable: CGRect] = [:] 96 | //} 97 | // 98 | //extension View { 99 | // 100 | // func customID(_ value: ID) -> some View where ID: Hashable { 101 | // 102 | // return GeometryReader { geometry in 103 | // self.body 104 | // .preference(key: LocationPreferenceKey.self, value: [value: geometry.frame(in: .named(CustomScrollViewProxy.customScrollViewCoordinateSpaceName))]) 105 | // } 106 | // } 107 | //} 108 | // 109 | // 110 | //// MARK: - OpenScrollView 111 | // 112 | //struct CustomScrollView: View where Content: View { 113 | // 114 | // @State private var offset: CGSize = .zero 115 | // @State private var accumulatedOffset: CGSize = .zero 116 | // 117 | // @State private var axis: Axis.Set = [.vertical] 118 | // @EnvironmentObject private var scrollDestination: ScrollDestination 119 | // 120 | // var content: () -> Content 121 | // 122 | // var body: some View { 123 | // GeometryReader { outerGeometry in 124 | // self.content() 125 | // .contentShape(Rectangle()) 126 | // .background(GeometryReader { innerGeometry in 127 | // EmptyView() 128 | // .onChange(of: accumulatedOffset.height) { _ in 129 | // if accumulatedOffset.height > 0 { 130 | // accumulatedOffset.height = 0 131 | // } else if abs(accumulatedOffset.height) > innerGeometry.size.height - outerGeometry.size.height { 132 | // accumulatedOffset.height = -1*(innerGeometry.size.height - outerGeometry.size.height) 133 | // } 134 | // } 135 | // .onChange(of: accumulatedOffset.width) { _ in 136 | // if accumulatedOffset.width > 0 { 137 | // accumulatedOffset.width = 0 138 | // } else if abs(accumulatedOffset.width) > innerGeometry.size.width - outerGeometry.size.width { 139 | // accumulatedOffset.width = -1*(innerGeometry.size.width - outerGeometry.size.width) 140 | // } 141 | // } 142 | // }) 143 | // .offset(x: axis.contains(.horizontal) ? accumulatedOffset.width + offset.width : 0 , y: axis.contains(.vertical) ? accumulatedOffset.height + offset.height : 0) 144 | // .coordinateSpace(name: CustomScrollViewProxy.customScrollViewCoordinateSpaceName) 145 | // .animation(.spring(), value: offset) 146 | // .animation(.spring(), value: accumulatedOffset) 147 | // .onReceive(scrollDestination.$point) { point in 148 | // accumulatedOffset.height -= point.y 149 | // accumulatedOffset.width -= point.x 150 | // } 151 | // .gesture( 152 | // DragGesture() 153 | // .onChanged { gesture in 154 | // offset = gesture.translation 155 | // } 156 | // .onEnded { gesture in 157 | // accumulatedOffset.height += gesture.translation.height 158 | // accumulatedOffset.width += gesture.translation.width 159 | // offset = .zero 160 | // } 161 | // ) 162 | // } 163 | // } 164 | //} 165 | // 166 | //// MARK: - List Element 167 | // 168 | //struct ListElement8: View { 169 | // 170 | // let stringNumber: String 171 | // 172 | // var body: some View { 173 | // 174 | // return Text("Hello World \(stringNumber)") 175 | // } 176 | //} 177 | // 178 | //// MARK: - Preview 179 | // 180 | //struct ContentView9_Previews: PreviewProvider { 181 | // static var previews: some View { 182 | // ContentView9() 183 | // } 184 | //} 185 | -------------------------------------------------------------------------------- /Tests/OpenSwiftUIViewsTests/SwiftUI_AnimationTestTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import OpenSwiftUIViews 3 | 4 | final class SwiftUI_AnimationTestTests: XCTestCase { 5 | func testExample() throws { 6 | 7 | } 8 | } 9 | --------------------------------------------------------------------------------