├── FlexiblePicker.gif ├── FlexiblePickerExample.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── jedrzejcholuj.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── FlexiblePickerExample ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── ContentView.swift ├── FlexiblePicker.swift ├── FlexiblePickerExampleApp.swift ├── FontWeight.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SelectableModel.swift └── String+Extensions.swift ├── FlexiblePickerExampleTests └── FlexiblePickerExampleTests.swift ├── FlexiblePickerExampleUITests ├── FlexiblePickerExampleUITests.swift └── FlexiblePickerExampleUITestsLaunchTests.swift └── README.md /FlexiblePicker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcholuj/FlexiblePickerExample/d18f1606b181739a26c6989e5ed7f53847501cd9/FlexiblePicker.gif -------------------------------------------------------------------------------- /FlexiblePickerExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B6F9C802279D451F0091752F /* FlexiblePickerExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9C801279D451F0091752F /* FlexiblePickerExampleApp.swift */; }; 11 | B6F9C804279D451F0091752F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9C803279D451F0091752F /* ContentView.swift */; }; 12 | B6F9C806279D452E0091752F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B6F9C805279D452E0091752F /* Assets.xcassets */; }; 13 | B6F9C809279D452E0091752F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B6F9C808279D452E0091752F /* Preview Assets.xcassets */; }; 14 | B6F9C813279D452E0091752F /* FlexiblePickerExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9C812279D452E0091752F /* FlexiblePickerExampleTests.swift */; }; 15 | B6F9C81D279D452E0091752F /* FlexiblePickerExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9C81C279D452E0091752F /* FlexiblePickerExampleUITests.swift */; }; 16 | B6F9C81F279D452E0091752F /* FlexiblePickerExampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9C81E279D452E0091752F /* FlexiblePickerExampleUITestsLaunchTests.swift */; }; 17 | B6F9C82C279D45600091752F /* FontWeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9C82B279D45600091752F /* FontWeight.swift */; }; 18 | B6F9C82E279D45830091752F /* SelectableModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9C82D279D45830091752F /* SelectableModel.swift */; }; 19 | B6F9C830279D45A60091752F /* FlexiblePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9C82F279D45A60091752F /* FlexiblePicker.swift */; }; 20 | B6F9C832279D45FB0091752F /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9C831279D45FB0091752F /* String+Extensions.swift */; }; 21 | B6F9C834279F22570091752F /* FlexiblePicker.gif in Resources */ = {isa = PBXBuildFile; fileRef = B6F9C833279F22570091752F /* FlexiblePicker.gif */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXContainerItemProxy section */ 25 | B6F9C80F279D452E0091752F /* PBXContainerItemProxy */ = { 26 | isa = PBXContainerItemProxy; 27 | containerPortal = B6F9C7F6279D451F0091752F /* Project object */; 28 | proxyType = 1; 29 | remoteGlobalIDString = B6F9C7FD279D451F0091752F; 30 | remoteInfo = FlexiblePickerExample; 31 | }; 32 | B6F9C819279D452E0091752F /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = B6F9C7F6279D451F0091752F /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = B6F9C7FD279D451F0091752F; 37 | remoteInfo = FlexiblePickerExample; 38 | }; 39 | /* End PBXContainerItemProxy section */ 40 | 41 | /* Begin PBXFileReference section */ 42 | B6F9C7FE279D451F0091752F /* FlexiblePickerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FlexiblePickerExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | B6F9C801279D451F0091752F /* FlexiblePickerExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexiblePickerExampleApp.swift; sourceTree = ""; }; 44 | B6F9C803279D451F0091752F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 45 | B6F9C805279D452E0091752F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 46 | B6F9C808279D452E0091752F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 47 | B6F9C80E279D452E0091752F /* FlexiblePickerExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FlexiblePickerExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | B6F9C812279D452E0091752F /* FlexiblePickerExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexiblePickerExampleTests.swift; sourceTree = ""; }; 49 | B6F9C818279D452E0091752F /* FlexiblePickerExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FlexiblePickerExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | B6F9C81C279D452E0091752F /* FlexiblePickerExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexiblePickerExampleUITests.swift; sourceTree = ""; }; 51 | B6F9C81E279D452E0091752F /* FlexiblePickerExampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexiblePickerExampleUITestsLaunchTests.swift; sourceTree = ""; }; 52 | B6F9C82B279D45600091752F /* FontWeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontWeight.swift; sourceTree = ""; }; 53 | B6F9C82D279D45830091752F /* SelectableModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableModel.swift; sourceTree = ""; }; 54 | B6F9C82F279D45A60091752F /* FlexiblePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexiblePicker.swift; sourceTree = ""; }; 55 | B6F9C831279D45FB0091752F /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; 56 | B6F9C833279F22570091752F /* FlexiblePicker.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = FlexiblePicker.gif; sourceTree = ""; }; 57 | /* End PBXFileReference section */ 58 | 59 | /* Begin PBXFrameworksBuildPhase section */ 60 | B6F9C7FB279D451F0091752F /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | B6F9C80B279D452E0091752F /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | B6F9C815279D452E0091752F /* Frameworks */ = { 75 | isa = PBXFrameworksBuildPhase; 76 | buildActionMask = 2147483647; 77 | files = ( 78 | ); 79 | runOnlyForDeploymentPostprocessing = 0; 80 | }; 81 | /* End PBXFrameworksBuildPhase section */ 82 | 83 | /* Begin PBXGroup section */ 84 | B6F9C7F5279D451F0091752F = { 85 | isa = PBXGroup; 86 | children = ( 87 | B6F9C833279F22570091752F /* FlexiblePicker.gif */, 88 | B6F9C800279D451F0091752F /* FlexiblePickerExample */, 89 | B6F9C811279D452E0091752F /* FlexiblePickerExampleTests */, 90 | B6F9C81B279D452E0091752F /* FlexiblePickerExampleUITests */, 91 | B6F9C7FF279D451F0091752F /* Products */, 92 | ); 93 | sourceTree = ""; 94 | }; 95 | B6F9C7FF279D451F0091752F /* Products */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | B6F9C7FE279D451F0091752F /* FlexiblePickerExample.app */, 99 | B6F9C80E279D452E0091752F /* FlexiblePickerExampleTests.xctest */, 100 | B6F9C818279D452E0091752F /* FlexiblePickerExampleUITests.xctest */, 101 | ); 102 | name = Products; 103 | sourceTree = ""; 104 | }; 105 | B6F9C800279D451F0091752F /* FlexiblePickerExample */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | B6F9C801279D451F0091752F /* FlexiblePickerExampleApp.swift */, 109 | B6F9C803279D451F0091752F /* ContentView.swift */, 110 | B6F9C805279D452E0091752F /* Assets.xcassets */, 111 | B6F9C807279D452E0091752F /* Preview Content */, 112 | B6F9C82B279D45600091752F /* FontWeight.swift */, 113 | B6F9C82D279D45830091752F /* SelectableModel.swift */, 114 | B6F9C82F279D45A60091752F /* FlexiblePicker.swift */, 115 | B6F9C831279D45FB0091752F /* String+Extensions.swift */, 116 | ); 117 | path = FlexiblePickerExample; 118 | sourceTree = ""; 119 | }; 120 | B6F9C807279D452E0091752F /* Preview Content */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | B6F9C808279D452E0091752F /* Preview Assets.xcassets */, 124 | ); 125 | path = "Preview Content"; 126 | sourceTree = ""; 127 | }; 128 | B6F9C811279D452E0091752F /* FlexiblePickerExampleTests */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | B6F9C812279D452E0091752F /* FlexiblePickerExampleTests.swift */, 132 | ); 133 | path = FlexiblePickerExampleTests; 134 | sourceTree = ""; 135 | }; 136 | B6F9C81B279D452E0091752F /* FlexiblePickerExampleUITests */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | B6F9C81C279D452E0091752F /* FlexiblePickerExampleUITests.swift */, 140 | B6F9C81E279D452E0091752F /* FlexiblePickerExampleUITestsLaunchTests.swift */, 141 | ); 142 | path = FlexiblePickerExampleUITests; 143 | sourceTree = ""; 144 | }; 145 | /* End PBXGroup section */ 146 | 147 | /* Begin PBXNativeTarget section */ 148 | B6F9C7FD279D451F0091752F /* FlexiblePickerExample */ = { 149 | isa = PBXNativeTarget; 150 | buildConfigurationList = B6F9C822279D452E0091752F /* Build configuration list for PBXNativeTarget "FlexiblePickerExample" */; 151 | buildPhases = ( 152 | B6F9C7FA279D451F0091752F /* Sources */, 153 | B6F9C7FB279D451F0091752F /* Frameworks */, 154 | B6F9C7FC279D451F0091752F /* Resources */, 155 | ); 156 | buildRules = ( 157 | ); 158 | dependencies = ( 159 | ); 160 | name = FlexiblePickerExample; 161 | productName = FlexiblePickerExample; 162 | productReference = B6F9C7FE279D451F0091752F /* FlexiblePickerExample.app */; 163 | productType = "com.apple.product-type.application"; 164 | }; 165 | B6F9C80D279D452E0091752F /* FlexiblePickerExampleTests */ = { 166 | isa = PBXNativeTarget; 167 | buildConfigurationList = B6F9C825279D452E0091752F /* Build configuration list for PBXNativeTarget "FlexiblePickerExampleTests" */; 168 | buildPhases = ( 169 | B6F9C80A279D452E0091752F /* Sources */, 170 | B6F9C80B279D452E0091752F /* Frameworks */, 171 | B6F9C80C279D452E0091752F /* Resources */, 172 | ); 173 | buildRules = ( 174 | ); 175 | dependencies = ( 176 | B6F9C810279D452E0091752F /* PBXTargetDependency */, 177 | ); 178 | name = FlexiblePickerExampleTests; 179 | productName = FlexiblePickerExampleTests; 180 | productReference = B6F9C80E279D452E0091752F /* FlexiblePickerExampleTests.xctest */; 181 | productType = "com.apple.product-type.bundle.unit-test"; 182 | }; 183 | B6F9C817279D452E0091752F /* FlexiblePickerExampleUITests */ = { 184 | isa = PBXNativeTarget; 185 | buildConfigurationList = B6F9C828279D452E0091752F /* Build configuration list for PBXNativeTarget "FlexiblePickerExampleUITests" */; 186 | buildPhases = ( 187 | B6F9C814279D452E0091752F /* Sources */, 188 | B6F9C815279D452E0091752F /* Frameworks */, 189 | B6F9C816279D452E0091752F /* Resources */, 190 | ); 191 | buildRules = ( 192 | ); 193 | dependencies = ( 194 | B6F9C81A279D452E0091752F /* PBXTargetDependency */, 195 | ); 196 | name = FlexiblePickerExampleUITests; 197 | productName = FlexiblePickerExampleUITests; 198 | productReference = B6F9C818279D452E0091752F /* FlexiblePickerExampleUITests.xctest */; 199 | productType = "com.apple.product-type.bundle.ui-testing"; 200 | }; 201 | /* End PBXNativeTarget section */ 202 | 203 | /* Begin PBXProject section */ 204 | B6F9C7F6279D451F0091752F /* Project object */ = { 205 | isa = PBXProject; 206 | attributes = { 207 | BuildIndependentTargetsInParallel = 1; 208 | LastSwiftUpdateCheck = 1320; 209 | LastUpgradeCheck = 1320; 210 | TargetAttributes = { 211 | B6F9C7FD279D451F0091752F = { 212 | CreatedOnToolsVersion = 13.2; 213 | }; 214 | B6F9C80D279D452E0091752F = { 215 | CreatedOnToolsVersion = 13.2; 216 | TestTargetID = B6F9C7FD279D451F0091752F; 217 | }; 218 | B6F9C817279D452E0091752F = { 219 | CreatedOnToolsVersion = 13.2; 220 | TestTargetID = B6F9C7FD279D451F0091752F; 221 | }; 222 | }; 223 | }; 224 | buildConfigurationList = B6F9C7F9279D451F0091752F /* Build configuration list for PBXProject "FlexiblePickerExample" */; 225 | compatibilityVersion = "Xcode 13.0"; 226 | developmentRegion = en; 227 | hasScannedForEncodings = 0; 228 | knownRegions = ( 229 | en, 230 | Base, 231 | ); 232 | mainGroup = B6F9C7F5279D451F0091752F; 233 | productRefGroup = B6F9C7FF279D451F0091752F /* Products */; 234 | projectDirPath = ""; 235 | projectRoot = ""; 236 | targets = ( 237 | B6F9C7FD279D451F0091752F /* FlexiblePickerExample */, 238 | B6F9C80D279D452E0091752F /* FlexiblePickerExampleTests */, 239 | B6F9C817279D452E0091752F /* FlexiblePickerExampleUITests */, 240 | ); 241 | }; 242 | /* End PBXProject section */ 243 | 244 | /* Begin PBXResourcesBuildPhase section */ 245 | B6F9C7FC279D451F0091752F /* Resources */ = { 246 | isa = PBXResourcesBuildPhase; 247 | buildActionMask = 2147483647; 248 | files = ( 249 | B6F9C809279D452E0091752F /* Preview Assets.xcassets in Resources */, 250 | B6F9C806279D452E0091752F /* Assets.xcassets in Resources */, 251 | B6F9C834279F22570091752F /* FlexiblePicker.gif in Resources */, 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | }; 255 | B6F9C80C279D452E0091752F /* Resources */ = { 256 | isa = PBXResourcesBuildPhase; 257 | buildActionMask = 2147483647; 258 | files = ( 259 | ); 260 | runOnlyForDeploymentPostprocessing = 0; 261 | }; 262 | B6F9C816279D452E0091752F /* Resources */ = { 263 | isa = PBXResourcesBuildPhase; 264 | buildActionMask = 2147483647; 265 | files = ( 266 | ); 267 | runOnlyForDeploymentPostprocessing = 0; 268 | }; 269 | /* End PBXResourcesBuildPhase section */ 270 | 271 | /* Begin PBXSourcesBuildPhase section */ 272 | B6F9C7FA279D451F0091752F /* Sources */ = { 273 | isa = PBXSourcesBuildPhase; 274 | buildActionMask = 2147483647; 275 | files = ( 276 | B6F9C804279D451F0091752F /* ContentView.swift in Sources */, 277 | B6F9C82E279D45830091752F /* SelectableModel.swift in Sources */, 278 | B6F9C830279D45A60091752F /* FlexiblePicker.swift in Sources */, 279 | B6F9C82C279D45600091752F /* FontWeight.swift in Sources */, 280 | B6F9C832279D45FB0091752F /* String+Extensions.swift in Sources */, 281 | B6F9C802279D451F0091752F /* FlexiblePickerExampleApp.swift in Sources */, 282 | ); 283 | runOnlyForDeploymentPostprocessing = 0; 284 | }; 285 | B6F9C80A279D452E0091752F /* Sources */ = { 286 | isa = PBXSourcesBuildPhase; 287 | buildActionMask = 2147483647; 288 | files = ( 289 | B6F9C813279D452E0091752F /* FlexiblePickerExampleTests.swift in Sources */, 290 | ); 291 | runOnlyForDeploymentPostprocessing = 0; 292 | }; 293 | B6F9C814279D452E0091752F /* Sources */ = { 294 | isa = PBXSourcesBuildPhase; 295 | buildActionMask = 2147483647; 296 | files = ( 297 | B6F9C81F279D452E0091752F /* FlexiblePickerExampleUITestsLaunchTests.swift in Sources */, 298 | B6F9C81D279D452E0091752F /* FlexiblePickerExampleUITests.swift in Sources */, 299 | ); 300 | runOnlyForDeploymentPostprocessing = 0; 301 | }; 302 | /* End PBXSourcesBuildPhase section */ 303 | 304 | /* Begin PBXTargetDependency section */ 305 | B6F9C810279D452E0091752F /* PBXTargetDependency */ = { 306 | isa = PBXTargetDependency; 307 | target = B6F9C7FD279D451F0091752F /* FlexiblePickerExample */; 308 | targetProxy = B6F9C80F279D452E0091752F /* PBXContainerItemProxy */; 309 | }; 310 | B6F9C81A279D452E0091752F /* PBXTargetDependency */ = { 311 | isa = PBXTargetDependency; 312 | target = B6F9C7FD279D451F0091752F /* FlexiblePickerExample */; 313 | targetProxy = B6F9C819279D452E0091752F /* PBXContainerItemProxy */; 314 | }; 315 | /* End PBXTargetDependency section */ 316 | 317 | /* Begin XCBuildConfiguration section */ 318 | B6F9C820279D452E0091752F /* Debug */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | ALWAYS_SEARCH_USER_PATHS = NO; 322 | CLANG_ANALYZER_NONNULL = YES; 323 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 324 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 325 | CLANG_CXX_LIBRARY = "libc++"; 326 | CLANG_ENABLE_MODULES = YES; 327 | CLANG_ENABLE_OBJC_ARC = YES; 328 | CLANG_ENABLE_OBJC_WEAK = YES; 329 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 330 | CLANG_WARN_BOOL_CONVERSION = YES; 331 | CLANG_WARN_COMMA = YES; 332 | CLANG_WARN_CONSTANT_CONVERSION = YES; 333 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 334 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 335 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 336 | CLANG_WARN_EMPTY_BODY = YES; 337 | CLANG_WARN_ENUM_CONVERSION = YES; 338 | CLANG_WARN_INFINITE_RECURSION = YES; 339 | CLANG_WARN_INT_CONVERSION = YES; 340 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 341 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 342 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 343 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 344 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 345 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 346 | CLANG_WARN_STRICT_PROTOTYPES = YES; 347 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 348 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 349 | CLANG_WARN_UNREACHABLE_CODE = YES; 350 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 351 | COPY_PHASE_STRIP = NO; 352 | DEBUG_INFORMATION_FORMAT = dwarf; 353 | ENABLE_STRICT_OBJC_MSGSEND = YES; 354 | ENABLE_TESTABILITY = YES; 355 | GCC_C_LANGUAGE_STANDARD = gnu11; 356 | GCC_DYNAMIC_NO_PIC = NO; 357 | GCC_NO_COMMON_BLOCKS = YES; 358 | GCC_OPTIMIZATION_LEVEL = 0; 359 | GCC_PREPROCESSOR_DEFINITIONS = ( 360 | "DEBUG=1", 361 | "$(inherited)", 362 | ); 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 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 370 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 371 | MTL_FAST_MATH = YES; 372 | ONLY_ACTIVE_ARCH = YES; 373 | SDKROOT = iphoneos; 374 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 375 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 376 | }; 377 | name = Debug; 378 | }; 379 | B6F9C821279D452E0091752F /* Release */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | ALWAYS_SEARCH_USER_PATHS = NO; 383 | CLANG_ANALYZER_NONNULL = YES; 384 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 385 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 386 | CLANG_CXX_LIBRARY = "libc++"; 387 | CLANG_ENABLE_MODULES = YES; 388 | CLANG_ENABLE_OBJC_ARC = YES; 389 | CLANG_ENABLE_OBJC_WEAK = YES; 390 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 391 | CLANG_WARN_BOOL_CONVERSION = YES; 392 | CLANG_WARN_COMMA = YES; 393 | CLANG_WARN_CONSTANT_CONVERSION = YES; 394 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 395 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 396 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 397 | CLANG_WARN_EMPTY_BODY = YES; 398 | CLANG_WARN_ENUM_CONVERSION = YES; 399 | CLANG_WARN_INFINITE_RECURSION = YES; 400 | CLANG_WARN_INT_CONVERSION = YES; 401 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 402 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 403 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 404 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 405 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 406 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 407 | CLANG_WARN_STRICT_PROTOTYPES = YES; 408 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 409 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 410 | CLANG_WARN_UNREACHABLE_CODE = YES; 411 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 412 | COPY_PHASE_STRIP = NO; 413 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 414 | ENABLE_NS_ASSERTIONS = NO; 415 | ENABLE_STRICT_OBJC_MSGSEND = YES; 416 | GCC_C_LANGUAGE_STANDARD = gnu11; 417 | GCC_NO_COMMON_BLOCKS = YES; 418 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 419 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 420 | GCC_WARN_UNDECLARED_SELECTOR = YES; 421 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 422 | GCC_WARN_UNUSED_FUNCTION = YES; 423 | GCC_WARN_UNUSED_VARIABLE = YES; 424 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 425 | MTL_ENABLE_DEBUG_INFO = NO; 426 | MTL_FAST_MATH = YES; 427 | SDKROOT = iphoneos; 428 | SWIFT_COMPILATION_MODE = wholemodule; 429 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 430 | VALIDATE_PRODUCT = YES; 431 | }; 432 | name = Release; 433 | }; 434 | B6F9C823279D452E0091752F /* Debug */ = { 435 | isa = XCBuildConfiguration; 436 | buildSettings = { 437 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 438 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 439 | CODE_SIGN_STYLE = Automatic; 440 | CURRENT_PROJECT_VERSION = 1; 441 | DEVELOPMENT_ASSET_PATHS = "\"FlexiblePickerExample/Preview Content\""; 442 | DEVELOPMENT_TEAM = TYN595GFH6; 443 | ENABLE_PREVIEWS = YES; 444 | GENERATE_INFOPLIST_FILE = YES; 445 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 446 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 447 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 448 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 449 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 450 | LD_RUNPATH_SEARCH_PATHS = ( 451 | "$(inherited)", 452 | "@executable_path/Frameworks", 453 | ); 454 | MARKETING_VERSION = 1.0; 455 | PRODUCT_BUNDLE_IDENTIFIER = com.jedrzejcholuj.FlexiblePickerExample; 456 | PRODUCT_NAME = "$(TARGET_NAME)"; 457 | SWIFT_EMIT_LOC_STRINGS = YES; 458 | SWIFT_VERSION = 5.0; 459 | TARGETED_DEVICE_FAMILY = "1,2"; 460 | }; 461 | name = Debug; 462 | }; 463 | B6F9C824279D452E0091752F /* Release */ = { 464 | isa = XCBuildConfiguration; 465 | buildSettings = { 466 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 467 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 468 | CODE_SIGN_STYLE = Automatic; 469 | CURRENT_PROJECT_VERSION = 1; 470 | DEVELOPMENT_ASSET_PATHS = "\"FlexiblePickerExample/Preview Content\""; 471 | DEVELOPMENT_TEAM = TYN595GFH6; 472 | ENABLE_PREVIEWS = YES; 473 | GENERATE_INFOPLIST_FILE = YES; 474 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 475 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 476 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 477 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 478 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 479 | LD_RUNPATH_SEARCH_PATHS = ( 480 | "$(inherited)", 481 | "@executable_path/Frameworks", 482 | ); 483 | MARKETING_VERSION = 1.0; 484 | PRODUCT_BUNDLE_IDENTIFIER = com.jedrzejcholuj.FlexiblePickerExample; 485 | PRODUCT_NAME = "$(TARGET_NAME)"; 486 | SWIFT_EMIT_LOC_STRINGS = YES; 487 | SWIFT_VERSION = 5.0; 488 | TARGETED_DEVICE_FAMILY = "1,2"; 489 | }; 490 | name = Release; 491 | }; 492 | B6F9C826279D452E0091752F /* Debug */ = { 493 | isa = XCBuildConfiguration; 494 | buildSettings = { 495 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 496 | BUNDLE_LOADER = "$(TEST_HOST)"; 497 | CODE_SIGN_STYLE = Automatic; 498 | CURRENT_PROJECT_VERSION = 1; 499 | DEVELOPMENT_TEAM = TYN595GFH6; 500 | GENERATE_INFOPLIST_FILE = YES; 501 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 502 | MARKETING_VERSION = 1.0; 503 | PRODUCT_BUNDLE_IDENTIFIER = com.jedrzejcholuj.FlexiblePickerExampleTests; 504 | PRODUCT_NAME = "$(TARGET_NAME)"; 505 | SWIFT_EMIT_LOC_STRINGS = NO; 506 | SWIFT_VERSION = 5.0; 507 | TARGETED_DEVICE_FAMILY = "1,2"; 508 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FlexiblePickerExample.app/FlexiblePickerExample"; 509 | }; 510 | name = Debug; 511 | }; 512 | B6F9C827279D452E0091752F /* Release */ = { 513 | isa = XCBuildConfiguration; 514 | buildSettings = { 515 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 516 | BUNDLE_LOADER = "$(TEST_HOST)"; 517 | CODE_SIGN_STYLE = Automatic; 518 | CURRENT_PROJECT_VERSION = 1; 519 | DEVELOPMENT_TEAM = TYN595GFH6; 520 | GENERATE_INFOPLIST_FILE = YES; 521 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 522 | MARKETING_VERSION = 1.0; 523 | PRODUCT_BUNDLE_IDENTIFIER = com.jedrzejcholuj.FlexiblePickerExampleTests; 524 | PRODUCT_NAME = "$(TARGET_NAME)"; 525 | SWIFT_EMIT_LOC_STRINGS = NO; 526 | SWIFT_VERSION = 5.0; 527 | TARGETED_DEVICE_FAMILY = "1,2"; 528 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FlexiblePickerExample.app/FlexiblePickerExample"; 529 | }; 530 | name = Release; 531 | }; 532 | B6F9C829279D452E0091752F /* Debug */ = { 533 | isa = XCBuildConfiguration; 534 | buildSettings = { 535 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 536 | CODE_SIGN_STYLE = Automatic; 537 | CURRENT_PROJECT_VERSION = 1; 538 | DEVELOPMENT_TEAM = TYN595GFH6; 539 | GENERATE_INFOPLIST_FILE = YES; 540 | MARKETING_VERSION = 1.0; 541 | PRODUCT_BUNDLE_IDENTIFIER = com.jedrzejcholuj.FlexiblePickerExampleUITests; 542 | PRODUCT_NAME = "$(TARGET_NAME)"; 543 | SWIFT_EMIT_LOC_STRINGS = NO; 544 | SWIFT_VERSION = 5.0; 545 | TARGETED_DEVICE_FAMILY = "1,2"; 546 | TEST_TARGET_NAME = FlexiblePickerExample; 547 | }; 548 | name = Debug; 549 | }; 550 | B6F9C82A279D452E0091752F /* Release */ = { 551 | isa = XCBuildConfiguration; 552 | buildSettings = { 553 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 554 | CODE_SIGN_STYLE = Automatic; 555 | CURRENT_PROJECT_VERSION = 1; 556 | DEVELOPMENT_TEAM = TYN595GFH6; 557 | GENERATE_INFOPLIST_FILE = YES; 558 | MARKETING_VERSION = 1.0; 559 | PRODUCT_BUNDLE_IDENTIFIER = com.jedrzejcholuj.FlexiblePickerExampleUITests; 560 | PRODUCT_NAME = "$(TARGET_NAME)"; 561 | SWIFT_EMIT_LOC_STRINGS = NO; 562 | SWIFT_VERSION = 5.0; 563 | TARGETED_DEVICE_FAMILY = "1,2"; 564 | TEST_TARGET_NAME = FlexiblePickerExample; 565 | }; 566 | name = Release; 567 | }; 568 | /* End XCBuildConfiguration section */ 569 | 570 | /* Begin XCConfigurationList section */ 571 | B6F9C7F9279D451F0091752F /* Build configuration list for PBXProject "FlexiblePickerExample" */ = { 572 | isa = XCConfigurationList; 573 | buildConfigurations = ( 574 | B6F9C820279D452E0091752F /* Debug */, 575 | B6F9C821279D452E0091752F /* Release */, 576 | ); 577 | defaultConfigurationIsVisible = 0; 578 | defaultConfigurationName = Release; 579 | }; 580 | B6F9C822279D452E0091752F /* Build configuration list for PBXNativeTarget "FlexiblePickerExample" */ = { 581 | isa = XCConfigurationList; 582 | buildConfigurations = ( 583 | B6F9C823279D452E0091752F /* Debug */, 584 | B6F9C824279D452E0091752F /* Release */, 585 | ); 586 | defaultConfigurationIsVisible = 0; 587 | defaultConfigurationName = Release; 588 | }; 589 | B6F9C825279D452E0091752F /* Build configuration list for PBXNativeTarget "FlexiblePickerExampleTests" */ = { 590 | isa = XCConfigurationList; 591 | buildConfigurations = ( 592 | B6F9C826279D452E0091752F /* Debug */, 593 | B6F9C827279D452E0091752F /* Release */, 594 | ); 595 | defaultConfigurationIsVisible = 0; 596 | defaultConfigurationName = Release; 597 | }; 598 | B6F9C828279D452E0091752F /* Build configuration list for PBXNativeTarget "FlexiblePickerExampleUITests" */ = { 599 | isa = XCConfigurationList; 600 | buildConfigurations = ( 601 | B6F9C829279D452E0091752F /* Debug */, 602 | B6F9C82A279D452E0091752F /* Release */, 603 | ); 604 | defaultConfigurationIsVisible = 0; 605 | defaultConfigurationName = Release; 606 | }; 607 | /* End XCConfigurationList section */ 608 | }; 609 | rootObject = B6F9C7F6279D451F0091752F /* Project object */; 610 | } 611 | -------------------------------------------------------------------------------- /FlexiblePickerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FlexiblePickerExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FlexiblePickerExample.xcodeproj/xcuserdata/jedrzejcholuj.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | FlexiblePickerExample.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /FlexiblePickerExample/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 | -------------------------------------------------------------------------------- /FlexiblePickerExample/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 | -------------------------------------------------------------------------------- /FlexiblePickerExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /FlexiblePickerExample/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // FlexiblePickerExample 4 | // 5 | // Created by Jędrzej Chołuj on 23/01/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class ContentViewModel: ObservableObject { 11 | @Published var data: [SelectableModel] = [ 12 | "bagel", "beans", "beer", "biscuit", "bread", "broth", "burger", "butter", "cake", "candy", "caramel", "caviar", "cheese", "chili", "chocolate", "cider", "cocoa", "coffee", "cookie", "cream", "croissant", "crumble", "cuisine", "curd", "dessert", "dish", "drink", "eggs", "filet", "fish", "flour", "foie gras", "food", "grill", "hamburger", "ice", "juice", "ketchup", "kitchen", "liquor", "margarine", "mayo", "mayonnaise", "meat", "milk", "mousse", "muffin", "mushroom", "noodle", "nut", "oil", "olive", "omelette", "pan", "pasta", "paste", "pie", "pizza", "plate", "poutine", "pudding", "recipe", "rice", "salad", "salsa", "sandwich", "sauce", "soda", "soup", "soy", "spice", "steak", "syrup", "tartar", "taste", "tea", "toast", "vinegar", "waffle", "water", "wheat", "wine", "wok", "yeast", "yogurt" 13 | ].map { SelectableModel(displayedName: $0) } 14 | } 15 | 16 | struct ContentView: View { 17 | 18 | @ObservedObject var viewModel: ContentViewModel = ContentViewModel() 19 | 20 | var body: some View { 21 | FlexiblePicker(inputData: $viewModel.data) 22 | } 23 | } 24 | 25 | struct ContentView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | ContentView() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /FlexiblePickerExample/FlexiblePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlexiblePicker.swift 3 | // FlexiblePickerExample 4 | // 5 | // Created by Jędrzej Chołuj on 23/01/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FlexiblePicker: View { 11 | 12 | @Binding var inputData: [T] 13 | 14 | var fontWeight: FontWeight = .medium 15 | var fontSize: CGFloat = 16 16 | var spacing: CGFloat = 12 17 | var textPadding: CGFloat = 8 18 | var textColor: Color = .black 19 | var selectedColor: Color = .blue 20 | var notSelectedColor: Color = .clear 21 | var borderWidth: CGFloat = 2 22 | var borderColor: Color = .blue 23 | var alignment: HorizontalAlignment = .center 24 | var cornerRadius: CGFloat = 10 25 | var isSelectable: Bool = true 26 | 27 | var body: some View { 28 | GeometryReader { geo in 29 | VStack(alignment: alignment, spacing: spacing) { 30 | ForEach( 31 | divideDataIntoLines(lineWidth: geo.size.width) 32 | .map { (data: $0, id: UUID()) }, 33 | id: \.id 34 | ) { dataArray in 35 | Group { 36 | HStack(spacing: spacing) { 37 | ForEach(dataArray.data, id: \.id) { data in 38 | Button(action: { updateSelectedData(with: data) }) { 39 | Text(data.displayedName) 40 | .lineLimit(1) 41 | .foregroundColor(textColor) 42 | .font(.system( 43 | size: fontSize, 44 | weight: fontWeight.swiftUIFontWeight) 45 | ) 46 | .padding(textPadding) 47 | } 48 | .background( 49 | data.isSelected 50 | ? selectedColor.opacity(0.5) 51 | : notSelectedColor.opacity(0.5) 52 | ) 53 | .cornerRadius(10) 54 | .disabled(!isSelectable) 55 | .overlay(RoundedRectangle(cornerRadius: 10) 56 | .stroke(borderColor, lineWidth: borderWidth)) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | .frame(width: geo.size.width, height: calculateVStackHeight(width: geo.size.width)) 63 | } 64 | } 65 | 66 | private func updateSelectedData(with data: T) { 67 | guard let index = inputData.indices 68 | .first(where: { inputData[$0] == data }) else { return } 69 | inputData[index].isSelected.toggle() 70 | } 71 | 72 | private func divideDataIntoLines(lineWidth: CGFloat) -> [[T]] { 73 | let data = calculateWidths(for: inputData) 74 | var singleLineWidth = lineWidth 75 | var allLinesResult = [[T]]() 76 | var singleLineResult = [T]() 77 | var partialWidthResult: CGFloat = 0 78 | data.forEach { (selectableType, width) in 79 | partialWidthResult = singleLineWidth - width 80 | if partialWidthResult > 0 { 81 | singleLineResult.append(selectableType) 82 | singleLineWidth -= width 83 | } else { 84 | allLinesResult.append(singleLineResult) 85 | singleLineResult = [selectableType] 86 | singleLineWidth = lineWidth - width 87 | } 88 | } 89 | guard !singleLineResult.isEmpty else { return allLinesResult } 90 | allLinesResult.append(singleLineResult) 91 | return allLinesResult 92 | } 93 | 94 | private func calculateWidths(for data: [T]) -> [(value: T, width: CGFloat)] { 95 | return data.map { selectableType -> (T, CGFloat) in 96 | let font = UIFont.systemFont(ofSize: fontSize, weight: fontWeight.uiFontWeight) 97 | let textWidth = selectableType.displayedName.getWidth(with: font) 98 | let width = [textPadding, textPadding, borderWidth, borderWidth, spacing] 99 | .reduce(textWidth, +) 100 | return (selectableType, width) 101 | } 102 | } 103 | 104 | private func calculateVStackHeight(width: CGFloat) -> CGFloat { 105 | let data = divideDataIntoLines(lineWidth: width) 106 | let font = UIFont.systemFont(ofSize: fontSize, weight: fontWeight.uiFontWeight) 107 | guard let textHeight = data.first?.first?.displayedName 108 | .getHeight(with: font) else { return 16 } 109 | let result = [textPadding, textPadding, borderWidth, borderWidth, spacing] 110 | .reduce(textHeight, +) 111 | return result * CGFloat(data.count) 112 | } 113 | } 114 | 115 | struct FlexiblePicker_Previews: PreviewProvider { 116 | static var previews: some View { 117 | FlexiblePicker(inputData: .constant([])) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /FlexiblePickerExample/FlexiblePickerExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlexiblePickerExampleApp.swift 3 | // FlexiblePickerExample 4 | // 5 | // Created by Jędrzej Chołuj on 23/01/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct FlexiblePickerExampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /FlexiblePickerExample/FontWeight.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontWeight.swift 3 | // FlexiblePickerExample 4 | // 5 | // Created by Jędrzej Chołuj on 23/01/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum FontWeight { 11 | case light 12 | case thin 13 | case medium 14 | case regular 15 | case semibold 16 | case bold 17 | case ultralight 18 | case heavy 19 | case black 20 | 21 | var swiftUIFontWeight: Font.Weight { 22 | switch self { 23 | case .light: return .light 24 | case .thin: return .thin 25 | case .medium: return .medium 26 | case .regular: return .regular 27 | case .semibold: return .semibold 28 | case .bold: return .bold 29 | case .ultralight: return .ultraLight 30 | case .heavy: return .heavy 31 | case .black: return .black 32 | } 33 | } 34 | 35 | var uiFontWeight: UIFont.Weight { 36 | switch self { 37 | case .light: return .light 38 | case .thin: return .thin 39 | case .medium: return .medium 40 | case .regular: return .regular 41 | case .semibold: return .semibold 42 | case .bold: return .bold 43 | case .ultralight: return .ultraLight 44 | case .heavy: return .heavy 45 | case .black: return .black 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /FlexiblePickerExample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /FlexiblePickerExample/SelectableModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectableModel.swift 3 | // FlexiblePickerExample 4 | // 5 | // Created by Jędrzej Chołuj on 23/01/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol Selectable: Identifiable, Hashable { 11 | var displayedName: String { get } 12 | var isSelected: Bool { get set } 13 | 14 | init(displayedName: String) 15 | } 16 | 17 | struct SelectableModel: Selectable, Identifiable { 18 | static func == (lhs: SelectableModel, rhs: SelectableModel) -> Bool { 19 | lhs.id == rhs.id 20 | } 21 | 22 | func hash(into hasher: inout Hasher) { 23 | hasher.combine(self.id) 24 | } 25 | 26 | var displayedName: String 27 | var isSelected: Bool 28 | let id: UUID = UUID() 29 | 30 | init(displayedName: String) { 31 | self.displayedName = displayedName 32 | self.isSelected = false 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /FlexiblePickerExample/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // FlexiblePickerExample 4 | // 5 | // Created by Jędrzej Chołuj on 23/01/2022. 6 | // 7 | 8 | import UIKit 9 | 10 | extension String { 11 | func getWidth(with font: UIFont) -> CGFloat { 12 | let fontAttributes = [NSAttributedString.Key.font: font] 13 | let size = self.size(withAttributes: fontAttributes) 14 | return size.width 15 | } 16 | 17 | func getHeight(with font: UIFont) -> CGFloat { 18 | let fontAttributes = [NSAttributedString.Key.font: font] 19 | let size = self.size(withAttributes: fontAttributes) 20 | return size.height 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /FlexiblePickerExampleTests/FlexiblePickerExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlexiblePickerExampleTests.swift 3 | // FlexiblePickerExampleTests 4 | // 5 | // Created by Jędrzej Chołuj on 23/01/2022. 6 | // 7 | 8 | import XCTest 9 | @testable import FlexiblePickerExample 10 | 11 | class FlexiblePickerExampleTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /FlexiblePickerExampleUITests/FlexiblePickerExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlexiblePickerExampleUITests.swift 3 | // FlexiblePickerExampleUITests 4 | // 5 | // Created by Jędrzej Chołuj on 23/01/2022. 6 | // 7 | 8 | import XCTest 9 | 10 | class FlexiblePickerExampleUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /FlexiblePickerExampleUITests/FlexiblePickerExampleUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlexiblePickerExampleUITestsLaunchTests.swift 3 | // FlexiblePickerExampleUITests 4 | // 5 | // Created by Jędrzej Chołuj on 23/01/2022. 6 | // 7 | 8 | import XCTest 9 | 10 | class FlexiblePickerExampleUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flexible Picker 2 | 3 | 4 | The implementation of custom Flexible Picker. Thanks to that view component we can create selectable view with many options to select from. Moreover, it is a reusable and flexible component, as we can provide many different data types as input, as long as they are conforming to Selectable protocol. 5 |

6 | animated 7 |


8 | 9 | ## Customization 10 | Flexible Picker is full customizable, as we can change: 11 | 12 | - subviews alignment 13 | - border color 14 | - text color 15 | - background color 16 | - font size and weight 17 | - selected background color 18 | - disable selection 19 | 20 | ## Implementation description 21 | 22 | The implementation has been described in the Medium's article:
23 | https://betterprogramming.pub/flexible-picker-with-swiftui-5817ffe9fddf 24 | 25 | ## Libraries 26 | 27 | - SwiftUI 28 | 29 | ## License 30 | 31 | MIT 32 | --------------------------------------------------------------------------------