├── .gitignore ├── GridTrainer.xcodeproj └── project.pbxproj ├── GridTrainer ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── Sources │ ├── Extensions │ ├── Alignment.swift │ ├── ClosedRange.swift │ ├── GridItem.Size.swift │ ├── GridItem.swift │ ├── HorizontalAlignment.swift │ └── View.swift │ ├── Grid Configuration │ ├── GridConfiguration.swift │ ├── IdentifiableGridItem.swift │ └── Popover Views │ │ ├── ColumnSetupView.swift │ │ └── ConfigPopover.swift │ ├── Main │ ├── ColumnHeader.swift │ ├── GridHeader.swift │ ├── GridTrainer.swift │ └── GridTrainerApp.swift │ └── Preferences │ ├── GridInfo.swift │ └── GridPreferences.swift ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | .DS_Store 5 | 6 | ## User settings 7 | xcuserdata/ 8 | 9 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 10 | *.xcscmblueprint 11 | *.xccheckout 12 | 13 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 14 | build/ 15 | DerivedData/ 16 | *.moved-aside 17 | *.pbxuser 18 | !default.pbxuser 19 | *.mode1v3 20 | !default.mode1v3 21 | *.mode2v3 22 | !default.mode2v3 23 | *.perspectivev3 24 | !default.perspectivev3 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | 29 | ## App packaging 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | # Package.resolved 44 | # *.xcodeproj 45 | # 46 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 47 | # hence it is not needed unless you have added a package configuration file to your project 48 | # .swiftpm 49 | 50 | .build/ 51 | 52 | # CocoaPods 53 | # 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | # 58 | # Pods/ 59 | # 60 | # Add this line if you want to avoid checking in source code from the Xcode workspace 61 | *.xcworkspace 62 | 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | -------------------------------------------------------------------------------- /GridTrainer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3B50919324CC0AE2007B2F27 /* GridTrainerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B50919224CC0AE2007B2F27 /* GridTrainerApp.swift */; }; 11 | 3B50919724CC0AE3007B2F27 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3B50919624CC0AE3007B2F27 /* Assets.xcassets */; }; 12 | 3B50919A24CC0AE3007B2F27 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3B50919924CC0AE3007B2F27 /* Preview Assets.xcassets */; }; 13 | 3B5091B124CC0B68007B2F27 /* ConfigPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091A224CC0B68007B2F27 /* ConfigPopover.swift */; }; 14 | 3B5091B224CC0B68007B2F27 /* GridTrainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091A324CC0B68007B2F27 /* GridTrainer.swift */; }; 15 | 3B5091B324CC0B68007B2F27 /* GridHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091A424CC0B68007B2F27 /* GridHeader.swift */; }; 16 | 3B5091B424CC0B68007B2F27 /* ColumnSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091A524CC0B68007B2F27 /* ColumnSetupView.swift */; }; 17 | 3B5091B524CC0B68007B2F27 /* GridConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091A724CC0B68007B2F27 /* GridConfiguration.swift */; }; 18 | 3B5091B624CC0B68007B2F27 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091A924CC0B68007B2F27 /* View.swift */; }; 19 | 3B5091B724CC0B68007B2F27 /* HorizontalAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091AA24CC0B68007B2F27 /* HorizontalAlignment.swift */; }; 20 | 3B5091B824CC0B68007B2F27 /* Alignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091AB24CC0B68007B2F27 /* Alignment.swift */; }; 21 | 3B5091BA24CC0B68007B2F27 /* GridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091AD24CC0B68007B2F27 /* GridItem.swift */; }; 22 | 3B5091BB24CC0B68007B2F27 /* GridItem.Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091AE24CC0B68007B2F27 /* GridItem.Size.swift */; }; 23 | 3B5091BC24CC0B68007B2F27 /* GridInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091AF24CC0B68007B2F27 /* GridInfo.swift */; }; 24 | 3B5091BD24CC0B68007B2F27 /* GridPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091B024CC0B68007B2F27 /* GridPreferences.swift */; }; 25 | 3B5091C124CC0BE7007B2F27 /* IdentifiableGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091C024CC0BE7007B2F27 /* IdentifiableGridItem.swift */; }; 26 | 3B5091C524CC0CFE007B2F27 /* ClosedRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5091C424CC0CFE007B2F27 /* ClosedRange.swift */; }; 27 | 3BA8083724CC289C00384769 /* ColumnHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA8083624CC289C00384769 /* ColumnHeader.swift */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 3B50918F24CC0AE2007B2F27 /* GridTrainer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GridTrainer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | 3B50919224CC0AE2007B2F27 /* GridTrainerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridTrainerApp.swift; sourceTree = ""; }; 33 | 3B50919624CC0AE3007B2F27 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 34 | 3B50919924CC0AE3007B2F27 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 35 | 3B50919B24CC0AE3007B2F27 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 36 | 3B5091A224CC0B68007B2F27 /* ConfigPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigPopover.swift; sourceTree = ""; }; 37 | 3B5091A324CC0B68007B2F27 /* GridTrainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridTrainer.swift; sourceTree = ""; }; 38 | 3B5091A424CC0B68007B2F27 /* GridHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridHeader.swift; sourceTree = ""; }; 39 | 3B5091A524CC0B68007B2F27 /* ColumnSetupView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColumnSetupView.swift; sourceTree = ""; }; 40 | 3B5091A724CC0B68007B2F27 /* GridConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridConfiguration.swift; sourceTree = ""; }; 41 | 3B5091A924CC0B68007B2F27 /* View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; 42 | 3B5091AA24CC0B68007B2F27 /* HorizontalAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalAlignment.swift; sourceTree = ""; }; 43 | 3B5091AB24CC0B68007B2F27 /* Alignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Alignment.swift; sourceTree = ""; }; 44 | 3B5091AD24CC0B68007B2F27 /* GridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridItem.swift; sourceTree = ""; }; 45 | 3B5091AE24CC0B68007B2F27 /* GridItem.Size.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridItem.Size.swift; sourceTree = ""; }; 46 | 3B5091AF24CC0B68007B2F27 /* GridInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridInfo.swift; sourceTree = ""; }; 47 | 3B5091B024CC0B68007B2F27 /* GridPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridPreferences.swift; sourceTree = ""; }; 48 | 3B5091C024CC0BE7007B2F27 /* IdentifiableGridItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiableGridItem.swift; sourceTree = ""; }; 49 | 3B5091C424CC0CFE007B2F27 /* ClosedRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosedRange.swift; sourceTree = ""; }; 50 | 3BA8083624CC289C00384769 /* ColumnHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnHeader.swift; sourceTree = ""; }; 51 | /* End PBXFileReference section */ 52 | 53 | /* Begin PBXFrameworksBuildPhase section */ 54 | 3B50918C24CC0AE2007B2F27 /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | 3B50918624CC0AE2007B2F27 = { 65 | isa = PBXGroup; 66 | children = ( 67 | 3B50919124CC0AE2007B2F27 /* GridTrainer */, 68 | 3B50919024CC0AE2007B2F27 /* Products */, 69 | ); 70 | sourceTree = ""; 71 | }; 72 | 3B50919024CC0AE2007B2F27 /* Products */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 3B50918F24CC0AE2007B2F27 /* GridTrainer.app */, 76 | ); 77 | name = Products; 78 | sourceTree = ""; 79 | }; 80 | 3B50919124CC0AE2007B2F27 /* GridTrainer */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 3B5091A124CC0B68007B2F27 /* Sources */, 84 | 3B50919624CC0AE3007B2F27 /* Assets.xcassets */, 85 | 3B50919B24CC0AE3007B2F27 /* Info.plist */, 86 | 3B50919824CC0AE3007B2F27 /* Preview Content */, 87 | ); 88 | path = GridTrainer; 89 | sourceTree = ""; 90 | }; 91 | 3B50919824CC0AE3007B2F27 /* Preview Content */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 3B50919924CC0AE3007B2F27 /* Preview Assets.xcassets */, 95 | ); 96 | path = "Preview Content"; 97 | sourceTree = ""; 98 | }; 99 | 3B5091A124CC0B68007B2F27 /* Sources */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 3B5091C324CC0C0E007B2F27 /* Main */, 103 | 3B5091A824CC0B68007B2F27 /* Extensions */, 104 | 3B5091C224CC0BF0007B2F27 /* Grid Configuration */, 105 | 3B5091BF24CC0BA6007B2F27 /* Preferences */, 106 | ); 107 | path = Sources; 108 | sourceTree = ""; 109 | }; 110 | 3B5091A824CC0B68007B2F27 /* Extensions */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 3B5091A924CC0B68007B2F27 /* View.swift */, 114 | 3B5091AA24CC0B68007B2F27 /* HorizontalAlignment.swift */, 115 | 3B5091AB24CC0B68007B2F27 /* Alignment.swift */, 116 | 3B5091AD24CC0B68007B2F27 /* GridItem.swift */, 117 | 3B5091AE24CC0B68007B2F27 /* GridItem.Size.swift */, 118 | 3B5091C424CC0CFE007B2F27 /* ClosedRange.swift */, 119 | ); 120 | path = Extensions; 121 | sourceTree = ""; 122 | }; 123 | 3B5091BE24CC0B7F007B2F27 /* Popover Views */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 3B5091A224CC0B68007B2F27 /* ConfigPopover.swift */, 127 | 3B5091A524CC0B68007B2F27 /* ColumnSetupView.swift */, 128 | ); 129 | path = "Popover Views"; 130 | sourceTree = ""; 131 | }; 132 | 3B5091BF24CC0BA6007B2F27 /* Preferences */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 3B5091AF24CC0B68007B2F27 /* GridInfo.swift */, 136 | 3B5091B024CC0B68007B2F27 /* GridPreferences.swift */, 137 | ); 138 | path = Preferences; 139 | sourceTree = ""; 140 | }; 141 | 3B5091C224CC0BF0007B2F27 /* Grid Configuration */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 3B5091BE24CC0B7F007B2F27 /* Popover Views */, 145 | 3B5091A724CC0B68007B2F27 /* GridConfiguration.swift */, 146 | 3B5091C024CC0BE7007B2F27 /* IdentifiableGridItem.swift */, 147 | ); 148 | path = "Grid Configuration"; 149 | sourceTree = ""; 150 | }; 151 | 3B5091C324CC0C0E007B2F27 /* Main */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | 3B50919224CC0AE2007B2F27 /* GridTrainerApp.swift */, 155 | 3B5091A324CC0B68007B2F27 /* GridTrainer.swift */, 156 | 3B5091A424CC0B68007B2F27 /* GridHeader.swift */, 157 | 3BA8083624CC289C00384769 /* ColumnHeader.swift */, 158 | ); 159 | path = Main; 160 | sourceTree = ""; 161 | }; 162 | /* End PBXGroup section */ 163 | 164 | /* Begin PBXNativeTarget section */ 165 | 3B50918E24CC0AE2007B2F27 /* GridTrainer */ = { 166 | isa = PBXNativeTarget; 167 | buildConfigurationList = 3B50919E24CC0AE3007B2F27 /* Build configuration list for PBXNativeTarget "GridTrainer" */; 168 | buildPhases = ( 169 | 3B50918B24CC0AE2007B2F27 /* Sources */, 170 | 3B50918C24CC0AE2007B2F27 /* Frameworks */, 171 | 3B50918D24CC0AE2007B2F27 /* Resources */, 172 | ); 173 | buildRules = ( 174 | ); 175 | dependencies = ( 176 | ); 177 | name = GridTrainer; 178 | productName = GridTrainer; 179 | productReference = 3B50918F24CC0AE2007B2F27 /* GridTrainer.app */; 180 | productType = "com.apple.product-type.application"; 181 | }; 182 | /* End PBXNativeTarget section */ 183 | 184 | /* Begin PBXProject section */ 185 | 3B50918724CC0AE2007B2F27 /* Project object */ = { 186 | isa = PBXProject; 187 | attributes = { 188 | LastSwiftUpdateCheck = 1200; 189 | LastUpgradeCheck = 1200; 190 | TargetAttributes = { 191 | 3B50918E24CC0AE2007B2F27 = { 192 | CreatedOnToolsVersion = 12.0; 193 | }; 194 | }; 195 | }; 196 | buildConfigurationList = 3B50918A24CC0AE2007B2F27 /* Build configuration list for PBXProject "GridTrainer" */; 197 | compatibilityVersion = "Xcode 9.3"; 198 | developmentRegion = en; 199 | hasScannedForEncodings = 0; 200 | knownRegions = ( 201 | en, 202 | Base, 203 | ); 204 | mainGroup = 3B50918624CC0AE2007B2F27; 205 | productRefGroup = 3B50919024CC0AE2007B2F27 /* Products */; 206 | projectDirPath = ""; 207 | projectRoot = ""; 208 | targets = ( 209 | 3B50918E24CC0AE2007B2F27 /* GridTrainer */, 210 | ); 211 | }; 212 | /* End PBXProject section */ 213 | 214 | /* Begin PBXResourcesBuildPhase section */ 215 | 3B50918D24CC0AE2007B2F27 /* Resources */ = { 216 | isa = PBXResourcesBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | 3B50919A24CC0AE3007B2F27 /* Preview Assets.xcassets in Resources */, 220 | 3B50919724CC0AE3007B2F27 /* Assets.xcassets in Resources */, 221 | ); 222 | runOnlyForDeploymentPostprocessing = 0; 223 | }; 224 | /* End PBXResourcesBuildPhase section */ 225 | 226 | /* Begin PBXSourcesBuildPhase section */ 227 | 3B50918B24CC0AE2007B2F27 /* Sources */ = { 228 | isa = PBXSourcesBuildPhase; 229 | buildActionMask = 2147483647; 230 | files = ( 231 | 3B5091B624CC0B68007B2F27 /* View.swift in Sources */, 232 | 3B5091B324CC0B68007B2F27 /* GridHeader.swift in Sources */, 233 | 3BA8083724CC289C00384769 /* ColumnHeader.swift in Sources */, 234 | 3B5091C524CC0CFE007B2F27 /* ClosedRange.swift in Sources */, 235 | 3B5091B224CC0B68007B2F27 /* GridTrainer.swift in Sources */, 236 | 3B5091B424CC0B68007B2F27 /* ColumnSetupView.swift in Sources */, 237 | 3B5091B724CC0B68007B2F27 /* HorizontalAlignment.swift in Sources */, 238 | 3B5091BA24CC0B68007B2F27 /* GridItem.swift in Sources */, 239 | 3B5091B824CC0B68007B2F27 /* Alignment.swift in Sources */, 240 | 3B5091BB24CC0B68007B2F27 /* GridItem.Size.swift in Sources */, 241 | 3B5091B524CC0B68007B2F27 /* GridConfiguration.swift in Sources */, 242 | 3B5091C124CC0BE7007B2F27 /* IdentifiableGridItem.swift in Sources */, 243 | 3B5091BC24CC0B68007B2F27 /* GridInfo.swift in Sources */, 244 | 3B5091BD24CC0B68007B2F27 /* GridPreferences.swift in Sources */, 245 | 3B5091B124CC0B68007B2F27 /* ConfigPopover.swift in Sources */, 246 | 3B50919324CC0AE2007B2F27 /* GridTrainerApp.swift in Sources */, 247 | ); 248 | runOnlyForDeploymentPostprocessing = 0; 249 | }; 250 | /* End PBXSourcesBuildPhase section */ 251 | 252 | /* Begin XCBuildConfiguration section */ 253 | 3B50919C24CC0AE3007B2F27 /* Debug */ = { 254 | isa = XCBuildConfiguration; 255 | buildSettings = { 256 | ALWAYS_SEARCH_USER_PATHS = NO; 257 | CLANG_ANALYZER_NONNULL = YES; 258 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 259 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 260 | CLANG_CXX_LIBRARY = "libc++"; 261 | CLANG_ENABLE_MODULES = YES; 262 | CLANG_ENABLE_OBJC_ARC = YES; 263 | CLANG_ENABLE_OBJC_WEAK = YES; 264 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 265 | CLANG_WARN_BOOL_CONVERSION = YES; 266 | CLANG_WARN_COMMA = YES; 267 | CLANG_WARN_CONSTANT_CONVERSION = YES; 268 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 269 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 270 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 271 | CLANG_WARN_EMPTY_BODY = YES; 272 | CLANG_WARN_ENUM_CONVERSION = YES; 273 | CLANG_WARN_INFINITE_RECURSION = YES; 274 | CLANG_WARN_INT_CONVERSION = YES; 275 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 276 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 277 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 278 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 279 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 280 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 281 | CLANG_WARN_STRICT_PROTOTYPES = YES; 282 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 283 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 284 | CLANG_WARN_UNREACHABLE_CODE = YES; 285 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 286 | COPY_PHASE_STRIP = NO; 287 | DEBUG_INFORMATION_FORMAT = dwarf; 288 | ENABLE_STRICT_OBJC_MSGSEND = YES; 289 | ENABLE_TESTABILITY = YES; 290 | GCC_C_LANGUAGE_STANDARD = gnu11; 291 | GCC_DYNAMIC_NO_PIC = NO; 292 | GCC_NO_COMMON_BLOCKS = YES; 293 | GCC_OPTIMIZATION_LEVEL = 0; 294 | GCC_PREPROCESSOR_DEFINITIONS = ( 295 | "DEBUG=1", 296 | "$(inherited)", 297 | ); 298 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 299 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 300 | GCC_WARN_UNDECLARED_SELECTOR = YES; 301 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 302 | GCC_WARN_UNUSED_FUNCTION = YES; 303 | GCC_WARN_UNUSED_VARIABLE = YES; 304 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 305 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 306 | MTL_FAST_MATH = YES; 307 | ONLY_ACTIVE_ARCH = YES; 308 | SDKROOT = iphoneos; 309 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 310 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 311 | }; 312 | name = Debug; 313 | }; 314 | 3B50919D24CC0AE3007B2F27 /* Release */ = { 315 | isa = XCBuildConfiguration; 316 | buildSettings = { 317 | ALWAYS_SEARCH_USER_PATHS = NO; 318 | CLANG_ANALYZER_NONNULL = YES; 319 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 320 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 321 | CLANG_CXX_LIBRARY = "libc++"; 322 | CLANG_ENABLE_MODULES = YES; 323 | CLANG_ENABLE_OBJC_ARC = YES; 324 | CLANG_ENABLE_OBJC_WEAK = YES; 325 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 326 | CLANG_WARN_BOOL_CONVERSION = YES; 327 | CLANG_WARN_COMMA = YES; 328 | CLANG_WARN_CONSTANT_CONVERSION = YES; 329 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 330 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 331 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 332 | CLANG_WARN_EMPTY_BODY = YES; 333 | CLANG_WARN_ENUM_CONVERSION = YES; 334 | CLANG_WARN_INFINITE_RECURSION = YES; 335 | CLANG_WARN_INT_CONVERSION = YES; 336 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 337 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 338 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 339 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 340 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 341 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 342 | CLANG_WARN_STRICT_PROTOTYPES = YES; 343 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 344 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 345 | CLANG_WARN_UNREACHABLE_CODE = YES; 346 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 347 | COPY_PHASE_STRIP = NO; 348 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 349 | ENABLE_NS_ASSERTIONS = NO; 350 | ENABLE_STRICT_OBJC_MSGSEND = YES; 351 | GCC_C_LANGUAGE_STANDARD = gnu11; 352 | GCC_NO_COMMON_BLOCKS = YES; 353 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 354 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 355 | GCC_WARN_UNDECLARED_SELECTOR = YES; 356 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 357 | GCC_WARN_UNUSED_FUNCTION = YES; 358 | GCC_WARN_UNUSED_VARIABLE = YES; 359 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 360 | MTL_ENABLE_DEBUG_INFO = NO; 361 | MTL_FAST_MATH = YES; 362 | SDKROOT = iphoneos; 363 | SWIFT_COMPILATION_MODE = wholemodule; 364 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 365 | VALIDATE_PRODUCT = YES; 366 | }; 367 | name = Release; 368 | }; 369 | 3B50919F24CC0AE3007B2F27 /* Debug */ = { 370 | isa = XCBuildConfiguration; 371 | buildSettings = { 372 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 373 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 374 | CODE_SIGN_STYLE = Automatic; 375 | DEVELOPMENT_ASSET_PATHS = "\"GridTrainer/Preview Content\""; 376 | DEVELOPMENT_TEAM = ""; 377 | ENABLE_PREVIEWS = YES; 378 | INFOPLIST_FILE = GridTrainer/Info.plist; 379 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 380 | LD_RUNPATH_SEARCH_PATHS = ( 381 | "$(inherited)", 382 | "@executable_path/Frameworks", 383 | ); 384 | PRODUCT_BUNDLE_IDENTIFIER = com.example.GridTrainer; 385 | PRODUCT_NAME = "$(TARGET_NAME)"; 386 | SWIFT_VERSION = 5.0; 387 | TARGETED_DEVICE_FAMILY = 2; 388 | }; 389 | name = Debug; 390 | }; 391 | 3B5091A024CC0AE3007B2F27 /* Release */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 395 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 396 | CODE_SIGN_STYLE = Automatic; 397 | DEVELOPMENT_ASSET_PATHS = "\"GridTrainer/Preview Content\""; 398 | DEVELOPMENT_TEAM = ""; 399 | ENABLE_PREVIEWS = YES; 400 | INFOPLIST_FILE = GridTrainer/Info.plist; 401 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 402 | LD_RUNPATH_SEARCH_PATHS = ( 403 | "$(inherited)", 404 | "@executable_path/Frameworks", 405 | ); 406 | PRODUCT_BUNDLE_IDENTIFIER = com.example.GridTrainer; 407 | PRODUCT_NAME = "$(TARGET_NAME)"; 408 | SWIFT_VERSION = 5.0; 409 | TARGETED_DEVICE_FAMILY = 2; 410 | }; 411 | name = Release; 412 | }; 413 | /* End XCBuildConfiguration section */ 414 | 415 | /* Begin XCConfigurationList section */ 416 | 3B50918A24CC0AE2007B2F27 /* Build configuration list for PBXProject "GridTrainer" */ = { 417 | isa = XCConfigurationList; 418 | buildConfigurations = ( 419 | 3B50919C24CC0AE3007B2F27 /* Debug */, 420 | 3B50919D24CC0AE3007B2F27 /* Release */, 421 | ); 422 | defaultConfigurationIsVisible = 0; 423 | defaultConfigurationName = Release; 424 | }; 425 | 3B50919E24CC0AE3007B2F27 /* Build configuration list for PBXNativeTarget "GridTrainer" */ = { 426 | isa = XCConfigurationList; 427 | buildConfigurations = ( 428 | 3B50919F24CC0AE3007B2F27 /* Debug */, 429 | 3B5091A024CC0AE3007B2F27 /* Release */, 430 | ); 431 | defaultConfigurationIsVisible = 0; 432 | defaultConfigurationName = Release; 433 | }; 434 | /* End XCConfigurationList section */ 435 | }; 436 | rootObject = 3B50918724CC0AE2007B2F27 /* Project object */; 437 | } 438 | -------------------------------------------------------------------------------- /GridTrainer/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 | -------------------------------------------------------------------------------- /GridTrainer/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /GridTrainer/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /GridTrainer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /GridTrainer/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /GridTrainer/Sources/Extensions/Alignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridTrainerApp.swift 3 | // Alignment Extension 4 | // 5 | // Created by SwiftUI-Lab on 25-Jul-2020. 6 | // https://swiftui-lab.com/impossible-grids 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Alignment: Hashable { 12 | public func hash(into hasher: inout Hasher) { 13 | hasher.combine(self.description) 14 | } 15 | } 16 | 17 | extension Alignment: CustomStringConvertible { 18 | public var description: String { 19 | switch self { 20 | case .leading: 21 | return ".leading" 22 | case .center: 23 | return ".center" 24 | case .trailing: 25 | return ".trailing" 26 | case .topLeading: 27 | return ".topLeading" 28 | case .top: 29 | return ".top" 30 | case .topTrailing: 31 | return ".topTrailing" 32 | case .bottomLeading: 33 | return ".bottomLeading" 34 | case .bottom: 35 | return ".bottom" 36 | case .bottomTrailing: 37 | return ".bottomTrailing" 38 | default: 39 | return "?" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /GridTrainer/Sources/Extensions/ClosedRange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridTrainerApp.swift 3 | // ClosedRange Extension 4 | // 5 | // Created by SwiftUI-Lab on 25-Jul-2020. 6 | // https://swiftui-lab.com/impossible-grids 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension ClosedRange: Identifiable where Bound == Int { 12 | // This is a little dirty, but for this short demo is good enough 13 | public var id: Int { self.lowerBound + self.upperBound * 100000 } 14 | } 15 | -------------------------------------------------------------------------------- /GridTrainer/Sources/Extensions/GridItem.Size.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridTrainerApp.swift 3 | // GridItem.Size Extension 4 | // 5 | // Created by SwiftUI-Lab on 25-Jul-2020. 6 | // https://swiftui-lab.com/impossible-grids 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension GridItem.Size { 12 | var isAdaptive: Bool { self.sizeType == 3 } 13 | 14 | var sizeType: Int { 15 | switch self { 16 | case .fixed(_): 17 | return 1 18 | case .flexible(_, _): 19 | return 2 20 | case .adaptive(_, _): 21 | return 3 22 | @unknown default: 23 | fatalError() 24 | } 25 | } 26 | } 27 | 28 | extension GridItem.Size { 29 | var minMax: (CGFloat, CGFloat) { 30 | switch self { 31 | case .fixed(let minimum): 32 | return (minimum, minimum) 33 | case .flexible(minimum: let minimum, maximum: let maximum): 34 | return (minimum, maximum) 35 | case .adaptive(minimum: let minimum, maximum: let maximum): 36 | return (minimum, maximum) 37 | @unknown default: 38 | fatalError() 39 | } 40 | } 41 | } 42 | 43 | extension GridItem.Size { 44 | var hasMax: Bool { 45 | switch self { 46 | case .fixed(_): 47 | return false 48 | default: 49 | return true 50 | } 51 | } 52 | } 53 | 54 | extension GridItem.Size: CustomStringConvertible { 55 | public var description: String { 56 | switch self { 57 | case .fixed(let w): 58 | return ".fixed(\(Int(w)))" 59 | case .flexible(let min, let max): 60 | return ".flexible(minimum: \(Int(min)), maximum: \(Int(max)))" 61 | case .adaptive(let min, let max): 62 | return ".adaptive(minimum: \(Int(min)), maximum: \(Int(max)))" 63 | @unknown default: 64 | fatalError() 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /GridTrainer/Sources/Extensions/GridItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridTrainerApp.swift 3 | // GridItem Extension 4 | // 5 | // Created by SwiftUI-Lab on 25-Jul-2020. 6 | // https://swiftui-lab.com/impossible-grids 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension GridItem { 12 | var isAdaptive: Bool { self.size.isAdaptive } 13 | } 14 | 15 | extension GridItem: CustomStringConvertible { 16 | public var description: String { 17 | return "GridItem(size: \(self.size.description), spacing: \(Int(self.spacing!)), alignment: \(self.alignment!.description))" 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /GridTrainer/Sources/Extensions/HorizontalAlignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridTrainerApp.swift 3 | // HorizontalAlignment Extension 4 | // 5 | // Created by SwiftUI-Lab on 25-Jul-2020. 6 | // https://swiftui-lab.com/impossible-grids 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension HorizontalAlignment: Hashable { 12 | public func hash(into hasher: inout Hasher) { 13 | hasher.combine(self.description) 14 | } 15 | } 16 | 17 | extension HorizontalAlignment: CustomStringConvertible { 18 | public var description: String { 19 | switch self { 20 | case .leading: 21 | return ".leading" 22 | case .center: 23 | return ".center" 24 | case .trailing: 25 | return ".trailing" 26 | default: 27 | return "?" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /GridTrainer/Sources/Extensions/View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridTrainerApp.swift 3 | // View Extension 4 | // 5 | // Created by SwiftUI-Lab on 25-Jul-2020. 6 | // https://swiftui-lab.com/impossible-grids 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension View { 12 | func showBorder(_ color: Color, enabled: Bool) -> some View { 13 | self.border(color, width: enabled ? 6 : 0) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /GridTrainer/Sources/Grid Configuration/GridConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridTrainerApp.swift 3 | // GridConfiguration 4 | // 5 | // Created by SwiftUI-Lab on 25-Jul-2020. 6 | // https://swiftui-lab.com/impossible-grids 7 | // 8 | 9 | import SwiftUI 10 | 11 | 12 | class GridConfiguration: ObservableObject { 13 | @Published var gridItems = [IdentifiableGridItem]() 14 | @Published var spacing: CGFloat = 20 15 | @Published var width: CGFloat = 1000 16 | @Published var padding: CGFloat = 20 17 | @Published var cellCount: Double = 200 18 | @Published var alignment: HorizontalAlignment = .center 19 | @Published var showBorders: Bool = false 20 | 21 | var itemCount: Int { Int(cellCount) } 22 | 23 | init() { 24 | let s: CGFloat = 20 25 | 26 | self.gridItems = [ 27 | IdentifiableGridItem(1, GridItem(.fixed(80), spacing: s, alignment: .center)), 28 | IdentifiableGridItem(3, GridItem(.adaptive(minimum: 60, maximum: 80), spacing: s, alignment: .center)), 29 | IdentifiableGridItem(4, GridItem(.flexible(minimum: 60, maximum: 240), spacing: s, alignment: .center)), 30 | IdentifiableGridItem(5, GridItem(.fixed(90), spacing: s, alignment: .center)) 31 | ] 32 | } 33 | 34 | func colSpacing(_ idx: Int) -> Binding { 35 | Binding(get: { self.gridItems[idx].gridItem.spacing ?? 0}, 36 | set: { self.gridItems[idx].gridItem.spacing = $0 }) 37 | } 38 | 39 | func colAlignment(_ idx: Int) -> Binding { 40 | Binding(get: { self.gridItems[idx].gridItem.alignment ?? .center}, 41 | set: { self.gridItems[idx].gridItem.alignment = $0 }) 42 | } 43 | 44 | func colSizeType(_ idx: Int) -> Binding { 45 | Binding(get: { self.gridItems[idx].gridItem.size.sizeType }, 46 | set: { 47 | let (min, max) = self.gridItems[idx].gridItem.size.minMax 48 | 49 | switch $0 { 50 | case 1: // fixed 51 | self.gridItems[idx].gridItem.size = .fixed(min) 52 | case 2: // flexible 53 | self.gridItems[idx].gridItem.size = .flexible(minimum: min, maximum: max) 54 | case 3: // adaptive 55 | self.gridItems[idx].gridItem.size = .adaptive(minimum: min, maximum: max) 56 | default: 57 | fatalError() 58 | } 59 | }) 60 | } 61 | 62 | func colSizeMin(_ idx: Int) -> Binding { 63 | Binding(get: { self.gridItems[idx].gridItem.size.minMax.0 }, 64 | set: { 65 | switch self.gridItems[idx].gridItem.size { 66 | case .fixed(_): 67 | self.gridItems[idx].gridItem.size = .fixed($0) 68 | case .flexible(_, let m): 69 | self.gridItems[idx].gridItem.size = .flexible(minimum: $0, maximum: max(m, $0)) 70 | case .adaptive(_, let m): 71 | self.gridItems[idx].gridItem.size = .adaptive(minimum: $0, maximum: max(m, $0)) 72 | @unknown default: 73 | fatalError() 74 | } 75 | }) 76 | } 77 | 78 | func colSizeMax(_ idx: Int) -> Binding { 79 | Binding(get: { self.gridItems[idx].gridItem.size.minMax.1 }, 80 | set: { 81 | switch self.gridItems[idx].gridItem.size { 82 | case .flexible(let m, _): 83 | self.gridItems[idx].gridItem.size = .flexible(minimum: min(m, $0), maximum: $0) 84 | case .adaptive(let m, _): 85 | self.gridItems[idx].gridItem.size = .adaptive(minimum: min(m, $0), maximum: $0) 86 | default: 87 | break 88 | } 89 | }) 90 | } 91 | 92 | static var presetFixFixFix = [ 93 | IdentifiableGridItem(1, GridItem(.fixed(100), spacing: 10, alignment: .center)), 94 | IdentifiableGridItem(2, GridItem(.fixed(100), spacing: 10, alignment: .center)), 95 | IdentifiableGridItem(3, GridItem(.fixed(100), spacing: 10, alignment: .center)) 96 | ] 97 | 98 | static var presetFixFlexFix = [ 99 | IdentifiableGridItem(1, GridItem(.fixed(100), spacing: 10, alignment: .center)), 100 | IdentifiableGridItem(2, GridItem(.flexible(minimum: 50, maximum: 150), spacing: 10, alignment: .center)), 101 | IdentifiableGridItem(3, GridItem(.fixed(100), spacing: 10, alignment: .center)) 102 | ] 103 | 104 | static var presetFlexFixFlex = [ 105 | IdentifiableGridItem(1, GridItem(.flexible(minimum: 50, maximum: 150), spacing: 10, alignment: .center)), 106 | IdentifiableGridItem(2, GridItem(.fixed(100), spacing: 10, alignment: .center)), 107 | IdentifiableGridItem(3, GridItem(.flexible(minimum: 50, maximum: 150), spacing: 10, alignment: .center)), 108 | ] 109 | 110 | static var presetFixAdaptFix = [ 111 | IdentifiableGridItem(1, GridItem(.fixed(100), spacing: 10, alignment: .center)), 112 | IdentifiableGridItem(2, GridItem(.adaptive(minimum: 50, maximum: 150), spacing: 10, alignment: .center)), 113 | IdentifiableGridItem(3, GridItem(.fixed(100), spacing: 10, alignment: .center)), 114 | ] 115 | 116 | static var presetFixFlexAdaptFix = [ 117 | IdentifiableGridItem(1, GridItem(.fixed(100), spacing: 10, alignment: .center)), 118 | IdentifiableGridItem(2, GridItem(.flexible(minimum: 50, maximum: 150), spacing: 10, alignment: .center)), 119 | IdentifiableGridItem(3, GridItem(.adaptive(minimum: 50, maximum: 150), spacing: 10, alignment: .center)), 120 | IdentifiableGridItem(4, GridItem(.fixed(100), spacing: 10, alignment: .center)), 121 | ] 122 | 123 | } 124 | 125 | -------------------------------------------------------------------------------- /GridTrainer/Sources/Grid Configuration/IdentifiableGridItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridTrainerApp.swift 3 | // IdentifiableGridItem 4 | // 5 | // Created by SwiftUI-Lab on 25-Jul-2020. 6 | // https://swiftui-lab.com/impossible-grids 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct IdentifiableGridItem: Identifiable { 12 | let id: Int 13 | var gridItem: GridItem 14 | 15 | init(_ id: Int, _ gridItem: GridItem) { 16 | self.id = id 17 | self.gridItem = gridItem 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GridTrainer/Sources/Grid Configuration/Popover Views/ColumnSetupView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridTrainerApp.swift 3 | // ColumnSetupView 4 | // 5 | // Created by SwiftUI-Lab on 25-Jul-2020. 6 | // https://swiftui-lab.com/impossible-grids 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ColumnSetupView: View { 12 | @ObservedObject var cfg: GridConfiguration 13 | let idx: Int 14 | 15 | var body: some View { 16 | 17 | Form { 18 | Section(header: Text("Column #\(idx + 1)")) { 19 | 20 | if idx >= 0 && idx < self.cfg.gridItems.count { 21 | let gridItem = self.cfg.gridItems[idx].gridItem 22 | 23 | Text("\(gridItem.description)") 24 | .padding(10) 25 | 26 | VStack(alignment: .leading, spacing: 0) { 27 | Picker("Size Type", selection: self.cfg.colSizeType(idx)) { 28 | Text(".fixed()").tag(1) 29 | Text(".flexible()").tag(2) 30 | Text(".adaptive()").tag(3) 31 | } 32 | }.padding(5) 33 | 34 | VStack { 35 | HStack { 36 | VStack { 37 | Text(gridItem.size.hasMax ? "Minimum" : "Fixed Width") 38 | Slider(value: self.cfg.colSizeMin(idx), in: 0...300) 39 | } 40 | 41 | if gridItem.size.hasMax { 42 | VStack { 43 | Text("Maximum") 44 | Slider(value: self.cfg.colSizeMax(idx), in: 0...400) 45 | } 46 | } 47 | } 48 | } 49 | 50 | VStack { 51 | VStack { 52 | Text("Spacing") 53 | Slider(value: self.cfg.colSpacing(idx), in: 0...100) 54 | } 55 | } 56 | 57 | // Changing the alignment will not produce any visible effects, 58 | // because the cell views are expanding to occupy all the space 59 | // offered by the grid. If the cells are changed to a different size 60 | // this option will become relevant. 61 | VStack(alignment: .leading, spacing: 0) { 62 | Picker("Alignment", selection: self.cfg.colAlignment(idx)) { 63 | Text(".leading").tag(Alignment.leading) 64 | Text(".center").tag(Alignment.center) 65 | Text(".trailing").tag(Alignment.trailing) 66 | } 67 | }.padding(5) 68 | } 69 | } 70 | } 71 | .navigationBarTitleDisplayMode(.inline) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /GridTrainer/Sources/Grid Configuration/Popover Views/ConfigPopover.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridTrainerApp.swift 3 | // ConfigPopover 4 | // 5 | // Created by SwiftUI-Lab on 25-Jul-2020. 6 | // https://swiftui-lab.com/impossible-grids 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ConfigPopover: View { 12 | @ObservedObject var cfg: GridConfiguration 13 | 14 | var body: some View { 15 | NavigationView { 16 | Form { 17 | Section(header: Text("Grid Configuration")) { 18 | VStack(alignment: .leading, spacing: 0) { 19 | Text("Item Count \(Int(self.cfg.cellCount))") 20 | Slider(value: self.$cfg.cellCount, in: 0...1000, step: 1) 21 | } 22 | 23 | VStack(alignment: .leading, spacing: 0) { 24 | Text("Width \(Int(self.cfg.width))") 25 | Slider(value: self.$cfg.width, in: 0...1500) 26 | } 27 | 28 | VStack(alignment: .leading, spacing: 0) { 29 | Text("Spacing \(Int(self.cfg.spacing))") 30 | Slider(value: self.$cfg.spacing, in: 0...100) 31 | } 32 | 33 | VStack(alignment: .leading, spacing: 0) { 34 | Text("Padding \(Int(self.cfg.padding))") 35 | Slider(value: self.$cfg.padding, in: 0...100) 36 | } 37 | 38 | VStack(alignment: .leading, spacing: 0) { 39 | Picker("Alignment", selection: self.$cfg.alignment) { 40 | Text(".leading").tag(HorizontalAlignment.leading) 41 | Text(".center").tag(HorizontalAlignment.center) 42 | Text(".trailing").tag(HorizontalAlignment.trailing) 43 | } 44 | }.padding(5) 45 | 46 | Toggle("Show borders", isOn: self.$cfg.showBorders) 47 | } 48 | 49 | Section(header: Text("Columns")) { 50 | ForEach(cfg.gridItems) { item in 51 | 52 | let col = item.gridItem 53 | 54 | if let idx = cfg.gridItems.firstIndex(where: { $0.id == item.id }) { 55 | 56 | NavigationLink(destination: ColumnSetupView(cfg: self.cfg, idx: idx)) { 57 | Text("\(col.description)") 58 | } 59 | .padding(20) 60 | 61 | } 62 | 63 | } 64 | .onDelete(perform: deleteGridItem) 65 | .onMove(perform: moveGridItem) 66 | 67 | Button("Add Column") { 68 | self.cfg.gridItems.append(IdentifiableGridItem((self.cfg.gridItems.last?.id ?? 0) + 1, self.cfg.gridItems.last?.gridItem ?? GridItem(.fixed(50), spacing: 0, alignment: .center))) 69 | } 70 | } 71 | } 72 | .navigationBarHidden(true) 73 | .padding(.trailing, 10) 74 | }.frame(width: 300, height: 700) 75 | 76 | } 77 | 78 | func deleteGridItem(offsets: IndexSet) { 79 | self.cfg.gridItems.remove(atOffsets: offsets) 80 | } 81 | 82 | func moveGridItem(from: IndexSet, to: Int) { 83 | self.cfg.gridItems.move(fromOffsets: from, toOffset: to) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /GridTrainer/Sources/Main/ColumnHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColumnHeader.swift 3 | // ColumnHeader 4 | // 5 | // Created by SwiftUI-Lab on 25-Jul-2020. 6 | // https://swiftui-lab.com/impossible-grids 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ColumnTitle: View { 12 | let info: GridInfo 13 | let lCell: Int 14 | let rCell: Int 15 | let column: GridItem 16 | let addMargin: Bool 17 | 18 | var body: some View { 19 | let width = calculateWidth(lCell: lCell, rCell: rCell) 20 | 21 | return HStack(spacing: 0) { 22 | RoundedRectangle(cornerRadius: 6) 23 | .fill(Color.gray) 24 | .frame(width: width, height: 50) 25 | .overlay(Text("\(column.size.description)")) 26 | 27 | if addMargin { 28 | Spacer().frame(width: column.spacing ?? 0) 29 | } 30 | } 31 | } 32 | 33 | func calculateWidth(lCell: Int, rCell: Int) -> CGFloat { 34 | guard rCell < self.info.cells.count else { return 0 } 35 | 36 | return (self.info.cells[rCell].bounds.maxX - self.info.cells[lCell].bounds.minX) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /GridTrainer/Sources/Main/GridHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridHeader.swift 3 | // GridHeader 4 | // 5 | // Created by SwiftUI-Lab on 25-Jul-2020. 6 | // https://swiftui-lab.com/impossible-grids 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct GridHeader: View { 12 | let cfg: GridConfiguration 13 | let info: GridInfo 14 | 15 | internal init(_ cfg: GridConfiguration, _ info: GridInfo) { 16 | self.cfg = cfg 17 | self.info = info 18 | } 19 | 20 | var body: some View { 21 | Group { 22 | if let correspondance = calculateCorrespondance() { 23 | HStack(spacing: 0) { 24 | ForEach(correspondance) { (range: ClosedRange) -> ColumnTitle? in 25 | if let idx = correspondance.firstIndex(where: { $0 == range }) { 26 | return ColumnTitle(info: info, 27 | lCell: range.lowerBound, 28 | rCell: range.upperBound, 29 | column: self.cfg.gridItems[idx].gridItem, 30 | addMargin: range != correspondance.last) 31 | } else { 32 | return nil 33 | } 34 | } 35 | } 36 | .frame(width: info.size.width, alignment: alignmentForHStack()) 37 | 38 | } else { 39 | RoundedRectangle(cornerRadius: 6) 40 | .fill(Color.gray) 41 | .frame(height: 50) 42 | .overlay(Text("With more than one adaptive GridItem, it is difficult to determine which cells belong to which columns")) 43 | 44 | } 45 | }.font(.footnote) 46 | 47 | } 48 | 49 | func alignmentForHStack() -> Alignment { 50 | switch self.cfg.alignment { 51 | case .leading: 52 | return .leading 53 | case .center: 54 | return .center 55 | case .trailing: 56 | return .trailing 57 | default: 58 | return .center 59 | } 60 | } 61 | 62 | func calculateCorrespondance() -> [ClosedRange]? { 63 | var array = [ClosedRange]() 64 | 65 | let adaptiveCount = self.cfg.gridItems.filter({ $0.gridItem.isAdaptive }).count 66 | 67 | guard adaptiveCount < 2 else { 68 | // If more than one adaptive GridItem is specified, it is hard to determine 69 | // which of the actual columns belong to which adaptive GridItem. There is 70 | // some guessing that can be done, but it is not worth the effort for this 71 | // project 72 | return nil 73 | } 74 | 75 | // Match cells columns, with their corresponding GridItem 76 | var k = 0 77 | 78 | for col in 0.. Color { 29 | colors[idx % max(1, min(info.columnCount, 7))] 30 | } 31 | 32 | var body: some View { 33 | let titleCount = "Column count = \(self.info.columnCount)" 34 | let titleRendered = "\(self.cfg.itemCount == 0 ? "" : ", \(self.info.renderedCells) of \(self.cfg.itemCount)")" 35 | 36 | return NavigationView { 37 | VStack { 38 | ScrollView(.vertical) { 39 | LazyVGrid(columns: cfg.gridItems.compactMap { $0.gridItem }, 40 | alignment: cfg.alignment, 41 | spacing: cfg.spacing, 42 | pinnedViews: .sectionHeaders) { 43 | 44 | Section(header: GridHeader(self.cfg, self.info) ) { 45 | 46 | ForEach(0.. 0 else { return 0 } 19 | 20 | let left = cells[0] 21 | let right = cells[self.columnCount - 1] 22 | 23 | return (right.bounds.maxX - left.bounds.minX) 24 | } 25 | 26 | var renderedCells: String { 27 | guard let f = cells.first?.id, let l = cells.last?.id else { return "" } 28 | 29 | return "rendering cells from \(f + 1) to \(l + 1)" 30 | } 31 | 32 | func cellWidth(_ col: Int) -> CGFloat { 33 | columnCount > 0 ? cells[col % columnCount].bounds.width : 0 34 | } 35 | 36 | mutating func recalculate() { 37 | self.columnCount = calculateColumnCount() 38 | } 39 | 40 | func calculateColumnCount() -> Int { 41 | 42 | guard cells.count > 1 else { return cells.count } 43 | 44 | var k = 1 45 | 46 | for i in 1.. cells[i-1].bounds.origin.x { 48 | k += 1 49 | } else { 50 | break 51 | } 52 | } 53 | 54 | return k 55 | } 56 | 57 | // Made equatable, to avoid unnecessary view body computations 58 | struct Item: Equatable { 59 | let id: Int 60 | let bounds: CGRect 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /GridTrainer/Sources/Preferences/GridPreferences.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridTrainerApp.swift 3 | // GridInfoPreference 4 | // 5 | // Created by SwiftUI-Lab on 25-Jul-2020. 6 | // https://swiftui-lab.com/impossible-grids 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct GridInfoPreference { 12 | let id: Int 13 | let bounds: Anchor 14 | } 15 | 16 | struct GridPreferenceKey: PreferenceKey { 17 | static var defaultValue: [GridInfoPreference] = [] 18 | 19 | static func reduce(value: inout [GridInfoPreference], nextValue: () -> [GridInfoPreference]) { 20 | return value.append(contentsOf: nextValue()) 21 | } 22 | } 23 | 24 | extension View { 25 | func gridInfo(id: Int) -> some View { 26 | self.anchorPreference(key: GridPreferenceKey.self, value: .bounds) { 27 | [GridInfoPreference(id: id, bounds: $0)] 28 | } 29 | } 30 | 31 | func gridConfiguration(_ info: Binding) -> some View { 32 | self.backgroundPreferenceValue(GridPreferenceKey.self) { prefs in 33 | GeometryReader { proxy -> Color in 34 | DispatchQueue.main.async { 35 | info.wrappedValue.size = proxy.size 36 | info.wrappedValue.cells = prefs.compactMap { 37 | GridInfo.Item(id: $0.id, bounds: proxy[$0.bounds]) 38 | } 39 | } 40 | 41 | return Color.clear 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SwiftUI-Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GridTrainer 2 | 3 | This small app sample is designed to help you understand how grids work in SwiftUI. It is part of the article: [Impossible Grids with SwiftUI](https://swiftui-lab.com/impossible-grids) 4 | --------------------------------------------------------------------------------