├── .gitignore ├── .swiftlint.yml ├── .travis.yml ├── Example ├── .gitignore ├── .swiftlint.yml ├── Example.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Grid_Example.xcscheme └── Example │ ├── App.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── dog.imageset │ │ ├── Contents.json │ │ └── dog.jpg │ ├── Base.lproj │ └── LaunchScreen.xib │ ├── Calculator │ ├── CalcButton.swift │ ├── Calculator.swift │ └── MathOperation.swift │ ├── ContentModeExample.swift │ ├── ContentView.swift │ ├── Extesions │ ├── Color+brightness.swift │ ├── Color+hex.swift │ └── Helpers.swift │ ├── FlowExample.swift │ ├── PackingExample.swift │ ├── SpacingExample.swift │ ├── SpansExample.swift │ ├── StartsExample.swift │ └── Views │ ├── CardViews.swift │ └── ColorView.swift ├── ExyteGrid.podspec ├── Grid.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── Grid.xcscheme ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── Cache │ ├── Cache.swift │ └── GridCacheMode.swift ├── Configuration │ └── Constants.swift ├── Extensions │ ├── CGRect+Hashable.swift │ ├── CGSize+Hashable.swift │ ├── LayoutArrangement+description.swift │ └── View+If.swift ├── Grid.h ├── GridLayoutMath │ ├── LayoutArranging.swift │ └── LayoutPositioning.swift ├── Info.plist ├── Models │ ├── ArrangedItem.swift │ ├── GridAlignment.swift │ ├── GridContentMode.swift │ ├── GridElement.swift │ ├── GridFlow.swift │ ├── GridIndex.swift │ ├── GridPacking.swift │ ├── GridSpacing.swift │ ├── GridSpan.swift │ ├── GridStart.swift │ ├── GridTrack.swift │ ├── LayoutArrangement.swift │ ├── PositionedItem.swift │ └── Preferences │ │ ├── GridBackgroundPreference.swift │ │ ├── GridCellPreference.swift │ │ ├── GridOverlayPreference.swift │ │ └── GridPreference.swift └── View │ ├── Grid.swift │ ├── GridGroup.swift │ ├── Inits │ ├── ForEach+GridViewsContaining.swift │ ├── Grid+Init.swift │ ├── Grid+Inits_Data.swift │ ├── GridBuilder.swift │ ├── GridContentViewsProtocols.swift │ ├── GridElement+asGridElements.swift │ ├── GridGroup+Init.swift │ └── GridGroup+Inits_Data.swift │ ├── View+Environment.swift │ └── View+GridPreferences.swift └── Tests ├── ArrangingTest.swift ├── ArrangingWithStartsTest.swift ├── MacTestsInfo.plist ├── PositionColumnScrollTest.swift ├── PositionRowsScrollTest.swift └── iOSTestsInfo.plist /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | *.~LOCAL 22 | 23 | # Bundler 24 | .bundle 25 | 26 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 27 | # Carthage/Checkouts 28 | 29 | Carthage/Build 30 | 31 | # We recommend against adding the Pods directory to your .gitignore. However 32 | # you should judge for yourself, the pros and cons are mentioned at: 33 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 34 | # 35 | # Note: if you ignore the Pods directory, make sure to uncomment 36 | # `pod install` in .travis.yml 37 | # 38 | # Pods/ 39 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - control_statement 3 | - trailing_whitespace 4 | - nesting 5 | - type_name 6 | opt_in_rules: # some rules are only opt-in 7 | - empty_count 8 | # Find all the available rules by running: 9 | # swiftlint rules 10 | whitelist_rules: 11 | excluded: # paths to ignore during linting. Takes precedence over `included`. 12 | - Carthage 13 | - Pods 14 | - init.swift 15 | 16 | force_cast: warning 17 | 18 | force_try: 19 | severity: warning # explicitly 20 | 21 | identifier_name: 22 | min_length: # only min_length 23 | error: 2 # only error 24 | excluded: # excluded via string array 25 | - id 26 | - URL 27 | - vc 28 | - vm 29 | 30 | line_length: 31 | - 230 32 | - 500 33 | type_body_length: 34 | - 300 # warning 35 | - 400 # error 36 | file_length: 37 | - 500 # warning 38 | - 800 # error 39 | large_tuple: 40 | - 3 # warning 41 | - 4 # error 42 | function_parameter_count: 43 | - 6 # warning 44 | - 8 # error 45 | cyclomatic_complexity: 46 | warning: 13 47 | function_body_length: 48 | warning: 60 49 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle) 50 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode12 3 | 4 | branches: 5 | only: 6 | - master 7 | 8 | script: 9 | - set -o pipefail && xcodebuild test -project Grid.xcodeproj -scheme 'Grid' -sdk iphonesimulator14.0 ONLY_ACTIVE_ARCH=NO -destination 'platform=iOS Simulator,OS=14.0,name=iPhone 11 Pro' | xcpretty; 10 | 11 | notifications: 12 | slack: exyte:j0jYcgVm6XU9FEKP8WBAiJJj 13 | -------------------------------------------------------------------------------- /Example/.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | Pods/ 22 | Podfile.lock -------------------------------------------------------------------------------- /Example/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - control_statement 3 | - trailing_whitespace 4 | - nesting 5 | - type_name 6 | - multiple_closures_with_trailing_closure 7 | opt_in_rules: # some rules are only opt-in 8 | - empty_count 9 | # Find all the available rules by running: 10 | # swiftlint rules 11 | whitelist_rules: 12 | excluded: # paths to ignore during linting. Takes precedence over `included`. 13 | - Carthage 14 | - Pods 15 | - init.swift 16 | 17 | force_cast: warning 18 | 19 | force_try: 20 | severity: warning # explicitly 21 | 22 | identifier_name: 23 | min_length: # only min_length 24 | error: 2 # only error 25 | excluded: # excluded via string array 26 | - id 27 | - URL 28 | - vc 29 | - vm 30 | 31 | line_length: 32 | - 230 33 | - 500 34 | type_body_length: 35 | - 300 # warning 36 | - 400 # error 37 | file_length: 38 | - 500 # warning 39 | - 800 # error 40 | large_tuple: 41 | - 3 # warning 42 | - 4 # error 43 | function_parameter_count: 44 | - 6 # warning 45 | - 8 # error 46 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle) 47 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 53; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 360BA329247E87AF006C51FC /* CalcButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 360BA328247E87AE006C51FC /* CalcButton.swift */; }; 11 | 360BA32B247E87D6006C51FC /* MathOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 360BA32A247E87D6006C51FC /* MathOperation.swift */; }; 12 | 36161EBB247EC73C0076173B /* PackingExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36161EBA247EC73C0076173B /* PackingExample.swift */; }; 13 | 36161EBD247ECDAD0076173B /* SpacingExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36161EBC247ECDAC0076173B /* SpacingExample.swift */; }; 14 | 366A041829C839D90059EB43 /* ExyteGrid in Frameworks */ = {isa = PBXBuildFile; productRef = 366A041729C839D90059EB43 /* ExyteGrid */; }; 15 | 366A041E29C83BA40059EB43 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 366A041D29C83BA40059EB43 /* App.swift */; }; 16 | 366A042029C83F0A0059EB43 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; platformFilter = ios; }; 17 | 366B25072474F78E00F1DD09 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 366B25062474F78E00F1DD09 /* ContentView.swift */; }; 18 | 36A6C9FB247FBB8E005F5E7E /* ColorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A6C9FA247FBB8E005F5E7E /* ColorView.swift */; }; 19 | 36A6C9FD247FBBDC005F5E7E /* Color+brightness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A6C9FC247FBBDC005F5E7E /* Color+brightness.swift */; }; 20 | 36A6C9FF247FBC25005F5E7E /* FlowExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A6C9FE247FBC25005F5E7E /* FlowExample.swift */; }; 21 | 36A6CA01247FBC67005F5E7E /* ContentModeExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A6CA00247FBC66005F5E7E /* ContentModeExample.swift */; }; 22 | 36A6CA03247FBC7C005F5E7E /* CardViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A6CA02247FBC7C005F5E7E /* CardViews.swift */; }; 23 | 36A6CA05247FBD05005F5E7E /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A6CA04247FBD05005F5E7E /* Helpers.swift */; }; 24 | 36A6CA07247FD813005F5E7E /* SpansExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A6CA06247FD813005F5E7E /* SpansExample.swift */; }; 25 | 36A6CA0924801EA6005F5E7E /* StartsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A6CA0824801EA6005F5E7E /* StartsExample.swift */; }; 26 | 36B11E512475539700CDAE27 /* Calculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B11E502475539700CDAE27 /* Calculator.swift */; platformFilter = ios; }; 27 | 36BECA6E29C82DF30077D30F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 36AA6FE225304C110072AEBA /* Assets.xcassets */; }; 28 | 36F6C22A2476D919001818F2 /* Color+hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F6C2292476D919001818F2 /* Color+hex.swift */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 360BA328247E87AE006C51FC /* CalcButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalcButton.swift; sourceTree = ""; }; 33 | 360BA32A247E87D6006C51FC /* MathOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MathOperation.swift; sourceTree = ""; }; 34 | 36161EBA247EC73C0076173B /* PackingExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackingExample.swift; sourceTree = ""; }; 35 | 36161EBC247ECDAC0076173B /* SpacingExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacingExample.swift; sourceTree = ""; }; 36 | 366A041529C839A20059EB43 /* Grid */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Grid; path = ..; sourceTree = ""; }; 37 | 366A041D29C83BA40059EB43 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 38 | 366B25062474F78E00F1DD09 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 39 | 36A6C9FA247FBB8E005F5E7E /* ColorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorView.swift; sourceTree = ""; }; 40 | 36A6C9FC247FBBDC005F5E7E /* Color+brightness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+brightness.swift"; sourceTree = ""; }; 41 | 36A6C9FE247FBC25005F5E7E /* FlowExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowExample.swift; sourceTree = ""; }; 42 | 36A6CA00247FBC66005F5E7E /* ContentModeExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentModeExample.swift; sourceTree = ""; }; 43 | 36A6CA02247FBC7C005F5E7E /* CardViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardViews.swift; sourceTree = ""; }; 44 | 36A6CA04247FBD05005F5E7E /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; 45 | 36A6CA06247FD813005F5E7E /* SpansExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpansExample.swift; sourceTree = ""; }; 46 | 36A6CA0824801EA6005F5E7E /* StartsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartsExample.swift; sourceTree = ""; }; 47 | 36AA6FE225304C110072AEBA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | 36B11E502475539700CDAE27 /* Calculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calculator.swift; sourceTree = ""; }; 49 | 36F6C2292476D919001818F2 /* Color+hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+hex.swift"; sourceTree = ""; }; 50 | 607FACD01AFB9204008FA782 /* Grid_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Grid_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 52 | /* End PBXFileReference section */ 53 | 54 | /* Begin PBXFrameworksBuildPhase section */ 55 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | 366A041829C839D90059EB43 /* ExyteGrid in Frameworks */, 60 | ); 61 | runOnlyForDeploymentPostprocessing = 0; 62 | }; 63 | /* End PBXFrameworksBuildPhase section */ 64 | 65 | /* Begin PBXGroup section */ 66 | 366A041429C839A20059EB43 /* Packages */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | 366A041529C839A20059EB43 /* Grid */, 70 | ); 71 | name = Packages; 72 | sourceTree = ""; 73 | }; 74 | 36BECA6B29C82D110077D30F /* Calculator */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 36B11E502475539700CDAE27 /* Calculator.swift */, 78 | 360BA328247E87AE006C51FC /* CalcButton.swift */, 79 | 360BA32A247E87D6006C51FC /* MathOperation.swift */, 80 | ); 81 | path = Calculator; 82 | sourceTree = ""; 83 | }; 84 | 36BECA6C29C82D310077D30F /* Extesions */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 36F6C2292476D919001818F2 /* Color+hex.swift */, 88 | 36A6C9FC247FBBDC005F5E7E /* Color+brightness.swift */, 89 | 36A6CA04247FBD05005F5E7E /* Helpers.swift */, 90 | ); 91 | path = Extesions; 92 | sourceTree = ""; 93 | }; 94 | 36BECA6D29C82D4B0077D30F /* Views */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 36A6C9FA247FBB8E005F5E7E /* ColorView.swift */, 98 | 36A6CA02247FBC7C005F5E7E /* CardViews.swift */, 99 | ); 100 | path = Views; 101 | sourceTree = ""; 102 | }; 103 | 607FACC71AFB9204008FA782 = { 104 | isa = PBXGroup; 105 | children = ( 106 | 366A041429C839A20059EB43 /* Packages */, 107 | 607FACD21AFB9204008FA782 /* Example */, 108 | 607FACD11AFB9204008FA782 /* Products */, 109 | ); 110 | sourceTree = ""; 111 | }; 112 | 607FACD11AFB9204008FA782 /* Products */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 607FACD01AFB9204008FA782 /* Grid_Example.app */, 116 | ); 117 | name = Products; 118 | sourceTree = ""; 119 | }; 120 | 607FACD21AFB9204008FA782 /* Example */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | 36BECA6D29C82D4B0077D30F /* Views */, 124 | 36BECA6C29C82D310077D30F /* Extesions */, 125 | 36BECA6B29C82D110077D30F /* Calculator */, 126 | 36161EBA247EC73C0076173B /* PackingExample.swift */, 127 | 36161EBC247ECDAC0076173B /* SpacingExample.swift */, 128 | 36A6C9FE247FBC25005F5E7E /* FlowExample.swift */, 129 | 36A6CA00247FBC66005F5E7E /* ContentModeExample.swift */, 130 | 36A6CA06247FD813005F5E7E /* SpansExample.swift */, 131 | 36A6CA0824801EA6005F5E7E /* StartsExample.swift */, 132 | 366B25062474F78E00F1DD09 /* ContentView.swift */, 133 | 366A041D29C83BA40059EB43 /* App.swift */, 134 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 135 | 36AA6FE225304C110072AEBA /* Assets.xcassets */, 136 | ); 137 | path = Example; 138 | sourceTree = ""; 139 | }; 140 | /* End PBXGroup section */ 141 | 142 | /* Begin PBXNativeTarget section */ 143 | 607FACCF1AFB9204008FA782 /* Grid_Example */ = { 144 | isa = PBXNativeTarget; 145 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "Grid_Example" */; 146 | buildPhases = ( 147 | 36B55FD124752944003F792F /* SwiftLint */, 148 | 607FACCC1AFB9204008FA782 /* Sources */, 149 | 607FACCD1AFB9204008FA782 /* Frameworks */, 150 | 607FACCE1AFB9204008FA782 /* Resources */, 151 | ); 152 | buildRules = ( 153 | ); 154 | dependencies = ( 155 | ); 156 | name = Grid_Example; 157 | packageProductDependencies = ( 158 | 366A041729C839D90059EB43 /* ExyteGrid */, 159 | ); 160 | productName = Grid; 161 | productReference = 607FACD01AFB9204008FA782 /* Grid_Example.app */; 162 | productType = "com.apple.product-type.application"; 163 | }; 164 | /* End PBXNativeTarget section */ 165 | 166 | /* Begin PBXProject section */ 167 | 607FACC81AFB9204008FA782 /* Project object */ = { 168 | isa = PBXProject; 169 | attributes = { 170 | BuildIndependentTargetsInParallel = YES; 171 | LastSwiftUpdateCheck = 1200; 172 | LastUpgradeCheck = 1430; 173 | ORGANIZATIONNAME = Exyte; 174 | TargetAttributes = { 175 | 607FACCF1AFB9204008FA782 = { 176 | CreatedOnToolsVersion = 6.3.1; 177 | DevelopmentTeam = FZXCM5CJ7P; 178 | LastSwiftMigration = 1140; 179 | }; 180 | }; 181 | }; 182 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "Example" */; 183 | compatibilityVersion = "Xcode 3.2"; 184 | developmentRegion = en; 185 | hasScannedForEncodings = 0; 186 | knownRegions = ( 187 | en, 188 | Base, 189 | ); 190 | mainGroup = 607FACC71AFB9204008FA782; 191 | packageReferences = ( 192 | ); 193 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 194 | projectDirPath = ""; 195 | projectRoot = ""; 196 | targets = ( 197 | 607FACCF1AFB9204008FA782 /* Grid_Example */, 198 | ); 199 | }; 200 | /* End PBXProject section */ 201 | 202 | /* Begin PBXResourcesBuildPhase section */ 203 | 607FACCE1AFB9204008FA782 /* Resources */ = { 204 | isa = PBXResourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | 366A042029C83F0A0059EB43 /* LaunchScreen.xib in Resources */, 208 | 36BECA6E29C82DF30077D30F /* Assets.xcassets in Resources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | /* End PBXResourcesBuildPhase section */ 213 | 214 | /* Begin PBXShellScriptBuildPhase section */ 215 | 36B55FD124752944003F792F /* SwiftLint */ = { 216 | isa = PBXShellScriptBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | ); 220 | inputFileListPaths = ( 221 | ); 222 | inputPaths = ( 223 | ); 224 | name = SwiftLint; 225 | outputFileListPaths = ( 226 | ); 227 | outputPaths = ( 228 | ); 229 | runOnlyForDeploymentPostprocessing = 0; 230 | shellPath = /bin/sh; 231 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 232 | }; 233 | /* End PBXShellScriptBuildPhase section */ 234 | 235 | /* Begin PBXSourcesBuildPhase section */ 236 | 607FACCC1AFB9204008FA782 /* Sources */ = { 237 | isa = PBXSourcesBuildPhase; 238 | buildActionMask = 2147483647; 239 | files = ( 240 | 36A6C9FD247FBBDC005F5E7E /* Color+brightness.swift in Sources */, 241 | 36A6CA03247FBC7C005F5E7E /* CardViews.swift in Sources */, 242 | 36A6C9FF247FBC25005F5E7E /* FlowExample.swift in Sources */, 243 | 360BA32B247E87D6006C51FC /* MathOperation.swift in Sources */, 244 | 36A6C9FB247FBB8E005F5E7E /* ColorView.swift in Sources */, 245 | 360BA329247E87AF006C51FC /* CalcButton.swift in Sources */, 246 | 36A6CA0924801EA6005F5E7E /* StartsExample.swift in Sources */, 247 | 36161EBB247EC73C0076173B /* PackingExample.swift in Sources */, 248 | 36161EBD247ECDAD0076173B /* SpacingExample.swift in Sources */, 249 | 36A6CA01247FBC67005F5E7E /* ContentModeExample.swift in Sources */, 250 | 36A6CA05247FBD05005F5E7E /* Helpers.swift in Sources */, 251 | 366A041E29C83BA40059EB43 /* App.swift in Sources */, 252 | 36F6C22A2476D919001818F2 /* Color+hex.swift in Sources */, 253 | 36B11E512475539700CDAE27 /* Calculator.swift in Sources */, 254 | 36A6CA07247FD813005F5E7E /* SpansExample.swift in Sources */, 255 | 366B25072474F78E00F1DD09 /* ContentView.swift in Sources */, 256 | ); 257 | runOnlyForDeploymentPostprocessing = 0; 258 | }; 259 | /* End PBXSourcesBuildPhase section */ 260 | 261 | /* Begin PBXVariantGroup section */ 262 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { 263 | isa = PBXVariantGroup; 264 | children = ( 265 | 607FACDF1AFB9204008FA782 /* Base */, 266 | ); 267 | name = LaunchScreen.xib; 268 | sourceTree = ""; 269 | }; 270 | /* End PBXVariantGroup section */ 271 | 272 | /* Begin XCBuildConfiguration section */ 273 | 607FACED1AFB9204008FA782 /* Debug */ = { 274 | isa = XCBuildConfiguration; 275 | buildSettings = { 276 | ALWAYS_SEARCH_USER_PATHS = NO; 277 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 278 | CLANG_CXX_LIBRARY = "libc++"; 279 | CLANG_ENABLE_MODULES = YES; 280 | CLANG_ENABLE_OBJC_ARC = YES; 281 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 282 | CLANG_WARN_BOOL_CONVERSION = YES; 283 | CLANG_WARN_COMMA = YES; 284 | CLANG_WARN_CONSTANT_CONVERSION = YES; 285 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 286 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 287 | CLANG_WARN_EMPTY_BODY = YES; 288 | CLANG_WARN_ENUM_CONVERSION = YES; 289 | CLANG_WARN_INFINITE_RECURSION = YES; 290 | CLANG_WARN_INT_CONVERSION = YES; 291 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 292 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 293 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 294 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 295 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 296 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 297 | CLANG_WARN_STRICT_PROTOTYPES = YES; 298 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 299 | CLANG_WARN_UNREACHABLE_CODE = YES; 300 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 301 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 302 | COPY_PHASE_STRIP = NO; 303 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 304 | ENABLE_STRICT_OBJC_MSGSEND = YES; 305 | ENABLE_TESTABILITY = YES; 306 | GCC_C_LANGUAGE_STANDARD = gnu99; 307 | GCC_DYNAMIC_NO_PIC = NO; 308 | GCC_NO_COMMON_BLOCKS = YES; 309 | GCC_OPTIMIZATION_LEVEL = 0; 310 | GCC_PREPROCESSOR_DEFINITIONS = ( 311 | "DEBUG=1", 312 | "$(inherited)", 313 | ); 314 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 315 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 316 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 317 | GCC_WARN_UNDECLARED_SELECTOR = YES; 318 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 319 | GCC_WARN_UNUSED_FUNCTION = YES; 320 | GCC_WARN_UNUSED_VARIABLE = YES; 321 | MTL_ENABLE_DEBUG_INFO = YES; 322 | ONLY_ACTIVE_ARCH = YES; 323 | SDKROOT = iphoneos; 324 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 325 | }; 326 | name = Debug; 327 | }; 328 | 607FACEE1AFB9204008FA782 /* Release */ = { 329 | isa = XCBuildConfiguration; 330 | buildSettings = { 331 | ALWAYS_SEARCH_USER_PATHS = NO; 332 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 333 | CLANG_CXX_LIBRARY = "libc++"; 334 | CLANG_ENABLE_MODULES = YES; 335 | CLANG_ENABLE_OBJC_ARC = YES; 336 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 337 | CLANG_WARN_BOOL_CONVERSION = YES; 338 | CLANG_WARN_COMMA = YES; 339 | CLANG_WARN_CONSTANT_CONVERSION = YES; 340 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 341 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 342 | CLANG_WARN_EMPTY_BODY = YES; 343 | CLANG_WARN_ENUM_CONVERSION = YES; 344 | CLANG_WARN_INFINITE_RECURSION = YES; 345 | CLANG_WARN_INT_CONVERSION = YES; 346 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 347 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 348 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 349 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 350 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 351 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 352 | CLANG_WARN_STRICT_PROTOTYPES = YES; 353 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 354 | CLANG_WARN_UNREACHABLE_CODE = YES; 355 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 356 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 357 | COPY_PHASE_STRIP = NO; 358 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 359 | ENABLE_NS_ASSERTIONS = NO; 360 | ENABLE_STRICT_OBJC_MSGSEND = YES; 361 | GCC_C_LANGUAGE_STANDARD = gnu99; 362 | GCC_NO_COMMON_BLOCKS = YES; 363 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 364 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 365 | GCC_WARN_UNDECLARED_SELECTOR = YES; 366 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 367 | GCC_WARN_UNUSED_FUNCTION = YES; 368 | GCC_WARN_UNUSED_VARIABLE = YES; 369 | MTL_ENABLE_DEBUG_INFO = NO; 370 | SDKROOT = iphoneos; 371 | SWIFT_COMPILATION_MODE = wholemodule; 372 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 373 | VALIDATE_PRODUCT = YES; 374 | }; 375 | name = Release; 376 | }; 377 | 607FACF01AFB9204008FA782 /* Debug */ = { 378 | isa = XCBuildConfiguration; 379 | buildSettings = { 380 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 381 | CURRENT_PROJECT_VERSION = 1.0; 382 | DEVELOPMENT_TEAM = FZXCM5CJ7P; 383 | GENERATE_INFOPLIST_FILE = YES; 384 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen.xib; 385 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 386 | LD_RUNPATH_SEARCH_PATHS = ( 387 | "$(inherited)", 388 | "@executable_path/Frameworks", 389 | ); 390 | MACOSX_DEPLOYMENT_TARGET = 11.0; 391 | MARKETING_VERSION = 1.0; 392 | MODULE_NAME = ExampleApp; 393 | PRODUCT_BUNDLE_IDENTIFIER = exyte.GridExample; 394 | PRODUCT_NAME = Grid_Example; 395 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; 396 | SUPPORTS_MACCATALYST = NO; 397 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 398 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 399 | SWIFT_VERSION = 5.0; 400 | TARGETED_DEVICE_FAMILY = "1,3"; 401 | TVOS_DEPLOYMENT_TARGET = 14.0; 402 | }; 403 | name = Debug; 404 | }; 405 | 607FACF11AFB9204008FA782 /* Release */ = { 406 | isa = XCBuildConfiguration; 407 | buildSettings = { 408 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 409 | CURRENT_PROJECT_VERSION = 1.0; 410 | DEVELOPMENT_TEAM = FZXCM5CJ7P; 411 | GENERATE_INFOPLIST_FILE = YES; 412 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen.xib; 413 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 414 | LD_RUNPATH_SEARCH_PATHS = ( 415 | "$(inherited)", 416 | "@executable_path/Frameworks", 417 | ); 418 | MACOSX_DEPLOYMENT_TARGET = 11.0; 419 | MARKETING_VERSION = 1.0; 420 | MODULE_NAME = ExampleApp; 421 | PRODUCT_BUNDLE_IDENTIFIER = exyte.GridExample; 422 | PRODUCT_NAME = Grid_Example; 423 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; 424 | SUPPORTS_MACCATALYST = NO; 425 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 426 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 427 | SWIFT_VERSION = 5.0; 428 | TARGETED_DEVICE_FAMILY = "1,3"; 429 | TVOS_DEPLOYMENT_TARGET = 14.0; 430 | }; 431 | name = Release; 432 | }; 433 | /* End XCBuildConfiguration section */ 434 | 435 | /* Begin XCConfigurationList section */ 436 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "Example" */ = { 437 | isa = XCConfigurationList; 438 | buildConfigurations = ( 439 | 607FACED1AFB9204008FA782 /* Debug */, 440 | 607FACEE1AFB9204008FA782 /* Release */, 441 | ); 442 | defaultConfigurationIsVisible = 0; 443 | defaultConfigurationName = Release; 444 | }; 445 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "Grid_Example" */ = { 446 | isa = XCConfigurationList; 447 | buildConfigurations = ( 448 | 607FACF01AFB9204008FA782 /* Debug */, 449 | 607FACF11AFB9204008FA782 /* Release */, 450 | ); 451 | defaultConfigurationIsVisible = 0; 452 | defaultConfigurationName = Release; 453 | }; 454 | /* End XCConfigurationList section */ 455 | 456 | /* Begin XCSwiftPackageProductDependency section */ 457 | 366A041729C839D90059EB43 /* ExyteGrid */ = { 458 | isa = XCSwiftPackageProductDependency; 459 | productName = ExyteGrid; 460 | }; 461 | /* End XCSwiftPackageProductDependency section */ 462 | }; 463 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 464 | } 465 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/xcshareddata/xcschemes/Grid_Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Example/Example/App.swift: -------------------------------------------------------------------------------- 1 | // 2 | // App.swift 3 | // Example 4 | // 5 | // Created by Denis Obukhov on 20.03.2023. 6 | // Copyright © 2023 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @main 12 | struct MyApp: App { 13 | var body: some Scene { 14 | WindowGroup { 15 | ContentView() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/Example/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 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/dog.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dog.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/dog.imageset/dog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/Grid/7c745488d64f63533fd0fe066910777fb4dbdbef/Example/Example/Assets.xcassets/dog.imageset/dog.jpg -------------------------------------------------------------------------------- /Example/Example/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Example/Example/Calculator/CalcButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalcButton.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 27.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CalcButton: View { 12 | let operation: MathOperation 13 | 14 | init(_ operation: MathOperation) { 15 | self.operation = operation 16 | } 17 | 18 | var body: some View { 19 | Button(action: {}) { 20 | Capsule() 21 | .fill(self.fillColor) 22 | .overlay( 23 | Text(self.operation.description) 24 | .font(.custom("PingFang TC", size: 32)) 25 | .fontWeight(.regular) 26 | .minimumScaleFactor(0.01) 27 | .foregroundColor(self.textColor) 28 | .padding(10) 29 | ) 30 | } 31 | } 32 | 33 | var fillColor: Color { 34 | if [.divide, .multiply, .substract, .add, .equal].contains(self.operation) { 35 | return Color(hex: "#fca00b") 36 | } 37 | if [.clear, .sign, .percent].contains(self.operation) { 38 | return Color(hex: "#a4a5a6") 39 | } 40 | if case .function = self.operation { 41 | return Color(hex: "#202122") 42 | } 43 | return Color(hex: "#323334") 44 | } 45 | 46 | var textColor: Color { 47 | if [.clear, .sign, .percent].contains(self.operation) { 48 | return Color.black 49 | } 50 | return Color.white 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Example/Example/Calculator/Calculator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Calculator.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 20.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import ExyteGrid 11 | 12 | struct Calculator: View { 13 | enum Mode: CustomStringConvertible { 14 | case system 15 | case second 16 | 17 | var description: String { 18 | switch self { 19 | case .system: 20 | return "System" 21 | case .second: 22 | return "Alternative" 23 | } 24 | } 25 | } 26 | 27 | @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? 28 | @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? 29 | @State var mode: Mode = .system 30 | 31 | var isPortrait: Bool { 32 | let result = verticalSizeClass == .regular && horizontalSizeClass == .compact 33 | return result 34 | } 35 | 36 | var digits: [MathOperation] { 37 | (1..<10).map { .digit($0) } + [.digit(0)] 38 | } 39 | 40 | var body: some View { 41 | GeometryReader { geometry in 42 | ZStack { 43 | Color.black 44 | .edgesIgnoringSafeArea(.all) 45 | 46 | VStack(alignment: .trailing, spacing: 0) { 47 | self.modesPicker 48 | Spacer() 49 | Text("565 626") 50 | .foregroundColor(.white) 51 | .font(.custom("PingFang TC", size: 90)) 52 | .fontWeight(.light) 53 | .minimumScaleFactor(0.01) 54 | 55 | Grid(tracks: self.tracks, spacing: 10) { 56 | self.mainArithmeticButtons 57 | 58 | if self.mode == .system { 59 | CalcButton(.percent) 60 | .animation(nil) 61 | CalcButton(.sign) 62 | .animation(nil) 63 | 64 | GridGroup(MathOperation.clear) { 65 | CalcButton($0) 66 | } 67 | } 68 | 69 | GridGroup(self.digits, id: \.self) { 70 | if self.mode == .system && $0 == .digit(0) { 71 | CalcButton($0) 72 | .gridSpan(column: 2) 73 | } else { 74 | CalcButton($0) 75 | } 76 | } 77 | 78 | GridGroup(MathOperation.point) { 79 | CalcButton($0) 80 | } 81 | 82 | self.landscapeButtons 83 | } 84 | .gridContentMode(.fill) 85 | .gridAnimation(.easeInOut) 86 | .frame(width: self.proportionalSize(geometry: geometry).width, 87 | height: self.proportionalSize(geometry: geometry).height) 88 | } 89 | } 90 | } 91 | .environment(\.colorScheme, .dark) 92 | } 93 | 94 | var tracks: [GridTrack] { 95 | self.isPortrait ? (self.mode == .system ? 4 : 5) : (self.mode == .system ? 10 : 11) 96 | } 97 | 98 | var landscapeButtons: some View { 99 | let funcCount = (self.mode == .system ? 30 : 24) 100 | let functions: [MathOperation] = self.isPortrait ? [] : (0.. CGSize { 143 | let height: CGFloat 144 | if self.isPortrait { 145 | height = geometry.size.width * 5 / 4 146 | } else { 147 | height = geometry.size.width * 4 / 10 148 | } 149 | return CGSize(width: geometry.size.width, 150 | height: height) 151 | } 152 | 153 | private var modesPicker: some View { 154 | Picker("Mode", selection: $mode) { 155 | ForEach([Mode.system, Mode.second], id: \.self) { 156 | Text($0.description) 157 | .tag($0) 158 | } 159 | } 160 | .pickerStyle(SegmentedPickerStyle()) 161 | } 162 | } 163 | 164 | struct Calculator_Previews: PreviewProvider { 165 | static var previews: some View { 166 | Calculator() 167 | .environment(\.verticalSizeClass, .regular) 168 | .environment(\.horizontalSizeClass, .compact) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Example/Example/Calculator/MathOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MathOperation.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 27.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum MathOperation: Hashable { 12 | case digit(Int) 13 | case clear, sign, percent, equal, point 14 | case divide, multiply, add, substract 15 | case function(Int) 16 | } 17 | 18 | extension MathOperation: CustomStringConvertible { 19 | var description: String { 20 | switch self { 21 | case .digit(let digit): 22 | return String(digit) 23 | case .clear: 24 | return "C" 25 | case .sign: 26 | return "±" 27 | case .percent: 28 | return "%" 29 | case .equal: 30 | return "=" 31 | case .point: 32 | return "." 33 | case .divide: 34 | return "÷" 35 | case .multiply: 36 | return "×" 37 | case .add: 38 | return "+" 39 | case .substract: 40 | return "−" 41 | case .function(let number): 42 | return "f\(number + 1)" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Example/Example/ContentModeExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentModeExample.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 28.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import ExyteGrid 11 | 12 | struct ContentModeExample: View { 13 | private struct Model: Hashable { 14 | let text: String 15 | let span: GridSpan 16 | let color = GridColor.random.lighter(by: 50) 17 | } 18 | 19 | @State var contentMode: GridContentMode = .scroll 20 | 21 | var body: some View { 22 | VStack { 23 | self.modesPicker 24 | 25 | Grid(models, id: \.self, tracks: 3) { 26 | VCardView(text: $0.text, color: $0.color) 27 | .gridSpan($0.span) 28 | } 29 | .gridContentMode(self.contentMode) 30 | .gridFlow(.rows) 31 | } 32 | } 33 | 34 | private let models: [Model] = [ 35 | Model(text: placeholderText(length: 30), span: [1, 1]), 36 | Model(text: placeholderText(length: 50), span: [1, 1]), 37 | Model(text: placeholderText(length: 20), span: [1, 1]), 38 | Model(text: placeholderText(length: 100), span: [2, 1]), 39 | Model(text: placeholderText(length: 150), span: [1, 1]), 40 | Model(text: placeholderText(length: 75), span: [1, 1]), 41 | Model(text: placeholderText(length: 155), span: [1, 1]), 42 | Model(text: placeholderText(length: 150), span: [1, 2]), 43 | Model(text: placeholderText(length: 160), span: [2, 1]), 44 | Model(text: placeholderText(length: 300), span: [3, 1]) 45 | ] 46 | 47 | private var modesPicker: some View { 48 | Picker("Mode", selection: $contentMode) { 49 | ForEach([GridContentMode.scroll, GridContentMode.fill], id: \.self) { 50 | Text($0 == .scroll ? "Scroll" : "Fill") 51 | .tag($0) 52 | } 53 | } 54 | .pickerStyle(SegmentedPickerStyle()) 55 | } 56 | } 57 | 58 | struct ContentModeExample_Previews: PreviewProvider { 59 | static var previews: some View { 60 | ContentModeExample() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Example/Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 29.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import ExyteGrid 11 | 12 | struct ContentView: View { 13 | @State var showingCalculator = false 14 | @State var showingSpans = false 15 | @State var showingStarts = false 16 | @State var showingFlow = false 17 | @State var showingContentMode = false 18 | @State var showingPacking = false 19 | @State var showingSpacing = false 20 | 21 | #if os(iOS) 22 | var body: some View { 23 | NavigationView { 24 | List { 25 | Button("Calculator") { self.showingCalculator.toggle() } 26 | .sheet(isPresented: $showingCalculator) { Calculator() } 27 | 28 | Button("Spans") { self.showingSpans.toggle() } 29 | .sheet(isPresented: $showingSpans) { SpansExample() } 30 | 31 | Button("Starts") { self.showingStarts.toggle() } 32 | .sheet(isPresented: $showingStarts) { StartsExample() } 33 | 34 | Button("Flow") { self.showingFlow.toggle() } 35 | .sheet(isPresented: $showingFlow) { FlowExample() } 36 | 37 | Button("Content mode") { self.showingContentMode.toggle() } 38 | .sheet(isPresented: $showingContentMode) { ContentModeExample() } 39 | 40 | Button("Packing") { self.showingPacking.toggle() } 41 | .sheet(isPresented: $showingPacking) { PackingExample() } 42 | 43 | Button("Spacing") { self.showingSpacing.toggle() } 44 | .sheet(isPresented: $showingSpacing) { SpacingExample() } 45 | } 46 | .navigationBarTitle(Text("ExyteGrid"), displayMode: .inline) 47 | } 48 | } 49 | #endif 50 | 51 | #if os(macOS) || os(tvOS) 52 | var body: some View { 53 | NavigationView { 54 | List { 55 | NavigationLink("Spans", destination: SpansExample()) 56 | NavigationLink("Starts", destination: StartsExample()) 57 | NavigationLink("Flow", destination: FlowExample()) 58 | NavigationLink("Content mode", destination: ContentModeExample()) 59 | NavigationLink("Packing", destination: PackingExample()) 60 | NavigationLink("Spacing", destination: SpacingExample()) 61 | } 62 | } 63 | } 64 | #endif 65 | } 66 | 67 | struct ContentView_Previews: PreviewProvider { 68 | static var previews: some View { 69 | ContentView() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Example/Example/Extesions/Color+brightness.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+brightness.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 28.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(watchOS) || os(tvOS) 10 | 11 | import UIKit 12 | typealias GridColor = UIColor 13 | 14 | #else 15 | 16 | import AppKit 17 | typealias GridColor = NSColor 18 | 19 | #endif 20 | 21 | extension GridColor { 22 | func lighter(by percentage: CGFloat = 30.0) -> GridColor { 23 | return self.adjust(by: abs(percentage) ) 24 | } 25 | 26 | func darker(by percentage: CGFloat = 30.0) -> GridColor { 27 | return self.adjust(by: -1 * abs(percentage) ) 28 | } 29 | 30 | func adjust(by percentage: CGFloat = 30.0) -> GridColor { 31 | var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 32 | 33 | #if os(iOS) || os(watchOS) || os(tvOS) 34 | self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) 35 | #else 36 | self.usingColorSpace(.deviceRGB)?.getRed(&red, green: &green, blue: &blue, alpha: &alpha) 37 | #endif 38 | 39 | return GridColor(red: min(red + percentage / 100, 1.0), 40 | green: min(green + percentage / 100, 1.0), 41 | blue: min(blue + percentage / 100, 1.0), 42 | alpha: alpha) 43 | } 44 | 45 | static var random: GridColor { 46 | GridColor(hue: CGFloat(arc4random_uniform(255)) / 255.0, saturation: 1, brightness: 1, alpha: 1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Example/Example/Extesions/Color+hex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+hex.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 21.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | // swiftlint:disable identifier_name 10 | 11 | import SwiftUI 12 | 13 | extension Color { 14 | init(hex: String) { 15 | let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) 16 | var int: UInt64 = 0 17 | Scanner(string: hex).scanHexInt64(&int) 18 | let a, r, g, b: UInt64 19 | switch hex.count { 20 | case 3: // RGB (12-bit) 21 | (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) 22 | case 6: // RGB (24-bit) 23 | (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) 24 | case 8: // ARGB (32-bit) 25 | (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) 26 | default: 27 | (a, r, g, b) = (1, 1, 1, 0) 28 | } 29 | 30 | self.init( 31 | .sRGB, 32 | red: Double(r) / 255, 33 | green: Double(g) / 255, 34 | blue: Double(b) / 255, 35 | opacity: Double(a) / 255 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/Example/Extesions/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 28.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ExyteGrid 11 | 12 | // swiftlint:disable line_length 13 | func placeholderText(length: Int? = nil) -> String { 14 | let placeholderText = 15 | """ 16 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna 17 | """ 18 | 19 | return placeholderText.prefix(length ?? placeholderText.count) + "!" 20 | } 21 | -------------------------------------------------------------------------------- /Example/Example/FlowExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlowExample.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 28.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import ExyteGrid 11 | 12 | struct FlowExample: View { 13 | @State var flow: GridFlow = .rows 14 | 15 | var body: some View { 16 | VStack { 17 | self.flowPicker 18 | 19 | Grid(0..<15, tracks: 5, flow: self.flow, spacing: 5) { 20 | ColorView($0.isMultiple(of: 2) ? .black : .orange) 21 | .overlay( 22 | Text(String($0)) 23 | .font(.system(size: 35)) 24 | .foregroundColor(.white) 25 | ) 26 | } 27 | .gridAnimation(.default) 28 | } 29 | } 30 | 31 | private var flowPicker: some View { 32 | Picker("Flow", selection: $flow) { 33 | withAnimation { 34 | ForEach([GridFlow.rows, GridFlow.columns], id: \.self) { 35 | Text($0 == .rows ? "ROWS" : "COLUMNS") 36 | .tag($0) 37 | } 38 | } 39 | } 40 | .pickerStyle(SegmentedPickerStyle()) 41 | } 42 | } 43 | 44 | struct FlowExample_Previews: PreviewProvider { 45 | static var previews: some View { 46 | FlowExample() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Example/Example/PackingExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PackingExample.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 27.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import ExyteGrid 11 | 12 | struct PackingExample: View { 13 | 14 | @State var gridPacking = GridPacking.sparse 15 | 16 | private var packingPicker: some View { 17 | Picker("Packing", selection: $gridPacking) { 18 | withAnimation { 19 | ForEach([GridPacking.sparse, GridPacking.dense], id: \.self) { 20 | Text($0 == .sparse ? "SPARSE" : "DENSE") 21 | .tag($0) 22 | } 23 | } 24 | } 25 | .pickerStyle(SegmentedPickerStyle()) 26 | } 27 | 28 | var body: some View { 29 | VStack { 30 | self.packingPicker 31 | 32 | Grid(tracks: 4) { 33 | ColorView(.red) 34 | 35 | ColorView(.black) 36 | .gridSpan(column: 4) 37 | 38 | ColorView(.purple) 39 | 40 | ColorView(.orange) 41 | ColorView(.green) 42 | } 43 | .gridPacking(self.gridPacking) 44 | .gridAnimation(.default) 45 | } 46 | } 47 | } 48 | 49 | struct PackingExample_Previews: PreviewProvider { 50 | static var previews: some View { 51 | PackingExample() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/Example/SpacingExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PackingExample.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 27.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import ExyteGrid 11 | 12 | struct SpacingExample: View { 13 | 14 | @State var vSpacing: CGFloat = 0 15 | @State var hSpacing: CGFloat = 0 16 | 17 | var body: some View { 18 | VStack { 19 | self.sliders 20 | 21 | Grid(tracks: 3, spacing: [hSpacing, vSpacing]) { 22 | ForEach(0..<21) { 23 | // Inner image used to measure size 24 | self.image 25 | .aspectRatio(contentMode: .fit) 26 | .opacity(0) 27 | .gridSpan(column: max(1, $0 % 4)) 28 | .gridCellOverlay { 29 | // This one is to display 30 | self.image 31 | .aspectRatio(contentMode: .fill) 32 | .frame(width: $0?.width, height: $0?.height) 33 | .cornerRadius(5) 34 | .clipped() 35 | .shadow(color: self.shadowColor, radius: 10, x: 0, y: 0) 36 | } 37 | } 38 | } 39 | .background(self.backgroundColor) 40 | .gridContentMode(.scroll) 41 | .gridPacking(.dense) 42 | } 43 | } 44 | 45 | var sliders: some View { 46 | VStack(spacing: 10) { 47 | #if !os(tvOS) 48 | HStack { 49 | Text("hSpace: ") 50 | Slider(value: $hSpacing, in: 0...50, step: 1) 51 | } 52 | HStack { 53 | Text("vSpace: ") 54 | Slider(value: $vSpacing, in: 0...50, step: 1) 55 | } 56 | #endif 57 | } 58 | .padding() 59 | .background(Color.white) 60 | } 61 | 62 | var image: Image { 63 | Image("dog") 64 | .resizable() 65 | } 66 | 67 | var shadowColor: Color { 68 | Color.black.opacity(Double(max(self.vSpacing, self.hSpacing)) / 20) 69 | } 70 | 71 | var backgroundColor: Color { 72 | Color.black.opacity(1 - Double(max(self.vSpacing, self.hSpacing)) / 20) 73 | } 74 | } 75 | 76 | struct SpacingExample_Previews: PreviewProvider { 77 | static var previews: some View { 78 | SpacingExample() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Example/Example/SpansExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpansExample.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 28.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import ExyteGrid 11 | 12 | struct SpansExample: View { 13 | var body: some View { 14 | Grid(tracks: [.fr(1), .fit, .fit], spacing: 10) { 15 | VCardView(text: placeholderText(), 16 | color: .red) 17 | 18 | VCardView(text: placeholderText(length: 30), 19 | color: .orange) 20 | .frame(maxWidth: 70) 21 | 22 | VCardView(text: placeholderText(length: 120), 23 | color: .green) 24 | .frame(maxWidth: 100) 25 | .gridSpan(column: 1, row: 2) 26 | 27 | VCardView(text: placeholderText(length: 160), 28 | color: .magenta) 29 | .gridSpan(column: 2, row: 1) 30 | 31 | VCardView(text: placeholderText(length: 190), 32 | color: .cyan) 33 | .gridSpan(column: 3, row: 1) 34 | } 35 | } 36 | } 37 | 38 | struct SpansExample_Previews: PreviewProvider { 39 | static var previews: some View { 40 | SpansExample() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Example/Example/StartsExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartsExample.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 28.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import ExyteGrid 11 | 12 | struct StartsExample: View { 13 | var body: some View { 14 | 15 | Grid(tracks: [.pt(50), .fr(1), .fr(1.5), .fit]) { 16 | ForEach(0..<6) { _ in 17 | ColorView(.black) 18 | } 19 | 20 | ColorView(.brown) 21 | .gridSpan(column: 3) 22 | 23 | ColorView(.blue) 24 | .gridSpan(column: 2) 25 | 26 | ColorView(.orange) 27 | .gridSpan(row: 3) 28 | 29 | ColorView(.red) 30 | .gridStart(row: 1) 31 | .gridSpan(column: 2, row: 2) 32 | 33 | ColorView(.yellow) 34 | .gridStart(row: 2) 35 | 36 | ColorView(.purple) 37 | .frame(maxWidth: 50) 38 | .gridStart(column: 3, row: 0) 39 | .gridSpan(row: 9) 40 | 41 | ColorView(.green) 42 | .gridSpan(column: 2, row: 3) 43 | 44 | ColorView(.cyan) 45 | 46 | ColorView(.gray) 47 | .gridStart(column: 2) 48 | } 49 | } 50 | } 51 | 52 | struct StartsExample_Previews: PreviewProvider { 53 | static var previews: some View { 54 | StartsExample() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Example/Example/Views/CardViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardViews.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 28.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct VCardView: View { 12 | let text: String 13 | let color: GridColor 14 | 15 | var body: some View { 16 | VStack { 17 | Image("dog") 18 | .resizable() 19 | .aspectRatio(contentMode: .fit) 20 | .cornerRadius(5) 21 | .frame(minHeight: 25) 22 | 23 | Text(self.text) 24 | .layoutPriority(.greatestFiniteMagnitude) 25 | .minimumScaleFactor(0.5) 26 | } 27 | .padding(5) 28 | .gridCellBackground { _ in 29 | ColorView(self.color) 30 | } 31 | .gridCellOverlay { _ in 32 | RoundedRectangle(cornerRadius: 5) 33 | .strokeBorder(Color(self.color.darker()), 34 | lineWidth: 3) 35 | } 36 | } 37 | } 38 | 39 | struct HCardView: View { 40 | let text: String 41 | let color: GridColor 42 | 43 | var body: some View { 44 | HStack { 45 | Image("dog") 46 | .resizable() 47 | .aspectRatio(contentMode: .fit) 48 | .cornerRadius(5) 49 | .frame(minHeight: 25) 50 | 51 | Text(self.text) 52 | .layoutPriority(.greatestFiniteMagnitude) 53 | .minimumScaleFactor(0.5) 54 | .frame(maxWidth: 200) 55 | } 56 | .padding(5) 57 | .gridCellBackground { _ in 58 | ColorView(self.color) 59 | } 60 | .gridCellOverlay { _ in 61 | RoundedRectangle(cornerRadius: 5) 62 | .strokeBorder(Color(self.color.darker()), 63 | lineWidth: 3) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Example/Example/Views/ColorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorView.swift 3 | // Grid_Example 4 | // 5 | // Created by Denis Obukhov on 28.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ColorView: View { 12 | 13 | let color: GridColor 14 | 15 | init(_ color: GridColor) { 16 | self.color = color 17 | } 18 | 19 | var body: some View { 20 | RoundedRectangle(cornerRadius: 5) 21 | .fill( 22 | LinearGradient(gradient: 23 | Gradient(colors: 24 | [Color(self.color.lighter()), 25 | Color(self.color.darker())]), 26 | startPoint: .topLeading, 27 | endPoint: .bottomTrailing) 28 | ) 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ExyteGrid.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint Grid.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'ExyteGrid' 11 | s.version = '1.5.0' 12 | s.summary = 'The most powerful Grid container missed in SwiftUI' 13 | 14 | s.homepage = 'https://github.com/exyte/Grid.git' 15 | s.license = { :type => 'MIT', :file => 'LICENSE' } 16 | s.author = { 'Exyte' => 'info@exyte.com' } 17 | s.source = { :git => 'https://github.com/exyte/Grid.git', :tag => s.version.to_s } 18 | s.social_media_url = 'http://exyte.com' 19 | 20 | s.ios.deployment_target = '14.0' 21 | s.tvos.deployment_target = '14.0' 22 | s.osx.deployment_target = "10.15" 23 | s.source_files = 'Sources/**/*.swift' 24 | s.swift_version = "5.5" 25 | 26 | end 27 | -------------------------------------------------------------------------------- /Grid.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Grid.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Grid.xcodeproj/xcshareddata/xcschemes/Grid.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Denis Obukhov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ExyteGrid", 7 | platforms: [ 8 | .iOS(.v14), 9 | .macOS(.v10_15), 10 | .tvOS(.v14), 11 | ], 12 | products: [ 13 | .library(name: "ExyteGrid", targets: ["ExyteGrid"]) 14 | ], 15 | dependencies: [ 16 | ], 17 | targets: [ 18 | .target( 19 | name: "ExyteGrid", 20 | dependencies: [], 21 | path: "Sources") 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |       4 | 5 | 6 | 7 | 8 | # Grid 9 | 10 | Grid view inspired by CSS Grid and written with SwiftUI 11 | 12 | Read Article » 13 | 14 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fexyte%2FGrid%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/exyte/Grid) 15 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fexyte%2FGrid%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/exyte/Grid) 16 | [![SPM Compatible](https://img.shields.io/badge/SwiftPM-Compatible-brightgreen.svg)](https://swiftpackageindex.com/exyte/Grid) 17 | [![Cocoapods Compatible](https://img.shields.io/badge/cocoapods-Compatible-brightgreen.svg)](https://cocoapods.org/pods/Grid) 18 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-brightgreen.svg?style=flat)](https://github.com/Carthage/Carthage) 19 | [![License: MIT](https://img.shields.io/badge/License-MIT-black.svg)](https://opensource.org/licenses/MIT) 20 | 21 | ## Overview 22 | 23 | Grid is a powerful and easy way to layout your views in SwiftUI: 24 | 25 | 26 | 27 | Check out [full documentation](#documentation) below. 28 | 29 | ## Installation 30 | 31 | #### CocoaPods 32 | 33 | Grid is available through [CocoaPods](https://cocoapods.org). To install 34 | it, simply add the following line to your Podfile: 35 | 36 | ```ruby 37 | pod 'ExyteGrid' 38 | ``` 39 | 40 | #### Swift Package Manager 41 | 42 | Grid is available through [Swift Package Manager](https://swift.org/package-manager). 43 | 44 | Add it to an existing Xcode project as a package dependency: 45 | 46 | 1. From the **File** menu, select **Swift Packages › Add Package Dependency…** 47 | 2. Enter "https://github.com/exyte/Grid" into the package repository URL text field 48 | 49 | ## Requirements 50 | 51 | * iOS 14.0+ (the latest iOS 13 support is in [v0.1.0](https://github.com/exyte/Grid/releases/tag/0.1.0)) 52 | * MacOS 10.15+ 53 | * Xcode 12+ 54 | 55 | ## Building from sources 56 | 57 | ```shell 58 | git clone git@github.com:exyte/Grid.git 59 | cd Grid/Example/ 60 | pod install 61 | open Example.xcworkspace/ 62 | ``` 63 | 64 | ## Documentation 65 | - [**Initialization**](#1-initialization) 66 | - [**View containers**](#2-containers) 67 | - [ForEach](#foreach) 68 | - [GridGroup](#gridgroup) 69 | - [**Track sizes:**](#3-track-sizes) 70 | - [Flexible `.fr(...)`](#flexible-sized-track-frn) 71 | - [Fixed `.pt(...)`](#fixed-sized-track) 72 | - [Content-based `.fit`](#content-based-size-fit) 73 | - [**Grid cell background and overlay**](#4-grid-cell-background-and-overlay) 74 | - [**Spanning grid views:**](#5-spans) 75 | - by rows 76 | - by columns 77 | - [**View position specifying:**](#6-starts) 78 | - automatically (implicitly) 79 | - start row 80 | - start column 81 | - both row and column 82 | - [**Flow direction:**](#7-flow) 83 | - [by rows](#rows) 84 | - [by columns](#columns) 85 | - [**Content mode:**](#8-content-mode) 86 | - [fill a container](#fill) 87 | - [scrollable content](#scroll) 88 | - [**Packing mode:**](#9-packing) 89 | - [sparse](#sparse) 90 | - [dense](#dense) 91 | - [**Vertical and horizontal spacing**](#10-spacing) 92 | - [**Alignment**](#11-alignment) 93 | - [**Content updates can be animated**](#12-animations) 94 | - [**Caching**](#13-caching) 95 | - [**Conditional statements / @GridBuilder**](#14-beta-conditional-statements--gridbuilder) 96 | - [**Release notes**](#release-notes) 97 | - [**Roadmap**](#roadmap) 98 | 99 | ### 1. Initialization 100 | 101 | 102 | 103 | You can instantiate Grid in different ways: 104 | 1. Just specify tracks and your views inside ViewBuilder closure: 105 | ```swift 106 | Grid(tracks: 3) { 107 | ColorView(.blue) 108 | ColorView(.purple) 109 | ColorView(.red) 110 | ColorView(.cyan) 111 | ColorView(.green) 112 | ColorView(.orange) 113 | } 114 | ``` 115 | 116 | 2. Use Range: 117 | ```swift 118 | Grid(0..<6, tracks: 3) { _ in 119 | ColorView(.random) 120 | } 121 | ``` 122 | 123 | 3. Use Identifiable enitites: 124 | ```swift 125 | Grid(colorModels, tracks: 3) { 126 | ColorView($0) 127 | } 128 | ``` 129 | 130 | 4. Use explicitly defined ID: 131 | ```swift 132 | Grid(colorModels, id: \.self, tracks: 3) { 133 | ColorView($0) 134 | } 135 | ``` 136 | 137 | ------------ 138 | 139 | ### 2. Containers 140 | #### ForEach 141 | Inside ViewBuilder you also can use regular `ForEach` statement. 142 | *There is no way to get KeyPath id value from the initialized ForEach view. Its inner content will be distinguished by views order while doing animations. It's better to use `ForEach` with `Identifiable` models or [GridGroup](#gridgroup) created either with explicit ID value or `Identifiable` models to keep track of the grid views and their `View` representations in animations.* 143 | 144 | 145 | 146 | ```swift 147 | Grid(tracks: 4) { 148 | ColorView(.red) 149 | ColorView(.purple) 150 | 151 | ForEach(0..<4) { _ in 152 | ColorView(.black) 153 | } 154 | 155 | ColorView(.orange) 156 | ColorView(.green) 157 | } 158 | ``` 159 | 160 | #### GridGroup 161 | Number of views in `ViewBuilder` closure is limited to 10. It's impossible to obtain content views from regular SwiftUI `Group` view. To exceed that limit you could use `GridGroup`. Every view in `GridGroup` is placed as a separate grid item. Unlike the `Group` view any outer method modifications of `GridView` are not applied to the descendant views. So it's just an enumerable container. Also `GridGroup` could be created by `Range`, `Identifiable` models, by ID specified explicitly. 162 | 163 | You can bind a view’s identity to the given single `Hashable` or `Identifiable` value also using `GridGroup`. This will produce transition animation to a new view with the same identity. 164 | 165 | *There is no way to use View's `.id()` modifier as inner `ForEach` view clears that value* 166 | 167 | You can use `GridGroup.empty` to define a content absence. 168 | 169 | Examples: 170 | 171 | ```swift 172 | var arithmeticButtons: GridGroup { 173 | GridGroup { 174 | CalcButton(.divide) 175 | CalcButton(.multiply) 176 | CalcButton(.substract) 177 | CalcButton(.equal) 178 | } 179 | } 180 | ``` 181 | 182 | ```swift 183 | var arithmeticButtons: GridGroup { 184 | let operations: [MathOperation] = 185 | [.divide, .multiply, .substract, .add, .equal] 186 | 187 | return GridGroup(operations, id: \.self) { 188 | CalcButton($0) 189 | } 190 | } 191 | ``` 192 | 193 | ```swift 194 | var arithmeticButtons: GridGroup { 195 | let operations: [MathOperation] = 196 | [.divide, .multiply, .substract, .add, .equal] 197 | 198 | return GridGroup { 199 | ForEach(operations, id: \.self) { 200 | CalcButton($0) 201 | } 202 | } 203 | } 204 | 205 | ``` 206 | 207 | ```swift 208 | var arithmeticButtons: GridGroup { 209 | let operations: [MathOperation] = 210 | [.divide, .multiply, .substract, .add, .equal] 211 | return GridGroup(operations, id: \.self) { 212 | CalcButton($0) 213 | } 214 | } 215 | ``` 216 | 217 | ```swift 218 | var arithmeticButtons: GridGroup { 219 | let operations: [MathOperation] = 220 | [.divide, .multiply, .substract, .add, .equal] 221 | return GridGroup(operations, id: \.self) { 222 | CalcButton($0) 223 | } 224 | } 225 | ``` 226 | 227 | ```swift 228 | Grid { 229 | ... 230 | GridGroup(MathOperation.clear) { 231 | CalcButton($0) 232 | } 233 | } 234 | ``` 235 | 236 | ------------ 237 | 238 | ### 3. Track sizes 239 | 240 | There are 3 types of track sizes that you could mix with each other: 241 | 242 | 243 | 244 | #### Fixed-sized track: 245 | `.pt(N)` where N - points count. 246 | 247 | ```swift 248 | Grid(tracks: [.pt(50), .pt(200), .pt(100)]) { 249 | ColorView(.blue) 250 | ColorView(.purple) 251 | ColorView(.red) 252 | ColorView(.cyan) 253 | ColorView(.green) 254 | ColorView(.orange) 255 | } 256 | ``` 257 | 258 | 259 | 260 | #### Content-based size: `.fit` 261 | 262 | Defines the track size as a maximum of the content sizes of every view in track 263 | 264 | ```swift 265 | Grid(0..<6, tracks: [.fit, .fit, .fit]) { 266 | ColorView(.random) 267 | .frame(maxWidth: 50 + 15 * CGFloat($0)) 268 | } 269 | ``` 270 | 271 | Pay attention to limiting a size of views that fills the entire space provided by parent and `Text()` views which tend to draw as a single line. 272 | 273 | #### Flexible sized track: `.fr(N)` 274 | 275 | 276 | 277 | Fr is a fractional unit and `.fr(1)` is for 1 part of the unassigned space in the grid. Flexible-sized tracks are computed at the very end after all non-flexible sized tracks ([.pt](#fixed-sized-track) and [.fit](#content-based-size-fit)). 278 | So the available space to distribute for them is the difference of the total size available and the sum of non-flexible track sizes. 279 | 280 | ```swift 281 | Grid(tracks: [.pt(100), .fr(1), .fr(2.5)]) { 282 | ColorView(.blue) 283 | ColorView(.purple) 284 | ColorView(.red) 285 | ColorView(.cyan) 286 | ColorView(.green) 287 | ColorView(.orange) 288 | } 289 | ``` 290 | 291 | Also, you could specify just an `Int` literal as a track size. It's equal to repeating `.fr(1)` track sizes: 292 | ```swift 293 | Grid(tracks: 3) { ... } 294 | ``` 295 | is equal to: 296 | ```swift 297 | Grid(tracks: [.fr(1), .fr(1), .fr(1)]) { ... } 298 | ``` 299 | 300 | ------------ 301 | 302 | ### 4. Grid cell background and overlay 303 | When using non-flexible track sizes it's possible that the extra space to be allocated will be greater than a grid item is able to take up. To fill that space you could use `.gridCellBackground(...)` and `gridCellOverlay(...)` modifiers. 304 | 305 | See [Content mode](#8-content-mode) and [Spacing](#10-spacing) examples. 306 | 307 | ------------ 308 | 309 | ### 5. Spans 310 | 311 | 312 | 313 | Every grid view may span across the provided number of grid tracks. You can achieve it using `.gridSpan(column: row:)` modifier. The default span is 1. 314 | 315 | *View with span >= 2 that spans across the tracks with flexible size doesn't take part in the sizes distribution for these tracks. This view will fit to the spanned tracks. So it's possible to place a view with unlimited size that spans tracks with content-based sizes ([.fit](#content-based-size-fit))* 316 | 317 | ```swift 318 | Grid(tracks: [.fr(1), .pt(150), .fr(2)]) { 319 | ColorView(.blue) 320 | .gridSpan(column: 2) 321 | ColorView(.purple) 322 | .gridSpan(row: 2) 323 | ColorView(.red) 324 | ColorView(.cyan) 325 | ColorView(.green) 326 | .gridSpan(column: 2, row: 3) 327 | ColorView(.orange) 328 | ColorView(.magenta) 329 | .gridSpan(row: 2) 330 | } 331 | ``` 332 | 333 | 334 | 335 | Spanning across tracks with different size types: 336 | 337 | ```swift 338 | var body: some View { 339 | Grid(tracks: [.fr(1), .fit, .fit], spacing: 10) { 340 | VCardView(text: placeholderText(), 341 | color: .red) 342 | 343 | VCardView(text: placeholderText(length: 30), 344 | color: .orange) 345 | .frame(maxWidth: 70) 346 | 347 | VCardView(text: placeholderText(length: 120), 348 | color: .green) 349 | .frame(maxWidth: 100) 350 | .gridSpan(column: 1, row: 2) 351 | 352 | VCardView(text: placeholderText(length: 160), 353 | color: .magenta) 354 | .gridSpan(column: 2, row: 1) 355 | 356 | VCardView(text: placeholderText(length: 190), 357 | color: .cyan) 358 | .gridSpan(column: 3, row: 1) 359 | } 360 | } 361 | ``` 362 | 363 | ------------ 364 | 365 | ### 6. Starts 366 | For every view you are able to set explicit start position by specifying a column, a row or both. 367 | View will be positioned automatically if there is no start position specified. 368 | Firstly, views with both column and row start positions are placed. 369 | Secondly, the auto-placing algorithm tries to place views with either column or row start position. If there are any conflicts - such views are placed automatically and you see warning in the console. 370 | And at the very end views with no explicit start position are placed. 371 | 372 | Start position is defined using `.gridStart(column: row:)` modifier. 373 | 374 | 375 | 376 | ```swift 377 | Grid(tracks: [.pt(50), .fr(1), .fr(1.5), .fit]) { 378 | ForEach(0..<6) { _ in 379 | ColorView(.black) 380 | } 381 | 382 | ColorView(.brown) 383 | .gridSpan(column: 3) 384 | 385 | ColorView(.blue) 386 | .gridSpan(column: 2) 387 | 388 | ColorView(.orange) 389 | .gridSpan(row: 3) 390 | 391 | ColorView(.red) 392 | .gridStart(row: 1) 393 | .gridSpan(column: 2, row: 2) 394 | 395 | ColorView(.yellow) 396 | .gridStart(row: 2) 397 | 398 | ColorView(.purple) 399 | .frame(maxWidth: 50) 400 | .gridStart(column: 3, row: 0) 401 | .gridSpan(row: 9) 402 | 403 | ColorView(.green) 404 | .gridSpan(column: 2, row: 3) 405 | 406 | ColorView(.cyan) 407 | 408 | ColorView(.gray) 409 | .gridStart(column: 2) 410 | } 411 | ``` 412 | ------------ 413 | 414 | ### 7. Flow 415 | Grid has 2 types of tracks. The first one is where you specify [track sizes](#3-track-sizes) - the fixed one. Fixed means that a count of tracks is known. The second one and orthogonal to the fixed is growing tracks type: where your content grows. Grid flow defines the direction where items grow: 416 | 417 | #### **Rows** 418 | *Default.* The number of columns is fixed and [defined as track sizes](#3-track-sizes). Grid items are placed moving between columns and switching to the next row after the last column. Rows count is growing. 419 | 420 | #### **Columns** 421 | The number of rows is fixed and [defined as track sizes](#3-track-sizes). Grid items are placed moving between rows and switching to the next column after the last row. Columns count is growing. 422 | 423 | *Grid flow could be specified in a grid constructor as well as using `.gridFlow(...)` grid modifier. The first option has more priority.* 424 | 425 | 426 | 427 | ```swift 428 | struct ContentView: View { 429 | @State var flow: GridFlow = .rows 430 | 431 | var body: some View { 432 | VStack { 433 | if self.flow == .rows { 434 | Button(action: { self.flow = .columns }) { 435 | Text("Flow: ROWS") 436 | } 437 | } else { 438 | Button(action: { self.flow = .rows }) { 439 | Text("Flow: COLUMNS") 440 | } 441 | } 442 | 443 | Grid(0..<15, tracks: 5, flow: self.flow, spacing: 5) { 444 | ColorView($0.isMultiple(of: 2) ? .black : .orange) 445 | .overlay( 446 | Text(String($0)) 447 | .font(.system(size: 35)) 448 | .foregroundColor(.white) 449 | ) 450 | } 451 | .animation(.default) 452 | } 453 | } 454 | } 455 | ``` 456 | ------------ 457 | 458 | ### 8. Content mode 459 | There are 2 kinds of content modes: 460 | 461 | #### Scroll 462 | In this mode the inner grid content is able to scroll to the [growing direction](#7-flow). Grid tracks that orthogonal to the grid flow direction (growing) are implicitly assumed to have [.fit](#content-based-size-fit) size. This means that their sizes have to be defined in the respective dimension. 463 | 464 | *Grid content mode could be specified in a grid constructor as well as using `.gridContentMode(...)` grid modifier. The first option has more priority.* 465 | 466 | ###### Rows-flow scroll: 467 | 468 | 469 | 470 | ```swift 471 | struct VCardView: View { 472 | let text: String 473 | let color: UIColor 474 | 475 | var body: some View { 476 | VStack { 477 | Image("dog") 478 | .resizable() 479 | .aspectRatio(contentMode: .fit) 480 | .cornerRadius(5) 481 | .frame(minWidth: 100, minHeight: 50) 482 | 483 | Text(self.text) 484 | .layoutPriority(.greatestFiniteMagnitude) 485 | } 486 | .padding(5) 487 | .gridCellBackground { _ in 488 | ColorView(self.color) 489 | } 490 | .gridCellOverlay { _ in 491 | RoundedRectangle(cornerRadius: 5) 492 | .strokeBorder(Color(self.color.darker()), 493 | lineWidth: 3) 494 | } 495 | } 496 | } 497 | 498 | struct ContentView: View { 499 | var body: some View { 500 | Grid(tracks: 3) { 501 | ForEach(0..<40) { _ in 502 | VCardView(text: randomText(), color: .random) 503 | .gridSpan(column: self.randomSpan) 504 | } 505 | } 506 | .gridContentMode(.scroll) 507 | .gridPacking(.dense) 508 | .gridFlow(.rows) 509 | } 510 | 511 | var randomSpan: Int { 512 | Int(arc4random_uniform(3)) + 1 513 | } 514 | } 515 | ``` 516 | 517 | ###### Columns-flow scroll: 518 | 519 | 520 | 521 | ```swift 522 | struct HCardView: View { 523 | let text: String 524 | let color: UIColor 525 | 526 | var body: some View { 527 | HStack { 528 | Image("dog") 529 | .resizable() 530 | .aspectRatio(contentMode: .fit) 531 | .cornerRadius(5) 532 | 533 | Text(self.text) 534 | .frame(maxWidth: 200) 535 | } 536 | .padding(5) 537 | .gridCellBackground { _ in 538 | ColorView(self.color) 539 | } 540 | .gridCellOverlay { _ in 541 | RoundedRectangle(cornerRadius: 5) 542 | .strokeBorder(Color(self.color.darker()), 543 | lineWidth: 3) 544 | } 545 | } 546 | } 547 | 548 | struct ContentView: View { 549 | var body: some View { 550 | Grid(tracks: 3) { 551 | ForEach(0..<8) { _ in 552 | HCardView(text: randomText(), color: .random) 553 | .gridSpan(row: self.randomSpan) 554 | } 555 | } 556 | .gridContentMode(.scroll) 557 | .gridFlow(.columns) 558 | .gridPacking(.dense) 559 | } 560 | 561 | var randomSpan: Int { 562 | Int(arc4random_uniform(3)) + 1 563 | } 564 | } 565 | ``` 566 | 567 | 568 | 569 | 570 | #### Fill 571 | *Default.* In this mode, grid view tries to fill the entire space provided by the parent view with its content. Grid tracks that orthogonal to the grid flow direction (growing) are implicitly assumed to have [.fr(1)](#flexible-sized-track-frn) size. 572 | 573 | ```swift 574 | @State var contentMode: GridContentMode = .scroll 575 | 576 | var body: some View { 577 | VStack { 578 | self.modesPicker 579 | 580 | Grid(models, id: \.self, tracks: 3) { 581 | VCardView(text: $0.text, color: $0.color) 582 | .gridSpan($0.span) 583 | } 584 | .gridContentMode(self.contentMode) 585 | .gridFlow(.rows) 586 | .gridAnimation(.default) 587 | } 588 | } 589 | ``` 590 | 591 | ------------ 592 | 593 | ### 9. Packing 594 | Auto-placing algorithm could stick to one of two strategies: 595 | 596 | #### Sparse 597 | *Default.* The placement algorithm only ever moves “forward” in the grid when placing items, never backtracking to fill holes. This ensures that all of the auto-placed items appear “in order”, even if this leaves holes that could have been filled by later items. 598 | 599 | #### Dense 600 | Attempts to fill in holes earlier in the grid if smaller items come up later. This may cause items to appear out-of-order, when doing so would fill in holes left by larger items. 601 | 602 | *Grid packing could be specified in a grid constructor as well as using `.gridPacking(...)` grid modifier. The first option has more priority.* 603 | 604 | Example: 605 | 606 | 607 | 608 | ```swift 609 | @State var gridPacking = GridPacking.sparse 610 | 611 | var body: some View { 612 | VStack { 613 | self.packingPicker 614 | 615 | Grid(tracks: 4) { 616 | ColorView(.red) 617 | 618 | ColorView(.black) 619 | .gridSpan(column: 4) 620 | 621 | ColorView(.purple) 622 | 623 | ColorView(.orange) 624 | ColorView(.green) 625 | } 626 | .gridPacking(self.gridPacking) 627 | .gridAnimation(.default) 628 | } 629 | } 630 | ``` 631 | 632 | ------------ 633 | 634 | ### 10. Spacing 635 | There are several ways to define the horizontal and vertical spacings between tracks: 636 | 637 | - Using `Int` literal which means equal spacing in all directions: 638 | ```swift 639 | Grid(tracks: 4, spacing: 5) { ... } 640 | ``` 641 | - Using explicit init 642 | ```swift 643 | Grid(tracks: 4, spacing: GridSpacing(horizontal: 10, vertical: 5)) { ... } 644 | ``` 645 | - Using array literal: 646 | ```swift 647 | Grid(tracks: 4, spacing: [10, 5]) { ... } 648 | ``` 649 | 650 | Example: 651 | 652 | 653 | 654 | ```swift 655 | @State var vSpacing: CGFloat = 0 656 | @State var hSpacing: CGFloat = 0 657 | 658 | var body: some View { 659 | VStack { 660 | self.sliders 661 | 662 | Grid(tracks: 3, spacing: [hSpacing, vSpacing]) { 663 | ForEach(0..<21) { 664 | //Inner image used to measure size 665 | self.image 666 | .aspectRatio(contentMode: .fit) 667 | .opacity(0) 668 | .gridSpan(column: max(1, $0 % 4)) 669 | .gridCellOverlay { 670 | //This one is to display 671 | self.image 672 | .aspectRatio(contentMode: .fill) 673 | .frame(width: $0?.width, 674 | height: $0?.height) 675 | .cornerRadius(5) 676 | .clipped() 677 | .shadow(color: self.shadowColor, 678 | radius: 10, x: 0, y: 0) 679 | } 680 | } 681 | } 682 | .background(self.backgroundColor) 683 | .gridContentMode(.scroll) 684 | .gridPacking(.dense) 685 | } 686 | } 687 | ``` 688 | 689 | ------------ 690 | 691 | ### 11. Alignment 692 | 693 | #### `.gridItemAlignment` 694 | Use this to specify the alignment for a specific single grid item. It has higher priority than `gridCommonItemsAlignment` 695 | 696 | 697 | #### `.gridCommonItemsAlignment` 698 | Applies to every item as `gridItemAlignment`, but doesn't override its individual `gridItemAlignment` value. 699 | 700 | 701 | #### `.gridContentAlignment` 702 | Applies to the whole grid content. Takes effect when content size is less than the space available for the grid. 703 | 704 | Example: 705 | 706 | 707 | 708 | ```swift 709 | struct SingleAlignmentExample: View { 710 | var body: some View { 711 | Grid(tracks: 3) { 712 | TextCardView(text: "Hello", color: .red) 713 | .gridItemAlignment(.leading) 714 | 715 | TextCardView(text: "world", color: .blue) 716 | } 717 | .gridCommonItemsAlignment(.center) 718 | .gridContentAlignment(.trailing) 719 | } 720 | } 721 | 722 | struct TextCardView: View { 723 | let text: String 724 | let color: UIColor 725 | var textColor: UIColor = .white 726 | 727 | var body: some View { 728 | Text(self.text) 729 | .foregroundColor(Color(self.textColor)) 730 | .padding(5) 731 | .gridCellBackground { _ in 732 | ColorView(color) 733 | } 734 | .gridCellOverlay { _ in 735 | RoundedRectangle(cornerRadius: 5) 736 | .strokeBorder(Color(self.color.darker()), 737 | lineWidth: 3) 738 | } 739 | } 740 | } 741 | 742 | ``` 743 | ------------ 744 | 745 | ### 12. Animations 746 | You can define a specific animation that will be applied to the inner `ZStack` using `.gridAnimation()` grid modifier. 747 | By default, every view in the grid is associated with subsequent index as it's ID. Hence SwiftUI relies on the grid view position in the initial and final state to perform animation transition. 748 | You can associate a specific ID to a grid view using [ForEach](#foreach) or [GridGroup](#gridgroup) initialized by `Identifiable` models or by explicit KeyPath as ID to force an animation to perform in the right way. 749 | 750 | *There is no way to get KeyPath id value from the initialized ForEach view. Its inner content will be distinguished by views order while doing animations. It's better to use [ForEach](#foreach) with `Identifiable` models or [GridGroup](#gridgroup) created either with explicit ID value or `Identifiable` models to keep track of the grid views and their `View` representations in animations.* 751 | 752 | ------------ 753 | 754 | ### 13. Caching 755 | It's possible to cache grid layouts through the lifecycle of Grid. 756 | 757 | *Supported for iOS only* 758 | 759 | *Grid caching could be specified in a grid constructor as well as using `.gridCaching(...)` grid modifier. The first option has more priority.* 760 | 761 | #### In memory cache 762 | *Default.* Cache is implemented with the leverage of NSCache. It will clear all the cached layouts on the memory warning notification. 763 | 764 | #### No cache 765 | No cache is used. Layout calculations will be executed at every step of Grid lifecycle. 766 | 767 | ------------ 768 | 769 | ### 14. Conditional statements / @GridBuilder 770 | 771 | Starting with Swift 5.3 we can use custom function builders without [any issues](https://github.com/apple/swift/pull/29626). 772 | That gives us: 773 | 774 | - Full support of `if/if else`, `if let/if let else`, `switch` statements within the `Grid` and `GridGroup` bodies. 775 | 776 | - A better way to propagate view ID from nested `GridGroup` and `ForEach` 777 | 778 | - An ability to return heterogeneous views from functions and vars using `@GridBuilder` attribute and `some View` retrun type: 779 | 780 | ```swift 781 | @GridBuilder 782 | func headerSegment(flag: Bool) -> some View { 783 | if flag { 784 | return GridGroup { ... } 785 | else { 786 | return ColorView(.black) 787 | } 788 | } 789 | ``` 790 | 791 | ------------ 792 | 793 | ## Release notes: 794 | ##### [v1.5.0](https://github.com/exyte/Grid/releases/tag/1.5.0): 795 | - add tvOS support, migrate Examples project to Xcode universal target, swift app lifecycle, SPM 796 | 797 | ##### [v1.4.2](https://github.com/exyte/Grid/releases/tag/1.4.2): 798 | - fixes not working gridItemAlignment and gridCommonItemsAlignment for vertical axis 799 | 800 |
801 | Previous releases 802 | ##### [v1.4.1](https://github.com/exyte/Grid/releases/tag/1.4.1): 803 | - fixes the issue when Grid doesn’t update its content 804 | 805 | Issue: 806 | If any content item within GridBuilder uses any outer data then Grid doesn't update it. 807 | For example: 808 | 809 | ``` 810 | @State var titleText: String = "title" 811 | 812 | Grid(tracks: 2) { 813 | Text(titleText) 814 | Text("hello") 815 | } 816 | 817 | ``` 818 | 819 | Grid didn't update titleText even if it's changed. 820 | 821 | ##### [v1.4.0](https://github.com/exyte/Grid/releases/tag/1.4.0): 822 | - adds `gridItemAlignment` modifier to align per item 823 | - adds `gridCommonItemsAlignment` modifier to align all items 824 | - adds `gridContentAlignment` modifier to align the whole grid content 825 | 826 | ##### [v1.3.1.beta](https://github.com/exyte/Grid/releases/tag/1.3.1.beta): 827 | - adds `gridAlignment` modifier to align per item 828 | 829 | ##### [v1.2.1.beta](https://github.com/exyte/Grid/releases/tag/1.2.1.beta): 830 | - adds `gridCommonItemsAlignment` modifier to align all items in Grid 831 | 832 | ##### [v1.1.1.beta](https://github.com/exyte/Grid/releases/tag/1.1.1.beta): 833 | - adds WidgetKit support by conditionally rendering ScrollView 834 | 835 | ##### [v1.1.0](https://github.com/exyte/Grid/releases/tag/1.1.0): 836 | - adds MacOS support 837 | 838 | ##### [v1.0.1](https://github.com/exyte/Grid/releases/tag/1.0.1): 839 | - adds full support of conditional statements 840 | - adds `@GridBuilder` function builder 841 | 842 | ##### [v0.1.0](https://github.com/exyte/Grid/releases/tag/0.1.0): 843 | - adds layout caching 844 | - adds `GridGroup` init using a single `Identifiable` or `Hashable` value 845 | 846 | ##### [v0.0.3](https://github.com/exyte/Grid/releases/tag/0.0.3): 847 | - fixes any issues when Grid is conditionally presented 848 | - fixes wrong grid position with scrollable content after a device rotation 849 | - fixes "Bound preference ** tried to update multiple times per frame" warnings in iOS 14 and reduces them in iOS 13 850 | - simplifies the process of collecting grid preferences under the hood 851 | 852 | ##### [v0.0.2](https://github.com/exyte/Grid/releases/tag/0.0.2) 853 | - added support for Swift Package Manager 854 | 855 |
856 | 857 | ------------ 858 | 859 | ## Roadmap: 860 | 861 | - [ ] add WidgetKit example 862 | - [ ] add alignment per tracks 863 | - [ ] add regions or settings for GridGroup to specify position 864 | - [ ] dual dimension track sizes (grid-template-rows, grid-template-columns). 865 | - [ ] grid-auto-rows, grid-auto-columns 866 | - [ ] improve dense placement algorithm 867 | - [ ] ? grid min/ideal sizes 868 | - [ ] ? landscape/portrait layout 869 | - [ ] ? calculate layout in background thread 870 | - [x] add GridIdentified-like item to track the same Views in animations 871 | - [x] support if clauses using function builder 872 | - [x] add GridGroup 873 | - [x] grid item explicit row and/or column position 874 | - [x] different spacing for rows and columns 875 | - [x] intrinsic sized tracks (fit-content) 876 | - [x] forEach support 877 | - [x] dense/sparse placement algorithm 878 | - [x] add horizontal axis 879 | - [x] init via Identifiable models 880 | - [x] scrollable content 881 | 882 | 883 | ## License 884 | 885 | Grid is available under the MIT license. See the LICENSE file for more info. 886 | 887 | ## Our other open source SwiftUI libraries 888 | [PopupView](https://github.com/exyte/PopupView) - Toasts and popups library 889 | [AnchoredPopup](https://github.com/exyte/AnchoredPopup) - Anchored Popup grows "out" of a trigger view (similar to Hero animation) 890 | [ScalingHeaderScrollView](https://github.com/exyte/ScalingHeaderScrollView) - A scroll view with a sticky header which shrinks as you scroll 891 | [AnimatedTabBar](https://github.com/exyte/AnimatedTabBar) - A tabbar with a number of preset animations 892 | [MediaPicker](https://github.com/exyte/mediapicker) - Customizable media picker 893 | [Chat](https://github.com/exyte/chat) - Chat UI framework with fully customizable message cells, input view, and a built-in media picker 894 | [OpenAI](https://github.com/exyte/OpenAI) Wrapper lib for [OpenAI REST API](https://platform.openai.com/docs/api-reference/introduction) 895 | [AnimatedGradient](https://github.com/exyte/AnimatedGradient) - Animated linear gradient 896 | [ConcentricOnboarding](https://github.com/exyte/ConcentricOnboarding) - Animated onboarding flow 897 | [FloatingButton](https://github.com/exyte/FloatingButton) - Floating button menu 898 | [ActivityIndicatorView](https://github.com/exyte/ActivityIndicatorView) - A number of animated loading indicators 899 | [ProgressIndicatorView](https://github.com/exyte/ProgressIndicatorView) - A number of animated progress indicators 900 | [FlagAndCountryCode](https://github.com/exyte/FlagAndCountryCode) - Phone codes and flags for every country 901 | [SVGView](https://github.com/exyte/SVGView) - SVG parser 902 | [LiquidSwipe](https://github.com/exyte/LiquidSwipe) - Liquid navigation animation 903 | -------------------------------------------------------------------------------- /Sources/Cache/Cache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cache.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 14.08.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(watchOS) || os(tvOS) 10 | 11 | import UIKit.UIApplication 12 | 13 | private class ObjectWrapper { 14 | let value: Any 15 | 16 | init(_ value: Any) { 17 | self.value = value 18 | } 19 | } 20 | 21 | private class KeyWrapper: NSObject { 22 | let key: KeyType 23 | init(_ key: KeyType) { 24 | self.key = key 25 | } 26 | 27 | override var hash: Int { 28 | return key.hashValue 29 | } 30 | 31 | override func isEqual(_ object: Any?) -> Bool { 32 | guard let other = object as? KeyWrapper else { 33 | return false 34 | } 35 | return key == other.key 36 | } 37 | } 38 | 39 | class Cache { 40 | private let cache: NSCache, ObjectWrapper> = NSCache() 41 | 42 | init(lowMemoryAware: Bool = true) { 43 | guard lowMemoryAware else { return } 44 | NotificationCenter.default.addObserver( 45 | self, 46 | selector: #selector(onLowMemory), 47 | name: UIApplication.didReceiveMemoryWarningNotification, 48 | object: nil) 49 | } 50 | 51 | @objc private func onLowMemory() { 52 | removeAllObjects() 53 | } 54 | 55 | var name: String { 56 | get { return cache.name } 57 | set { cache.name = newValue } 58 | } 59 | 60 | weak open var delegate: NSCacheDelegate? { 61 | get { return cache.delegate } 62 | set { cache.delegate = newValue } 63 | } 64 | 65 | func object(forKey key: KeyType) -> ObjectType? { 66 | return cache.object(forKey: KeyWrapper(key))?.value as? ObjectType 67 | } 68 | 69 | func setObject(_ obj: ObjectType, forKey key: KeyType) { // 0 cost 70 | return cache.setObject(ObjectWrapper(obj), forKey: KeyWrapper(key)) 71 | } 72 | 73 | func setObject(_ obj: ObjectType, forKey key: KeyType, cost: Int) { 74 | return cache.setObject(ObjectWrapper(obj), forKey: KeyWrapper(key), cost: cost) 75 | } 76 | 77 | func removeObject(forKey key: KeyType) { 78 | return cache.removeObject(forKey: KeyWrapper(key)) 79 | } 80 | 81 | func removeAllObjects() { 82 | return cache.removeAllObjects() 83 | } 84 | 85 | var totalCostLimit: Int { 86 | get { return cache.totalCostLimit } 87 | set { cache.totalCostLimit = newValue } 88 | } 89 | 90 | var countLimit: Int { 91 | get { return cache.countLimit } 92 | set { cache.countLimit = newValue } 93 | } 94 | 95 | var evictsObjectsWithDiscardedContent: Bool { 96 | get { return cache.evictsObjectsWithDiscardedContent } 97 | set { cache.evictsObjectsWithDiscardedContent = newValue } 98 | } 99 | } 100 | 101 | #endif 102 | -------------------------------------------------------------------------------- /Sources/Cache/GridCacheMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridCacheMode.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 14.08.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum GridCacheMode { 12 | case inMemoryCache 13 | case noCache 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Configuration/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 19.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreGraphics 11 | import SwiftUI 12 | 13 | public struct Constants { 14 | public static let defaultColumnSpan = 1 15 | public static let defaultRowSpan = 1 16 | public static let defaultSpacing: GridSpacing = 5.0 17 | public static let defaultFractionSize = 1.0 as CGFloat 18 | public static let defaultContentMode: GridContentMode = .fill 19 | public static let defaultFlow: GridFlow = .rows 20 | public static let defaultPacking: GridPacking = .sparse 21 | public static let defaultCacheMode: GridCacheMode = .inMemoryCache 22 | public static let defaultCommonItemsAlignment: GridAlignment = .center 23 | public static let defaultContentAlignment: GridAlignment = .center 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Extensions/CGRect+Hashable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGRect+Hashable.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 14.08.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | extension CGRect: Hashable { 12 | public func hash(into hasher: inout Hasher) { 13 | hasher.combine(minX) 14 | hasher.combine(minY) 15 | hasher.combine(maxX) 16 | hasher.combine(maxY) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Extensions/CGSize+Hashable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSize+Hashable.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 14.08.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | extension CGSize: Hashable { 12 | public func hash(into hasher: inout Hasher) { 13 | hasher.combine(width) 14 | hasher.combine(height) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Extensions/LayoutArrangement+description.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutArrangement+description.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 16.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension LayoutArrangement: CustomStringConvertible { 12 | var description: String { 13 | guard !items.isEmpty else { return "" } 14 | var result = "" 15 | var items = self.items.map { (arrangement: $0, area: $0.area) } 16 | 17 | for row in 0...self.rowsCount { 18 | columnsCycle: for column in 0..(_ conditional: Bool, content: (Self) -> Content) -> some View { 14 | if conditional { 15 | content(self) 16 | } else { 17 | self 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Grid.h: -------------------------------------------------------------------------------- 1 | // 2 | // Grid.h 3 | // Grid 4 | // 5 | // Created by Denis Obukhov on 20.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Grid. 12 | FOUNDATION_EXPORT double GridVersionNumber; 13 | 14 | //! Project version string for Grid. 15 | FOUNDATION_EXPORT const unsigned char GridVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/GridLayoutMath/LayoutArranging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutArranging.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 16.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreGraphics 11 | 12 | protocol LayoutArranging { 13 | func arrange(task: ArrangingTask) -> LayoutArrangement 14 | } 15 | 16 | struct ArrangementInfo: Equatable, Hashable { 17 | var gridElement: GridElement 18 | var start: GridStart 19 | var span: GridSpan 20 | } 21 | 22 | struct ArrangingTask: Equatable, Hashable { 23 | var itemsInfo: [ArrangementInfo] 24 | var tracks: [GridTrack] 25 | var flow: GridFlow 26 | var packing: GridPacking 27 | } 28 | 29 | extension LayoutArranging { 30 | 31 | func arrange(task: ArrangingTask) -> LayoutArrangement { 32 | let fixedTracksCount = task.tracks.count 33 | guard fixedTracksCount > 0 else { return .zero } 34 | let flow = task.flow 35 | let packing = task.packing 36 | var arrangedItems: [ArrangedItem] = [] 37 | var occupiedIndices: [GridIndex] = [] 38 | var growingTracksCount = 0 39 | 40 | var items: [ArrangementInfo] = 41 | task.itemsInfo.compactMap { itemInfo in 42 | var correctedSpan = itemInfo.span 43 | correctedSpan[keyPath: flow.spanIndex(.fixed)] = min(fixedTracksCount, correctedSpan[keyPath: flow.spanIndex(.fixed)]) 44 | 45 | var correctedStart = itemInfo.start 46 | if let fixedStart = itemInfo.start[keyPath: flow.startIndex(.fixed)], 47 | fixedStart > fixedTracksCount - 1 { 48 | print("Warning: grid item start \(correctedStart) exceeds fixed tracks count: \(fixedTracksCount)") 49 | correctedStart[keyPath: flow.startIndex(.fixed)] = nil 50 | } 51 | return .init(gridElement: itemInfo.gridElement, start: correctedStart, span: correctedSpan) 52 | } 53 | 54 | arrangedItems += self.arrangeFullyFrozenItems( 55 | &items, 56 | flow: flow, 57 | occupiedIndices: &occupiedIndices, 58 | growingTracksCount: &growingTracksCount 59 | ) 60 | arrangedItems += self.arrangeSemiFrozenItems( 61 | &items, 62 | flow: flow, 63 | fixedTracksCount: fixedTracksCount, 64 | occupiedIndices: &occupiedIndices, 65 | growingTracksCount: &growingTracksCount 66 | ) 67 | arrangedItems += self.arrangeNonFrozenItem( 68 | items, 69 | flow: flow, 70 | fixedTracksCount: fixedTracksCount, 71 | packing: packing, 72 | occupiedIndices: &occupiedIndices, 73 | growingTracksCount: &growingTracksCount 74 | ) 75 | 76 | var arrangement = LayoutArrangement(columnsCount: 0, rowsCount: 0, items: arrangedItems) 77 | arrangement[keyPath: flow.arrangementCount(.fixed)] = fixedTracksCount 78 | arrangement[keyPath: flow.arrangementCount(.growing)] = growingTracksCount 79 | return arrangement 80 | } 81 | 82 | private func arrangeFullyFrozenItems( 83 | _ items: inout [ArrangementInfo], 84 | flow: GridFlow, 85 | occupiedIndices: inout [GridIndex], 86 | growingTracksCount: inout Int 87 | ) -> [ArrangedItem] { 88 | var result: [ArrangedItem] = [] 89 | let staticItems: [(item: GridElement, span: GridSpan, start: GridIndex)] = 90 | items.compactMap { 91 | guard 92 | let columnStart = $0.start.column, 93 | let rowStart = $0.start.row 94 | else { 95 | return nil 96 | } 97 | return ($0.gridElement, $0.span, GridIndex(column: columnStart, row: rowStart)) 98 | } 99 | 100 | // Arrange fully static items 101 | staticItems.forEach { staticItem in 102 | let itemIndex = items.firstIndex(where: { $0.gridElement == staticItem.item })! 103 | guard 104 | !occupiedIndices.contains(staticItem.start, span: staticItem.span) else { 105 | print("Warning: grid item position is occupied: \(staticItem.start), \(staticItem.span)") 106 | 107 | // Place that item automatically 108 | let prevItem = items[itemIndex] 109 | items[itemIndex...itemIndex] = [.init(gridElement: prevItem.gridElement, start: GridStart.default, span: prevItem.span)] 110 | return 111 | } 112 | occupiedIndices.appendPointsFrom(index: staticItem.start, span: staticItem.span) 113 | let arrangedItem = ArrangedItem( 114 | item: staticItem.item, 115 | startIndex: staticItem.start, 116 | span: staticItem.span 117 | ) 118 | growingTracksCount = max(growingTracksCount, arrangedItem.endIndex[keyPath: flow.index(.growing)] + 1) 119 | result.append(arrangedItem) 120 | items.remove(at: itemIndex) 121 | } 122 | return result 123 | } 124 | 125 | private func arrangeSemiFrozenItems( 126 | _ items: inout [ArrangementInfo], 127 | flow: GridFlow, 128 | fixedTracksCount: Int, 129 | occupiedIndices: inout [GridIndex], 130 | growingTracksCount: inout Int 131 | ) -> [ArrangedItem] { 132 | var result: [ArrangedItem] = [] 133 | // Arrange static items with frozen start in fixed flow dimension 134 | for dimension in GridFlowDimension.allCases { 135 | let semiStaticFixedItems: [(item: GridElement, span: GridSpan, start: GridStart)] = items.compactMap { 136 | guard let frozenIndex = $0.start[keyPath: flow.startIndex(dimension)] else { 137 | return nil 138 | } 139 | var correctedSpan = $0.span 140 | if dimension == .fixed { 141 | correctedSpan[keyPath: flow.spanIndex(.fixed)] = 142 | min(fixedTracksCount - frozenIndex, correctedSpan[keyPath: flow.spanIndex(.fixed)]) 143 | } 144 | return ($0.gridElement, correctedSpan, $0.start) 145 | } 146 | 147 | semiStaticFixedItems.forEach { semiStaticItem in 148 | let itemIndex = items.firstIndex(where: { $0.gridElement == semiStaticItem.item })! 149 | let frozenIndex = semiStaticItem.start[keyPath: flow.startIndex(dimension)]! 150 | 151 | var currentIndex: GridIndex = .zero 152 | currentIndex[keyPath: flow.index(dimension)] = frozenIndex 153 | 154 | while occupiedIndices.contains(currentIndex, span: semiStaticItem.span) { 155 | guard let nextIndex = currentIndex.nextIndex( 156 | fixedTracksCount: fixedTracksCount, 157 | flow: flow, 158 | span: semiStaticItem.span, 159 | frozenDimension: dimension 160 | ) 161 | else { 162 | print("Warning: unable to place semi-fixed grid item with start: \(semiStaticItem.start), span: \(semiStaticItem.span)") 163 | // Place that item automatically 164 | 165 | let prevItem = items[itemIndex] 166 | items[itemIndex...itemIndex] = [.init(gridElement: prevItem.gridElement, start: GridStart.default, span: prevItem.span)] 167 | return 168 | } 169 | currentIndex = nextIndex 170 | } 171 | 172 | occupiedIndices.appendPointsFrom(index: currentIndex, span: semiStaticItem.span) 173 | let arrangedItem = ArrangedItem( 174 | item: semiStaticItem.item, 175 | startIndex: currentIndex, 176 | span: semiStaticItem.span 177 | ) 178 | growingTracksCount = max(growingTracksCount, arrangedItem.endIndex[keyPath: flow.index(.growing)] + 1) 179 | result.append(arrangedItem) 180 | items.remove(at: itemIndex) 181 | } 182 | } 183 | return result 184 | } 185 | 186 | private func arrangeNonFrozenItem( 187 | _ items: [ArrangementInfo], 188 | flow: GridFlow, 189 | fixedTracksCount: Int, 190 | packing: GridPacking, 191 | occupiedIndices: inout [GridIndex], 192 | growingTracksCount: inout Int 193 | ) -> [ArrangedItem] { 194 | // Arrange dynamic items 195 | var result: [ArrangedItem] = [] 196 | var lastIndex: GridIndex = .zero 197 | for spanPreference in items { 198 | var currentIndex: GridIndex 199 | 200 | switch packing { 201 | case .sparse: 202 | currentIndex = lastIndex 203 | case .dense: 204 | currentIndex = .zero 205 | } 206 | 207 | while occupiedIndices.contains(currentIndex, span: spanPreference.span) { 208 | currentIndex = currentIndex.nextIndex( 209 | fixedTracksCount: fixedTracksCount, 210 | flow: flow, 211 | span: spanPreference.span 212 | )! 213 | } 214 | 215 | occupiedIndices.appendPointsFrom(index: currentIndex, span: spanPreference.span) 216 | let arrangedItem = ArrangedItem( 217 | item: spanPreference.gridElement, 218 | startIndex: currentIndex, 219 | span: spanPreference.span 220 | ) 221 | 222 | growingTracksCount = max(growingTracksCount, arrangedItem.endIndex[keyPath: flow.index(.growing)] + 1) 223 | result.append(arrangedItem) 224 | lastIndex = currentIndex 225 | } 226 | return result 227 | } 228 | } 229 | 230 | fileprivate extension GridIndex { 231 | func nextIndex( 232 | fixedTracksCount: Int, 233 | flow: GridFlow, 234 | span: GridSpan, 235 | frozenDimension: GridFlowDimension? = nil 236 | ) -> GridIndex? { 237 | var fixedIndex = self[keyPath: flow.index(.fixed)] 238 | var growingIndex = self[keyPath: flow.index(.growing)] 239 | let fixedSpan = span[keyPath: flow.spanIndex(.fixed)] 240 | 241 | func outerIncrementer() { 242 | switch frozenDimension { 243 | case .fixed: 244 | () 245 | case .growing: 246 | fixedIndex += 1 247 | case .none: 248 | fixedIndex += 1 249 | } 250 | } 251 | 252 | func nextTrackTrigger() -> Bool? { 253 | switch frozenDimension { 254 | case .fixed: 255 | return true 256 | case .growing: 257 | guard fixedIndex + fixedSpan <= fixedTracksCount else { 258 | return nil 259 | } 260 | return false 261 | case .none: 262 | return fixedIndex + fixedSpan > fixedTracksCount 263 | } 264 | } 265 | 266 | func innerIncrementer() { 267 | switch frozenDimension { 268 | case .fixed: 269 | growingIndex += 1 270 | case .growing: 271 | () 272 | case .none: 273 | fixedIndex = 0 274 | growingIndex += 1 275 | } 276 | } 277 | 278 | outerIncrementer() 279 | guard let result = nextTrackTrigger() else { 280 | return nil 281 | } 282 | if result { 283 | innerIncrementer() 284 | } 285 | 286 | var nextIndex = GridIndex.zero 287 | nextIndex[keyPath: flow.index(.fixed)] = fixedIndex 288 | nextIndex[keyPath: flow.index(.growing)] = growingIndex 289 | return nextIndex 290 | } 291 | } 292 | 293 | fileprivate extension Array where Element == GridIndex { 294 | func contains(_ startIndex: GridIndex, span: GridSpan) -> Bool { 295 | for row in startIndex.row.. PositionedLayout 13 | } 14 | 15 | struct PositioningTask: Equatable, Hashable { 16 | let items: [PositionedItem] 17 | var arrangement: LayoutArrangement 18 | var boundingSize: CGSize 19 | var tracks: [GridTrack] 20 | var contentMode: GridContentMode 21 | var flow: GridFlow 22 | 23 | subscript(arrangedItem: ArrangedItem) -> PositionedItem? { 24 | items.first(where: { $0.gridElement == arrangedItem.gridElement }) 25 | } 26 | } 27 | 28 | struct PositionedLayout: Equatable { 29 | let items: [PositionedItem] 30 | let totalSize: CGSize? 31 | 32 | static let empty = PositionedLayout(items: [], totalSize: nil) 33 | 34 | subscript(gridElement: GridElement) -> PositionedItem? { 35 | items.first(where: { $0.gridElement == gridElement }) 36 | } 37 | 38 | subscript(arrangedItem: ArrangedItem) -> PositionedItem? { 39 | items.first(where: { $0.gridElement == arrangedItem.gridElement }) 40 | } 41 | } 42 | 43 | private struct PositionedTrack { 44 | let track: GridTrack 45 | var baseSize: CGFloat 46 | } 47 | 48 | extension LayoutPositioning { 49 | func reposition(_ task: PositioningTask) -> PositionedLayout { 50 | let flow = task.flow 51 | let arrangement = task.arrangement 52 | let boundingSize = task.boundingSize 53 | let tracks = task.tracks 54 | 55 | /// 1. Calculate growing track sizes as max of all the items within a track 56 | let growingTracks: [GridTrack] 57 | let growingBoundingSize: CGFloat 58 | switch task.contentMode { 59 | case .scroll: 60 | growingTracks = [GridTrack](repeating: .fit, count: arrangement[keyPath: flow.arrangementCount(.growing)]) 61 | growingBoundingSize = .infinity 62 | case .fill: 63 | growingTracks = [GridTrack](repeating: .fr(1), count: arrangement[keyPath: flow.arrangementCount(.growing)]) 64 | growingBoundingSize = boundingSize[keyPath: flow.size(.growing)] 65 | } 66 | let growingTracksSizes: [CGFloat] = self.calculateTrackSizes( 67 | task: task, 68 | boundingSize: growingBoundingSize, 69 | tracks: growingTracks, 70 | dimension: .growing 71 | ) 72 | 73 | /// 2. Calculate fixed track sizes 74 | let fixedTracksSizes = self.calculateTrackSizes( 75 | task: task, 76 | boundingSize: boundingSize[keyPath: flow.size(.fixed)], 77 | tracks: tracks, 78 | dimension: .fixed 79 | ) 80 | return positionedItems(task: task, growingTracksSizes: growingTracksSizes, fixedTracksSizes: fixedTracksSizes) 81 | } 82 | 83 | private func calculateTrackSizes( 84 | task: PositioningTask, boundingSize: CGFloat, 85 | tracks: [GridTrack], dimension: GridFlowDimension 86 | ) -> [CGFloat] { 87 | let flow = task.flow 88 | let arrangement = task.arrangement 89 | 90 | /// 1. Initialize sizes 91 | var growingTracksSizes: [PositionedTrack] = 92 | tracks.map { track in 93 | switch track { 94 | case .fr: 95 | return PositionedTrack(track: track, baseSize: 0) 96 | case .pt(let size): 97 | return PositionedTrack(track: track, baseSize: CGFloat(size)) 98 | case .fit: 99 | return PositionedTrack(track: track, baseSize: 0) 100 | } 101 | } 102 | 103 | /// 2. Resolve Intrinsic Track Sizes 104 | /// 2.1. Size tracks to fit non-spanning items 105 | self.sizeToFitNonSpanning( 106 | growingTracksSizes: &growingTracksSizes, arrangement: arrangement, 107 | flow: flow, dimension: dimension, task: task 108 | ) 109 | 110 | /// 2.2. Increase sizes to accommodate spanning items 111 | let arrangedSpannedItems = arrangement.items 112 | .filter { $0.span[keyPath: flow.spanIndex(dimension)] > 1 } 113 | 114 | var spansMap: [Int: [ArrangedItem]] = [:] 115 | arrangedSpannedItems.forEach { arrangedItem in 116 | let items = spansMap[arrangedItem.span[keyPath: flow.spanIndex(dimension)]] ?? [] 117 | spansMap[arrangedItem.span[keyPath: flow.spanIndex(dimension)]] = items + [arrangedItem] 118 | } 119 | 120 | for span in spansMap.keys.sorted(by: <) { 121 | var plannedIncreases = [CGFloat?](repeating: 0, count: growingTracksSizes.count) 122 | for arrangedItem in spansMap[span] ?? [] { 123 | let start = arrangedItem.startIndex[keyPath: flow.index(dimension)] 124 | let end = arrangedItem.endIndex[keyPath: flow.index(dimension)] 125 | 126 | /// Consider the items that do not span a track with a flexible size 127 | if (tracks[start...end].contains { $0.isFlexible }) { continue } 128 | 129 | let trackSizes = growingTracksSizes[start...end].map(\.baseSize).reduce(0, +) 130 | let itemSize = task[arrangedItem]?.bounds.size[keyPath: flow.size(dimension)] 131 | let spaceToDistribute = max(0, (itemSize ?? 0) - trackSizes) 132 | (start...end).forEach { 133 | let plannedIncrease = plannedIncreases[$0] ?? 0 134 | plannedIncreases[$0] = max(plannedIncrease, spaceToDistribute / CGFloat(span)) 135 | } 136 | } 137 | 138 | plannedIncreases = plannedIncreases.map { ($0?.rounded() ?? 0) > 0.0 ? $0?.rounded() : nil } 139 | 140 | let existingSizes = growingTracksSizes.map(\.baseSize).reduce(0, +) 141 | let totalPlannedIncrease = plannedIncreases.compactMap({ $0 }).reduce(0, +) 142 | let freeSpace = max(0, boundingSize - existingSizes) 143 | let exceededIncrease = max(0, totalPlannedIncrease - freeSpace) 144 | 145 | /// 2.2.1 Subtract exceeded increase proportionally to the planned ones 146 | if let minValue = plannedIncreases.compactMap({ $0 }).min() { 147 | let normalizedIncreases = plannedIncreases.map { increase -> CGFloat? in 148 | guard let plannedIncrease = increase else { return increase } 149 | return plannedIncrease / minValue 150 | } 151 | let fractionValue = exceededIncrease / normalizedIncreases.compactMap({ $0 }).reduce(0, +) 152 | for (index, plannedIncrease) in plannedIncreases.enumerated() { 153 | guard 154 | let plannedIncrease = plannedIncrease, 155 | let normalizedIncrease = normalizedIncreases[index] 156 | else { continue } 157 | plannedIncreases[index] = plannedIncrease - normalizedIncrease * fractionValue 158 | } 159 | } 160 | 161 | for (index, plannedIncrease) in plannedIncreases.enumerated() { 162 | guard let plannedIncrease = plannedIncrease else { continue } 163 | growingTracksSizes[index].baseSize += plannedIncrease 164 | } 165 | } 166 | 167 | self.expandFlexibleTracks(&growingTracksSizes, boundingSize) 168 | 169 | return growingTracksSizes.map(\.baseSize) 170 | } 171 | 172 | private func positionedItems(task: PositioningTask, growingTracksSizes: [CGFloat], fixedTracksSizes: [CGFloat]) -> PositionedLayout { 173 | /// 4. Position items using calculated track sizes 174 | let flow = task.flow 175 | let arrangement = task.arrangement 176 | let boundingSize = task.boundingSize 177 | 178 | var newPositions: [PositionedItem] = [] 179 | 180 | for positionedItem in task.items { 181 | guard let arrangedItem = arrangement[positionedItem.gridElement] else { continue } 182 | let itemGrowingSize: CGFloat 183 | let growingPosition: CGFloat 184 | 185 | switch task.contentMode { 186 | case .fill: 187 | let growingSize = boundingSize[keyPath: flow.size(.growing)] / CGFloat(arrangement[keyPath: flow.arrangementCount(.growing)]) 188 | itemGrowingSize = growingSize * CGFloat(arrangedItem[keyPath: flow.arrangedItemCount(.growing)]) 189 | growingPosition = growingSize * CGFloat(arrangedItem.startIndex[keyPath: flow.index(.growing)]) 190 | case .scroll: 191 | let indexRange = arrangedItem.startIndex[keyPath: flow.index(.growing)]...arrangedItem.endIndex[keyPath: flow.index(.growing)] 192 | itemGrowingSize = indexRange.reduce(0, { result, index in 193 | return result + growingTracksSizes[index] 194 | }) 195 | let alignmentCorrection: CGFloat 196 | 197 | switch (flow, positionedItem.alignment) { 198 | case 199 | (_, .center), 200 | (_, .none), 201 | (.rows, .leading), 202 | (.columns, .top), 203 | (.rows, .trailing), 204 | (.columns, .bottom): 205 | alignmentCorrection = (itemGrowingSize - positionedItem.bounds.size[keyPath: flow.size(.growing)]) / 2 206 | 207 | case 208 | (.columns, .leading), 209 | (.columns, .bottomLeading), 210 | (.columns, .topLeading), 211 | (.rows, .top), 212 | (.rows, .topLeading), 213 | (.rows, .topTrailing): 214 | alignmentCorrection = 0 215 | 216 | case 217 | (.columns, .trailing), 218 | (.columns, .topTrailing), 219 | (.columns, .bottomTrailing), 220 | (.rows, .bottom), 221 | (.rows, .bottomTrailing), 222 | (.rows, .bottomLeading): 223 | alignmentCorrection = (itemGrowingSize - positionedItem.bounds.size[keyPath: flow.size(.growing)]) 224 | } 225 | 226 | growingPosition = (0.. 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 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/Models/ArrangedItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrangedItem.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 16.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Specfies the abstract position of a grid item in a grid view 12 | struct ArrangedItem: Equatable, Hashable { 13 | let gridElement: GridElement 14 | let startIndex: GridIndex 15 | let endIndex: GridIndex 16 | 17 | var area: Int { self.rowsCount * self.columnsCount } 18 | var columnsCount: Int { endIndex.column - startIndex.column + 1 } 19 | var rowsCount: Int { endIndex.row - startIndex.row + 1 } 20 | var span: GridSpan { return GridSpan(column: endIndex.column - startIndex.column + 1, 21 | row: endIndex.row - startIndex.row + 1) } 22 | 23 | func contains(_ index: GridIndex) -> Bool { 24 | return index.column >= startIndex.column 25 | && index.column <= endIndex.column 26 | && index.row >= startIndex.row 27 | && index.row <= endIndex.row 28 | } 29 | } 30 | 31 | extension ArrangedItem { 32 | init(item: GridElement, startIndex: GridIndex, span: GridSpan) { 33 | let endRow: Int = startIndex.row + span.row - 1 34 | let endColumn: Int = startIndex.column + span.column - 1 35 | self = ArrangedItem(gridElement: item, startIndex: startIndex, endIndex: GridIndex(column: endColumn, row: endRow)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Models/GridAlignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridItemAlignment.swift 3 | // Grid 4 | // 5 | // Created by Denis Obukhov on 15.12.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public enum GridAlignment: Hashable { 12 | case top, bottom, center 13 | case leading, trailing 14 | case topLeading, topTrailing 15 | case bottomLeading, bottomTrailing 16 | } 17 | 18 | extension GridAlignment { 19 | var swiftUIAlignment: Alignment { 20 | switch self { 21 | case .top: 22 | return .top 23 | case .bottom: 24 | return .bottom 25 | case .center: 26 | return .center 27 | case .leading: 28 | return .leading 29 | case .trailing: 30 | return .trailing 31 | case .topLeading: 32 | return .topLeading 33 | case .topTrailing: 34 | return .topTrailing 35 | case .bottomLeading: 36 | return .bottomLeading 37 | case .bottomTrailing: 38 | return .bottomTrailing 39 | } 40 | } 41 | } 42 | 43 | extension GridAlignment: CustomStringConvertible { 44 | public var description: String { 45 | switch self { 46 | case .top: 47 | return "top" 48 | case .bottom: 49 | return "bottom" 50 | case .center: 51 | return "center" 52 | case .leading: 53 | return "leading" 54 | case .trailing: 55 | return "trailing" 56 | case .topLeading: 57 | return "topLeading" 58 | case .topTrailing: 59 | return "topTrailing" 60 | case .bottomLeading: 61 | return "bottomLeading" 62 | case .bottomTrailing: 63 | return "bottomTrailing" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/Models/GridContentMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridContentMode.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 24.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Grid behaviour inside its parent 12 | public enum GridContentMode { 13 | /// Scrolls inside parent container 14 | case scroll 15 | 16 | /// Fills the entire space of the parent container 17 | case fill 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Models/GridElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridElement.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 16.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Fundamental identifiable element in a grid view 12 | public struct GridElement: Identifiable { 13 | public var id: AnyHashable? 14 | public let view: AnyView 15 | let debugID = UUID() 16 | 17 | public init(_ view: T, id: AnyHashable?) { 18 | self.view = AnyView(view) 19 | self.id = id 20 | } 21 | } 22 | 23 | extension GridElement: Equatable { 24 | public static func == (lhs: GridElement, 25 | rhs: GridElement) -> Bool { 26 | return lhs.id == rhs.id 27 | } 28 | } 29 | 30 | extension GridElement: Hashable { 31 | public func hash(into hasher: inout Hasher) { 32 | hasher.combine(id) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Models/GridFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridFlow.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 24.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | public enum GridFlow { 12 | case rows 13 | case columns 14 | } 15 | 16 | enum GridFlowDimension: CaseIterable { 17 | case fixed 18 | case growing 19 | } 20 | 21 | extension GridFlow { 22 | 23 | func index(_ dimension: GridFlowDimension) -> WritableKeyPath { 24 | switch dimension { 25 | case .fixed: 26 | return (self == .rows ? \GridIndex.column : \GridIndex.row) 27 | case .growing: 28 | return (self == .columns ? \GridIndex.column : \GridIndex.row) 29 | } 30 | } 31 | 32 | func spanIndex(_ dimension: GridFlowDimension) -> WritableKeyPath { 33 | switch dimension { 34 | case .fixed: 35 | return (self == .rows ? \GridSpan.column : \GridSpan.row) 36 | case .growing: 37 | return (self == .columns ? \GridSpan.column : \GridSpan.row) 38 | } 39 | } 40 | 41 | func size(_ dimension: GridFlowDimension) -> WritableKeyPath { 42 | switch dimension { 43 | case .fixed: 44 | return (self == .rows ? \CGSize.width : \CGSize.height) 45 | case .growing: 46 | return (self == .columns ? \CGSize.width : \CGSize.height) 47 | } 48 | } 49 | 50 | func arrangedItemCount(_ dimension: GridFlowDimension) -> KeyPath { 51 | switch dimension { 52 | case .fixed: 53 | return (self == .columns ? \ArrangedItem.rowsCount : \ArrangedItem.columnsCount) 54 | case .growing: 55 | return (self == .rows ? \ArrangedItem.rowsCount : \ArrangedItem.columnsCount) 56 | } 57 | } 58 | 59 | func arrangementCount(_ dimension: GridFlowDimension) -> WritableKeyPath { 60 | switch dimension { 61 | case .fixed: 62 | return (self == .columns ? \LayoutArrangement.rowsCount : \LayoutArrangement.columnsCount) 63 | case .growing: 64 | return (self == .rows ? \LayoutArrangement.rowsCount : \LayoutArrangement.columnsCount) 65 | } 66 | } 67 | 68 | func cgPointIndex(_ dimension: GridFlowDimension) -> WritableKeyPath { 69 | switch dimension { 70 | case .fixed: 71 | return (self == .rows ? \CGPoint.x : \CGPoint.y) 72 | case .growing: 73 | return (self == .columns ? \CGPoint.x : \CGPoint.y) 74 | } 75 | } 76 | 77 | func startIndex(_ dimension: GridFlowDimension) -> WritableKeyPath { 78 | switch dimension { 79 | case .fixed: 80 | return (self == .rows ? \GridStart.column : \GridStart.row) 81 | case .growing: 82 | return (self == .columns ? \GridStart.column : \GridStart.row) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Models/GridIndex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridIndex.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 16.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct GridIndex: Equatable, Hashable { 12 | var column: Int 13 | var row: Int 14 | 15 | static let zero = GridIndex(column: 0, row: 0) 16 | } 17 | 18 | extension GridIndex: ExpressibleByArrayLiteral { 19 | init(arrayLiteral elements: Int...) { 20 | assert(elements.count == 2) 21 | self = GridIndex(column: elements[0], row: elements[1]) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Models/GridPacking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridPacking.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 05.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum GridPacking { 12 | case sparse 13 | case dense 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Models/GridSpacing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridSpacing.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 14.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | public struct GridSpacing: Hashable { 12 | let horizontal: CGFloat 13 | let vertical: CGFloat 14 | static let zero = GridSpacing(horizontal: 0, vertical: 0) 15 | 16 | public init(horizontal: CGFloat, vertical: CGFloat) { 17 | self.horizontal = horizontal 18 | self.vertical = vertical 19 | } 20 | } 21 | 22 | extension GridSpacing: ExpressibleByFloatLiteral { 23 | public init(floatLiteral value: Double) { 24 | self = Self.init(horizontal: CGFloat(value), vertical: CGFloat(value)) 25 | } 26 | } 27 | 28 | extension GridSpacing: ExpressibleByIntegerLiteral { 29 | public init(integerLiteral value: Int) { 30 | self = Self.init(horizontal: CGFloat(value), vertical: CGFloat(value)) 31 | } 32 | } 33 | 34 | extension GridSpacing: ExpressibleByArrayLiteral { 35 | public init(arrayLiteral elements: CGFloat...) { 36 | assert(elements.count <= 2) 37 | var vertical: CGFloat = 0 38 | var horizontal: CGFloat = 0 39 | 40 | if elements.count > 1 { 41 | horizontal = elements[0] 42 | vertical = elements[1] 43 | } else if elements.count == 1 { 44 | vertical = elements[0] 45 | horizontal = elements[0] 46 | } 47 | 48 | self = Self.init(horizontal: horizontal, vertical: vertical) 49 | } 50 | } 51 | 52 | extension GridSpacing: ExpressibleByNilLiteral { 53 | public init(nilLiteral: ()) { 54 | self = .zero 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Models/GridSpan.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridSpan.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 19.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct GridSpan: Equatable, Hashable { 12 | var column: Int = Constants.defaultColumnSpan 13 | var row: Int = Constants.defaultRowSpan 14 | 15 | static let `default` = GridSpan() 16 | } 17 | 18 | extension GridSpan: ExpressibleByArrayLiteral { 19 | public init(arrayLiteral elements: Int...) { 20 | assert(elements.count == 2) 21 | self = GridSpan(column: elements[0], row: elements[1]) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Models/GridStart.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridStart.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 19.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct GridStart: Equatable, Hashable { 12 | var column: Int? 13 | var row: Int? 14 | 15 | static let `default` = GridStart(column: nil, row: nil) 16 | } 17 | 18 | extension GridStart: ExpressibleByArrayLiteral { 19 | public init(arrayLiteral elements: Int?...) { 20 | assert(elements.count == 2) 21 | self = GridStart(column: elements[0], row: elements[1]) 22 | } 23 | } 24 | 25 | extension GridStart: ExpressibleByNilLiteral { 26 | public init(nilLiteral: ()) { 27 | self = .default 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Models/GridTrack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridTrack.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 22.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | // swiftlint:disable identifier_name 12 | 13 | /// Size of the each track. 14 | /// fr(N) sizes a track proportionally to the bounding rect with the respect of specified fraction N as a part of total fractions count. 15 | /// const(N) sizes a track to be equal to the specified size N. 16 | public enum GridTrack { 17 | case fr(CGFloat) 18 | case pt(CGFloat) 19 | case fit 20 | 21 | var isIntrinsic: Bool { 22 | switch self { 23 | case .fr: 24 | return false 25 | case .pt: 26 | return false 27 | case .fit: 28 | return true 29 | } 30 | } 31 | 32 | var isFlexible: Bool { 33 | switch self { 34 | case .fr: 35 | return true 36 | case .pt: 37 | return false 38 | case .fit: 39 | return false 40 | } 41 | } 42 | } 43 | 44 | extension Array: ExpressibleByIntegerLiteral where Element == GridTrack { 45 | public typealias IntegerLiteralType = Int 46 | public init(integerLiteral value: Self.IntegerLiteralType) { 47 | self = .init(repeating: .fr(Constants.defaultFractionSize), count: value) 48 | } 49 | } 50 | 51 | extension GridTrack: Equatable, Hashable { } 52 | -------------------------------------------------------------------------------- /Sources/Models/LayoutArrangement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutArrangement.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 16.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | /// Encapsulates the arranged items and total columns and rows count of a grid view 12 | struct LayoutArrangement: Equatable, Hashable { 13 | var columnsCount: Int 14 | var rowsCount: Int 15 | let items: [ArrangedItem] 16 | 17 | static var zero = LayoutArrangement(columnsCount: 0, rowsCount: 0, items: []) 18 | 19 | subscript(gridElement: GridElement) -> ArrangedItem? { 20 | items.first(where: { $0.gridElement == gridElement }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Models/PositionedItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PositionedItem.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 20.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreGraphics 11 | 12 | /// Specfies the final position of a grid item in a grid view on the screen 13 | struct PositionedItem: Equatable, Hashable { 14 | let bounds: CGRect 15 | let gridElement: GridElement 16 | var alignment: GridAlignment? 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Models/Preferences/GridBackgroundPreference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpanPreference.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 18.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct GridBackgroundPreference: GridCellPreference { 12 | var content: (_ rect: CGSize) -> AnyView 13 | } 14 | 15 | struct GridBackgroundPreferenceKey: PreferenceKey { 16 | typealias Value = GridBackgroundPreference 17 | 18 | static var defaultValue = GridBackgroundPreference(content: { _ in AnyView(EmptyView())}) 19 | 20 | static func reduce(value: inout GridBackgroundPreference, nextValue: () -> GridBackgroundPreference) { 21 | value = nextValue() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Models/Preferences/GridCellPreference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridCellPreference.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 29.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | protocol GridCellPreference { 12 | var content: (_ rect: CGSize) -> AnyView { get } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Models/Preferences/GridOverlayPreference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpanPreference.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 18.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct GridOverlayPreference: GridCellPreference { 12 | var content: (_ rect: CGSize) -> AnyView 13 | } 14 | 15 | struct GridOverlayPreferenceKey: PreferenceKey { 16 | typealias Value = GridOverlayPreference 17 | 18 | static var defaultValue = GridOverlayPreference(content: { _ in AnyView(EmptyView())}) 19 | 20 | static func reduce(value: inout GridOverlayPreference, nextValue: () -> GridOverlayPreference) { 21 | value = nextValue() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Models/Preferences/GridPreference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridPreference.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 16.07.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GridPreference: Equatable { 11 | struct ItemInfo: Equatable { 12 | var positionedItem: PositionedItem? 13 | var span: GridSpan? 14 | var start: GridStart? 15 | var alignment: GridAlignment? 16 | 17 | static let empty = ItemInfo() 18 | } 19 | 20 | struct Environment: Equatable { 21 | var tracks: [GridTrack] 22 | var contentMode: GridContentMode 23 | var flow: GridFlow 24 | var packing: GridPacking 25 | var boundingSize: CGSize 26 | } 27 | 28 | var itemsInfo: [ItemInfo] = [] 29 | var environment: Environment? 30 | 31 | static let `default` = GridPreference(itemsInfo: []) 32 | } 33 | 34 | struct GridPreferenceKey: PreferenceKey { 35 | static var defaultValue = GridPreference.default 36 | 37 | static func reduce(value: inout GridPreference, nextValue: () -> GridPreference) { 38 | value = GridPreference(itemsInfo: value.itemsInfo + nextValue().itemsInfo, 39 | environment: nextValue().environment ?? value.environment) 40 | } 41 | } 42 | 43 | extension Array where Element == GridPreference.ItemInfo { 44 | var mergedToSingleValue: Self { 45 | let positionedItem = self.compactMap(\.positionedItem).first 46 | let span = self.compactMap(\.span).first ?? .default 47 | let start = self.compactMap(\.start).first ?? .default 48 | let alignment = self.compactMap(\.alignment).first 49 | let itemInfo = GridPreference.ItemInfo(positionedItem: positionedItem, 50 | span: span, 51 | start: start, 52 | alignment: alignment) 53 | return [itemInfo] 54 | } 55 | 56 | var asArrangementInfo: [ArrangementInfo] { 57 | return self.compactMap { 58 | guard 59 | let gridElement = $0.positionedItem?.gridElement, 60 | let start = $0.start, 61 | let span = $0.span 62 | else { 63 | return nil 64 | } 65 | return ArrangementInfo(gridElement: gridElement, 66 | start: start, 67 | span: span) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/View/Grid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Grid.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 17.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct Grid: View, LayoutArranging, LayoutPositioning { 12 | @State private var positions: PositionedLayout = .empty 13 | @State private var isLoaded: Bool = false 14 | @State private var alignments: [GridElement: GridAlignment] = [:] 15 | #if os(iOS) || os(watchOS) || os(tvOS) 16 | @State private var internalLayoutCache = Cache() 17 | @State private var internalPositionsCache = Cache() 18 | #endif 19 | @Environment(\.gridContentMode) private var environmentContentMode 20 | @Environment(\.gridFlow) private var environmentFlow 21 | @Environment(\.gridPacking) private var environmentPacking 22 | @Environment(\.gridAnimation) private var gridAnimation 23 | @Environment(\.gridCache) private var environmentCacheMode 24 | @Environment(\.gridCommonItemsAlignment) private var environmentCommonItemsAlignment 25 | @Environment(\.gridContentAlignment) private var environmentContentAlignment 26 | 27 | let itemsBuilder: () -> [GridElement] 28 | let spacing: GridSpacing 29 | let trackSizes: [GridTrack] 30 | var internalFlow: GridFlow? 31 | var internalPacking: GridPacking? 32 | var internalContentMode: GridContentMode? 33 | var internalCacheMode: GridCacheMode? 34 | var internalCommonItemsAlignment: GridAlignment? 35 | var internalContentAlignment: GridAlignment? 36 | 37 | private var flow: GridFlow { 38 | self.internalFlow ?? self.environmentFlow ?? Constants.defaultFlow 39 | } 40 | 41 | private var packing: GridPacking { 42 | self.internalPacking ?? self.environmentPacking ?? Constants.defaultPacking 43 | } 44 | 45 | private var contentMode: GridContentMode { 46 | self.internalContentMode ?? self.environmentContentMode ?? Constants.defaultContentMode 47 | } 48 | 49 | private var cacheMode: GridCacheMode { 50 | self.internalCacheMode ?? self.environmentCacheMode ?? Constants.defaultCacheMode 51 | } 52 | 53 | private var commonItemsAlignment: GridAlignment { 54 | self.internalCommonItemsAlignment ?? self.environmentCommonItemsAlignment ?? Constants.defaultCommonItemsAlignment 55 | } 56 | 57 | private var contentAlignment: GridAlignment { 58 | self.internalContentAlignment ?? self.environmentContentAlignment ?? Constants.defaultContentAlignment 59 | } 60 | 61 | #if os(iOS) || os(watchOS) || os(tvOS) 62 | 63 | private var layoutCache: Cache? { 64 | switch self.cacheMode { 65 | case .inMemoryCache: 66 | return self.internalLayoutCache 67 | case .noCache: 68 | return nil 69 | } 70 | } 71 | 72 | private var positionsCache: Cache? { 73 | switch self.cacheMode { 74 | case .inMemoryCache: 75 | return self.internalPositionsCache 76 | case .noCache: 77 | return nil 78 | } 79 | } 80 | 81 | #endif 82 | 83 | public var body: some View { 84 | GeometryReader { mainGeometry in 85 | ZStack(alignment: .topLeading) { 86 | ForEach(itemsBuilder()) { item in 87 | item.view 88 | .padding(spacing: self.spacing) 89 | .background( 90 | self.positionsPreferencesSetter( 91 | item: item, 92 | boundingSize: mainGeometry.size 93 | ) 94 | ) 95 | .transformPreference(GridPreferenceKey.self) { preference in 96 | preference.itemsInfo = preference.itemsInfo.mergedToSingleValue 97 | } 98 | .frame( 99 | flow: self.flow, 100 | size: self.positions[item]?.bounds.size, 101 | contentMode: self.contentMode, 102 | alignment: alignmentForItem(item) 103 | ) 104 | .backgroundPreferenceValue( 105 | GridBackgroundPreferenceKey.self, 106 | alignment: alignmentForItem(item) 107 | ) { preference in 108 | self.cellPreferenceView(item: item, preference: preference) 109 | } 110 | .overlayPreferenceValue( 111 | GridOverlayPreferenceKey.self, 112 | alignment: alignmentForItem(item) 113 | ) { preference in 114 | self.cellPreferenceView(item: item, preference: preference) 115 | } 116 | .alignmentGuide(.leading, computeValue: { _ in self.leadingGuide(item: item) }) 117 | .alignmentGuide(.top, computeValue: { _ in self.topGuide(item: item) }) 118 | } 119 | } 120 | .animation(self.gridAnimation) 121 | .frame( 122 | flow: self.flow, 123 | size: mainGeometry.size, 124 | contentMode: self.contentMode, 125 | alignment: self.contentAlignment.swiftUIAlignment 126 | ) 127 | .if(contentMode == .scroll) { content in 128 | ScrollView(self.scrollAxis) { content } 129 | } 130 | .onPreferenceChange(GridPreferenceKey.self) { preference in 131 | self.calculateLayout( 132 | preference: preference, 133 | boundingSize: mainGeometry.size 134 | ) 135 | self.saveAlignmentsFrom(preference: preference) 136 | } 137 | } 138 | .id(self.isLoaded) 139 | } 140 | 141 | private func calculateLayout(preference: GridPreference, boundingSize: CGSize) { 142 | let task = ArrangingTask(itemsInfo: preference.itemsInfo.asArrangementInfo, 143 | tracks: self.trackSizes, 144 | flow: self.flow, 145 | packing: self.packing) 146 | 147 | let calculatedLayout: LayoutArrangement 148 | 149 | #if os(iOS) || os(watchOS) || os(tvOS) 150 | if let cachedLayout = self.layoutCache?.object(forKey: task) { 151 | calculatedLayout = cachedLayout 152 | } else { 153 | calculatedLayout = self.arrange(task: task) 154 | self.layoutCache?.setObject(calculatedLayout, forKey: task) 155 | } 156 | #else 157 | calculatedLayout = self.arrange(task: task) 158 | #endif 159 | let positionedItems = preference.itemsInfo.compactMap { 160 | var item = $0.positionedItem 161 | item?.alignment = $0.alignment ?? commonItemsAlignment 162 | return item 163 | } 164 | let positionTask = PositioningTask( 165 | items: positionedItems, 166 | arrangement: calculatedLayout, 167 | boundingSize: self.corrected(size: boundingSize), 168 | tracks: self.trackSizes, 169 | contentMode: self.contentMode, 170 | flow: self.flow 171 | ) 172 | let positions: PositionedLayout 173 | 174 | #if os(iOS) || os(watchOS) || os(tvOS) 175 | if let cachedPositions = self.positionsCache?.object(forKey: positionTask) { 176 | positions = cachedPositions 177 | } else { 178 | positions = self.reposition(positionTask) 179 | self.positionsCache?.setObject(positions, forKey: positionTask) 180 | } 181 | #else 182 | positions = self.reposition(positionTask) 183 | #endif 184 | 185 | self.positions = positions 186 | self.isLoaded = true 187 | } 188 | 189 | private func saveAlignmentsFrom(preference: GridPreference) { 190 | var alignments: [GridElement: GridAlignment] = [:] 191 | preference.itemsInfo.forEach { 192 | guard let gridElement = $0.positionedItem?.gridElement else { return } 193 | alignments[gridElement] = $0.alignment 194 | } 195 | self.alignments = alignments 196 | } 197 | 198 | private func corrected(size: CGSize) -> CGSize { 199 | return CGSize(width: size.width - self.spacing.horizontal, 200 | height: size.height - self.spacing.vertical) 201 | } 202 | 203 | private var scrollAxis: Axis.Set { 204 | switch self.contentMode { 205 | case .fill: 206 | return [] 207 | case .scroll: 208 | return self.flow == .rows ? .vertical : .horizontal 209 | } 210 | } 211 | 212 | private func leadingGuide(item: GridElement) -> CGFloat { 213 | -(self.positions[item]?.bounds.origin.x ?? CGFloat(-self.spacing.horizontal) / 2.0) 214 | } 215 | 216 | private func topGuide(item: GridElement) -> CGFloat { 217 | -(self.positions[item]?.bounds.origin.y ?? CGFloat(-self.spacing.vertical) / 2.0) 218 | } 219 | 220 | private func cellPreferenceView(item: GridElement, preference: T) -> some View { 221 | GeometryReader { geometry in 222 | preference.content(geometry.size) 223 | } 224 | .padding(spacing: self.spacing) 225 | .frame( 226 | width: self.positions[item]?.bounds.width, 227 | height: self.positions[item]?.bounds.height 228 | ) 229 | } 230 | 231 | private func positionsPreferencesSetter(item: GridElement, boundingSize: CGSize) -> some View { 232 | GeometryReader { geometry in 233 | Color.clear 234 | .transformPreference(GridPreferenceKey.self, { preference in 235 | let positionedItem = PositionedItem( 236 | bounds: CGRect(origin: .zero, size: geometry.size), 237 | gridElement: item 238 | ) 239 | let info = GridPreference.ItemInfo(positionedItem: positionedItem) 240 | let environment = GridPreference.Environment( 241 | tracks: self.trackSizes, 242 | contentMode: self.contentMode, 243 | flow: self.flow, 244 | packing: self.packing, 245 | boundingSize: boundingSize 246 | ) 247 | preference = GridPreference(itemsInfo: [info], environment: environment) 248 | }) 249 | } 250 | } 251 | 252 | private func alignmentForItem(_ item: GridElement) -> Alignment { 253 | self.alignments[item]?.swiftUIAlignment ?? commonItemsAlignment.swiftUIAlignment 254 | } 255 | } 256 | 257 | extension View { 258 | fileprivate func frame( 259 | flow: GridFlow, 260 | size: CGSize?, 261 | contentMode: GridContentMode, 262 | alignment: Alignment = .center 263 | ) -> some View { 264 | let width: CGFloat? 265 | let height: CGFloat? 266 | 267 | switch contentMode { 268 | case .fill: 269 | width = size?.width 270 | height = size?.height 271 | case .scroll: 272 | width = (flow == .rows ? size?.width : nil) 273 | height = (flow == .columns ? size?.height : nil) 274 | } 275 | 276 | return frame(width: width, height: height, alignment: alignment) 277 | } 278 | 279 | fileprivate func padding(spacing: GridSpacing) -> some View { 280 | var edgeInsets = EdgeInsets() 281 | edgeInsets.top = spacing.vertical / 2 282 | edgeInsets.bottom = spacing.vertical / 2 283 | edgeInsets.leading = spacing.horizontal / 2 284 | edgeInsets.trailing = spacing.horizontal / 2 285 | return self.padding(edgeInsets) 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /Sources/View/GridGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridGroup.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 18.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct GridGroup: View, GridGroupContaining { 12 | 13 | public static var empty = GridGroup(contentViews: []) 14 | 15 | var contentViews: [GridElement] 16 | 17 | public var body = EmptyView() 18 | } 19 | 20 | #if DEBUG 21 | 22 | // To be available on preview canvas 23 | 24 | extension ModifiedContent: GridGroupContaining where Content: GridGroupContaining, Modifier == _IdentifiedModifier<__DesignTimeSelectionIdentifier> { 25 | var contentViews: [GridElement] { 26 | return self.content.contentViews 27 | } 28 | } 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /Sources/View/Inits/ForEach+GridViewsContaining.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForEach+GridViewsContaining.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 07.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension ForEach: GridForEachRangeInt where Data == Range, ID == Int, Content: View { 12 | var contentViews: [GridElement] { 13 | self.data.flatMap { 14 | self.content($0).extractContentViews() 15 | } 16 | } 17 | } 18 | 19 | extension ForEach: GridForEachIdentifiable where ID == Data.Element.ID, Content: View, Data.Element: Identifiable { 20 | var contentViews: [GridElement] { 21 | self.data.enumerated().flatMap { (_, dataElement: Data.Element) -> [GridElement] in 22 | let view = self.content(dataElement) 23 | return view.extractContentViews().enumerated().map { 24 | var identifiedView = $0.element 25 | if let identifiedHash = identifiedView.id { 26 | identifiedView.id = AnyHashable([ 27 | identifiedHash, 28 | AnyHashable(dataElement.id) 29 | ]) 30 | } else { 31 | identifiedView.id = AnyHashable([ 32 | AnyHashable(dataElement.id), 33 | AnyHashable($0.offset) 34 | ]) 35 | } 36 | return identifiedView 37 | } 38 | } 39 | } 40 | } 41 | 42 | extension ForEach: GridForEachID where Content: View { 43 | var contentViews: [GridElement] { 44 | self.data.flatMap { 45 | self.content($0).extractContentViews() 46 | } 47 | } 48 | } 49 | 50 | #if DEBUG 51 | 52 | // To be available on preview canvas 53 | 54 | extension ModifiedContent: GridForEachRangeInt where Content: GridForEachRangeInt, Modifier == _IdentifiedModifier<__DesignTimeSelectionIdentifier> { 55 | var contentViews: [GridElement] { 56 | return self.content.contentViews 57 | } 58 | } 59 | 60 | extension ModifiedContent: GridForEachIdentifiable where Content: GridForEachIdentifiable, Modifier == _IdentifiedModifier<__DesignTimeSelectionIdentifier> { 61 | var contentViews: [GridElement] { 62 | return self.content.contentViews 63 | } 64 | } 65 | 66 | extension ModifiedContent: GridForEachID where Content: GridForEachID, Modifier == _IdentifiedModifier<__DesignTimeSelectionIdentifier> { 67 | var contentViews: [GridElement] { 68 | return self.content.contentViews 69 | } 70 | } 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /Sources/View/Inits/Grid+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Grid+Inits_TupleView.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 18.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Grid { 12 | public init( 13 | tracks: [GridTrack] = 1, 14 | contentMode: GridContentMode? = nil, 15 | flow: GridFlow? = nil, 16 | packing: GridPacking? = nil, 17 | spacing: GridSpacing = Constants.defaultSpacing, 18 | commonItemsAlignment: GridAlignment? = nil, 19 | contentAlignment: GridAlignment? = nil, 20 | cache: GridCacheMode? = nil, 21 | @GridBuilder content: @escaping () -> GridBuilderResult) { 22 | self.trackSizes = tracks 23 | self.spacing = spacing 24 | self.internalContentMode = contentMode 25 | self.internalFlow = flow 26 | self.internalPacking = packing 27 | self.internalCacheMode = cache 28 | self.internalCommonItemsAlignment = commonItemsAlignment 29 | self.internalContentAlignment = contentAlignment 30 | 31 | itemsBuilder = { 32 | let content = content() 33 | var index = 0 34 | return content.contentViews.asGridElements(index: &index) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/View/Inits/Grid+Inits_Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Grid+Inits_Data.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 07.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Grid { 12 | public init( 13 | _ data: Data, 14 | id: KeyPath, 15 | tracks: [GridTrack] = 1, 16 | contentMode: GridContentMode? = nil, 17 | flow: GridFlow? = nil, 18 | packing: GridPacking? = nil, 19 | spacing: GridSpacing = Constants.defaultSpacing, 20 | commonItemsAlignment: GridAlignment? = nil, 21 | contentAlignment: GridAlignment? = nil, 22 | cache: GridCacheMode? = nil, 23 | @GridBuilder item: @escaping (Data.Element) -> GridBuilderResult 24 | ) where Data: RandomAccessCollection, ID: Hashable { 25 | itemsBuilder = { 26 | var index = 0 27 | return data.flatMap { 28 | item($0).contentViews.asGridElements( 29 | index: &index, 30 | baseHash: AnyHashable([AnyHashable($0[keyPath: id]), AnyHashable(id)]) 31 | ) 32 | } 33 | } 34 | 35 | self.trackSizes = tracks 36 | self.spacing = spacing 37 | self.internalContentMode = contentMode 38 | self.internalFlow = flow 39 | self.internalPacking = packing 40 | self.internalCacheMode = cache 41 | self.internalCommonItemsAlignment = commonItemsAlignment 42 | self.internalContentAlignment = contentAlignment 43 | } 44 | 45 | public init( 46 | _ data: Range, 47 | tracks: [GridTrack] = 1, 48 | contentMode: GridContentMode? = nil, 49 | flow: GridFlow? = nil, 50 | packing: GridPacking? = nil, 51 | spacing: GridSpacing = Constants.defaultSpacing, 52 | commonItemsAlignment: GridAlignment? = nil, 53 | contentAlignment: GridAlignment? = nil, 54 | cache: GridCacheMode? = nil, 55 | @GridBuilder item: @escaping (Int) -> GridBuilderResult 56 | ) { 57 | itemsBuilder = { 58 | var index = 0 59 | return data.flatMap { 60 | item($0).contentViews.asGridElements(index: &index) 61 | } 62 | } 63 | 64 | self.trackSizes = tracks 65 | self.spacing = spacing 66 | self.internalContentMode = contentMode 67 | self.internalFlow = flow 68 | self.internalPacking = packing 69 | self.internalCacheMode = cache 70 | self.internalCommonItemsAlignment = commonItemsAlignment 71 | self.internalContentAlignment = contentAlignment 72 | } 73 | 74 | public init( 75 | _ data: Data, 76 | tracks: [GridTrack] = 1, 77 | contentMode: GridContentMode? = nil, 78 | flow: GridFlow? = nil, 79 | packing: GridPacking? = nil, 80 | spacing: GridSpacing = Constants.defaultSpacing, 81 | commonItemsAlignment: GridAlignment? = nil, 82 | contentAlignment: GridAlignment? = nil, 83 | cache: GridCacheMode? = nil, 84 | @GridBuilder item: @escaping (Data.Element) -> GridBuilderResult 85 | ) where Data: RandomAccessCollection, Data.Element: Identifiable { 86 | itemsBuilder = { 87 | var index = 0 88 | return data.flatMap { 89 | item($0).contentViews.asGridElements( 90 | index: &index, 91 | baseHash: AnyHashable($0.id) 92 | ) 93 | } 94 | } 95 | 96 | self.trackSizes = tracks 97 | self.spacing = spacing 98 | self.internalContentMode = contentMode 99 | self.internalFlow = flow 100 | self.internalPacking = packing 101 | self.internalCacheMode = cache 102 | self.internalCommonItemsAlignment = commonItemsAlignment 103 | self.internalContentAlignment = contentAlignment 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/View/Inits/GridBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Builder.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 10.08.2020. 6 | // 7 | 8 | // swiftlint:disable function_parameter_count identifier_name line_length 9 | 10 | import SwiftUI 11 | 12 | public struct GridBuilderResult: View { 13 | public var body = EmptyView() 14 | var contentViews: [GridElement] 15 | } 16 | 17 | @resultBuilder 18 | public struct GridBuilder { 19 | public static func buildBlock() -> GridBuilderResult { 20 | return GridBuilderResult(contentViews: []) 21 | } 22 | 23 | public static func buildBlock(_ content: C0) -> GridBuilderResult { 24 | var views: [GridElement] = [] 25 | views.append(contentsOf: content.extractContentViews()) 26 | return GridBuilderResult(contentViews: views) 27 | } 28 | 29 | public static func buildBlock(_ c0: C0, _ c1: C1) -> GridBuilderResult where C0: View, C1: View { 30 | var views: [GridElement] = [] 31 | views.append(contentsOf: c0.extractContentViews()) 32 | views.append(contentsOf: c1.extractContentViews()) 33 | return GridBuilderResult(contentViews: views) 34 | } 35 | 36 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2) -> GridBuilderResult where C0: View, C1: View, C2: View { 37 | var views: [GridElement] = [] 38 | views.append(contentsOf: c0.extractContentViews()) 39 | views.append(contentsOf: c1.extractContentViews()) 40 | views.append(contentsOf: c2.extractContentViews()) 41 | return GridBuilderResult(contentViews: views) 42 | } 43 | 44 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3) -> GridBuilderResult where C0: View, C1: View, C2: View, C3: View { 45 | var views: [GridElement] = [] 46 | views.append(contentsOf: c0.extractContentViews()) 47 | views.append(contentsOf: c1.extractContentViews()) 48 | views.append(contentsOf: c2.extractContentViews()) 49 | views.append(contentsOf: c3.extractContentViews()) 50 | return GridBuilderResult(contentViews: views) 51 | } 52 | 53 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4) -> GridBuilderResult where C0: View, C1: View, C2: View, C3: View, C4: View { 54 | var views: [GridElement] = [] 55 | views.append(contentsOf: c0.extractContentViews()) 56 | views.append(contentsOf: c1.extractContentViews()) 57 | views.append(contentsOf: c2.extractContentViews()) 58 | views.append(contentsOf: c3.extractContentViews()) 59 | views.append(contentsOf: c4.extractContentViews()) 60 | return GridBuilderResult(contentViews: views) 61 | } 62 | 63 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5) -> GridBuilderResult where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View { 64 | var views: [GridElement] = [] 65 | views.append(contentsOf: c0.extractContentViews()) 66 | views.append(contentsOf: c1.extractContentViews()) 67 | views.append(contentsOf: c2.extractContentViews()) 68 | views.append(contentsOf: c3.extractContentViews()) 69 | views.append(contentsOf: c4.extractContentViews()) 70 | views.append(contentsOf: c5.extractContentViews()) 71 | return GridBuilderResult(contentViews: views) 72 | } 73 | 74 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6) -> GridBuilderResult where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View { 75 | var views: [GridElement] = [] 76 | views.append(contentsOf: c0.extractContentViews()) 77 | views.append(contentsOf: c1.extractContentViews()) 78 | views.append(contentsOf: c2.extractContentViews()) 79 | views.append(contentsOf: c3.extractContentViews()) 80 | views.append(contentsOf: c4.extractContentViews()) 81 | views.append(contentsOf: c5.extractContentViews()) 82 | views.append(contentsOf: c6.extractContentViews()) 83 | return GridBuilderResult(contentViews: views) 84 | } 85 | 86 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7) -> GridBuilderResult where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View { 87 | var views: [GridElement] = [] 88 | views.append(contentsOf: c0.extractContentViews()) 89 | views.append(contentsOf: c1.extractContentViews()) 90 | views.append(contentsOf: c2.extractContentViews()) 91 | views.append(contentsOf: c3.extractContentViews()) 92 | views.append(contentsOf: c4.extractContentViews()) 93 | views.append(contentsOf: c5.extractContentViews()) 94 | views.append(contentsOf: c6.extractContentViews()) 95 | views.append(contentsOf: c7.extractContentViews()) 96 | return GridBuilderResult(contentViews: views) 97 | } 98 | 99 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8) -> GridBuilderResult where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View { 100 | var views: [GridElement] = [] 101 | views.append(contentsOf: c0.extractContentViews()) 102 | views.append(contentsOf: c1.extractContentViews()) 103 | views.append(contentsOf: c2.extractContentViews()) 104 | views.append(contentsOf: c3.extractContentViews()) 105 | views.append(contentsOf: c4.extractContentViews()) 106 | views.append(contentsOf: c5.extractContentViews()) 107 | views.append(contentsOf: c6.extractContentViews()) 108 | views.append(contentsOf: c7.extractContentViews()) 109 | views.append(contentsOf: c8.extractContentViews()) 110 | return GridBuilderResult(contentViews: views) 111 | } 112 | 113 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> GridBuilderResult where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View, C9: View { 114 | var views: [GridElement] = [] 115 | views.append(contentsOf: c0.extractContentViews()) 116 | views.append(contentsOf: c1.extractContentViews()) 117 | views.append(contentsOf: c2.extractContentViews()) 118 | views.append(contentsOf: c3.extractContentViews()) 119 | views.append(contentsOf: c4.extractContentViews()) 120 | views.append(contentsOf: c5.extractContentViews()) 121 | views.append(contentsOf: c6.extractContentViews()) 122 | views.append(contentsOf: c7.extractContentViews()) 123 | views.append(contentsOf: c8.extractContentViews()) 124 | views.append(contentsOf: c9.extractContentViews()) 125 | return GridBuilderResult(contentViews: views) 126 | } 127 | 128 | public static func buildEither(first: GridBuilderResult) -> GridBuilderResult { first } 129 | 130 | public static func buildEither(second: GridBuilderResult) -> GridBuilderResult { second } 131 | 132 | public static func buildIf(_ content: GridBuilderResult?) -> GridBuilderResult { 133 | content ?? GridBuilderResult(contentViews: []) 134 | } 135 | 136 | public static func buildOptional(_ component: GridBuilderResult?) -> GridBuilderResult { 137 | component ?? GridBuilderResult(contentViews: []) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/View/Inits/GridContentViewsProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridContentViewsProtocols.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 07.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | protocol GridForEachRangeInt { 12 | var contentViews: [GridElement] { get } 13 | } 14 | 15 | protocol GridForEachIdentifiable { 16 | var contentViews: [GridElement] { get } 17 | } 18 | 19 | protocol GridForEachID { 20 | var contentViews: [GridElement] { get } 21 | } 22 | 23 | protocol GridGroupContaining { 24 | var contentViews: [GridElement] { get } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/View/Inits/GridElement+asGridElements.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridElement+asGridElements.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 07.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Array where Element == GridElement { 12 | func asGridElements(index: inout Int, baseHash: T?) -> [GridElement] { 13 | let containerItems: [GridElement] = 14 | map { 15 | let gridHash: AnyHashable 16 | if let viewHash = $0.id { 17 | gridHash = viewHash 18 | } else { 19 | gridHash = AnyHashable([baseHash, AnyHashable(index)]) 20 | index += 1 21 | } 22 | return GridElement($0.view, id: gridHash) 23 | } 24 | return containerItems 25 | } 26 | 27 | func asGridElements(index: inout Int) -> [GridElement] { 28 | asGridElements(index: &index, baseHash: Int?.none) 29 | } 30 | } 31 | 32 | extension View { 33 | func extractContentViews() -> [GridElement] { 34 | if let container = self as? GridForEachRangeInt { 35 | return container.contentViews 36 | } else if let container = self as? GridForEachIdentifiable { 37 | return container.contentViews 38 | } else if let container = self as? GridForEachID { 39 | return container.contentViews 40 | } else if let container = self as? GridGroupContaining { 41 | return container.contentViews 42 | } else if let container = self as? GridBuilderResult { 43 | return container.contentViews 44 | } 45 | return [GridElement(AnyView(self), id: nil)] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/View/Inits/GridGroup+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridGroup+Inits_TupleView.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 26.05.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension GridGroup { 11 | public init(@GridBuilder content: () -> GridBuilderResult) { 12 | self.contentViews = content().contentViews 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/View/Inits/GridGroup+Inits_Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridGroup+Inits_Data.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 26.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension GridGroup { 12 | public init(_ data: Data, id: KeyPath, @GridBuilder item: @escaping (Data.Element) -> GridBuilderResult) where Data: RandomAccessCollection, ID: Hashable { 13 | self.contentViews = data.enumerated().flatMap { (_, dataElement: Data.Element) -> [GridElement] in 14 | let constructionItem = item(dataElement) 15 | let views: [GridElement] = constructionItem.contentViews.enumerated().map { 16 | var gridElement = $0.element 17 | if let identifiedHash = gridElement.id { 18 | gridElement.id = 19 | AnyHashable([identifiedHash, 20 | AnyHashable(dataElement[keyPath: id]), 21 | AnyHashable(id)]) 22 | } else { 23 | gridElement.id = 24 | AnyHashable([AnyHashable(dataElement[keyPath: id]), 25 | AnyHashable(id), 26 | AnyHashable($0.offset)]) 27 | } 28 | return gridElement 29 | } 30 | return views 31 | } 32 | } 33 | 34 | public init(_ data: Range, @GridBuilder item: @escaping (Int) -> GridBuilderResult) { 35 | self.contentViews = data.flatMap { item($0).contentViews } 36 | } 37 | 38 | public init(_ data: Data, @GridBuilder item: @escaping (Data.Element) -> GridBuilderResult) where Data: RandomAccessCollection, Data.Element: Identifiable { 39 | self.contentViews = data.enumerated().flatMap { (_, dataElement: Data.Element) -> [GridElement] in 40 | let constructionItem = item(dataElement) 41 | let views: [GridElement] = constructionItem.contentViews.enumerated().map { 42 | var gridElement = $0.element 43 | if let identifiedHash = gridElement.id { 44 | gridElement.id = 45 | AnyHashable([identifiedHash, 46 | dataElement.id]) 47 | } else { 48 | gridElement.id = 49 | AnyHashable([AnyHashable(dataElement.id), 50 | AnyHashable($0.offset)]) 51 | } 52 | return gridElement 53 | } 54 | return views 55 | } 56 | } 57 | 58 | public init(_ data: Data, @GridBuilder item: @escaping (Data) -> GridBuilderResult) { 59 | self.init([data], item: item) 60 | } 61 | 62 | public init(_ data: Data, @GridBuilder item: @escaping (Data) -> GridBuilderResult) { 63 | self.init([data], id: \.self, item: item) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/View/View+Environment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Environment.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 24.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension View { 12 | public func gridContentMode(_ contentMode: GridContentMode) -> some View { 13 | return self.environment(\.gridContentMode, contentMode) 14 | } 15 | 16 | public func gridFlow(_ flow: GridFlow) -> some View { 17 | return self.environment(\.gridFlow, flow) 18 | } 19 | 20 | public func gridPacking(_ packing: GridPacking) -> some View { 21 | return self.environment(\.gridPacking, packing) 22 | } 23 | 24 | public func gridAnimation(_ animation: Animation?) -> some View { 25 | return self.environment(\.gridAnimation, animation) 26 | } 27 | 28 | public func gridCache(_ cache: GridCacheMode?) -> some View { 29 | return self.environment(\.gridCache, cache) 30 | } 31 | 32 | public func gridCommonItemsAlignment(_ alignment: GridAlignment?) -> some View { 33 | return self.environment(\.gridCommonItemsAlignment, alignment) 34 | } 35 | 36 | public func gridContentAlignment(_ alignment: GridAlignment?) -> some View { 37 | return self.environment(\.gridContentAlignment, alignment) 38 | } 39 | } 40 | 41 | extension EnvironmentValues { 42 | var gridContentMode: GridContentMode? { 43 | get { self[EnvironmentKeys.ContentMode.self] } 44 | set { self[EnvironmentKeys.ContentMode.self] = newValue } 45 | } 46 | 47 | var gridFlow: GridFlow? { 48 | get { self[EnvironmentKeys.Flow.self] } 49 | set { self[EnvironmentKeys.Flow.self] = newValue } 50 | } 51 | 52 | var gridPacking: GridPacking? { 53 | get { self[EnvironmentKeys.Packing.self] } 54 | set { self[EnvironmentKeys.Packing.self] = newValue } 55 | } 56 | 57 | var gridAnimation: Animation? { 58 | get { self[EnvironmentKeys.GridAnimation.self] } 59 | set { self[EnvironmentKeys.GridAnimation.self] = newValue } 60 | } 61 | 62 | var gridCache: GridCacheMode? { 63 | get { self[EnvironmentKeys.GridCache.self] } 64 | set { self[EnvironmentKeys.GridCache.self] = newValue } 65 | } 66 | 67 | var gridCommonItemsAlignment: GridAlignment? { 68 | get { self[EnvironmentKeys.GridCommonItemsAlignment.self] } 69 | set { self[EnvironmentKeys.GridCommonItemsAlignment.self] = newValue } 70 | } 71 | 72 | var gridContentAlignment: GridAlignment? { 73 | get { self[EnvironmentKeys.GridContentAlignment.self] } 74 | set { self[EnvironmentKeys.GridContentAlignment.self] = newValue } 75 | } 76 | } 77 | 78 | private struct EnvironmentKeys { 79 | struct ContentMode: EnvironmentKey { 80 | static let defaultValue: GridContentMode? = nil 81 | } 82 | 83 | struct Flow: EnvironmentKey { 84 | static let defaultValue: GridFlow? = nil 85 | } 86 | 87 | struct Packing: EnvironmentKey { 88 | static let defaultValue: GridPacking? = nil 89 | } 90 | 91 | struct GridAnimation: EnvironmentKey { 92 | static let defaultValue: Animation? = nil 93 | } 94 | 95 | struct GridCache: EnvironmentKey { 96 | static let defaultValue: GridCacheMode? = nil 97 | } 98 | 99 | struct GridCommonItemsAlignment: EnvironmentKey { 100 | static let defaultValue: GridAlignment? = .center 101 | } 102 | 103 | struct GridContentAlignment: EnvironmentKey { 104 | static let defaultValue: GridAlignment? = .center 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/View/View+GridPreferences.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+GridPreferences.swift 3 | // ExyteGrid 4 | // 5 | // Created by Denis Obukhov on 28.04.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension View { 12 | public func gridSpan(column: Int = Constants.defaultColumnSpan, 13 | row: Int = Constants.defaultRowSpan 14 | ) -> some View { 15 | transformPreference(GridPreferenceKey.self, { preferences in 16 | var info = preferences.itemsInfo.first ?? .empty 17 | info.span = GridSpan(column: max(column, 1), row: max(row, 1)) 18 | preferences.itemsInfo = [info] 19 | }) 20 | } 21 | 22 | public func gridSpan(_ span: GridSpan) -> some View { 23 | transformPreference(GridPreferenceKey.self, { preferences in 24 | var info = preferences.itemsInfo.first ?? .empty 25 | info.span = GridSpan(column: max(span.column, 1), row: max(span.row, 1)) 26 | preferences.itemsInfo = [info] 27 | }) 28 | } 29 | 30 | public func gridStart(column: Int? = nil, row: Int? = nil) -> some View { 31 | transformPreference(GridPreferenceKey.self, { preferences in 32 | var info = preferences.itemsInfo.first ?? .empty 33 | info.start = GridStart(column: column.nilIfBelowZero, row: row.nilIfBelowZero) 34 | preferences.itemsInfo = [info] 35 | }) 36 | } 37 | 38 | public func gridStart(_ start: GridStart) -> some View { 39 | transformPreference(GridPreferenceKey.self, { preferences in 40 | var info = preferences.itemsInfo.first ?? .empty 41 | info.start = GridStart(column: start.column.nilIfBelowZero, row: start.row.nilIfBelowZero) 42 | preferences.itemsInfo = [info] 43 | }) 44 | } 45 | 46 | public func gridItemAlignment(_ alignment: GridAlignment?) -> some View { 47 | transformPreference(GridPreferenceKey.self, { preferences in 48 | var info = preferences.itemsInfo.first ?? .empty 49 | info.alignment = alignment 50 | preferences.itemsInfo = [info] 51 | }) 52 | } 53 | 54 | public func gridCellOverlay( 55 | @ViewBuilder content: @escaping (CGSize?) -> Content 56 | ) -> some View { 57 | preference( 58 | key: GridOverlayPreferenceKey.self, 59 | value: GridOverlayPreference { rect in 60 | AnyView(content(rect)) 61 | } 62 | ) 63 | } 64 | 65 | public func gridCellBackground( 66 | @ViewBuilder content: @escaping (CGSize?) -> Content 67 | ) -> some View { 68 | preference( 69 | key: GridBackgroundPreferenceKey.self, 70 | value: GridBackgroundPreference { rect in 71 | AnyView(content(rect)) 72 | }) 73 | } 74 | } 75 | 76 | extension Optional where Wrapped == Int { 77 | var nilIfBelowZero: Wrapped? { 78 | let correctedValue: Int? 79 | switch self { 80 | case .none: 81 | correctedValue = nil 82 | case .some(let value): 83 | correctedValue = value >= 0 ? value : nil 84 | } 85 | return correctedValue 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Tests/ArrangingTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrangingTest.swift 3 | // GridTests 4 | // 5 | // Created by Denis Obukhov on 06.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import SwiftUI 11 | 12 | #if os(iOS) || os(watchOS) || os(tvOS) 13 | @testable import Grid 14 | #else 15 | @testable import GridMac 16 | #endif 17 | 18 | class ArrangingTest: XCTestCase { 19 | 20 | private struct MockArranger: LayoutArranging {} 21 | private let arranger = MockArranger() 22 | private let gridElements = (0..<7).map { GridElement(AnyView(EmptyView()), id: AnyHashable($0)) } 23 | private let perfomanceGridElements = (0..<100).map { GridElement(AnyView(EmptyView()), id: AnyHashable($0)) } 24 | private lazy var spans: [GridSpan] = [ 25 | [3, 1], 26 | [2, 2], 27 | [1, 1], 28 | [1, 1], 29 | [1, 2], 30 | [2, 3], 31 | [1, 3] 32 | ] 33 | private let fixedPerfomanceTracksCount = 4 34 | 35 | private lazy var perfomanceSpans: [GridSpan] = { 36 | srand48(0) 37 | let randomSpan = { 38 | return max(1, Int(drand48() * 100) % self.fixedPerfomanceTracksCount) 39 | } 40 | 41 | return perfomanceGridElements.map { _ in [randomSpan(), randomSpan()] } 42 | }() 43 | 44 | private lazy var arrangementsInfo: [ArrangementInfo] = gridElements.enumerated().map { 45 | ArrangementInfo(gridElement: $1, start: .default, span: spans[$0]) 46 | } 47 | 48 | private lazy var perfomanceArrangementsInfo: [ArrangementInfo] = gridElements.enumerated().map { 49 | ArrangementInfo(gridElement: $1, start: .default, span: perfomanceSpans[$0]) 50 | } 51 | 52 | func testArrangementSparseRows() throws { 53 | let task = ArrangingTask(itemsInfo: arrangementsInfo, 54 | tracks: 4, 55 | flow: .rows, 56 | packing: .sparse) 57 | 58 | let arrangement = arranger.arrange(task: task) 59 | 60 | XCTAssertEqual(arrangement, LayoutArrangement(columnsCount: 4, rowsCount: 6, items: [ 61 | ArrangedItem(gridElement: gridElements[0], startIndex: GridIndex.zero, endIndex: [2, 0]), 62 | ArrangedItem(gridElement: gridElements[1], startIndex: [0, 1], endIndex: [1, 2]), 63 | ArrangedItem(gridElement: gridElements[2], startIndex: [2, 1], endIndex: [2, 1]), 64 | ArrangedItem(gridElement: gridElements[3], startIndex: [3, 1], endIndex: [3, 1]), 65 | ArrangedItem(gridElement: gridElements[4], startIndex: [2, 2], endIndex: [2, 3]), 66 | ArrangedItem(gridElement: gridElements[5], startIndex: [0, 3], endIndex: [1, 5]), 67 | ArrangedItem(gridElement: gridElements[6], startIndex: [3, 3], endIndex: [3, 5]) 68 | ])) 69 | } 70 | 71 | func testArrangementDenseRows() throws { 72 | let task = ArrangingTask(itemsInfo: arrangementsInfo, 73 | tracks: 4, 74 | flow: .rows, 75 | packing: .dense) 76 | 77 | let arrangement = arranger.arrange(task: task) 78 | 79 | XCTAssertEqual(arrangement, LayoutArrangement(columnsCount: 4, rowsCount: 6, items: [ 80 | ArrangedItem(gridElement: gridElements[0], startIndex: GridIndex.zero, endIndex: [2, 0]), 81 | ArrangedItem(gridElement: gridElements[1], startIndex: [0, 1], endIndex: [1, 2]), 82 | ArrangedItem(gridElement: gridElements[2], startIndex: [3, 0], endIndex: [3, 0]), 83 | ArrangedItem(gridElement: gridElements[3], startIndex: [2, 1], endIndex: [2, 1]), 84 | ArrangedItem(gridElement: gridElements[4], startIndex: [3, 1], endIndex: [3, 2]), 85 | ArrangedItem(gridElement: gridElements[5], startIndex: [0, 3], endIndex: [1, 5]), 86 | ArrangedItem(gridElement: gridElements[6], startIndex: [2, 2], endIndex: [2, 4]) 87 | ])) 88 | } 89 | 90 | func testArrangementSparsePerformance() throws { 91 | let task = ArrangingTask(itemsInfo: perfomanceArrangementsInfo, 92 | tracks: .init(repeating: .fr(1), count: fixedPerfomanceTracksCount), 93 | flow: .rows, 94 | packing: .sparse) 95 | 96 | self.measure { 97 | _ = arranger.arrange(task: task) 98 | } 99 | } 100 | 101 | func testArrangementDensePerformance() throws { 102 | let task = ArrangingTask(itemsInfo: perfomanceArrangementsInfo, 103 | tracks: .init(repeating: .fr(1), count: fixedPerfomanceTracksCount), 104 | flow: .rows, 105 | packing: .dense) 106 | 107 | self.measure { 108 | _ = arranger.arrange(task: task) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Tests/ArrangingWithStartsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrangingWithStartsTest.swift 3 | // GridTests 4 | // 5 | // Created by Denis Obukhov on 06.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import SwiftUI 11 | 12 | #if os(iOS) || os(watchOS) || os(tvOS) 13 | @testable import Grid 14 | #else 15 | @testable import GridMac 16 | #endif 17 | 18 | class ArrangingWithStartsTest: XCTestCase { 19 | 20 | private struct MockArranger: LayoutArranging {} 21 | private let arranger = MockArranger() 22 | 23 | func gridElement(index: Int) -> GridElement { 24 | GridElement(AnyView(EmptyView()), id: AnyHashable(index)) 25 | } 26 | private lazy var gridElements: [GridElement] = 27 | (0..<15).map { self.gridElement(index: $0) } 28 | 29 | private lazy var spans = 30 | [GridSpan](repeating: .default, count: 6) 31 | + [ 32 | [3, 1], 33 | [2, 1], 34 | [2, 2], 35 | [1, 1], 36 | [1, 10], 37 | [2, 3], 38 | [1, 3], 39 | [1, 1], 40 | [1, 1] 41 | ] 42 | 43 | private lazy var starts: [GridStart] = 44 | [GridStart](repeating: .default, count: 6) 45 | + [ 46 | nil, 47 | nil, 48 | [5, 1], 49 | [nil, 2], 50 | [3, 0], 51 | nil, 52 | nil, 53 | [2, nil], 54 | nil 55 | ] 56 | 57 | func testArrangementDenseRows() throws { 58 | let arrangementsInfo = 59 | gridElements.enumerated().map { ArrangementInfo(gridElement: $1, start: starts[$0], span: spans[$0]) } 60 | let task = ArrangingTask(itemsInfo: arrangementsInfo, 61 | tracks: 4, 62 | flow: .rows, 63 | packing: .dense) 64 | 65 | let arrangement = arranger.arrange(task: task) 66 | 67 | XCTAssertEqual(arrangement, LayoutArrangement(columnsCount: 4, rowsCount: 10, items: [ 68 | ArrangedItem(gridElement: gridElement(index: 10), startIndex: [3, 0], endIndex: [3, 9]), 69 | ArrangedItem(gridElement: gridElement(index: 13), startIndex: [2, 0], endIndex: [2, 0]), 70 | ArrangedItem(gridElement: gridElement(index: 8), startIndex: [0, 1], endIndex: [1, 2]), 71 | ArrangedItem(gridElement: gridElement(index: 9), startIndex: [2, 2], endIndex: [2, 2]), 72 | ArrangedItem(gridElement: gridElement(index: 0), startIndex: [0, 0], endIndex: [0, 0]), 73 | ArrangedItem(gridElement: gridElement(index: 1), startIndex: [1, 0], endIndex: [1, 0]), 74 | ArrangedItem(gridElement: gridElement(index: 2), startIndex: [2, 1], endIndex: [2, 1]), 75 | ArrangedItem(gridElement: gridElement(index: 3), startIndex: [0, 3], endIndex: [0, 3]), 76 | ArrangedItem(gridElement: gridElement(index: 4), startIndex: [1, 3], endIndex: [1, 3]), 77 | ArrangedItem(gridElement: gridElement(index: 5), startIndex: [2, 3], endIndex: [2, 3]), 78 | ArrangedItem(gridElement: gridElement(index: 6), startIndex: [0, 4], endIndex: [2, 4]), 79 | ArrangedItem(gridElement: gridElement(index: 7), startIndex: [0, 5], endIndex: [1, 5]), 80 | ArrangedItem(gridElement: gridElement(index: 11), startIndex: [0, 6], endIndex: [1, 8]), 81 | ArrangedItem(gridElement: gridElement(index: 12), startIndex: [2, 5], endIndex: [2, 7]), 82 | ArrangedItem(gridElement: gridElement(index: 14), startIndex: [2, 8], endIndex: [2, 8]) 83 | ])) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Tests/MacTestsInfo.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 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/PositionColumnScrollTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PositionColumnScrollTest.swift 3 | // GridTests 4 | // 5 | // Created by Denis Obukhov on 13.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import SwiftUI 11 | 12 | #if os(iOS) || os(watchOS) || os(tvOS) 13 | @testable import Grid 14 | #else 15 | @testable import GridMac 16 | #endif 17 | 18 | class PositionColumnScrollTest: XCTestCase { 19 | 20 | private struct MockPositioner: LayoutPositioning {} 21 | private let positioner = MockPositioner() 22 | private let mockView = AnyView(EmptyView()) 23 | 24 | func testScrollModeColumnsFlowStage1() throws { 25 | let gridElements = [ 26 | GridElement(self.mockView, id: AnyHashable(0)), 27 | GridElement(self.mockView, id: AnyHashable(1)), 28 | GridElement(self.mockView, id: AnyHashable(2)), 29 | GridElement(self.mockView, id: AnyHashable(3)), 30 | GridElement(self.mockView, id: AnyHashable(4)) 31 | ] 32 | 33 | let positionedItems: [PositionedItem] = [ 34 | PositionedItem(bounds: CGRect(x: -179.0, y: -20.0, width: 358.5, height: 162.5), gridElement: gridElements[0]), 35 | PositionedItem(bounds: CGRect(x: -179.0, y: -20.0, width: 70.00, height: 140.5), gridElement: gridElements[1]), 36 | PositionedItem(bounds: CGRect(x: -179.0, y: -20.0, width: 100.0, height: 360.5), gridElement: gridElements[2]), 37 | PositionedItem(bounds: CGRect(x: -179.0, y: -20.0, width: 358.5, height: 106.5), gridElement: gridElements[3]), 38 | PositionedItem(bounds: CGRect(x: -179.0, y: -20.0, width: 358.5, height: 128.5), gridElement: gridElements[4]) 39 | ] 40 | 41 | let arrangedItems: [ArrangedItem] = [ 42 | ArrangedItem(gridElement: gridElements[0], startIndex: [0, 0], endIndex: [0, 0]), 43 | ArrangedItem(gridElement: gridElements[1], startIndex: [1, 0], endIndex: [1, 0]), 44 | ArrangedItem(gridElement: gridElements[2], startIndex: [2, 0], endIndex: [2, 1]), 45 | ArrangedItem(gridElement: gridElements[3], startIndex: [0, 1], endIndex: [1, 1]), 46 | ArrangedItem(gridElement: gridElements[4], startIndex: [0, 2], endIndex: [2, 2]) 47 | ] 48 | let arrangement = LayoutArrangement(columnsCount: 3, rowsCount: 3, items: arrangedItems) 49 | 50 | let task = PositioningTask(items: positionedItems, 51 | arrangement: arrangement, 52 | boundingSize: CGSize(width: 375.0, height: 647.0), 53 | tracks: [.fr(1), .fit, .fit], 54 | contentMode: .scroll, 55 | flow: .rows) 56 | 57 | let resultPositions = self.positioner.reposition(task) 58 | 59 | let referencePositionedItems = [ 60 | PositionedItem(bounds: CGRect(x: 0.0, y: 23.0, width: 205.0, height: 209.0), gridElement: gridElements[0]), 61 | PositionedItem(bounds: CGRect(x: 205.0, y: 34.0, width: 70.00, height: 209.0), gridElement: gridElements[1]), 62 | PositionedItem(bounds: CGRect(x: 275.0, y: 0.0, width: 100.0, height: 362.0), gridElement: gridElements[2]), 63 | PositionedItem(bounds: CGRect(x: 0.0, y: 231.0, width: 275.0, height: 153.0), gridElement: gridElements[3]), 64 | PositionedItem(bounds: CGRect(x: 0.0, y: 361.0, width: 375.0, height: 129.0), gridElement: gridElements[4]) 65 | ] 66 | let referencePositions = PositionedLayout(items: referencePositionedItems, totalSize: CGSize(width: 375, height: 490)) 67 | 68 | XCTAssertEqual(resultPositions, referencePositions) 69 | } 70 | 71 | func testScrollModeColumnsFlowStage2() throws { 72 | let gridElements = [ 73 | GridElement(self.mockView, id: AnyHashable(0)), 74 | GridElement(self.mockView, id: AnyHashable(1)), 75 | GridElement(self.mockView, id: AnyHashable(2)), 76 | GridElement(self.mockView, id: AnyHashable(3)), 77 | GridElement(self.mockView, id: AnyHashable(4)) 78 | ] 79 | 80 | let positionedItems: [PositionedItem] = [ 81 | PositionedItem(bounds: CGRect(x: -187.5, y: 3.0, width: 205.0, height: 316.5), gridElement: gridElements[0]), 82 | PositionedItem(bounds: CGRect(x: 17.5, y: 14.0, width: 70.00, height: 140.5), gridElement: gridElements[1]), 83 | PositionedItem(bounds: CGRect(x: 87.5, y: -20.0, width: 100.0, height: 382.5), gridElement: gridElements[2]), 84 | PositionedItem(bounds: CGRect(x: -185.5, y: 212.0, width: 271.5, height: 150.5), gridElement: gridElements[3]), 85 | PositionedItem(bounds: CGRect(x: -179.0, y: 341.0, width: 358.5, height: 128.5), gridElement: gridElements[4]) 86 | ] 87 | 88 | let arrangedItems: [ArrangedItem] = [ 89 | ArrangedItem(gridElement: gridElements[0], startIndex: [0, 0], endIndex: [0, 0]), 90 | ArrangedItem(gridElement: gridElements[1], startIndex: [1, 0], endIndex: [1, 0]), 91 | ArrangedItem(gridElement: gridElements[2], startIndex: [2, 0], endIndex: [2, 1]), 92 | ArrangedItem(gridElement: gridElements[3], startIndex: [0, 1], endIndex: [1, 1]), 93 | ArrangedItem(gridElement: gridElements[4], startIndex: [0, 2], endIndex: [2, 2]) 94 | ] 95 | let arrangement = LayoutArrangement(columnsCount: 3, rowsCount: 3, items: arrangedItems) 96 | 97 | let task = PositioningTask(items: positionedItems, 98 | arrangement: arrangement, 99 | boundingSize: CGSize(width: 375.0, height: 647.0), 100 | tracks: [.fr(1), .fit, .fit], 101 | contentMode: .scroll, 102 | flow: .rows) 103 | 104 | let resultPositions = self.positioner.reposition(task) 105 | 106 | let referencePositionedItems = [ 107 | PositionedItem(bounds: CGRect(x: 0.0, y: 0.0, width: 205.0, height: 317.0), gridElement: gridElements[0]), 108 | PositionedItem(bounds: CGRect(x: 205.0, y: 88.0, width: 70.00, height: 317.0), gridElement: gridElements[1]), 109 | PositionedItem(bounds: CGRect(x: 275.0, y: 42.0, width: 100.0, height: 468.0), gridElement: gridElements[2]), 110 | PositionedItem(bounds: CGRect(x: 0.0, y: 316.0, width: 275.0, height: 151.0), gridElement: gridElements[3]), 111 | PositionedItem(bounds: CGRect(x: 0.0, y: 467.0, width: 375.0, height: 129.0), gridElement: gridElements[4]) 112 | ] 113 | 114 | let referencePosition = PositionedLayout(items: referencePositionedItems, totalSize: CGSize(width: 375, height: 596)) 115 | 116 | XCTAssertEqual(resultPositions, referencePosition) 117 | } 118 | 119 | func testScrollModeColumnsFlowStage3() throws { 120 | let gridElements = [ 121 | GridElement(self.mockView, id: AnyHashable(0)), 122 | GridElement(self.mockView, id: AnyHashable(1)), 123 | GridElement(self.mockView, id: AnyHashable(2)), 124 | GridElement(self.mockView, id: AnyHashable(3)), 125 | GridElement(self.mockView, id: AnyHashable(4)) 126 | ] 127 | 128 | let positionedItems: [PositionedItem] = [ 129 | PositionedItem(bounds: CGRect(x: 0.0, y: 0.0, width: 205.0, height: 316.5), gridElement: gridElements[0]), 130 | PositionedItem(bounds: CGRect(x: 205.0, y: 88.0, width: 70.00, height: 140.5), gridElement: gridElements[1]), 131 | PositionedItem(bounds: CGRect(x: 275.0, y: 42.0, width: 100.0, height: 382.5), gridElement: gridElements[2]), 132 | PositionedItem(bounds: CGRect(x: 2.0, y: 317.0, width: 271.5, height: 150.5), gridElement: gridElements[3]), 133 | PositionedItem(bounds: CGRect(x: 8.5, y: 467.0, width: 358.5, height: 128.5), gridElement: gridElements[4]) 134 | ] 135 | 136 | let arrangedItems: [ArrangedItem] = [ 137 | ArrangedItem(gridElement: gridElements[0], startIndex: [0, 0], endIndex: [0, 0]), 138 | ArrangedItem(gridElement: gridElements[1], startIndex: [1, 0], endIndex: [1, 0]), 139 | ArrangedItem(gridElement: gridElements[2], startIndex: [2, 0], endIndex: [2, 1]), 140 | ArrangedItem(gridElement: gridElements[3], startIndex: [0, 1], endIndex: [1, 1]), 141 | ArrangedItem(gridElement: gridElements[4], startIndex: [0, 2], endIndex: [2, 2]) 142 | ] 143 | let arrangement = LayoutArrangement(columnsCount: 3, rowsCount: 3, items: arrangedItems) 144 | 145 | let task = PositioningTask(items: positionedItems, 146 | arrangement: arrangement, 147 | boundingSize: CGSize(width: 375.0, height: 647.0), 148 | tracks: [.fr(1), .fit, .fit], 149 | contentMode: .scroll, 150 | flow: .rows) 151 | 152 | let resultPpositions = self.positioner.reposition(task) 153 | 154 | let referencePositionedItems = [ 155 | PositionedItem(bounds: CGRect(x: 0.0, y: 0.0, width: 205.0, height: 317.0), gridElement: gridElements[0]), 156 | PositionedItem(bounds: CGRect(x: 205.0, y: 88.0, width: 70.00, height: 317.0), gridElement: gridElements[1]), 157 | PositionedItem(bounds: CGRect(x: 275.0, y: 42.0, width: 100.0, height: 468.0), gridElement: gridElements[2]), 158 | PositionedItem(bounds: CGRect(x: 0.0, y: 316.0, width: 275.0, height: 151.0), gridElement: gridElements[3]), 159 | PositionedItem(bounds: CGRect(x: 0.0, y: 467.0, width: 375.0, height: 129.0), gridElement: gridElements[4]) 160 | ] 161 | 162 | let referencePosition = PositionedLayout(items: referencePositionedItems, totalSize: CGSize(width: 375, height: 596)) 163 | 164 | XCTAssertEqual(resultPpositions, referencePosition) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Tests/PositionRowsScrollTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PositionRowsScrollTest.swift 3 | // GridTests 4 | // 5 | // Created by Denis Obukhov on 13.05.2020. 6 | // Copyright © 2020 Exyte. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import SwiftUI 11 | 12 | #if os(iOS) || os(watchOS) || os(tvOS) 13 | @testable import Grid 14 | #else 15 | @testable import GridMac 16 | #endif 17 | 18 | class PositionRowsScrollTest: XCTestCase { 19 | 20 | private struct MockPositioner: LayoutPositioning {} 21 | private let positioner = MockPositioner() 22 | private let mockView = AnyView(EmptyView()) 23 | 24 | func testScrollModeColumnsFlowStage1() throws { 25 | let gridElements = [ 26 | GridElement(self.mockView, id: AnyHashable(0)), 27 | GridElement(self.mockView, id: AnyHashable(1)), 28 | GridElement(self.mockView, id: AnyHashable(2)), 29 | GridElement(self.mockView, id: AnyHashable(3)), 30 | GridElement(self.mockView, id: AnyHashable(4)) 31 | ] 32 | 33 | let positionedItems: [PositionedItem] = [ 34 | PositionedItem(bounds: CGRect(x: 0.0, y: -178.0, width: 250.0, height: 228.5), gridElement: gridElements[0]), 35 | PositionedItem(bounds: CGRect(x: 0.0, y: -178.0, width: 251.5, height: 40.5), gridElement: gridElements[1]), 36 | PositionedItem(bounds: CGRect(x: 0.0, y: -178.0, width: 210.0, height: 316.5), gridElement: gridElements[2]), 37 | PositionedItem(bounds: CGRect(x: 0.0, y: -178.0, width: 600.0, height: 84.5), gridElement: gridElements[3]), 38 | PositionedItem(bounds: CGRect(x: 0.0, y: -178.0, width: 300.0, height: 162.5), gridElement: gridElements[4]) 39 | ] 40 | 41 | let arrangedItems: [ArrangedItem] = [ 42 | ArrangedItem(gridElement: gridElements[0], startIndex: [0, 0], endIndex: [0, 0]), 43 | ArrangedItem(gridElement: gridElements[1], startIndex: [0, 1], endIndex: [0, 1]), 44 | ArrangedItem(gridElement: gridElements[2], startIndex: [1, 0], endIndex: [1, 1]), 45 | ArrangedItem(gridElement: gridElements[3], startIndex: [1, 2], endIndex: [2, 2]), 46 | ArrangedItem(gridElement: gridElements[4], startIndex: [2, 0], endIndex: [4, 0]) 47 | ] 48 | let arrangement = LayoutArrangement(columnsCount: 5, rowsCount: 3, items: arrangedItems) 49 | 50 | let task = PositioningTask(items: positionedItems, 51 | arrangement: arrangement, 52 | boundingSize: CGSize(width: 375.0, height: 647.0), 53 | tracks: [.fr(1), .fit, .fit], 54 | contentMode: .scroll, 55 | flow: .columns) 56 | 57 | let resultPositions = self.positioner.reposition(task) 58 | 59 | let referencePositionedItems = [ 60 | PositionedItem(bounds: CGRect(x: 0.0, y: 0.0, width: 253.0, height: 522.0), gridElement: gridElements[0]), 61 | PositionedItem(bounds: CGRect(x: 0.0, y: 522.0, width: 252.0, height: 41.0), gridElement: gridElements[1]), 62 | PositionedItem(bounds: CGRect(x: 349.0, y: 0.0, width: 405.0, height: 563.0), gridElement: gridElements[2]), 63 | PositionedItem(bounds: CGRect(x: 269.0, y: 562.0, width: 635.0, height: 85.0), gridElement: gridElements[3]), 64 | PositionedItem(bounds: CGRect(x: 656.0, y: 0.0, width: 301.0, height: 522.0), gridElement: gridElements[4]) 65 | ] 66 | 67 | let referencePosition = PositionedLayout(items: referencePositionedItems, totalSize: CGSize(width: 957.0, height: 647.0)) 68 | 69 | XCTAssertEqual(resultPositions, referencePosition) 70 | } 71 | 72 | func testScrollModeColumnsFlowStage2() throws { 73 | let gridElements = [ 74 | GridElement(self.mockView, id: AnyHashable(0)), 75 | GridElement(self.mockView, id: AnyHashable(1)), 76 | GridElement(self.mockView, id: AnyHashable(2)), 77 | GridElement(self.mockView, id: AnyHashable(3)), 78 | GridElement(self.mockView, id: AnyHashable(4)) 79 | ] 80 | 81 | let positionedItems: [PositionedItem] = [ 82 | PositionedItem(bounds: CGRect(x: -177.5, y: -197.0, width: 250.0, height: 228.5), gridElement: gridElements[0]), 83 | PositionedItem(bounds: CGRect(x: -178.5, y: 178.5, width: 251.5, height: 40.5), gridElement: gridElements[1]), 84 | PositionedItem(bounds: CGRect(x: 170.5, y: -220.5, width: 210.0, height: 316.5), gridElement: gridElements[2]), 85 | PositionedItem(bounds: CGRect(x: 90.5, y: 219.5, width: 610.0, height: 84.5), gridElement: gridElements[3]), 86 | PositionedItem(bounds: CGRect(x: 478.5, y: -164.0, width: 300.0, height: 162.5), gridElement: gridElements[4]) 87 | ] 88 | 89 | let arrangedItems: [ArrangedItem] = [ 90 | ArrangedItem(gridElement: gridElements[0], startIndex: [0, 0], endIndex: [0, 0]), 91 | ArrangedItem(gridElement: gridElements[1], startIndex: [0, 1], endIndex: [0, 1]), 92 | ArrangedItem(gridElement: gridElements[2], startIndex: [1, 0], endIndex: [1, 1]), 93 | ArrangedItem(gridElement: gridElements[3], startIndex: [1, 2], endIndex: [2, 2]), 94 | ArrangedItem(gridElement: gridElements[4], startIndex: [2, 0], endIndex: [4, 0]) 95 | ] 96 | let arrangement = LayoutArrangement(columnsCount: 5, rowsCount: 3, items: arrangedItems) 97 | 98 | let task = PositioningTask(items: positionedItems, 99 | arrangement: arrangement, 100 | boundingSize: CGSize(width: 375.0, height: 647.0), 101 | tracks: [.fr(1), .fit, .fit], 102 | contentMode: .scroll, 103 | flow: .columns) 104 | 105 | let resultPositions = self.positioner.reposition(task) 106 | 107 | let referencePositionedItems = [ 108 | PositionedItem(bounds: CGRect(x: 0.0, y: 0.0, width: 253.0, height: 522.0), gridElement: gridElements[0]), 109 | PositionedItem(bounds: CGRect(x: 0.0, y: 522.0, width: 252.0, height: 41.0), gridElement: gridElements[1]), 110 | PositionedItem(bounds: CGRect(x: 351.0, y: 0.0, width: 411.0, height: 563.0), gridElement: gridElements[2]), 111 | PositionedItem(bounds: CGRect(x: 268.0, y: 562.0, width: 643.0, height: 85.0), gridElement: gridElements[3]), 112 | PositionedItem(bounds: CGRect(x: 661.0, y: 0.0, width: 299.0, height: 522.0), gridElement: gridElements[4]) 113 | ] 114 | 115 | let referencePosition = PositionedLayout(items: referencePositionedItems, totalSize: CGSize(width: 961.0, height: 647.0)) 116 | 117 | XCTAssertEqual(resultPositions, referencePosition) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Tests/iOSTestsInfo.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 | 22 | 23 | --------------------------------------------------------------------------------