├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcuserdata │ └── namkennic.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Example ├── FrameLayoutKit.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── FrameLayoutKit-Example.xcscheme ├── FrameLayoutKit.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcuserdata │ │ └── namkennic.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ ├── WorkspaceSettings.xcsettings │ │ └── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist ├── FrameLayoutKit │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── CardView.swift │ ├── FrameLayoutKit_Example.entitlements │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── collapse_24x24.imageset │ │ │ ├── Contents.json │ │ │ ├── collapse_24x24@1x.png │ │ │ ├── collapse_24x24@2x.png │ │ │ └── collapse_24x24@3x.png │ │ ├── earth_48x48.imageset │ │ │ ├── Contents.json │ │ │ ├── earth_48x48@1x.png │ │ │ ├── earth_48x48@2x.png │ │ │ └── earth_48x48@3x.png │ │ ├── expand_24x24.imageset │ │ │ ├── Contents.json │ │ │ ├── expand_24x24@1x.png │ │ │ ├── expand_24x24@2x.png │ │ │ └── expand_24x24@3x.png │ │ └── rocket_32x32.imageset │ │ │ ├── Contents.json │ │ │ ├── rocket_32x32@1x.png │ │ │ ├── rocket_32x32@2x.png │ │ │ └── rocket_32x32@3x.png │ ├── Info.plist │ ├── NumberPadView.swift │ ├── TagListView.swift │ └── ViewController.swift ├── Podfile └── Podfile.lock ├── FrameLayoutKit.podspec ├── FrameLayoutKit └── Classes │ ├── .gitkeep │ ├── DoubleFrameLayout.swift │ ├── Extensions │ ├── DoubleFrameLayout+Chainable.swift │ ├── FlowFrameLayout+Chainable.swift │ ├── FrameLayout+Chainable.swift │ ├── FrameLayout+Extension.swift │ ├── GridFrameLayout+Chainable.swift │ ├── ScrollStackView+Chainable.swift │ ├── StackFrameLayout+Chainable.swift │ └── StackFrameLayout+DSL.swift │ ├── FLSkeletonView.swift │ ├── FLView.swift │ ├── FlowFrameLayout.swift │ ├── FrameLayout.swift │ ├── GridFrameLayout.swift │ ├── ScrollStackView.swift │ └── StackFrameLayout.swift ├── LICENSE ├── Package.swift ├── README.md ├── _Pods.xcodeproj └── images ├── FrameLayoutKit.png ├── banner.jpg ├── bechmark.png ├── example_1.png ├── example_2.png ├── example_3.png ├── frameLayoutSyntax.png ├── helloWorld.png └── no_constraint.png /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/namkennic.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | FrameLayoutKit.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | FrameLayoutKit 16 | 17 | primary 18 | 19 | 20 | FrameLayoutKitTests 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 11 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; }; 12 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 13 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 14 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 15 | 632A5FA524651ACB008DD793 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 632A5FA424651ACB008DD793 /* CardView.swift */; }; 16 | 632A5FA724651B14008DD793 /* NumberPadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 632A5FA624651B14008DD793 /* NumberPadView.swift */; }; 17 | 635B4D7F256530B70006C5B8 /* TagListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 635B4D7E256530B70006C5B8 /* TagListView.swift */; }; 18 | B0779CC809BA3EBEA8A7BA24 /* Pods_FrameLayoutKit_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88079A93676318D377A9E051 /* Pods_FrameLayoutKit_Example.framework */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 23024746510C90D82DBB9804 /* Pods-FrameLayoutKit_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameLayoutKit_Example.debug.xcconfig"; path = "Target Support Files/Pods-FrameLayoutKit_Example/Pods-FrameLayoutKit_Example.debug.xcconfig"; sourceTree = ""; }; 23 | 56C814E0EDE5EC3DE2FA2FE4 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 24 | 5B2624AAEE48DCD02D4F0B67 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 25 | 607FACD01AFB9204008FA782 /* FrameLayoutKit_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FrameLayoutKit_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 27 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 28 | 607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 29 | 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 30 | 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 31 | 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 32 | 632A5FA424651ACB008DD793 /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; 33 | 632A5FA624651B14008DD793 /* NumberPadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberPadView.swift; sourceTree = ""; }; 34 | 635B4D7E256530B70006C5B8 /* TagListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagListView.swift; sourceTree = ""; }; 35 | 6369D7AB2430D68E00C60584 /* FrameLayoutKit_Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FrameLayoutKit_Example.entitlements; sourceTree = ""; }; 36 | 88079A93676318D377A9E051 /* Pods_FrameLayoutKit_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FrameLayoutKit_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 8A9D6655F23A4A71202D1A3A /* Pods-FrameLayoutKit_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameLayoutKit_Tests.debug.xcconfig"; path = "Target Support Files/Pods-FrameLayoutKit_Tests/Pods-FrameLayoutKit_Tests.debug.xcconfig"; sourceTree = ""; }; 38 | 8EDFCE92FF181156BE7A04B8 /* Pods_FrameLayoutKit_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FrameLayoutKit_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | AB5553AC3A0BA7787C1FD21B /* Pods-FrameLayoutKit_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameLayoutKit_Example.release.xcconfig"; path = "Target Support Files/Pods-FrameLayoutKit_Example/Pods-FrameLayoutKit_Example.release.xcconfig"; sourceTree = ""; }; 40 | B4FB4222A4679285B6F26B60 /* FrameLayoutKit.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = FrameLayoutKit.podspec; path = ../FrameLayoutKit.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 41 | CC06034BECC9436AF0A3C901 /* Pods-FrameLayoutKit_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameLayoutKit_Tests.release.xcconfig"; path = "Target Support Files/Pods-FrameLayoutKit_Tests/Pods-FrameLayoutKit_Tests.release.xcconfig"; sourceTree = ""; }; 42 | /* End PBXFileReference section */ 43 | 44 | /* Begin PBXFrameworksBuildPhase section */ 45 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 46 | isa = PBXFrameworksBuildPhase; 47 | buildActionMask = 2147483647; 48 | files = ( 49 | B0779CC809BA3EBEA8A7BA24 /* Pods_FrameLayoutKit_Example.framework in Frameworks */, 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXFrameworksBuildPhase section */ 54 | 55 | /* Begin PBXGroup section */ 56 | 196EE48AEFF07E28138992B3 /* Pods */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 23024746510C90D82DBB9804 /* Pods-FrameLayoutKit_Example.debug.xcconfig */, 60 | AB5553AC3A0BA7787C1FD21B /* Pods-FrameLayoutKit_Example.release.xcconfig */, 61 | 8A9D6655F23A4A71202D1A3A /* Pods-FrameLayoutKit_Tests.debug.xcconfig */, 62 | CC06034BECC9436AF0A3C901 /* Pods-FrameLayoutKit_Tests.release.xcconfig */, 63 | ); 64 | path = Pods; 65 | sourceTree = ""; 66 | }; 67 | 607FACC71AFB9204008FA782 = { 68 | isa = PBXGroup; 69 | children = ( 70 | 607FACF51AFB993E008FA782 /* Podspec Metadata */, 71 | 607FACD21AFB9204008FA782 /* Example for FrameLayoutKit */, 72 | 607FACD11AFB9204008FA782 /* Products */, 73 | 196EE48AEFF07E28138992B3 /* Pods */, 74 | AAFC5113C9041CD699372BFA /* Frameworks */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 607FACD11AFB9204008FA782 /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 607FACD01AFB9204008FA782 /* FrameLayoutKit_Example.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 607FACD21AFB9204008FA782 /* Example for FrameLayoutKit */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */, 90 | 607FACD71AFB9204008FA782 /* ViewController.swift */, 91 | 632A5FA424651ACB008DD793 /* CardView.swift */, 92 | 632A5FA624651B14008DD793 /* NumberPadView.swift */, 93 | 635B4D7E256530B70006C5B8 /* TagListView.swift */, 94 | 607FACD91AFB9204008FA782 /* Main.storyboard */, 95 | 607FACDC1AFB9204008FA782 /* Images.xcassets */, 96 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 97 | 6369D7AB2430D68E00C60584 /* FrameLayoutKit_Example.entitlements */, 98 | 607FACD31AFB9204008FA782 /* Supporting Files */, 99 | ); 100 | name = "Example for FrameLayoutKit"; 101 | path = FrameLayoutKit; 102 | sourceTree = ""; 103 | }; 104 | 607FACD31AFB9204008FA782 /* Supporting Files */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 607FACD41AFB9204008FA782 /* Info.plist */, 108 | ); 109 | name = "Supporting Files"; 110 | sourceTree = ""; 111 | }; 112 | 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | B4FB4222A4679285B6F26B60 /* FrameLayoutKit.podspec */, 116 | 5B2624AAEE48DCD02D4F0B67 /* README.md */, 117 | 56C814E0EDE5EC3DE2FA2FE4 /* LICENSE */, 118 | ); 119 | name = "Podspec Metadata"; 120 | sourceTree = ""; 121 | }; 122 | AAFC5113C9041CD699372BFA /* Frameworks */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 88079A93676318D377A9E051 /* Pods_FrameLayoutKit_Example.framework */, 126 | 8EDFCE92FF181156BE7A04B8 /* Pods_FrameLayoutKit_Tests.framework */, 127 | ); 128 | name = Frameworks; 129 | sourceTree = ""; 130 | }; 131 | /* End PBXGroup section */ 132 | 133 | /* Begin PBXNativeTarget section */ 134 | 607FACCF1AFB9204008FA782 /* FrameLayoutKit_Example */ = { 135 | isa = PBXNativeTarget; 136 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "FrameLayoutKit_Example" */; 137 | buildPhases = ( 138 | 84B771FF5FFD4E26B6BC16D3 /* [CP] Check Pods Manifest.lock */, 139 | 607FACCC1AFB9204008FA782 /* Sources */, 140 | 607FACCD1AFB9204008FA782 /* Frameworks */, 141 | 607FACCE1AFB9204008FA782 /* Resources */, 142 | 34E235A52871F92793844191 /* [CP] Embed Pods Frameworks */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | ); 148 | name = FrameLayoutKit_Example; 149 | productName = FrameLayoutKit; 150 | productReference = 607FACD01AFB9204008FA782 /* FrameLayoutKit_Example.app */; 151 | productType = "com.apple.product-type.application"; 152 | }; 153 | /* End PBXNativeTarget section */ 154 | 155 | /* Begin PBXProject section */ 156 | 607FACC81AFB9204008FA782 /* Project object */ = { 157 | isa = PBXProject; 158 | attributes = { 159 | LastSwiftUpdateCheck = 0830; 160 | LastUpgradeCheck = 0940; 161 | ORGANIZATIONNAME = CocoaPods; 162 | TargetAttributes = { 163 | 607FACCF1AFB9204008FA782 = { 164 | CreatedOnToolsVersion = 6.3.1; 165 | DevelopmentTeam = 385YL4KG69; 166 | LastSwiftMigration = 0900; 167 | }; 168 | }; 169 | }; 170 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "FrameLayoutKit" */; 171 | compatibilityVersion = "Xcode 3.2"; 172 | developmentRegion = English; 173 | hasScannedForEncodings = 0; 174 | knownRegions = ( 175 | English, 176 | en, 177 | Base, 178 | ); 179 | mainGroup = 607FACC71AFB9204008FA782; 180 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 181 | projectDirPath = ""; 182 | projectRoot = ""; 183 | targets = ( 184 | 607FACCF1AFB9204008FA782 /* FrameLayoutKit_Example */, 185 | ); 186 | }; 187 | /* End PBXProject section */ 188 | 189 | /* Begin PBXResourcesBuildPhase section */ 190 | 607FACCE1AFB9204008FA782 /* Resources */ = { 191 | isa = PBXResourcesBuildPhase; 192 | buildActionMask = 2147483647; 193 | files = ( 194 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, 195 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, 196 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, 197 | ); 198 | runOnlyForDeploymentPostprocessing = 0; 199 | }; 200 | /* End PBXResourcesBuildPhase section */ 201 | 202 | /* Begin PBXShellScriptBuildPhase section */ 203 | 34E235A52871F92793844191 /* [CP] Embed Pods Frameworks */ = { 204 | isa = PBXShellScriptBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | ); 208 | inputPaths = ( 209 | "${PODS_ROOT}/Target Support Files/Pods-FrameLayoutKit_Example/Pods-FrameLayoutKit_Example-frameworks.sh", 210 | "${BUILT_PRODUCTS_DIR}/FrameLayoutKit/FrameLayoutKit.framework", 211 | ); 212 | name = "[CP] Embed Pods Frameworks"; 213 | outputPaths = ( 214 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FrameLayoutKit.framework", 215 | ); 216 | runOnlyForDeploymentPostprocessing = 0; 217 | shellPath = /bin/sh; 218 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FrameLayoutKit_Example/Pods-FrameLayoutKit_Example-frameworks.sh\"\n"; 219 | showEnvVarsInLog = 0; 220 | }; 221 | 84B771FF5FFD4E26B6BC16D3 /* [CP] Check Pods Manifest.lock */ = { 222 | isa = PBXShellScriptBuildPhase; 223 | buildActionMask = 2147483647; 224 | files = ( 225 | ); 226 | inputFileListPaths = ( 227 | ); 228 | inputPaths = ( 229 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 230 | "${PODS_ROOT}/Manifest.lock", 231 | ); 232 | name = "[CP] Check Pods Manifest.lock"; 233 | outputFileListPaths = ( 234 | ); 235 | outputPaths = ( 236 | "$(DERIVED_FILE_DIR)/Pods-FrameLayoutKit_Example-checkManifestLockResult.txt", 237 | ); 238 | runOnlyForDeploymentPostprocessing = 0; 239 | shellPath = /bin/sh; 240 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 241 | showEnvVarsInLog = 0; 242 | }; 243 | /* End PBXShellScriptBuildPhase section */ 244 | 245 | /* Begin PBXSourcesBuildPhase section */ 246 | 607FACCC1AFB9204008FA782 /* Sources */ = { 247 | isa = PBXSourcesBuildPhase; 248 | buildActionMask = 2147483647; 249 | files = ( 250 | 635B4D7F256530B70006C5B8 /* TagListView.swift in Sources */, 251 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */, 252 | 632A5FA524651ACB008DD793 /* CardView.swift in Sources */, 253 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, 254 | 632A5FA724651B14008DD793 /* NumberPadView.swift in Sources */, 255 | ); 256 | runOnlyForDeploymentPostprocessing = 0; 257 | }; 258 | /* End PBXSourcesBuildPhase section */ 259 | 260 | /* Begin PBXVariantGroup section */ 261 | 607FACD91AFB9204008FA782 /* Main.storyboard */ = { 262 | isa = PBXVariantGroup; 263 | children = ( 264 | 607FACDA1AFB9204008FA782 /* Base */, 265 | ); 266 | name = Main.storyboard; 267 | sourceTree = ""; 268 | }; 269 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { 270 | isa = PBXVariantGroup; 271 | children = ( 272 | 607FACDF1AFB9204008FA782 /* Base */, 273 | ); 274 | name = LaunchScreen.xib; 275 | sourceTree = ""; 276 | }; 277 | /* End PBXVariantGroup section */ 278 | 279 | /* Begin XCBuildConfiguration section */ 280 | 607FACED1AFB9204008FA782 /* Debug */ = { 281 | isa = XCBuildConfiguration; 282 | buildSettings = { 283 | ALWAYS_SEARCH_USER_PATHS = NO; 284 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 285 | CLANG_CXX_LIBRARY = "libc++"; 286 | CLANG_ENABLE_MODULES = YES; 287 | CLANG_ENABLE_OBJC_ARC = YES; 288 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 289 | CLANG_WARN_BOOL_CONVERSION = YES; 290 | CLANG_WARN_COMMA = YES; 291 | CLANG_WARN_CONSTANT_CONVERSION = YES; 292 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 293 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 294 | CLANG_WARN_EMPTY_BODY = YES; 295 | CLANG_WARN_ENUM_CONVERSION = YES; 296 | CLANG_WARN_INFINITE_RECURSION = YES; 297 | CLANG_WARN_INT_CONVERSION = YES; 298 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 299 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 300 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 301 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 302 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 303 | CLANG_WARN_STRICT_PROTOTYPES = YES; 304 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 305 | CLANG_WARN_UNREACHABLE_CODE = YES; 306 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 307 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 308 | COPY_PHASE_STRIP = NO; 309 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 310 | ENABLE_STRICT_OBJC_MSGSEND = YES; 311 | ENABLE_TESTABILITY = YES; 312 | GCC_C_LANGUAGE_STANDARD = gnu99; 313 | GCC_DYNAMIC_NO_PIC = NO; 314 | GCC_NO_COMMON_BLOCKS = YES; 315 | GCC_OPTIMIZATION_LEVEL = 0; 316 | GCC_PREPROCESSOR_DEFINITIONS = ( 317 | "DEBUG=1", 318 | "$(inherited)", 319 | ); 320 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 321 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 322 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 323 | GCC_WARN_UNDECLARED_SELECTOR = YES; 324 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 325 | GCC_WARN_UNUSED_FUNCTION = YES; 326 | GCC_WARN_UNUSED_VARIABLE = YES; 327 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 328 | MTL_ENABLE_DEBUG_INFO = YES; 329 | ONLY_ACTIVE_ARCH = YES; 330 | SDKROOT = iphoneos; 331 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 332 | SWIFT_VERSION = 5.0; 333 | }; 334 | name = Debug; 335 | }; 336 | 607FACEE1AFB9204008FA782 /* Release */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | ALWAYS_SEARCH_USER_PATHS = NO; 340 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 341 | CLANG_CXX_LIBRARY = "libc++"; 342 | CLANG_ENABLE_MODULES = YES; 343 | CLANG_ENABLE_OBJC_ARC = YES; 344 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 345 | CLANG_WARN_BOOL_CONVERSION = YES; 346 | CLANG_WARN_COMMA = YES; 347 | CLANG_WARN_CONSTANT_CONVERSION = YES; 348 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 349 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 350 | CLANG_WARN_EMPTY_BODY = YES; 351 | CLANG_WARN_ENUM_CONVERSION = YES; 352 | CLANG_WARN_INFINITE_RECURSION = YES; 353 | CLANG_WARN_INT_CONVERSION = YES; 354 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 355 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 356 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 357 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 358 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 359 | CLANG_WARN_STRICT_PROTOTYPES = YES; 360 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 361 | CLANG_WARN_UNREACHABLE_CODE = YES; 362 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 363 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 364 | COPY_PHASE_STRIP = NO; 365 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 366 | ENABLE_NS_ASSERTIONS = NO; 367 | ENABLE_STRICT_OBJC_MSGSEND = YES; 368 | GCC_C_LANGUAGE_STANDARD = gnu99; 369 | GCC_NO_COMMON_BLOCKS = YES; 370 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 371 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 372 | GCC_WARN_UNDECLARED_SELECTOR = YES; 373 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 374 | GCC_WARN_UNUSED_FUNCTION = YES; 375 | GCC_WARN_UNUSED_VARIABLE = YES; 376 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 377 | MTL_ENABLE_DEBUG_INFO = NO; 378 | SDKROOT = iphoneos; 379 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 380 | SWIFT_VERSION = 5.0; 381 | VALIDATE_PRODUCT = YES; 382 | }; 383 | name = Release; 384 | }; 385 | 607FACF01AFB9204008FA782 /* Debug */ = { 386 | isa = XCBuildConfiguration; 387 | baseConfigurationReference = 23024746510C90D82DBB9804 /* Pods-FrameLayoutKit_Example.debug.xcconfig */; 388 | buildSettings = { 389 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 390 | CODE_SIGN_ENTITLEMENTS = FrameLayoutKit/FrameLayoutKit_Example.entitlements; 391 | CODE_SIGN_IDENTITY = "iPhone Developer"; 392 | DEVELOPMENT_TEAM = 385YL4KG69; 393 | INFOPLIST_FILE = FrameLayoutKit/Info.plist; 394 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 395 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 396 | MODULE_NAME = ExampleApp; 397 | PRODUCT_BUNDLE_IDENTIFIER = com.kennic.FrameLayoutExample; 398 | PRODUCT_NAME = "$(TARGET_NAME)"; 399 | SUPPORTS_MACCATALYST = YES; 400 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 401 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 402 | SWIFT_VERSION = 5.0; 403 | TARGETED_DEVICE_FAMILY = "1,2,6"; 404 | }; 405 | name = Debug; 406 | }; 407 | 607FACF11AFB9204008FA782 /* Release */ = { 408 | isa = XCBuildConfiguration; 409 | baseConfigurationReference = AB5553AC3A0BA7787C1FD21B /* Pods-FrameLayoutKit_Example.release.xcconfig */; 410 | buildSettings = { 411 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 412 | CODE_SIGN_ENTITLEMENTS = FrameLayoutKit/FrameLayoutKit_Example.entitlements; 413 | CODE_SIGN_IDENTITY = "iPhone Developer"; 414 | DEVELOPMENT_TEAM = 385YL4KG69; 415 | INFOPLIST_FILE = FrameLayoutKit/Info.plist; 416 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 417 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 418 | MODULE_NAME = ExampleApp; 419 | PRODUCT_BUNDLE_IDENTIFIER = com.kennic.FrameLayoutExample; 420 | PRODUCT_NAME = "$(TARGET_NAME)"; 421 | SUPPORTS_MACCATALYST = YES; 422 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 423 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 424 | SWIFT_VERSION = 5.0; 425 | TARGETED_DEVICE_FAMILY = "1,2,6"; 426 | }; 427 | name = Release; 428 | }; 429 | /* End XCBuildConfiguration section */ 430 | 431 | /* Begin XCConfigurationList section */ 432 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "FrameLayoutKit" */ = { 433 | isa = XCConfigurationList; 434 | buildConfigurations = ( 435 | 607FACED1AFB9204008FA782 /* Debug */, 436 | 607FACEE1AFB9204008FA782 /* Release */, 437 | ); 438 | defaultConfigurationIsVisible = 0; 439 | defaultConfigurationName = Release; 440 | }; 441 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "FrameLayoutKit_Example" */ = { 442 | isa = XCConfigurationList; 443 | buildConfigurations = ( 444 | 607FACF01AFB9204008FA782 /* Debug */, 445 | 607FACF11AFB9204008FA782 /* Release */, 446 | ); 447 | defaultConfigurationIsVisible = 0; 448 | defaultConfigurationName = Release; 449 | }; 450 | /* End XCConfigurationList section */ 451 | }; 452 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 453 | } 454 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit.xcodeproj/xcshareddata/xcschemes/FrameLayoutKit-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 78 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit.xcworkspace/xcuserdata/namkennic.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/Example/FrameLayoutKit.xcworkspace/xcuserdata/namkennic.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Example/FrameLayoutKit.xcworkspace/xcuserdata/namkennic.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | IssueFilterStyle 12 | ShowActiveSchemeOnly 13 | LiveSourceIssuesEnabled 14 | 15 | ShowSharedSchemesAutomaticallyEnabled 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit.xcworkspace/xcuserdata/namkennic.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 07/12/2018. 6 | // Copyright (c) 2018 Nam Kennic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/CardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardView.swift 3 | // FrameLayoutKit_Example 4 | // 5 | // Created by Nam Kennic on 5/8/20. 6 | // Copyright © 2020 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FrameLayoutKit 11 | 12 | class CardView: UIView { 13 | let earthImageView = UIImageView(image: UIImage(named: "earth_48x48")) 14 | let rocketImageView = UIImageView(image: UIImage(named: "rocket_32x32")) 15 | let nameLabel = UILabel() 16 | let titleLabel = UILabel() 17 | let dateLabel = UILabel() 18 | let messageLabel = UILabel() 19 | let expandButton = UIButton() 20 | let frameLayout = HStackLayout { 21 | $0.spacing = 15.0 22 | $0.padding(15) 23 | } 24 | let blueView = UIView() 25 | let redView = UIView() 26 | var messageFrameLayout: FrameLayout! 27 | 28 | var onSizeChanged: ((CardView) -> Void)? 29 | 30 | init() { 31 | super.init(frame: .zero) 32 | 33 | layer.backgroundColor = UIColor.white.cgColor 34 | layer.shadowColor = UIColor.black.withAlphaComponent(0.5).cgColor 35 | layer.shadowOffset = .zero 36 | layer.shadowRadius = 5 37 | layer.shadowOpacity = 0.6 38 | layer.masksToBounds = false 39 | 40 | blueView.backgroundColor = .systemBlue 41 | redView.backgroundColor = .systemRed 42 | 43 | expandButton.setImage(UIImage(named: "collapse_24x24"), for: .normal) 44 | expandButton.setImage(UIImage(named: "expand_24x24"), for: .selected) 45 | expandButton.addTarget(self, action: #selector(onButtonTap), for: .touchUpInside) 46 | 47 | nameLabel.font = .systemFont(ofSize: 18, weight: .bold) 48 | nameLabel.text = "John Appleseed" 49 | 50 | titleLabel.textAlignment = .center 51 | titleLabel.font = .systemFont(ofSize: 14, weight: .regular) 52 | titleLabel.text = "Admin" 53 | titleLabel.textColor = .white 54 | titleLabel.backgroundColor = .purple 55 | titleLabel.layer.cornerRadius = 4.0 56 | titleLabel.layer.masksToBounds = true 57 | 58 | dateLabel.font = .systemFont(ofSize: 15, weight: .thin) 59 | dateLabel.text = "\(Date())" 60 | 61 | messageLabel.font = .systemFont(ofSize: 18, weight: .regular) 62 | messageLabel.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum" 63 | 64 | [nameLabel, dateLabel, messageLabel].forEach { 65 | $0.numberOfLines = 0 66 | $0.textColor = .black 67 | } 68 | 69 | [blueView, redView, earthImageView, rocketImageView, nameLabel, titleLabel, dateLabel, messageLabel, expandButton, frameLayout].forEach { addSubview($0) } 70 | 71 | // DSL syntax: 72 | 73 | frameLayout + VStackView { 74 | StackItem(earthImageView).align(vertical: .top, horizontal: .center) 75 | FlexibleSpace(10) 76 | StackItem(rocketImageView).align(vertical: .center, horizontal: .center).bindFrame(to: redView) 77 | }.bindFrame(to: blueView) 78 | 79 | frameLayout + VStackView { 80 | HStackView { 81 | StackItem(nameLabel) 82 | StackItem(titleLabel) 83 | FlexibleSpace() 84 | StackItem(expandButton) 85 | }.spacing(10) 86 | dateLabel 87 | StackItem(messageLabel).assign(to: &messageFrameLayout) 88 | FlexibleSpace() 89 | HStackView { 90 | Label(.yellow) 91 | Label(.green) 92 | Label(.brown) 93 | Label(.systemPink) 94 | Label(.blue) 95 | }.each { layout, _, _ in 96 | layout.didLayoutSubviewsBlock = { 97 | guard let label = $0.targetView as? UILabel else { return } 98 | let size = $0.frame.size 99 | label.text = "\(size.width) x \(size.height)" 100 | } 101 | }.distribution(.split(ratio: [0.5, -1, -1, 0.3])).spacing(10) 102 | }.flexible().spacing(5) 103 | 104 | 105 | // Standard syntax: 106 | /* 107 | frameLayout + VStackLayout { 108 | ($0 + earthImageView).alignment = (.top, .center) 109 | ($0 + 0).flexible() 110 | ($0 + rocketImageView).align(vertical: .center, horizontal: .center).bindFrame(to: redView) // Example of binding views: redView will stick together with rocketImageView 111 | $0.bindFrame(to: blueView) // Example of binding views: blueView will stick together with this VStackLayout 112 | } 113 | frameLayout + VStackLayout { 114 | $0 + HStackLayout { 115 | ($0 + nameLabel)//.flexible(ratio: 0.8) // takes 80% of flexible width, uncomment to try it 116 | ($0 + titleLabel).extends(size: CGSize(width: 10, height: 0)) 117 | ($0 + 0).flexible() 118 | $0 + expandButton 119 | 120 | $0.spacing(10) 121 | } 122 | $0 + dateLabel 123 | // $0 + 10.0 124 | messageFrameLayout = ($0 + messageLabel) 125 | 126 | //--- Example of split(ratio) distribution --- 127 | ($0 + 0.0).flexible() 128 | $0 + HStackLayout { 129 | $0.distribution = .split(ratio: [0.5, -1, -1, 0.3]) // -1 means auto 130 | $0.spacing = 10 131 | 132 | ($0 + [Label(.yellow), Label(.green), Label(.brown), Label(.systemPink), Label(.blue)]).forEach { 133 | $0.didLayoutSubviewsBlock = { sender in 134 | if let label = sender.targetView as? UILabel { 135 | let size = sender.frame.size 136 | label.text = "\(size.width) x \(size.height)" 137 | } 138 | } 139 | } 140 | } 141 | //--- 142 | 143 | $0.flexible() 144 | .spacing(5) 145 | } 146 | */ 147 | 148 | // frameLayout 149 | // .debug(true) 150 | // .isSkeletonMode(true) 151 | 152 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { 153 | self.frameLayout.isSkeletonMode(false) 154 | } 155 | } 156 | 157 | func Label(_ color: UIColor, _ text: String = " ") -> UILabel { 158 | let label = UILabel() 159 | label.textColor = .black 160 | label.backgroundColor = color 161 | label.text = text 162 | return label 163 | } 164 | 165 | required init?(coder aDecoder: NSCoder) { 166 | fatalError("init(coder:) has not been implemented") 167 | } 168 | 169 | override func sizeThatFits(_ size: CGSize) -> CGSize { 170 | return frameLayout.sizeThatFits(size) 171 | } 172 | 173 | override func layoutSubviews() { 174 | super.layoutSubviews() 175 | frameLayout.frame = bounds 176 | } 177 | 178 | @objc func onButtonTap() { 179 | messageFrameLayout.isEnabled.toggle() 180 | expandButton.isSelected = !messageFrameLayout.isEnabled 181 | UIView.animate(withDuration: 0.25) { self.messageLabel.alpha = self.messageFrameLayout.isEnabled ? 1.0 : 0.0 } 182 | 183 | setNeedsLayout() 184 | onSizeChanged?(self) 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/FrameLayoutKit_Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/collapse_24x24.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "collapse_24x24@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "collapse_24x24@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "collapse_24x24@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/collapse_24x24.imageset/collapse_24x24@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/Example/FrameLayoutKit/Images.xcassets/collapse_24x24.imageset/collapse_24x24@1x.png -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/collapse_24x24.imageset/collapse_24x24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/Example/FrameLayoutKit/Images.xcassets/collapse_24x24.imageset/collapse_24x24@2x.png -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/collapse_24x24.imageset/collapse_24x24@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/Example/FrameLayoutKit/Images.xcassets/collapse_24x24.imageset/collapse_24x24@3x.png -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/earth_48x48.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "earth_48x48@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "earth_48x48@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "earth_48x48@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/earth_48x48.imageset/earth_48x48@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/Example/FrameLayoutKit/Images.xcassets/earth_48x48.imageset/earth_48x48@1x.png -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/earth_48x48.imageset/earth_48x48@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/Example/FrameLayoutKit/Images.xcassets/earth_48x48.imageset/earth_48x48@2x.png -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/earth_48x48.imageset/earth_48x48@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/Example/FrameLayoutKit/Images.xcassets/earth_48x48.imageset/earth_48x48@3x.png -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/expand_24x24.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "expand_24x24@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "expand_24x24@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "expand_24x24@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/expand_24x24.imageset/expand_24x24@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/Example/FrameLayoutKit/Images.xcassets/expand_24x24.imageset/expand_24x24@1x.png -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/expand_24x24.imageset/expand_24x24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/Example/FrameLayoutKit/Images.xcassets/expand_24x24.imageset/expand_24x24@2x.png -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/expand_24x24.imageset/expand_24x24@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/Example/FrameLayoutKit/Images.xcassets/expand_24x24.imageset/expand_24x24@3x.png -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/rocket_32x32.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "rocket_32x32@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "rocket_32x32@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "rocket_32x32@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/rocket_32x32.imageset/rocket_32x32@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/Example/FrameLayoutKit/Images.xcassets/rocket_32x32.imageset/rocket_32x32@1x.png -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/rocket_32x32.imageset/rocket_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/Example/FrameLayoutKit/Images.xcassets/rocket_32x32.imageset/rocket_32x32@2x.png -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Images.xcassets/rocket_32x32.imageset/rocket_32x32@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/Example/FrameLayoutKit/Images.xcassets/rocket_32x32.imageset/rocket_32x32@3x.png -------------------------------------------------------------------------------- /Example/FrameLayoutKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/NumberPadView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberPadView.swift 3 | // FrameLayoutKit_Example 4 | // 5 | // Created by Nam Kennic on 5/8/20. 6 | // Copyright © 2020 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FrameLayoutKit 11 | 12 | class NumberPadView: UIView { 13 | let frameLayout = GridFrameLayout(axis: .horizontal, column: 3, rows: 4) 14 | let titleMap = "1 2 3 4 5 6 7 8 9 * 0 #" 15 | let colors: [UIColor] = [.red, .green, .blue, .brown, .gray, .yellow, .magenta, .black, .orange, .purple] 16 | 17 | fileprivate func color(index: Int? = nil) -> UIColor { 18 | let finalIndex = (index ?? Int(arc4random())) % colors.count 19 | return colors[finalIndex].withAlphaComponent(0.4) 20 | } 21 | 22 | init() { 23 | super.init(frame: .zero) 24 | backgroundColor = UIColor.black.withAlphaComponent(0.1) 25 | 26 | let titles = titleMap.components(separatedBy: " ") 27 | var i = 0 28 | let buttons = titles.map { (title) -> UIButton in 29 | let button = UIButton() 30 | button.setTitle(title, for: .normal) 31 | button.backgroundColor = color(index: i) 32 | button.showsTouchWhenHighlighted = true 33 | button.titleLabel?.font = .systemFont(ofSize: 24, weight: .medium) 34 | i += 1 35 | return button 36 | } 37 | 38 | frameLayout.edgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 39 | frameLayout.horizontalSpacing = 5 40 | frameLayout.verticalSpacing = 5 41 | // frameLayout.minColumnWidth = 150 42 | frameLayout.minRowHeight = 100 43 | frameLayout.isAutoSize = false 44 | frameLayout.views = buttons 45 | frameLayout.isUserInteractionEnabled = true 46 | addSubview(frameLayout) 47 | } 48 | 49 | required init?(coder aDecoder: NSCoder) { 50 | fatalError("init(coder:) has not been implemented") 51 | } 52 | 53 | override func sizeThatFits(_ size: CGSize) -> CGSize { 54 | return frameLayout.sizeThatFits(size) 55 | } 56 | 57 | override func layoutSubviews() { 58 | super.layoutSubviews() 59 | frameLayout.frame = bounds 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/TagListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagListView.swift 3 | // FrameLayoutKit_Example 4 | // 5 | // Created by Nam Kennic on 11/18/20. 6 | // Copyright © 2020 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FrameLayoutKit 11 | 12 | class TagListView: UIView { 13 | let flowLayout = FlowFrameLayout(axis: .horizontal) 14 | let addButton = UIButton() 15 | let removeButton = UIButton() 16 | let frameLayout = VStackLayout().spacing(4.0) 17 | let colors: [UIColor] = [.red, .green, .blue, .brown, .yellow, .magenta, .black, .orange, .purple, .systemPink] 18 | 19 | var onChanged: ((TagListView) -> Void)? 20 | 21 | init() { 22 | super.init(frame: .zero) 23 | 24 | backgroundColor = .gray 25 | 26 | flowLayout 27 | .interitemSpacing(4) 28 | .lineSpacing(4) 29 | .padding(top: 4, left: 4, bottom: 4, right: 4) 30 | .distribution(.left) 31 | 32 | addButton.setTitle("Add Item", for: .normal) 33 | addButton.backgroundColor = .systemBlue 34 | addButton.addTarget(self, action: #selector(addItem), for: .touchUpInside) 35 | addButton.showsTouchWhenHighlighted = true 36 | 37 | removeButton.setTitle("Remove Item", for: .normal) 38 | removeButton.backgroundColor = .systemPink 39 | removeButton.addTarget(self, action: #selector(removeLastItem), for: .touchUpInside) 40 | removeButton.showsTouchWhenHighlighted = true 41 | 42 | addSubview(flowLayout) 43 | addSubview(addButton) 44 | addSubview(removeButton) 45 | addSubview(frameLayout) 46 | 47 | // Disable justified last stack 48 | flowLayout.onNewStackBlock = { (sender, layout) in 49 | sender.stacks.forEach { 50 | $0.isJustified = $0 != sender.lastStack 51 | } 52 | } 53 | 54 | frameLayout + flowLayout 55 | frameLayout + HStackLayout { 56 | $0 + [removeButton, addButton] 57 | 58 | $0.distribution(.equal) 59 | .fixedSize(CGSize(width: 0, height: 50)) 60 | } 61 | } 62 | 63 | required init?(coder: NSCoder) { 64 | fatalError("init(coder:) has not been implemented") 65 | } 66 | 67 | override func sizeThatFits(_ size: CGSize) -> CGSize { 68 | return frameLayout.sizeThatFits(size) 69 | } 70 | 71 | override func layoutSubviews() { 72 | super.layoutSubviews() 73 | frameLayout.frame = bounds 74 | } 75 | 76 | fileprivate func color(index: Int? = nil) -> UIColor { 77 | let finalIndex = (index ?? Int(arc4random())) % colors.count 78 | return colors[finalIndex].withAlphaComponent(0.4) 79 | } 80 | 81 | @objc func addItem() { 82 | let count = flowLayout.views.count 83 | let title = Int.random(in: 0..<3) > 1 ? Int.random(in: 0..<1_000_000_000_000) : Int.random(in: 0..<100) 84 | let tagButton = UIButton() 85 | tagButton.titleLabel?.font = .systemFont(ofSize: 20) 86 | tagButton.titleLabel?.adjustsFontSizeToFitWidth = false 87 | tagButton.titleLabel?.lineBreakMode = .byClipping 88 | tagButton.setTitle(" [\(count)]: \(title) ", for: .normal) 89 | tagButton.setTitleColor(.white, for: .normal) 90 | tagButton.backgroundColor = color() 91 | tagButton.layer.cornerRadius = 5.0 92 | tagButton.layer.masksToBounds = true 93 | 94 | flowLayout + tagButton 95 | onChanged?(self) 96 | } 97 | 98 | @objc func removeLastItem() { 99 | guard let item = flowLayout.views.last else { return } 100 | item.removeFromSuperview() 101 | flowLayout.views.removeLast() 102 | onChanged?(self) 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /Example/FrameLayoutKit/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 07/12/2018. 6 | // Copyright (c) 2018 Nam Kennic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FrameLayoutKit 11 | 12 | class ViewController: UIViewController { 13 | let scrollStackView = ScrollStackView() 14 | let tagListView = TagListView() 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | view.backgroundColor = .lightGray 20 | 21 | var cardViews = [UIView]() 22 | for _ in 0..<5 { 23 | let cardView = CardView() 24 | cardView.onSizeChanged = { [weak self] sender in 25 | self?.scrollStackView.relayoutSubviews(animateDuration: 0.35) 26 | } 27 | cardViews.append(cardView) 28 | } 29 | 30 | tagListView.onChanged = { [weak self] sender in 31 | self?.scrollStackView.relayoutSubviews(animateDuration: 0.35) 32 | } 33 | 34 | scrollStackView + cardViews 35 | scrollStackView + NumberPadView() 36 | scrollStackView + tagListView 37 | 38 | scrollStackView 39 | .spacing(20) 40 | .padding(top: 50, left: 50, bottom: 50, right: 50) 41 | .distribution(.center) 42 | 43 | view.addSubview(scrollStackView) 44 | } 45 | 46 | override func viewDidLayoutSubviews() { 47 | super.viewDidLayoutSubviews() 48 | scrollStackView.frame = view.bounds 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | target 'FrameLayoutKit_Example' do 4 | pod 'FrameLayoutKit', :path => '../' 5 | end 6 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - FrameLayoutKit (6.2.0) 3 | 4 | DEPENDENCIES: 5 | - FrameLayoutKit (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | FrameLayoutKit: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | FrameLayoutKit: f320aa7b377d9e01ddbf69f29a840e197fec2f4d 13 | 14 | PODFILE CHECKSUM: ac097e09a36888b120812ddf1779def87475dc02 15 | 16 | COCOAPODS: 1.11.2 17 | -------------------------------------------------------------------------------- /FrameLayoutKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'FrameLayoutKit' 3 | s.version = '7.1.1' 4 | s.summary = 'FrameLayoutKit is a super fast and easy to use layout kit' 5 | s.description = <<-DESC 6 | FrameLayoutKit is a powerful Swift library designed to streamline the process of creating user interfaces. With its intuitive operator syntax and support for nested functions, developers can effortlessly construct complex UI layouts with minimal code. By leveraging the flexibility of operators, developers can easily position and arrange views within a container view, enabling precise control over the visual hierarchy. Additionally, the library offers a range of convenient functions for configuring view properties, such as setting dimensions, margins, and alignment. Whether you're building a simple screen or a complex interface, FrameLayoutKit simplifies the UI creation process, resulting in cleaner, more maintainable code. 7 | DESC 8 | 9 | s.homepage = 'https://github.com/kennic/FrameLayoutKit' 10 | s.license = { :type => 'MIT', :file => 'LICENSE' } 11 | s.author = { 'Nam Kennic' => 'namkennic@me.com' } 12 | s.source = { :git => 'https://github.com/kennic/FrameLayoutKit.git', :tag => s.version.to_s } 13 | s.social_media_url = 'https://twitter.com/namkennic' 14 | s.platform = :ios, "11.0" 15 | # s.platform = :tvos, "11.0" 16 | s.ios.deployment_target = '11.0' 17 | # s.tvos.deployment_target = '11.0' 18 | s.swift_version = "5.8" 19 | s.source_files = 'FrameLayoutKit/Classes/**/*.*' 20 | 21 | end 22 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/FrameLayoutKit/Classes/.gitkeep -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/DoubleFrameLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoubleFrameLayout.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 7/17/18. 6 | // 7 | 8 | import UIKit 9 | 10 | public enum NKLayoutAxis { 11 | case horizontal // left - right 12 | case vertical // top - bottom 13 | } 14 | 15 | public enum NKLayoutDistribution: Equatable { 16 | case top 17 | case bottom 18 | case equal 19 | case split(ratio: [CGFloat]) 20 | case center 21 | 22 | public static let left: NKLayoutDistribution = .top 23 | public static let right: NKLayoutDistribution = .bottom 24 | 25 | public init(split ratio: CGFloat...) { 26 | self = .split(ratio: ratio) 27 | } 28 | } 29 | 30 | /* 31 | @propertyWrapper 32 | public struct Clamping { 33 | var value: Value 34 | let range: ClosedRange 35 | 36 | public init(wrappedValue value: Value, _ range: ClosedRange) { 37 | precondition(range.contains(value)) 38 | self.value = value 39 | self.range = range 40 | } 41 | 42 | public var wrappedValue: Value { 43 | get { value } 44 | set { value = min(max(range.lowerBound, newValue), range.upperBound) } 45 | } 46 | } 47 | 48 | @propertyWrapper 49 | public struct UnitPercentage { 50 | @Clamping(0...1) 51 | public var wrappedValue: Value = .zero 52 | 53 | public init(wrappedValue value: Value) { 54 | self.wrappedValue = value 55 | } 56 | } 57 | */ 58 | 59 | open class DoubleFrameLayout: FrameLayout { 60 | public var distribution: NKLayoutDistribution = .top 61 | public var axis: NKLayoutAxis = .vertical 62 | 63 | public var spacing: CGFloat = 0 { 64 | didSet { 65 | if spacing != oldValue { 66 | setNeedsLayout() 67 | } 68 | } 69 | } 70 | 71 | override open var ignoreHiddenView: Bool { 72 | didSet { 73 | frameLayout1.ignoreHiddenView = ignoreHiddenView 74 | frameLayout2.ignoreHiddenView = ignoreHiddenView 75 | } 76 | } 77 | 78 | override open var shouldCacheSize: Bool { 79 | didSet { 80 | frameLayout1.shouldCacheSize = shouldCacheSize 81 | frameLayout2.shouldCacheSize = shouldCacheSize 82 | } 83 | } 84 | 85 | override open var debug: Bool { 86 | didSet { 87 | super.debug = debug 88 | frameLayout1.debug = debug 89 | frameLayout2.debug = debug 90 | } 91 | } 92 | 93 | override open var debugColor: UIColor?{ 94 | didSet { 95 | super.debugColor = debugColor 96 | frameLayout1.debugColor = debugColor 97 | frameLayout2.debugColor = debugColor 98 | } 99 | } 100 | 101 | override open var allowContentVerticalGrowing: Bool { 102 | didSet { 103 | frameLayout1.allowContentVerticalGrowing = allowContentVerticalGrowing 104 | frameLayout2.allowContentVerticalGrowing = allowContentVerticalGrowing 105 | } 106 | } 107 | 108 | override open var allowContentVerticalShrinking: Bool { 109 | didSet { 110 | frameLayout1.allowContentVerticalShrinking = allowContentVerticalShrinking 111 | frameLayout2.allowContentVerticalShrinking = allowContentVerticalShrinking 112 | } 113 | } 114 | 115 | override open var allowContentHorizontalGrowing: Bool { 116 | didSet { 117 | frameLayout1.allowContentHorizontalGrowing = allowContentHorizontalGrowing 118 | frameLayout2.allowContentHorizontalGrowing = allowContentHorizontalGrowing 119 | } 120 | } 121 | 122 | override open var allowContentHorizontalShrinking: Bool { 123 | didSet { 124 | frameLayout1.allowContentHorizontalShrinking = allowContentHorizontalShrinking 125 | frameLayout2.allowContentHorizontalShrinking = allowContentHorizontalShrinking 126 | } 127 | } 128 | 129 | override open var frame: CGRect { 130 | didSet { setNeedsLayout() } 131 | } 132 | 133 | override open var bounds: CGRect { 134 | didSet { setNeedsLayout() } 135 | } 136 | 137 | override open var description: String { 138 | return "[\(super.description)]\n[frameLayout1: \(String(describing: frameLayout1))]\n-[frameLayout2: \(String(describing: frameLayout2))]" 139 | } 140 | 141 | // Skeleton 142 | 143 | /// set color for skeleton mode 144 | override public var skeletonColor: UIColor { 145 | get { frameLayout1.skeletonColor } 146 | set { 147 | super.skeletonColor = newValue 148 | frameLayout1.skeletonColor = newValue 149 | frameLayout2.skeletonColor = newValue 150 | } 151 | } 152 | override public var skeletonMinSize: CGSize { 153 | get { frameLayout1.skeletonMinSize } 154 | set { 155 | frameLayout1.skeletonMinSize = newValue 156 | frameLayout2.skeletonMinSize = newValue 157 | } 158 | } 159 | override public var skeletonMaxSize: CGSize { 160 | get { frameLayout1.skeletonMaxSize } 161 | set { 162 | frameLayout1.skeletonMaxSize = newValue 163 | frameLayout2.skeletonMaxSize = newValue 164 | } 165 | } 166 | override public var isSkeletonMode: Bool { 167 | didSet { 168 | frameLayout1.isSkeletonMode = isSkeletonMode 169 | frameLayout2.isSkeletonMode = isSkeletonMode 170 | } 171 | } 172 | 173 | // MARK: - 174 | 175 | public var frameLayout1: FrameLayout = FrameLayout() { 176 | didSet { 177 | guard frameLayout1 != oldValue else { return } 178 | 179 | if oldValue.superview == self { 180 | oldValue.parent = nil 181 | oldValue.removeFromSuperview() 182 | } 183 | 184 | if frameLayout1 != self { 185 | frameLayout1.parent = self 186 | addSubview(frameLayout1) 187 | } 188 | } 189 | } 190 | 191 | public var frameLayout2: FrameLayout = FrameLayout() { 192 | didSet { 193 | guard frameLayout2 != oldValue else { return } 194 | 195 | if oldValue.superview == self { 196 | oldValue.parent = nil 197 | oldValue.removeFromSuperview() 198 | } 199 | 200 | if frameLayout2 != self { 201 | frameLayout2.parent = self 202 | addSubview(frameLayout2) 203 | } 204 | } 205 | } 206 | 207 | public var topFrameLayout: FrameLayout { 208 | get { frameLayout1 } 209 | set { frameLayout1 = newValue } 210 | } 211 | 212 | public var leftFrameLayout: FrameLayout { 213 | get { frameLayout1 } 214 | set { frameLayout1 = newValue } 215 | } 216 | 217 | public var bottomFrameLayout: FrameLayout { 218 | get { frameLayout2 } 219 | set { frameLayout2 = newValue } 220 | } 221 | 222 | public var rightFrameLayout: FrameLayout { 223 | get { frameLayout2 } 224 | set { frameLayout2 = newValue } 225 | } 226 | 227 | public var isOverlapped: Bool = false { 228 | didSet { setNeedsLayout() } 229 | } 230 | 231 | public override var isUserInteractionEnabled: Bool { 232 | didSet { 233 | frameLayout1.isUserInteractionEnabled = isUserInteractionEnabled 234 | frameLayout2.isUserInteractionEnabled = isUserInteractionEnabled 235 | } 236 | } 237 | 238 | // MARK: - 239 | 240 | @discardableResult 241 | public convenience init(_ block: (DoubleFrameLayout) throws -> Void) rethrows { 242 | self.init() 243 | try block(self) 244 | } 245 | 246 | convenience public init(axis: NKLayoutAxis, distribution: NKLayoutDistribution = .top, views: [UIView]? = nil) { 247 | self.init() 248 | 249 | self.axis = axis 250 | self.distribution = distribution 251 | 252 | defer { 253 | if let views { 254 | let count = views.count 255 | 256 | if count > 0 { 257 | var targetView = views[0] 258 | 259 | if targetView is FrameLayout && targetView.superview == nil { 260 | frameLayout1 = targetView as! FrameLayout 261 | } 262 | else { 263 | frameLayout1.targetView = targetView 264 | } 265 | 266 | if count > 1 { 267 | targetView = views[1] 268 | 269 | if targetView is FrameLayout && targetView.superview == nil { 270 | frameLayout2 = targetView as! FrameLayout 271 | } 272 | else { 273 | frameLayout2.targetView = targetView 274 | } 275 | 276 | #if DEBUG 277 | if count > 2 { 278 | print("[\(self)] This DoubleFrameLayout should has only 2 target views, currently set \(count) views. Switch to StackFrameLayout to handle multi views.") 279 | } 280 | #endif 281 | } 282 | } 283 | } 284 | } 285 | } 286 | 287 | public required init() { 288 | super.init() 289 | 290 | addSubview(frameLayout1) 291 | addSubview(frameLayout2) 292 | } 293 | 294 | public required init?(coder aDecoder: NSCoder) { 295 | super.init(coder: aDecoder) 296 | } 297 | 298 | // MARK: - 299 | 300 | @discardableResult 301 | open func setLeft(_ view: UIView?) -> FrameLayout { 302 | if let frameLayout = view as? FrameLayout, frameLayout.superview == nil { 303 | self.frameLayout1 = frameLayout 304 | return frameLayout 305 | } 306 | 307 | frameLayout1.targetView = view 308 | return frameLayout1 309 | } 310 | 311 | @discardableResult 312 | open func setRight(_ view: UIView?) -> FrameLayout { 313 | if let frameLayout = view as? FrameLayout, frameLayout.superview == nil { 314 | self.frameLayout2 = frameLayout 315 | return frameLayout 316 | } 317 | 318 | frameLayout2.targetView = view 319 | return frameLayout2 320 | } 321 | 322 | @discardableResult 323 | open func setTop(_ view: UIView?) -> FrameLayout { 324 | return setLeft(view) 325 | } 326 | 327 | @discardableResult 328 | open func setBottom(_ view: UIView?) -> FrameLayout { 329 | return setRight(view) 330 | } 331 | 332 | // MARK: - 333 | 334 | override open func setNeedsLayout() { 335 | super.setNeedsLayout() 336 | 337 | frameLayout1.setNeedsLayout() 338 | frameLayout2.setNeedsLayout() 339 | } 340 | 341 | open override func sizeThatFits(_ size: CGSize, ignoreHiddenView: Bool) -> CGSize { 342 | if !isEnabled { return .zero } 343 | 344 | willSizeThatFitsBlock?(self, size) 345 | 346 | var result: CGSize = size 347 | 348 | let verticalEdgeValues = edgeInsets.left + edgeInsets.right 349 | let horizontalEdgeValues = edgeInsets.top + edgeInsets.bottom 350 | 351 | if minSize == maxSize && minSize.width > 0 && minSize.height > 0 { 352 | result = minSize 353 | } 354 | else if heightRatio > 0 && !isIntrinsicSizeEnabled { 355 | result.height = result.width * heightRatio 356 | } 357 | else { 358 | let contentSize = CGSize(width: max(size.width - verticalEdgeValues, 0), height: max(size.height - horizontalEdgeValues, 0)) 359 | 360 | var frame1ContentSize: CGSize = .zero 361 | var frame2ContentSize: CGSize = .zero 362 | 363 | if isOverlapped { 364 | frame1ContentSize = frameLayout1.sizeThatFits(contentSize) 365 | frame2ContentSize = frameLayout2.sizeThatFits(contentSize) 366 | 367 | result.width = isIntrinsicSizeEnabled ? max(frame1ContentSize.width, frame2ContentSize.width) : size.width 368 | 369 | if heightRatio > 0 { 370 | result.height = result.width * heightRatio 371 | } 372 | else { 373 | result.height = max(frame1ContentSize.height, frame2ContentSize.height) 374 | } 375 | 376 | return result 377 | } 378 | 379 | var space: CGFloat = 0 380 | 381 | if axis == .horizontal { 382 | switch distribution { 383 | case .left, .top: 384 | frame1ContentSize = frameLayout1.sizeThatFits(contentSize) 385 | space = frame1ContentSize.width > 0 ? spacing : 0 386 | 387 | frame2ContentSize = frameLayout2.sizeThatFits(CGSize(width: contentSize.width - frame1ContentSize.width - space, height: contentSize.height), intrinsic: isIntrinsicSizeEnabled || frameLayout2.heightRatio == 0) 388 | break 389 | 390 | case .right, .bottom: 391 | frame2ContentSize = frameLayout2.sizeThatFits(contentSize) 392 | space = frame2ContentSize.width > 0 ? spacing : 0 393 | 394 | frame1ContentSize = frameLayout1.sizeThatFits(CGSize(width: contentSize.width - frame2ContentSize.width - space, height: contentSize.height), intrinsic: isIntrinsicSizeEnabled || frameLayout1.heightRatio == 0) 395 | break 396 | 397 | case .equal: 398 | var ratioValue: CGFloat = 0.5 399 | var spaceValue: CGFloat = spacing 400 | 401 | if frameLayout1.isEmpty { 402 | ratioValue = 0 403 | spaceValue = 0 404 | } 405 | 406 | if frameLayout2.isEmpty { 407 | ratioValue = 1 408 | spaceValue = 0 409 | } 410 | 411 | frame1ContentSize = frameLayout1.sizeThatFits(CGSize(width: (contentSize.width - spaceValue) * ratioValue, height: contentSize.height), intrinsic: isIntrinsicSizeEnabled || frameLayout1.heightRatio == 0) 412 | space = frame1ContentSize.width > 0 ? spaceValue : 0 413 | 414 | frame2ContentSize = frameLayout2.sizeThatFits(CGSize(width: contentSize.width - frame1ContentSize.width - space, height: contentSize.height), intrinsic: isIntrinsicSizeEnabled || frameLayout2.heightRatio == 0) 415 | 416 | if frame1ContentSize.width > frame2ContentSize.width { 417 | frame2ContentSize.width = frame1ContentSize.width 418 | 419 | if frameLayout2.heightRatio > 0 { 420 | frame2ContentSize.height = frame2ContentSize.width * heightRatio 421 | } 422 | } 423 | else if frame2ContentSize.width > frame1ContentSize.width { 424 | frame1ContentSize.width = frame2ContentSize.width 425 | 426 | if frameLayout1.heightRatio > 0 { 427 | frame1ContentSize.height = frame1ContentSize.width * heightRatio 428 | } 429 | } 430 | 431 | break 432 | 433 | case .split(let ratio): 434 | var ratioValue: CGFloat = ratio.first ?? 0.5 435 | var spaceValue: CGFloat = spacing 436 | 437 | if frameLayout1.isEmpty { 438 | ratioValue = 0 439 | spaceValue = 0 440 | } 441 | 442 | if frameLayout2.isEmpty { 443 | ratioValue = 1 444 | spaceValue = 0 445 | } 446 | 447 | frame1ContentSize = frameLayout1.sizeThatFits(CGSize(width: (contentSize.width - spaceValue) * ratioValue, height: contentSize.height), intrinsic: isIntrinsicSizeEnabled || frameLayout1.heightRatio == 0) 448 | space = frame1ContentSize.width > 0 ? spaceValue : 0 449 | 450 | frame2ContentSize = frameLayout2.sizeThatFits(CGSize(width: contentSize.width - frame1ContentSize.width - space, height: contentSize.height), intrinsic: isIntrinsicSizeEnabled || frameLayout2.heightRatio == 0) 451 | 452 | if frame1ContentSize.width > frame2ContentSize.width { 453 | frame2ContentSize.width = frame1ContentSize.width 454 | 455 | if frameLayout2.heightRatio > 0 { 456 | frame2ContentSize.height = frame2ContentSize.width * heightRatio 457 | } 458 | } 459 | else if frame2ContentSize.width > frame1ContentSize.width { 460 | frame1ContentSize.width = frame2ContentSize.width 461 | 462 | if frameLayout1.heightRatio > 0 { 463 | frame1ContentSize.height = frame1ContentSize.width * heightRatio 464 | } 465 | } 466 | break 467 | 468 | case .center: 469 | frame1ContentSize = frameLayout1.sizeThatFits(contentSize) 470 | space = frame1ContentSize.width > 0 ? spacing : 0 471 | 472 | frame2ContentSize = frameLayout2.sizeThatFits(CGSize(width: contentSize.width - frame1ContentSize.width - space, height: contentSize.height)) 473 | break 474 | } 475 | 476 | if isIntrinsicSizeEnabled { 477 | space = frame1ContentSize.width > 0 && frame2ContentSize.width > 0 ? spacing : 0 478 | result.width = frame1ContentSize.width + frame2ContentSize.width + space 479 | } 480 | else { 481 | result.width = size.width 482 | } 483 | 484 | if heightRatio > 0 { 485 | result.height = result.width * heightRatio 486 | } 487 | else { 488 | result.height = max(frame1ContentSize.height, frame2ContentSize.height) 489 | } 490 | } 491 | else { 492 | switch distribution { 493 | case .top, .left: 494 | frame1ContentSize = frameLayout1.sizeThatFits(contentSize) 495 | space = frame1ContentSize.height > 0 ? spacing : 0 496 | 497 | frame2ContentSize = frameLayout2.sizeThatFits(CGSize(width: contentSize.width, height: contentSize.height - frame1ContentSize.height - space)) 498 | 499 | if frame1ContentSize.width > frame2ContentSize.width { 500 | if frameLayout2.heightRatio > 0 { 501 | frame2ContentSize.height = frame1ContentSize.width * heightRatio 502 | } 503 | } 504 | else if frame2ContentSize.width > frame1ContentSize.width { 505 | if frameLayout1.heightRatio > 0 { 506 | frame1ContentSize.height = frame2ContentSize.width * heightRatio 507 | } 508 | } 509 | 510 | break 511 | 512 | case .bottom, .right: 513 | frame2ContentSize = frameLayout2.sizeThatFits(contentSize) 514 | space = frame2ContentSize.height > 0 ? spacing : 0 515 | 516 | frame1ContentSize = frameLayout1.sizeThatFits(CGSize(width: contentSize.width, height: contentSize.height - frame2ContentSize.height - space)) 517 | 518 | if frame1ContentSize.width > frame2ContentSize.width { 519 | if frameLayout2.heightRatio > 0 { 520 | frame2ContentSize.height = frame1ContentSize.width * heightRatio 521 | } 522 | } 523 | else if frame2ContentSize.width > frame1ContentSize.width { 524 | if frameLayout1.heightRatio > 0 { 525 | frame1ContentSize.height = frame2ContentSize.width * heightRatio 526 | } 527 | } 528 | 529 | break 530 | 531 | case .equal: 532 | var ratioValue: CGFloat = 0.5 533 | var spaceValue: CGFloat = spacing 534 | 535 | if frameLayout1.isEmpty { 536 | ratioValue = 0 537 | spaceValue = 0 538 | } 539 | 540 | if frameLayout2.isEmpty { 541 | ratioValue = 1 542 | spaceValue = 0 543 | } 544 | 545 | frame1ContentSize = frameLayout1.sizeThatFits(CGSize(width: contentSize.width, height: (contentSize.height - spaceValue) * ratioValue)) 546 | space = frame1ContentSize.height > 0 ? spaceValue : 0 547 | 548 | frame2ContentSize = frameLayout2.sizeThatFits(CGSize(width: contentSize.width, height: contentSize.height - frame1ContentSize.height - space)) 549 | 550 | if frameLayout2.heightRatio > 0 { 551 | if frame1ContentSize.width > frame2ContentSize.width { 552 | frame2ContentSize.height = frame1ContentSize.width * heightRatio 553 | } 554 | } 555 | 556 | if frameLayout1.heightRatio > 0 { 557 | if frame2ContentSize.width > frame1ContentSize.width { 558 | frame1ContentSize.height = frame2ContentSize.width * heightRatio 559 | } 560 | } 561 | 562 | if frame1ContentSize.height > frame2ContentSize.height { 563 | frame2ContentSize.height = frame1ContentSize.height 564 | } 565 | else if frame2ContentSize.height > frame1ContentSize.height { 566 | frame1ContentSize.height = frame2ContentSize.height 567 | } 568 | 569 | break 570 | 571 | case .split(let ratio): 572 | var ratioValue: CGFloat = ratio.first ?? 0.5 573 | var spaceValue: CGFloat = spacing 574 | 575 | if frameLayout1.isEmpty { 576 | ratioValue = 0 577 | spaceValue = 0 578 | } 579 | 580 | if frameLayout2.isEmpty { 581 | ratioValue = 1 582 | spaceValue = 0 583 | } 584 | 585 | frame1ContentSize = frameLayout1.sizeThatFits(CGSize(width: contentSize.width, height: (contentSize.height - spaceValue) * ratioValue)) 586 | space = frame1ContentSize.height > 0 ? spaceValue : 0 587 | 588 | frame2ContentSize = frameLayout2.sizeThatFits(CGSize(width: contentSize.width, height: contentSize.height - frame1ContentSize.height - space)) 589 | 590 | if frameLayout2.heightRatio > 0 { 591 | if frame1ContentSize.width > frame2ContentSize.width { 592 | frame2ContentSize.height = frame1ContentSize.width * heightRatio 593 | } 594 | } 595 | 596 | if frameLayout1.heightRatio > 0 { 597 | if frame2ContentSize.width > frame1ContentSize.width { 598 | frame1ContentSize.height = frame2ContentSize.width * heightRatio 599 | } 600 | } 601 | 602 | if frame1ContentSize.height > frame2ContentSize.height { 603 | frame2ContentSize.height = frame1ContentSize.height 604 | } 605 | else if frame2ContentSize.height > frame1ContentSize.height { 606 | frame1ContentSize.height = frame2ContentSize.height 607 | } 608 | break 609 | 610 | case .center: 611 | frame1ContentSize = frameLayout1.sizeThatFits(contentSize) 612 | frame2ContentSize = frameLayout2.sizeThatFits(contentSize) 613 | break 614 | } 615 | 616 | result.width = isIntrinsicSizeEnabled ? max(frame1ContentSize.width, frame2ContentSize.width) : size.width 617 | if heightRatio > 0 { 618 | result.height = result.width * heightRatio 619 | } 620 | else { 621 | space = frame1ContentSize.height > 0 && frame2ContentSize.height > 0 ? spacing : 0 622 | result.height = frame1ContentSize.height + frame2ContentSize.height + space 623 | } 624 | } 625 | 626 | result.limitedTo(minSize: minSize, maxSize: maxSize) 627 | } 628 | 629 | if result.width > 0 { result.width += verticalEdgeValues } 630 | if result.height > 0 { result.height += horizontalEdgeValues } 631 | 632 | result.width = min(result.width, size.width) 633 | result.height = min(result.height, size.height) 634 | 635 | return result 636 | } 637 | 638 | override open func layoutSubviews() { 639 | super.layoutSubviews() 640 | if !isEnabled { return } 641 | 642 | defer { 643 | didLayoutSubviewsBlock?(self) 644 | } 645 | 646 | #if swift(>=4.2) 647 | let containerFrame: CGRect = bounds.inset(by: edgeInsets) 648 | #else 649 | let containerFrame: CGRect = UIEdgeInsetsInsetRect(bounds, edgeInsets) 650 | #endif 651 | 652 | guard !containerFrame.isEmpty else { return } 653 | 654 | var frame1ContentSize: CGSize = .zero 655 | var frame2ContentSize: CGSize = .zero 656 | var targetFrame1: CGRect = containerFrame 657 | var targetFrame2: CGRect = containerFrame 658 | var space: CGFloat = 0 659 | 660 | if axis == .horizontal { 661 | switch distribution { 662 | case .top, .left: 663 | frame1ContentSize = frameLayout1.sizeThatFits(containerFrame.size) 664 | targetFrame1.size.width = frame1ContentSize.width 665 | 666 | if isOverlapped { 667 | if frameLayout2.isIntrinsicSizeEnabled && !frameLayout2.isFlexible { 668 | frame2ContentSize = frameLayout2.sizeThatFits(containerFrame.size) 669 | targetFrame2.size.width = min(frame2ContentSize.width, containerFrame.width) 670 | } 671 | else { 672 | targetFrame2.size.width = containerFrame.width 673 | } 674 | } 675 | else { 676 | space = frame1ContentSize.width > 0 ? spacing : 0 677 | 678 | frame2ContentSize = CGSize(width: containerFrame.width - frame1ContentSize.width - space, height: containerFrame.height) 679 | targetFrame2.origin.x = containerFrame.minX + frame1ContentSize.width + space 680 | targetFrame2.size.width = frame2ContentSize.width 681 | } 682 | break 683 | 684 | case .bottom, .right: 685 | frame2ContentSize = frameLayout2.sizeThatFits(containerFrame.size, intrinsic: true) 686 | targetFrame2.origin.x = containerFrame.minX + (containerFrame.width - frame2ContentSize.width) 687 | targetFrame2.size.width = frame2ContentSize.width 688 | 689 | if isOverlapped { 690 | if frameLayout1.isIntrinsicSizeEnabled && !frameLayout1.isFlexible { 691 | frame1ContentSize = frameLayout1.sizeThatFits(containerFrame.size) 692 | targetFrame1.size.width = min(frame1ContentSize.width, containerFrame.width) 693 | targetFrame1.origin.x = containerFrame.minX + (containerFrame.width - targetFrame1.width) 694 | } 695 | else { 696 | targetFrame1.size.width = containerFrame.width 697 | } 698 | } 699 | else { 700 | space = frame2ContentSize.width > 0 ? spacing : 0 701 | 702 | frame1ContentSize = CGSize(width: containerFrame.width - frame2ContentSize.width - space, height: containerFrame.height) 703 | targetFrame1.size.width = frame1ContentSize.width 704 | } 705 | break 706 | 707 | case .equal: 708 | if isOverlapped { 709 | targetFrame1 = containerFrame 710 | targetFrame2 = containerFrame 711 | } 712 | else { 713 | var ratioValue: CGFloat = 0.5 714 | var spaceValue = spacing 715 | 716 | if frameLayout1.isEmpty { 717 | ratioValue = 0 718 | spaceValue = 0 719 | } 720 | 721 | if frameLayout2.isEmpty { 722 | ratioValue = 1 723 | spaceValue = 0 724 | } 725 | 726 | frame1ContentSize = CGSize(width: (containerFrame.width - spaceValue) * ratioValue, height: containerFrame.height) 727 | targetFrame1.size.width = frame1ContentSize.width 728 | space = frame1ContentSize.width > 0 ? spaceValue : 0 729 | 730 | frame2ContentSize = CGSize(width: containerFrame.width - frame1ContentSize.width - space, height: containerFrame.height) 731 | targetFrame2.origin.x = containerFrame.minX + frame1ContentSize.width + space 732 | targetFrame2.size.width = frame2ContentSize.width 733 | } 734 | break 735 | 736 | case .split(let ratio): 737 | if isOverlapped { 738 | targetFrame1 = containerFrame 739 | targetFrame2 = containerFrame 740 | } 741 | else { 742 | var ratioValue = ratio.first ?? 0.5 743 | var spaceValue = spacing 744 | 745 | if frameLayout1.isEmpty { 746 | ratioValue = 0 747 | spaceValue = 0 748 | } 749 | 750 | if frameLayout2.isEmpty { 751 | ratioValue = 1 752 | spaceValue = 0 753 | } 754 | 755 | frame1ContentSize = CGSize(width: (containerFrame.width - spaceValue) * ratioValue, height: containerFrame.height) 756 | targetFrame1.size.width = frame1ContentSize.width 757 | space = frame1ContentSize.width > 0 ? spaceValue : 0 758 | 759 | frame2ContentSize = CGSize(width: containerFrame.width - frame1ContentSize.width - space, height: containerFrame.height) 760 | targetFrame2.origin.x = containerFrame.minX + frame1ContentSize.width + space 761 | targetFrame2.size.width = frame2ContentSize.width 762 | } 763 | break 764 | 765 | case .center: 766 | if isOverlapped { 767 | frame1ContentSize = frameLayout1.isFlexible ? containerFrame.size : frameLayout1.sizeThatFits(containerFrame.size) 768 | frame2ContentSize = frameLayout2.isFlexible ? containerFrame.size : frameLayout2.sizeThatFits(containerFrame.size) 769 | targetFrame1.size.width = min(frame1ContentSize.width, containerFrame.width) 770 | targetFrame2.size.width = min(frame2ContentSize.width, containerFrame.width) 771 | targetFrame1.origin.x = containerFrame.minX + (containerFrame.width - targetFrame1.width)/2 772 | targetFrame2.origin.x = containerFrame.minX + (containerFrame.width - targetFrame2.width)/2 773 | } 774 | else { 775 | frame1ContentSize = frameLayout1.sizeThatFits(containerFrame.size) 776 | space = frame1ContentSize.width > 0 ? spacing : 0 777 | 778 | frame2ContentSize = frameLayout2.sizeThatFits(CGSize(width: containerFrame.width - frame1ContentSize.width - space, height: containerFrame.height)) 779 | 780 | let totalWidth = frame1ContentSize.width + frame2ContentSize.width + space 781 | targetFrame1.origin.x = containerFrame.minX + (containerFrame.width - totalWidth)/2 782 | targetFrame1.size.width = frame1ContentSize.width 783 | 784 | targetFrame2.origin.x = targetFrame1.minX + frame1ContentSize.width + space 785 | targetFrame2.size.width = frame2ContentSize.width 786 | } 787 | break 788 | } 789 | } 790 | else { 791 | switch distribution { 792 | case .top, .left: 793 | frame1ContentSize = frameLayout1.sizeThatFits(containerFrame.size, intrinsic: frameLayout1.heightRatio == 0) 794 | targetFrame1.size.height = frame1ContentSize.height 795 | 796 | if isOverlapped { 797 | if frameLayout2.isIntrinsicSizeEnabled && !frameLayout2.isFlexible { 798 | frame2ContentSize = frameLayout2.sizeThatFits(containerFrame.size, intrinsic: frameLayout2.heightRatio == 0) 799 | targetFrame2.size.height = min(frame2ContentSize.height, containerFrame.height) 800 | } 801 | else { 802 | targetFrame2.size.height = containerFrame.height 803 | } 804 | } 805 | else { 806 | space = frame1ContentSize.height > 0 ? spacing : 0 807 | 808 | frame2ContentSize = CGSize(width: containerFrame.width, height: containerFrame.height - frame1ContentSize.height - space) 809 | targetFrame2.origin.y = containerFrame.minY + frame1ContentSize.height + space 810 | targetFrame2.size.height = frame2ContentSize.height 811 | } 812 | break 813 | 814 | case .bottom, .right: 815 | frame2ContentSize = frameLayout2.sizeThatFits(containerFrame.size, intrinsic: frameLayout2.heightRatio == 0) 816 | targetFrame2.origin.y = containerFrame.minY + (containerFrame.height - frame2ContentSize.height) 817 | targetFrame2.size.height = frame2ContentSize.height 818 | 819 | if isOverlapped { 820 | if frameLayout1.isIntrinsicSizeEnabled && !frameLayout1.isFlexible { 821 | frame1ContentSize = frameLayout1.sizeThatFits(containerFrame.size, intrinsic: frameLayout1.heightRatio == 0) 822 | targetFrame1.size.height = min(frame1ContentSize.height, containerFrame.height) 823 | targetFrame1.origin.y = containerFrame.minY + (containerFrame.height - targetFrame1.height) 824 | } 825 | else { 826 | targetFrame1.size.height = containerFrame.height 827 | } 828 | } 829 | else { 830 | space = frame2ContentSize.height > 0 ? spacing : 0 831 | 832 | frame1ContentSize = CGSize(width: containerFrame.width, height: containerFrame.height - frame2ContentSize.height - space) 833 | targetFrame1.size.height = frame1ContentSize.height 834 | } 835 | break 836 | 837 | case .equal: 838 | if isOverlapped { 839 | targetFrame1 = containerFrame 840 | targetFrame2 = containerFrame 841 | } 842 | else { 843 | var ratioValue: CGFloat = 0.5 844 | var spaceValue = spacing 845 | 846 | if frameLayout1.isEmpty { 847 | ratioValue = 0 848 | spaceValue = 0 849 | } 850 | 851 | if frameLayout2.isEmpty { 852 | ratioValue = 1 853 | spaceValue = 0 854 | } 855 | 856 | frame1ContentSize = CGSize(width: containerFrame.width, height: (containerFrame.height - spaceValue) * ratioValue) 857 | targetFrame1.size.height = frame1ContentSize.height 858 | space = frame1ContentSize.height > 0 ? spaceValue : 0 859 | 860 | frame2ContentSize = CGSize(width: containerFrame.width, height: containerFrame.height - frame1ContentSize.height - space) 861 | targetFrame2.origin.y = containerFrame.minY + targetFrame1.height + space 862 | targetFrame2.size.height = frame2ContentSize.height 863 | } 864 | break 865 | 866 | case .split(let ratio): 867 | if isOverlapped { 868 | targetFrame1 = containerFrame 869 | targetFrame2 = containerFrame 870 | } 871 | else { 872 | var ratioValue = ratio.first ?? 0.5 873 | var spaceValue = spacing 874 | 875 | if frameLayout1.isEmpty { 876 | ratioValue = 0 877 | spaceValue = 0 878 | } 879 | 880 | if frameLayout2.isEmpty { 881 | ratioValue = 1 882 | spaceValue = 0 883 | } 884 | 885 | frame1ContentSize = CGSize(width: containerFrame.width, height: (containerFrame.height - spaceValue) * ratioValue) 886 | targetFrame1.size.height = frame1ContentSize.height 887 | space = frame1ContentSize.height > 0 ? spaceValue : 0 888 | 889 | frame2ContentSize = CGSize(width: containerFrame.width, height: containerFrame.height - frame1ContentSize.height - space) 890 | targetFrame2.origin.y = containerFrame.minY + targetFrame1.height + space 891 | targetFrame2.size.height = frame2ContentSize.height 892 | } 893 | break 894 | 895 | case .center: 896 | if isOverlapped { 897 | frame1ContentSize = frameLayout1.isFlexible ? containerFrame.size : frameLayout1.sizeThatFits(containerFrame.size) 898 | frame2ContentSize = frameLayout2.isFlexible ? containerFrame.size : frameLayout2.sizeThatFits(containerFrame.size) 899 | targetFrame1.size.height = min(frame1ContentSize.height, containerFrame.height) 900 | targetFrame2.size.height = min(frame2ContentSize.height, containerFrame.height) 901 | targetFrame1.origin.y = containerFrame.minY + (containerFrame.height - targetFrame1.height)/2 902 | targetFrame2.origin.y = containerFrame.minY + (containerFrame.height - targetFrame2.height)/2 903 | } 904 | else { 905 | frame1ContentSize = frameLayout1.sizeThatFits(containerFrame.size) 906 | space = frame1ContentSize.height > 0 ? spacing : 0 907 | 908 | frame2ContentSize = frameLayout2.sizeThatFits(CGSize(width: containerFrame.width, height: containerFrame.height - frame1ContentSize.height - space)) 909 | 910 | let totalHeight: CGFloat = frame1ContentSize.height + frame2ContentSize.height + space 911 | targetFrame1.origin.y = containerFrame.minY + (containerFrame.height - totalHeight)/2 912 | targetFrame1.size.height = frame1ContentSize.height 913 | 914 | targetFrame2.origin.y = targetFrame1.minY + frame1ContentSize.height + space 915 | targetFrame2.size.height = frame2ContentSize.height 916 | } 917 | break 918 | } 919 | } 920 | 921 | targetFrame1 = targetFrame1.offsetBy(dx: translationOffset.x, dy: translationOffset.y) 922 | targetFrame2 = targetFrame2.offsetBy(dx: translationOffset.x, dy: translationOffset.y) 923 | 924 | frameLayout1.frame = targetFrame1.integral 925 | frameLayout2.frame = targetFrame2.integral 926 | } 927 | 928 | } 929 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/Extensions/DoubleFrameLayout+Chainable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoubleFrameLayout+Chainable.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 9/12/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension DoubleFrameLayout { 12 | 13 | @discardableResult public func axis(_ value: NKLayoutAxis) -> Self { 14 | axis = value 15 | return self 16 | } 17 | 18 | @discardableResult public func spacing(_ value: CGFloat) -> Self { 19 | spacing = value 20 | return self 21 | } 22 | 23 | @discardableResult public func distribution(_ value: NKLayoutDistribution) -> Self { 24 | distribution = value 25 | return self 26 | } 27 | 28 | @discardableResult public func overlapped(_ value: Bool) -> Self { 29 | isOverlapped = value 30 | return self 31 | } 32 | 33 | @discardableResult public func leftView(_ view: UIView?) -> Self { 34 | setLeft(view) 35 | return self 36 | } 37 | 38 | @discardableResult public func rightView(_ view: UIView?) -> Self { 39 | setRight(view) 40 | return self 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/Extensions/FlowFrameLayout+Chainable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlowFrameLayout+Chainable.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 9/12/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension FlowFrameLayout { 12 | 13 | @discardableResult public func axis(_ value: NKLayoutAxis) -> Self { 14 | axis = value 15 | return self 16 | } 17 | 18 | @discardableResult public func distribution(_ value: NKLayoutDistribution) -> Self { 19 | distribution = value 20 | return self 21 | } 22 | 23 | @discardableResult public func justified(_ value: Bool) -> Self { 24 | isJustified = value 25 | return self 26 | } 27 | 28 | @discardableResult public func interitemSpacing(_ value: CGFloat) -> Self { 29 | interItemSpacing = value 30 | return self 31 | } 32 | 33 | @discardableResult public func lineSpacing(_ value: CGFloat) -> Self { 34 | lineSpacing = value 35 | return self 36 | } 37 | 38 | @discardableResult public func intrinsicSizeEnabled(_ value: Bool) -> Self { 39 | isIntrinsicSizeEnabled = value 40 | return self 41 | } 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/Extensions/FrameLayout+Chainable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrameLayout+Chainable.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 9/12/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | /** 12 | Supports chaining syntax: 13 | 14 | frameLayout 15 | .flexible() 16 | .align(vertical: .center, horizontal: .left) 17 | .padding(top: 10, bottom: 20) 18 | .minHeight(100) 19 | */ 20 | 21 | extension FrameLayout { 22 | 23 | @discardableResult public func flexible(ratio: CGFloat = -1) -> Self { 24 | isFlexible = true 25 | flexibleRatio = ratio 26 | return self 27 | } 28 | 29 | @discardableResult public func inflexible() -> Self { 30 | isFlexible = false 31 | return self 32 | } 33 | 34 | @discardableResult public func padding(top: CGFloat = 0, left: CGFloat = 0, bottom: CGFloat = 0, right: CGFloat = 0) -> Self { 35 | edgeInsets = UIEdgeInsets(top: top, left: left, bottom: bottom, right: right) 36 | return self 37 | } 38 | 39 | @discardableResult public func padding(_ value: CGFloat) -> Self { 40 | edgeInsets = UIEdgeInsets(top: value, left: value, bottom: value, right: value) 41 | return self 42 | } 43 | 44 | @discardableResult public func addPadding(top: CGFloat = 0, left: CGFloat = 0, bottom: CGFloat = 0, right: CGFloat = 0) -> Self { 45 | edgeInsets = UIEdgeInsets(top: edgeInsets.top + top, left: edgeInsets.left + left, bottom: edgeInsets.bottom + bottom, right: edgeInsets.right + right) 46 | return self 47 | } 48 | 49 | @discardableResult public func align(vertical: NKContentVerticalAlignment? = nil, horizontal: NKContentHorizontalAlignment? = nil) -> Self { 50 | alignment = (vertical ?? alignment.vertical, horizontal ?? alignment.horizontal) 51 | return self 52 | } 53 | 54 | @discardableResult public func aligns(_ vertical: NKContentVerticalAlignment, _ horizontal: NKContentHorizontalAlignment) -> Self { 55 | alignment = (vertical, horizontal) 56 | return self 57 | } 58 | 59 | @discardableResult public func extends(size: CGSize) -> Self { 60 | extendSize = size 61 | return self 62 | } 63 | 64 | @discardableResult public func extends(width: CGFloat) -> Self { 65 | extendSize.width = width 66 | return self 67 | } 68 | 69 | @discardableResult public func extends(height: CGFloat) -> Self { 70 | extendSize.height = height 71 | return self 72 | } 73 | 74 | @discardableResult public func fixedSize(_ value: CGSize) -> Self { 75 | fixedSize = value 76 | return self 77 | } 78 | 79 | @discardableResult public func fixedHeight(_ value: CGFloat) -> Self { 80 | fixedHeight = value 81 | return self 82 | } 83 | 84 | @discardableResult public func fixedWidth(_ value: CGFloat) -> Self { 85 | fixedWidth = value 86 | return self 87 | } 88 | 89 | @discardableResult public func minSize(_ value: CGSize) -> Self { 90 | minSize = value 91 | return self 92 | } 93 | 94 | @discardableResult public func maxSize(_ value: CGSize) -> Self { 95 | maxSize = value 96 | return self 97 | } 98 | 99 | @discardableResult public func minWidth(_ value: CGFloat) -> Self { 100 | minWidth = value 101 | return self 102 | } 103 | 104 | @discardableResult public func maxWidth(_ value: CGFloat) -> Self { 105 | maxWidth = value 106 | return self 107 | } 108 | 109 | @discardableResult public func maxHeight(_ value: CGFloat) -> Self { 110 | maxHeight = value 111 | return self 112 | } 113 | 114 | @discardableResult public func minHeight(_ value: CGFloat) -> Self { 115 | minHeight = value 116 | return self 117 | } 118 | 119 | @discardableResult public func fixedContentSize(_ value: CGSize) -> Self { 120 | fixedContentSize = value 121 | return self 122 | } 123 | 124 | @discardableResult public func fixedContentWidth(_ value: CGFloat) -> Self { 125 | fixedContentWidth = value 126 | return self 127 | } 128 | 129 | @discardableResult public func fixedContentHeight(_ value: CGFloat) -> Self { 130 | fixedContentHeight = value 131 | return self 132 | } 133 | 134 | @discardableResult public func maxContentSize(_ value: CGSize) -> Self { 135 | maxContentSize = value 136 | return self 137 | } 138 | 139 | @discardableResult public func minContentSize(_ value: CGSize) -> Self { 140 | minContentSize = value 141 | return self 142 | } 143 | 144 | @discardableResult public func maxContentWidth(_ value: CGFloat) -> Self { 145 | maxContentWidth = value 146 | return self 147 | } 148 | 149 | @discardableResult public func maxContentHeight(_ value: CGFloat) -> Self { 150 | maxContentHeight = value 151 | return self 152 | } 153 | 154 | @discardableResult public func minContentWidth(_ value: CGFloat) -> Self { 155 | minContentWidth = value 156 | return self 157 | } 158 | 159 | @discardableResult public func minContentHeight(_ value: CGFloat) -> Self { 160 | minContentHeight = value 161 | return self 162 | } 163 | 164 | @discardableResult public func heightRatio(_ value: CGFloat) -> Self { 165 | heightRatio = value 166 | return self 167 | } 168 | 169 | @discardableResult public func translationOffset(_ value: CGPoint) -> Self { 170 | translationOffset = value 171 | return self 172 | } 173 | 174 | @discardableResult public func translationX(_ value: CGFloat) -> Self { 175 | translationX = value 176 | return self 177 | } 178 | 179 | @discardableResult public func translationY(_ value: CGFloat) -> Self { 180 | translationY = value 181 | return self 182 | } 183 | 184 | @discardableResult public func lazyBindFrame(to: @escaping (() -> UIView?)) -> Self { 185 | lazyBindingViews = { [to()] } 186 | return self 187 | } 188 | 189 | @discardableResult public func lazyBindFrame(to: @escaping (() -> [UIView?])) -> Self { 190 | lazyBindingViews = to 191 | return self 192 | } 193 | 194 | @discardableResult public func bindFrame(to views: UIView ...) -> Self { 195 | if let bindingViews, !bindingViews.isEmpty { 196 | self.bindingViews?.append(contentsOf: views) 197 | } 198 | else { 199 | bindingViews = views 200 | } 201 | return self 202 | } 203 | 204 | @discardableResult public func bindingEdgeInsets(_ value: UIEdgeInsets) -> Self { 205 | bindingEdgeInsets = value 206 | return self 207 | } 208 | 209 | @discardableResult public func assign(to instance: inout FrameLayout?) -> Self { 210 | instance = self 211 | return self 212 | } 213 | 214 | @discardableResult public func enable(_ value: Bool) -> Self { 215 | isEnabled = value 216 | return self 217 | } 218 | 219 | @discardableResult public func debug(_ value: Bool) -> Self { 220 | debug = value 221 | return self 222 | } 223 | 224 | @discardableResult public func debugColor(_ value: UIColor) -> Self { 225 | debugColor = value 226 | return self 227 | } 228 | 229 | @discardableResult public func contentVerticalGrowing(_ value: Bool) -> Self { 230 | allowContentVerticalGrowing = value 231 | return self 232 | } 233 | 234 | @discardableResult public func contentVerticalShrinking(_ value: Bool) -> Self { 235 | allowContentVerticalShrinking = value 236 | return self 237 | } 238 | 239 | @discardableResult public func contentHorizontalGrowing(_ value: Bool) -> Self { 240 | allowContentHorizontalGrowing = value 241 | return self 242 | } 243 | 244 | @discardableResult public func contentHorizontalShriking(_ value: Bool) -> Self { 245 | allowContentHorizontalShrinking = value 246 | return self 247 | } 248 | 249 | @discardableResult public func ignoreHiddenView(_ value: Bool) -> Self { 250 | ignoreHiddenView = value 251 | return self 252 | } 253 | 254 | @discardableResult public func isIntrinsicSizeEnabled(_ value: Bool) -> Self { 255 | isIntrinsicSizeEnabled = value 256 | return self 257 | } 258 | 259 | @discardableResult public func isFlexible(_ value: Bool) -> Self { 260 | isFlexible = value 261 | return self 262 | } 263 | 264 | @discardableResult public func willLayoutSubviews(_ block: @escaping (FrameLayout) -> Void) -> Self { 265 | willLayoutSubviewsBlock = block 266 | return self 267 | } 268 | 269 | @discardableResult public func didLayoutSubviews(_ block: @escaping (FrameLayout) -> Void) -> Self { 270 | didLayoutSubviewsBlock = block 271 | return self 272 | } 273 | 274 | @discardableResult public func willSizeThatFits(_ block: @escaping (FrameLayout, CGSize) -> Void) -> Self { 275 | willSizeThatFitsBlock = block 276 | return self 277 | } 278 | 279 | // UIView properties 280 | 281 | @discardableResult public func backgroundColor(_ color: UIColor) -> Self { 282 | backgroundColor = color 283 | return self 284 | } 285 | 286 | @discardableResult public func alpha(_ value: CGFloat) -> Self { 287 | alpha = value 288 | return self 289 | } 290 | 291 | @discardableResult public func clipsToBounds(_ value: Bool) -> Self { 292 | clipsToBounds = value 293 | return self 294 | } 295 | 296 | @discardableResult public func isUserInteractionEnabled(_ value: Bool) -> Self { 297 | isUserInteractionEnabled = value 298 | return self 299 | } 300 | 301 | // Skeleton 302 | 303 | @discardableResult public func isSkeletonMode(_ value: Bool) -> Self { 304 | isSkeletonMode = value 305 | return self 306 | } 307 | 308 | @discardableResult public func skeletonColor(_ value: UIColor) -> Self { 309 | skeletonColor = value 310 | return self 311 | } 312 | 313 | @discardableResult public func skeletonMinSize(_ value: CGSize) -> Self { 314 | skeletonMinSize = value 315 | return self 316 | } 317 | 318 | @discardableResult public func skeletonMaxSize(_ value: CGSize) -> Self { 319 | skeletonMaxSize = value 320 | return self 321 | } 322 | 323 | } 324 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/Extensions/FrameLayout+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrameLayout+Extension.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 4/28/20. 6 | // Copyright © 2020 Nam Kennic. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public extension FrameLayout { 13 | 14 | @discardableResult 15 | static func +(lhs: FrameLayout, rhs: UIView? = nil) -> FrameLayout { 16 | lhs.targetView = rhs 17 | return lhs 18 | } 19 | } 20 | 21 | infix operator --- 22 | 23 | public extension StackFrameLayout { 24 | 25 | @discardableResult 26 | static func ---(lhs: StackFrameLayout, _ size: CGFloat = 0) -> FrameLayout { 27 | return lhs.addSpace(size) 28 | } 29 | 30 | @discardableResult 31 | static func +(lhs: StackFrameLayout, rhs: UIView? = nil) -> FrameLayout { 32 | return lhs.add(rhs) 33 | } 34 | 35 | @discardableResult 36 | static func +(lhs: StackFrameLayout, rhs: [UIView]? = nil) -> [FrameLayout] { 37 | var results = [FrameLayout]() 38 | rhs?.forEach { results.append(lhs.add($0)) } 39 | return results 40 | } 41 | 42 | @discardableResult 43 | static func +(lhs: StackFrameLayout, rhs: CGFloat = 0) -> FrameLayout { 44 | return lhs.addSpace(rhs) 45 | } 46 | 47 | @discardableResult 48 | static func +(lhs: StackFrameLayout, rhs: Double = 0) -> FrameLayout { 49 | return lhs.addSpace(CGFloat(rhs)) 50 | } 51 | 52 | @discardableResult 53 | static func +(lhs: StackFrameLayout, rhs: Int = 0) -> FrameLayout { 54 | return lhs.addSpace(CGFloat(rhs)) 55 | } 56 | 57 | } 58 | 59 | public extension ScrollStackView { 60 | 61 | @discardableResult 62 | static func ---(lhs: ScrollStackView, _ size: CGFloat = 0) -> FrameLayout { 63 | return lhs.addSpace(size) 64 | } 65 | 66 | @discardableResult 67 | static func +(lhs: ScrollStackView, rhs: UIView? = nil) -> FrameLayout { 68 | return lhs.add(rhs) 69 | } 70 | 71 | @discardableResult 72 | static func +(lhs: ScrollStackView, rhs: [UIView]? = nil) -> [FrameLayout] { 73 | var results = [FrameLayout]() 74 | rhs?.forEach { results.append(lhs.add($0)) } 75 | return results 76 | } 77 | 78 | @discardableResult 79 | static func +(lhs: ScrollStackView, rhs: CGFloat = 0) -> FrameLayout { 80 | return lhs.addSpace(rhs) 81 | } 82 | 83 | @discardableResult 84 | static func +(lhs: ScrollStackView, rhs: Double = 0) -> FrameLayout { 85 | return lhs.addSpace(CGFloat(rhs)) 86 | } 87 | 88 | @discardableResult 89 | static func +(lhs: ScrollStackView, rhs: Int = 0) -> FrameLayout { 90 | return lhs.addSpace(CGFloat(rhs)) 91 | } 92 | 93 | } 94 | 95 | infix operator <+ // frameLayout <+ label 96 | infix operator +> // frameLayout +> imageView 97 | 98 | public extension DoubleFrameLayout { 99 | 100 | @discardableResult 101 | static func <+(lhs: DoubleFrameLayout, rhs: UIView? = nil) -> FrameLayout { 102 | if let frameLayout = rhs as? FrameLayout, frameLayout.superview == nil { 103 | lhs.leftFrameLayout = frameLayout 104 | } 105 | else { 106 | lhs.leftFrameLayout.targetView = rhs 107 | } 108 | 109 | return lhs.leftFrameLayout 110 | } 111 | 112 | @discardableResult 113 | static func +>(lhs: DoubleFrameLayout, rhs: UIView? = nil) -> FrameLayout { 114 | if let frameLayout = rhs as? FrameLayout, frameLayout.superview == nil { 115 | lhs.rightFrameLayout = frameLayout 116 | } 117 | else { 118 | lhs.rightFrameLayout.targetView = rhs 119 | } 120 | 121 | return lhs.rightFrameLayout 122 | } 123 | 124 | } 125 | 126 | // MARK: - 127 | 128 | open class StackLayout: StackFrameLayout { 129 | 130 | @discardableResult 131 | public init(_ block: (StackLayout) throws -> Void) rethrows { 132 | super.init() 133 | try block(self) 134 | } 135 | 136 | required public init?(coder aDecoder: NSCoder) { 137 | fatalError("init(coder:) has not been implemented") 138 | } 139 | 140 | public required init() { 141 | fatalError("init() has not been implemented") 142 | } 143 | 144 | } 145 | 146 | public extension FlowFrameLayout { 147 | 148 | @discardableResult 149 | static func +(lhs: FlowFrameLayout, rhs: UIView) -> UIView { 150 | return lhs.add(rhs) 151 | } 152 | } 153 | 154 | extension CGSize { 155 | 156 | mutating func limitedTo(minSize: CGSize, maxSize: CGSize) { 157 | self = self.limitTo(minSize: minSize, maxSize: maxSize) 158 | } 159 | 160 | func limitTo(minSize: CGSize, maxSize: CGSize) -> CGSize { 161 | var result = self 162 | 163 | result.width = max(minSize.width, result.width) 164 | result.height = max(minSize.height, result.height) 165 | 166 | if maxSize.width > 0 && maxSize.width >= minSize.width { 167 | result.width = min(maxSize.width, result.width) 168 | } 169 | if maxSize.height > 0 && maxSize.height >= minSize.height { 170 | result.height = min(maxSize.height, result.height) 171 | } 172 | 173 | return result 174 | } 175 | 176 | } 177 | 178 | internal extension Array where Element: Equatable { 179 | 180 | func replacingMultipleOccurrences(using array: (of: Element, with: Element)...) -> Array { 181 | return map { elem in array.first(where: { $0.of == elem })?.with ?? elem } 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/Extensions/GridFrameLayout+Chainable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridFrameLayout+Chainable.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 9/12/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension GridFrameLayout { 12 | 13 | @discardableResult public func axis(_ value: NKLayoutAxis) -> Self { 14 | axis = value 15 | return self 16 | } 17 | 18 | @discardableResult public func minRowHeight(_ value: CGFloat) -> Self { 19 | minRowHeight = value 20 | return self 21 | } 22 | 23 | @discardableResult public func maxRowHeight(_ value: CGFloat) -> Self { 24 | maxRowHeight = value 25 | return self 26 | } 27 | 28 | @discardableResult public func minColumnWidth(_ value: CGFloat) -> Self { 29 | minColumnWidth = value 30 | return self 31 | } 32 | 33 | @discardableResult public func maxColumnWidth(_ value: CGFloat) -> Self { 34 | maxColumnWidth = value 35 | return self 36 | } 37 | 38 | @discardableResult public func fixedRowHeight(_ value: CGFloat) -> Self { 39 | fixedRowHeight = value 40 | return self 41 | } 42 | 43 | @discardableResult public func fixedColumnWidth(_ value: CGFloat) -> Self { 44 | fixedColumnWidth = value 45 | return self 46 | } 47 | 48 | @discardableResult public func interitemSpacing(_ value: CGFloat) -> Self { 49 | verticalSpacing = value 50 | return self 51 | } 52 | 53 | @discardableResult public func lineSpacing(_ value: CGFloat) -> Self { 54 | horizontalSpacing = value 55 | return self 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/Extensions/ScrollStackView+Chainable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollStackView+Chainable.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 9/12/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension ScrollStackView { 12 | 13 | @discardableResult public func axis(_ value: NKLayoutAxis) -> Self { 14 | axis = value 15 | return self 16 | } 17 | 18 | @discardableResult public func spacing(_ value: CGFloat) -> Self { 19 | spacing = value 20 | return self 21 | } 22 | 23 | @discardableResult public func padding(_ value: CGFloat) -> Self { 24 | edgeInsets = UIEdgeInsets(top: value, left: value, bottom: value, right: value) 25 | return self 26 | } 27 | 28 | @discardableResult public func distribution(_ value: NKLayoutDistribution) -> Self { 29 | distribution = value 30 | return self 31 | } 32 | 33 | @discardableResult public func directionalLock(_ value: Bool) -> Self { 34 | isDirectionalLockEnabled = value 35 | return self 36 | } 37 | 38 | @discardableResult public func extends(size: CGSize) -> Self { 39 | extendSize = size 40 | return self 41 | } 42 | 43 | @discardableResult public func extends(width: CGFloat) -> Self { 44 | extendSize.width = width 45 | return self 46 | } 47 | 48 | @discardableResult public func extends(height: CGFloat) -> Self { 49 | extendSize.height = height 50 | return self 51 | } 52 | 53 | @discardableResult public func fixedSize(_ value: CGSize) -> Self { 54 | fixedSize = value 55 | return self 56 | } 57 | 58 | @discardableResult public func fixedHeight(_ value: CGFloat) -> Self { 59 | fixedHeight = value 60 | return self 61 | } 62 | 63 | @discardableResult public func fixedWidth(_ value: CGFloat) -> Self { 64 | fixedWidth = value 65 | return self 66 | } 67 | 68 | @discardableResult public func minSize(_ value: CGSize) -> Self { 69 | minSize = value 70 | return self 71 | } 72 | 73 | @discardableResult public func maxSize(_ value: CGSize) -> Self { 74 | maxSize = value 75 | return self 76 | } 77 | 78 | @discardableResult public func minWidth(_ value: CGFloat) -> Self { 79 | minWidth = value 80 | return self 81 | } 82 | 83 | @discardableResult public func maxWidth(_ value: CGFloat) -> Self { 84 | maxWidth = value 85 | return self 86 | } 87 | 88 | @discardableResult public func maxHeight(_ value: CGFloat) -> Self { 89 | maxHeight = value 90 | return self 91 | } 92 | 93 | @discardableResult public func minHeight(_ value: CGFloat) -> Self { 94 | minHeight = value 95 | return self 96 | } 97 | 98 | @discardableResult public func fixedContentSize(_ value: CGSize) -> Self { 99 | fixedContentSize = value 100 | return self 101 | } 102 | 103 | @discardableResult public func fixedContentWidth(_ value: CGFloat) -> Self { 104 | fixedContentWidth = value 105 | return self 106 | } 107 | 108 | @discardableResult public func fixedContentHeight(_ value: CGFloat) -> Self { 109 | fixedContentHeight = value 110 | return self 111 | } 112 | 113 | @discardableResult public func maxContentSize(_ value: CGSize) -> Self { 114 | maxContentSize = value 115 | return self 116 | } 117 | 118 | @discardableResult public func minContentSize(_ value: CGSize) -> Self { 119 | minContentSize = value 120 | return self 121 | } 122 | 123 | @discardableResult public func maxContentWidth(_ value: CGFloat) -> Self { 124 | maxContentWidth = value 125 | return self 126 | } 127 | 128 | @discardableResult public func maxContentHeight(_ value: CGFloat) -> Self { 129 | maxContentHeight = value 130 | return self 131 | } 132 | 133 | @discardableResult public func minContentWidth(_ value: CGFloat) -> Self { 134 | minContentWidth = value 135 | return self 136 | } 137 | 138 | @discardableResult public func minContentHeight(_ value: CGFloat) -> Self { 139 | minContentHeight = value 140 | return self 141 | } 142 | 143 | @discardableResult public func contentFitSize(_ value: CGSize) -> Self { 144 | contentFitSize = value 145 | return self 146 | } 147 | 148 | @discardableResult public func heightRatio(_ value: CGFloat) -> Self { 149 | heightRatio = value 150 | return self 151 | } 152 | 153 | @discardableResult public func flexible(_ value: Bool) -> Self { 154 | isFlexible = value 155 | return self 156 | } 157 | 158 | @discardableResult public func overlapped(_ value: Bool) -> Self { 159 | isOverlapped = value 160 | return self 161 | } 162 | 163 | @discardableResult public func minItemSize(_ value: CGSize) -> Self { 164 | minItemSize = value 165 | return self 166 | } 167 | 168 | @discardableResult public func maxItemSize(_ value: CGSize) -> Self { 169 | maxItemSize = value 170 | return self 171 | } 172 | 173 | @discardableResult public func fixedItemSize(_ value: CGSize) -> Self { 174 | fixedItemSize = value 175 | return self 176 | } 177 | 178 | @discardableResult public func debug(_ value: Bool) -> Self { 179 | debug = value 180 | return self 181 | } 182 | 183 | @discardableResult public func debugColor(_ value: UIColor) -> Self { 184 | debugColor = value 185 | return self 186 | } 187 | 188 | @discardableResult public func enable(_ value: Bool) -> Self { 189 | isEnabled = value 190 | return self 191 | } 192 | 193 | @discardableResult public func willLayoutSubviews(_ block: @escaping (ScrollStackView) -> Void) -> Self { 194 | willLayoutSubviewsBlock = block 195 | return self 196 | } 197 | 198 | @discardableResult public func didLayoutSubviews(_ block: @escaping (ScrollStackView) -> Void) -> Self { 199 | didLayoutSubviewsBlock = block 200 | return self 201 | } 202 | 203 | @discardableResult public func willSizeThatFits(_ block: @escaping (ScrollStackView, CGSize) -> Void) -> Self { 204 | willSizeThatFitsBlock = block 205 | return self 206 | } 207 | 208 | @discardableResult public func isIntrinsicSizeEnabled(_ value: Bool) -> Self { 209 | isIntrinsicSizeEnabled = value 210 | return self 211 | } 212 | 213 | // UIView properties 214 | 215 | @discardableResult public func backgroundColor(_ color: UIColor) -> Self { 216 | backgroundColor = color 217 | return self 218 | } 219 | 220 | @discardableResult public func alpha(_ value: CGFloat) -> Self { 221 | alpha = value 222 | return self 223 | } 224 | 225 | @discardableResult public func clipsToBounds(_ value: Bool) -> Self { 226 | clipsToBounds = value 227 | return self 228 | } 229 | 230 | @discardableResult public func isUserInteractionEnabled(_ value: Bool) -> Self { 231 | isUserInteractionEnabled = value 232 | return self 233 | } 234 | 235 | @discardableResult public func each(_ block: (FrameLayout, Int, inout Bool) -> Void) -> Self { 236 | enumerate(block) 237 | return self 238 | } 239 | 240 | // Skeleton 241 | 242 | @discardableResult public func isSkeletonMode(_ value: Bool) -> Self { 243 | isSkeletonMode = value 244 | return self 245 | } 246 | 247 | @discardableResult public func skeletonColor(_ value: UIColor) -> Self { 248 | skeletonColor = value 249 | return self 250 | } 251 | 252 | @discardableResult public func skeletonMinSize(_ value: CGSize) -> Self { 253 | skeletonMinSize = value 254 | return self 255 | } 256 | 257 | @discardableResult public func skeletonMaxSize(_ value: CGSize) -> Self { 258 | skeletonMaxSize = value 259 | return self 260 | } 261 | 262 | } 263 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/Extensions/StackFrameLayout+Chainable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackFrameLayout+Chainable.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 9/12/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension StackFrameLayout { 12 | 13 | @discardableResult public func axis(_ value: NKLayoutAxis) -> Self { 14 | axis = value 15 | return self 16 | } 17 | 18 | @discardableResult public func spacing(_ value: CGFloat) -> Self { 19 | spacing = value 20 | return self 21 | } 22 | 23 | @discardableResult public func distribution(_ value: NKLayoutDistribution) -> Self { 24 | distribution = value 25 | return self 26 | } 27 | 28 | @discardableResult public func overlapped(_ value: Bool) -> Self { 29 | isOverlapped = value 30 | return self 31 | } 32 | 33 | @discardableResult public func justified(_ value: Bool) -> Self { 34 | isJustified = value 35 | return self 36 | } 37 | 38 | @discardableResult public func justifyThreshold(_ value: CGFloat) -> Self { 39 | justifyThreshold = value 40 | return self 41 | } 42 | 43 | @discardableResult public func minItemSize(_ value: CGSize) -> Self { 44 | minItemSize = value 45 | return self 46 | } 47 | 48 | @discardableResult public func maxItemSize(_ value: CGSize) -> Self { 49 | maxItemSize = value 50 | return self 51 | } 52 | 53 | @discardableResult public func fixedItemSize(_ value: CGSize) -> Self { 54 | fixedItemSize = value 55 | return self 56 | } 57 | 58 | @discardableResult public func each(_ block: (FrameLayout, Int, inout Bool) -> Void) -> Self { 59 | enumerate(block) 60 | return self 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/Extensions/StackFrameLayout+DSL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackFrameLayout+DSL.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 9/25/23. 6 | // 7 | 8 | import UIKit 9 | 10 | @resultBuilder 11 | public struct FLViewBuilder { 12 | 13 | static public func buildBlock(_ views: UIView...) -> [UIView] { views } 14 | 15 | } 16 | 17 | /** 18 | Enable DSL syntax: 19 | 20 | ``` 21 | let stack = HStackView { 22 | UILabel() 23 | UIButton() 24 | } 25 | */ 26 | open class HStackView: HStackLayout { 27 | 28 | public init(@FLViewBuilder builder: () -> [UIView]) { 29 | super.init() 30 | add(builder()) 31 | } 32 | 33 | required public init?(coder: NSCoder) { 34 | super.init(coder: coder) 35 | } 36 | 37 | required public init() { 38 | fatalError("init() has not been implemented") 39 | } 40 | 41 | } 42 | 43 | /** 44 | Enable DSL syntax: 45 | 46 | ``` 47 | let stack = VStackView { 48 | UILabel() 49 | UIButton() 50 | } 51 | */ 52 | open class VStackView: VStackLayout { 53 | 54 | public init(@FLViewBuilder builder: () -> [UIView]) { 55 | super.init() 56 | add(builder()) 57 | } 58 | 59 | required public init?(coder: NSCoder) { 60 | super.init(coder: coder) 61 | } 62 | 63 | required public init() { 64 | fatalError("init() has not been implemented") 65 | } 66 | 67 | } 68 | 69 | /** 70 | Enable DSL syntax: 71 | 72 | ``` 73 | let stack = ZStackView { 74 | UILabel() 75 | UIButton() 76 | } 77 | */ 78 | open class ZStackView: ZStackLayout { 79 | 80 | public init(@FLViewBuilder builder: () -> [UIView]) { 81 | super.init() 82 | add(builder()) 83 | } 84 | 85 | required public init?(coder: NSCoder) { 86 | super.init(coder: coder) 87 | } 88 | 89 | required public init() { 90 | fatalError("init() has not been implemented") 91 | } 92 | 93 | } 94 | 95 | 96 | // MARK: - 97 | 98 | /** 99 | Enable DSL syntax: 100 | 101 | ``` 102 | let stack = VStackView { 103 | StackItem(label).padding(12) 104 | StackItem(button).aligns(.center, .center).padding(4) 105 | } 106 | */ 107 | open class StackItem: FrameLayout { 108 | public var content: T? 109 | 110 | public required init(_ view: T?) { 111 | super.init() 112 | 113 | targetView = view 114 | content = view 115 | } 116 | 117 | public required init() { 118 | super.init() 119 | } 120 | 121 | public required init?(coder aDecoder: NSCoder) { 122 | fatalError("init(coder:) has not been implemented") 123 | } 124 | 125 | } 126 | 127 | // MARK: - Spacing 128 | 129 | /** 130 | Enable DSL syntax: 131 | 132 | ``` 133 | let stack = VStackView { 134 | StackItem(label).padding(12) 135 | SpaceItem(10) 136 | StackItem(button).aligns(.center, .center).padding(4) 137 | } 138 | */ 139 | open class SpaceItem: FrameLayout { 140 | 141 | public required init(_ value: CGFloat = 0) { 142 | super.init() 143 | minSize = CGSize(width: value, height: value) 144 | } 145 | 146 | public required init() { 147 | super.init() 148 | } 149 | 150 | public required init?(coder aDecoder: NSCoder) { 151 | fatalError("init(coder:) has not been implemented") 152 | } 153 | 154 | } 155 | 156 | /** 157 | Enable DSL syntax: 158 | 159 | ``` 160 | let stack = VStackView { 161 | StackItem(label).padding(12) 162 | FlexibleSpace() 163 | StackItem(button).aligns(.center, .center).padding(4) 164 | } 165 | */ 166 | open class FlexibleSpace: FrameLayout { 167 | 168 | public required init(_ value: CGFloat = 0) { 169 | super.init() 170 | minSize = CGSize(width: value, height: value) 171 | flexible() 172 | } 173 | 174 | public required init() { 175 | super.init() 176 | flexible() 177 | } 178 | 179 | public required init?(coder aDecoder: NSCoder) { 180 | fatalError("init(coder:) has not been implemented") 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/FLSkeletonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FLSkeletonView.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 9/3/23. 6 | // 7 | 8 | import UIKit 9 | 10 | public class FLSkeletonView: UIView { 11 | let gradient = CAGradientLayer() 12 | let lightLayer = CAShapeLayer() 13 | let animation = CABasicAnimation(keyPath: "locations") 14 | 15 | public override var backgroundColor: UIColor? { 16 | didSet { 17 | lightLayer.fillColor = UIColor.lightText.cgColor 18 | } 19 | } 20 | 21 | init() { 22 | super.init(frame: .zero) 23 | 24 | layer.cornerRadius = 5 25 | layer.masksToBounds = true 26 | layer.addSublayer(lightLayer) 27 | 28 | let light = UIColor.clear.cgColor 29 | let dark = UIColor.black.cgColor 30 | 31 | gradient.colors = [dark, light, dark] 32 | gradient.startPoint = CGPoint(x: 0.0, y: 0.5) 33 | gradient.endPoint = CGPoint(x: 1.0, y: 0.525) 34 | gradient.locations = [0.4, 0.5, 0.6] 35 | lightLayer.mask = gradient 36 | 37 | animation.fromValue = [0.0, 0.1, 0.2] 38 | animation.toValue = [0.8, 0.9, 1.0] 39 | } 40 | 41 | required init?(coder aDecoder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | public override func didMoveToSuperview() { 46 | if superview != nil { 47 | startShimmering() 48 | } 49 | else { 50 | stopShimmering() 51 | } 52 | } 53 | 54 | public func startShimmering(duration: TimeInterval = 1.0, repeatCount: Float = HUGE, repeatDuration: TimeInterval = 0) { 55 | animation.duration = duration 56 | animation.repeatCount = repeatCount 57 | animation.repeatDuration = repeatDuration 58 | gradient.add(animation, forKey: "shimmer") 59 | } 60 | 61 | public func stopShimmering() { 62 | layer.mask?.removeAllAnimations() 63 | layer.mask = nil 64 | } 65 | 66 | public override func layoutSubviews() { 67 | super.layoutSubviews() 68 | 69 | lightLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath 70 | lightLayer.frame = bounds 71 | gradient.frame = CGRect(x: -bounds.size.width, y: 0, width: 3 * bounds.size.width, height: bounds.size.height) 72 | } 73 | 74 | deinit { 75 | stopShimmering() 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/FLView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FLView.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 10/16/21. 6 | // 7 | 8 | import UIKit 9 | 10 | open class FLView: UIView { 11 | public let frameLayout = T() 12 | 13 | override open func sizeThatFits(_ size: CGSize) -> CGSize { 14 | return frameLayout.sizeThatFits(size) 15 | } 16 | 17 | override open func layoutSubviews() { 18 | super.layoutSubviews() 19 | frameLayout.frame = bounds 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/FlowFrameLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlowFrameLayout.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 11/18/20. 6 | // 7 | 8 | import UIKit 9 | 10 | open class FlowFrameLayout: FrameLayout { 11 | public var axis: NKLayoutAxis = .horizontal { 12 | didSet { 13 | stackLayout.axis = axis == .horizontal ? .vertical : .horizontal 14 | setNeedsLayout() 15 | } 16 | } 17 | 18 | public var distribution: NKLayoutDistribution = .left { 19 | didSet { setNeedsLayout() } 20 | } 21 | 22 | public override var isIntrinsicSizeEnabled: Bool { 23 | get { stackLayout.isIntrinsicSizeEnabled } 24 | set { 25 | stackLayout.isIntrinsicSizeEnabled = newValue 26 | setNeedsLayout() 27 | } 28 | } 29 | 30 | override public var edgeInsets: UIEdgeInsets { 31 | get { stackLayout.edgeInsets } 32 | set { 33 | stackLayout.edgeInsets = newValue 34 | setNeedsLayout() 35 | } 36 | } 37 | 38 | override public var minSize: CGSize { 39 | didSet { 40 | stackLayout.minSize = minSize 41 | setNeedsLayout() 42 | } 43 | } 44 | 45 | override public var maxSize: CGSize { 46 | didSet { 47 | stackLayout.maxSize = minSize 48 | setNeedsLayout() 49 | } 50 | } 51 | 52 | override public var fixedSize: CGSize { 53 | didSet { 54 | stackLayout.fixedSize = fixedSize 55 | setNeedsLayout() 56 | } 57 | } 58 | 59 | override public var heightRatio: CGFloat { 60 | didSet { 61 | stackLayout.heightRatio = heightRatio 62 | setNeedsLayout() 63 | } 64 | } 65 | 66 | override public var debug: Bool { 67 | didSet { stackLayout.debug = debug } 68 | } 69 | 70 | override public var debugColor: UIColor? { 71 | didSet { stackLayout.debugColor = debugColor } 72 | } 73 | 74 | public var isJustified: Bool = false { 75 | didSet { setNeedsLayout() } 76 | } 77 | 78 | public var lineSpacing: CGFloat { 79 | get { stackLayout.spacing } 80 | set { 81 | stackLayout.spacing = newValue 82 | setNeedsLayout() 83 | } 84 | } 85 | 86 | public var interItemSpacing: CGFloat = 0 { 87 | didSet { 88 | stackLayout.frameLayouts.filter { $0 is StackFrameLayout }.forEach { ($0 as? StackFrameLayout)?.spacing = interItemSpacing } 89 | setNeedsLayout() 90 | } 91 | } 92 | 93 | /* 94 | public override var isUserInteractionEnabled: Bool { 95 | didSet { 96 | stackLayout.frameLayouts.forEach { $0.isUserInteractionEnabled = isUserInteractionEnabled } 97 | } 98 | } 99 | */ 100 | 101 | public var stackCount: Int { stackLayout.frameLayouts.count } 102 | public var stacks: [StackFrameLayout] { stackLayout.frameLayouts as? [StackFrameLayout] ?? [] } 103 | public var firstStack: StackFrameLayout? { stackLayout.firstFrameLayout as? StackFrameLayout } 104 | public var lastStack: StackFrameLayout? { stackLayout.lastFrameLayout as? StackFrameLayout } 105 | 106 | let stackLayout = ScrollStackView(axis: .vertical, distribution: .top) 107 | 108 | fileprivate var lastSize: CGSize = .zero 109 | public fileprivate(set) var viewCount: Int = 0 110 | 111 | /// Array of views that needs to be filled in this flow layout 112 | public var views: [UIView] = [] { 113 | didSet { 114 | lastSize = .zero 115 | viewCount = views.count 116 | setNeedsLayout() 117 | } 118 | } 119 | 120 | /// This block will be called when a new StackFrameLayout was added to a new row 121 | public var onNewStackBlock: ((FlowFrameLayout, StackFrameLayout) -> Void)? = nil 122 | 123 | /// This block will be called when a new StackFrameLayout was added to a new row 124 | public func onNewStackBlock(_ block: @escaping (_ flowLayout: FlowFrameLayout, _ addedStack: StackFrameLayout) -> Void) -> Self { 125 | onNewStackBlock = block 126 | return self 127 | } 128 | // MARK: - 129 | 130 | public convenience init(axis: NKLayoutAxis) { 131 | self.init() 132 | self.axis = axis 133 | } 134 | 135 | public required init() { 136 | super.init() 137 | 138 | axis = .horizontal 139 | isIntrinsicSizeEnabled = true 140 | stackLayout.scrollView.clipsToBounds = true 141 | 142 | addSubview(stackLayout) 143 | } 144 | 145 | public required init?(coder aDecoder: NSCoder) { 146 | super.init(coder: aDecoder) 147 | } 148 | 149 | // MARK: - 150 | 151 | @discardableResult 152 | public func add(_ view: UIView) -> UIView { 153 | views.append(view) 154 | setNeedsLayout() 155 | return view 156 | } 157 | 158 | public func removeFirst() { 159 | views.removeFirst() 160 | setNeedsLayout() 161 | } 162 | 163 | public func removeLast() { 164 | views.removeLast() 165 | setNeedsLayout() 166 | } 167 | 168 | public func remove(at index: Int) { 169 | views.remove(at: index) 170 | setNeedsLayout() 171 | } 172 | 173 | public func removeAll() { 174 | views.removeAll() 175 | setNeedsLayout() 176 | } 177 | 178 | public func viewAt(row: Int, column: Int) -> UIView? { 179 | return frameLayout(row: row, column: column)?.targetView 180 | } 181 | 182 | public func viewsAt(stack: Int) -> [UIView]? { 183 | return stacks(at: stack)?.frameLayouts.compactMap { $0.targetView } 184 | } 185 | 186 | public func stacks(at index: Int) -> StackFrameLayout? { 187 | guard index > -1, index < stackLayout.frameLayouts.count, let frameLayout = stackLayout.frameLayouts[index] as? StackFrameLayout else { return nil } 188 | return frameLayout 189 | } 190 | 191 | public func frameLayout(row: Int, column: Int) -> FrameLayout? { 192 | guard row > -1, row < stackLayout.frameLayouts.count else { return nil } 193 | guard let rowLayout = stackLayout.frameLayouts[row] as? StackFrameLayout else { return nil } 194 | return rowLayout.frameLayout(at: column) 195 | } 196 | 197 | public func allFrameLayouts() -> [FrameLayout] { 198 | return stackLayout.frameLayouts.compactMap { $0 as? StackFrameLayout }.flatMap { $0.frameLayouts } 199 | } 200 | 201 | public func lastFrameLayout(containsView: Bool = false) -> FrameLayout? { 202 | guard let lastStack = lastStack else { return nil } 203 | 204 | if containsView { 205 | return lastStack.frameLayouts.last(where: { $0.targetView != nil }) 206 | } 207 | else { 208 | return lastStack.frameLayouts.last 209 | } 210 | } 211 | 212 | // MARK: - 213 | 214 | fileprivate func newStack() -> StackFrameLayout { 215 | let layout = StackFrameLayout(axis: axis, distribution: distribution) 216 | layout.spacing = axis == .horizontal ? interItemSpacing : lineSpacing 217 | layout.isJustified = isJustified 218 | layout.debug = debug 219 | layout.parent = self 220 | 221 | return layout 222 | } 223 | 224 | /** 225 | Returns size that fits and map of number of items per row 226 | - parameter fitSize: Size that needs to be fit in 227 | - returns Size that fits all contents, and map of number of items per row, format: `[row: numberOfItems]` 228 | */ 229 | public func calculateSize(fitSize: CGSize) -> (size: CGSize, map: [Int: Int]) { 230 | var result = CGSize.zero 231 | var sizeMap = [Int: Int]() 232 | 233 | let verticalEdgeValues = edgeInsets.left + edgeInsets.right 234 | let horizontalEdgeValues = edgeInsets.top + edgeInsets.bottom 235 | let lastView = views.last 236 | 237 | if minSize == maxSize && minSize.width > 0 && minSize.height > 0 { 238 | result = minSize 239 | } 240 | else if heightRatio > 0 && !isIntrinsicSizeEnabled { 241 | result.height = result.width * heightRatio 242 | } 243 | else { 244 | let fitSize = CGSize(width: max(fitSize.width - verticalEdgeValues, 0), height: max(fitSize.height - horizontalEdgeValues, 0)) 245 | 246 | if axis == .horizontal { 247 | var rowHeight: CGFloat = 0.0 248 | var row = 1 249 | var col = 1 250 | var remainingSize = fitSize 251 | var previousRowHeight: CGFloat? 252 | 253 | for view in views { 254 | if view.isHidden && ignoreHiddenView { continue } 255 | 256 | if remainingSize.width > 0 { 257 | let contentSize = view.sizeThatFits(remainingSize) 258 | let space = contentSize.width > 0 ? contentSize.width + (view != lastView ? interItemSpacing : 0) : 0 259 | remainingSize.width -= space 260 | rowHeight = max(rowHeight, contentSize.height) 261 | 262 | if col > 1 && previousRowHeight != nil && contentSize.height > previousRowHeight! { 263 | remainingSize.width = -1 // to trigger the following block 264 | } 265 | 266 | if row == 1 { 267 | previousRowHeight = rowHeight 268 | } 269 | } 270 | else if remainingSize.width == 0 { 271 | rowHeight = 0 272 | remainingSize.width -= 1 // to trigger the following block 273 | } 274 | 275 | result.width = max(result.width, fitSize.width - remainingSize.width) 276 | 277 | if remainingSize.width < 0, col > 1 { 278 | remainingSize.width = fitSize.width 279 | remainingSize.height -= result.height 280 | 281 | let contentSize = view.sizeThatFits(remainingSize) 282 | let space = contentSize.width > 0 ? contentSize.width + (view != lastView ? interItemSpacing : 0) : 0 283 | remainingSize.width -= space 284 | 285 | rowHeight = max(contentSize.height, 0) 286 | if rowHeight > 0 { 287 | result.height += (lineSpacing + rowHeight) 288 | previousRowHeight = rowHeight 289 | } 290 | 291 | row += 1 292 | col = 1 293 | } 294 | 295 | sizeMap[row] = col 296 | result.height = max(result.height, rowHeight) 297 | 298 | col += 1 299 | } 300 | } 301 | else { // axis = .vertical 302 | let fitSize = CGSize(width: fitSize.width, height: maxHeight <= 0 ? 32_000 : maxHeight) 303 | var colWidth: CGFloat = 0.0 304 | var row = 1 305 | var col = 1 306 | var remainingSize = fitSize 307 | 308 | for view in views { 309 | if view.isHidden && ignoreHiddenView { continue } 310 | 311 | if remainingSize.height > 0 { 312 | let contentSize = view.sizeThatFits(remainingSize) 313 | let space = contentSize.height > 0 ? contentSize.height + (view != lastView ? lineSpacing : 0) : 0 314 | remainingSize.height -= space 315 | colWidth = max(colWidth, contentSize.width) 316 | } 317 | else if remainingSize.height == 0 { 318 | remainingSize.height -= 1 // to trigger the following block 319 | } 320 | 321 | result.height = max(result.height, fitSize.height - remainingSize.height) 322 | 323 | if remainingSize.height < 0, row > 1 { 324 | remainingSize.width -= result.width 325 | remainingSize.height = fitSize.height 326 | 327 | let contentSize = view.sizeThatFits(remainingSize) 328 | let space = contentSize.height > 0 ? contentSize.height + (view != lastView ? lineSpacing : 0) : 0 329 | remainingSize.height -= space 330 | 331 | colWidth = max(contentSize.width, 0) 332 | if colWidth > 0 { result.width += (interItemSpacing + colWidth) } 333 | 334 | col += 1 335 | row = 1 336 | } 337 | 338 | sizeMap[col] = row 339 | result.width = max(result.width, colWidth) 340 | 341 | row += 1 342 | } 343 | } 344 | } 345 | 346 | if result.width > 0 { result.width += verticalEdgeValues } 347 | if result.height > 0 { result.height += horizontalEdgeValues } 348 | 349 | if axis == .horizontal { 350 | result.width = min(result.width, fitSize.width) 351 | } 352 | else { 353 | result.height = min(result.height, fitSize.height) 354 | } 355 | 356 | return (result, sizeMap) 357 | } 358 | 359 | override open func sizeThatFits(_ size: CGSize) -> CGSize { 360 | if !isEnabled { return .zero } 361 | 362 | willSizeThatFitsBlock?(self, size) 363 | return calculateSize(fitSize: size).size.limitTo(minSize: minSize, maxSize: maxSize) 364 | } 365 | 366 | open override func layoutSubviews() { 367 | super.layoutSubviews() 368 | if !isEnabled { return } 369 | 370 | defer { 371 | didLayoutSubviewsBlock?(self) 372 | } 373 | 374 | let boundSize = bounds.size 375 | let contentSize = calculateSize(fitSize: boundSize) 376 | 377 | if lastSize != bounds.size { 378 | lastSize = bounds.size 379 | 380 | let map = contentSize.map 381 | stackLayout.removeAll() 382 | 383 | var index = 0 384 | let numberOfStack = map.keys.count 385 | for i in 0.. [UIView?]?)? 42 | /// FrameLayout that contains this 43 | public weak var parent: FrameLayout? 44 | /// If set to `true`, `sizeThatFits(size:)` will returns `.zero` if `targetView` is hidden. 45 | public var ignoreHiddenView = true 46 | /// If set to `false`, it will return .zero in sizeThatFits and ignore running layoutSubviews. It will also ignore `willSizeThatFits` and `willLayoutSubviews` blocks. 47 | public var isEnabled = true { 48 | didSet { skeletonView?.isHidden = targetView == nil || !isEnabled || isEmpty } 49 | } 50 | /// Padding edge insets 51 | public var edgeInsets: UIEdgeInsets = .zero 52 | /// Add translation position to view 53 | public var translationOffset: CGPoint = .zero 54 | /// Add x translation to view 55 | public var translationX: CGFloat { 56 | get { translationOffset.x } 57 | set { 58 | translationOffset.x = newValue 59 | setNeedsLayout() 60 | } 61 | } 62 | /// Add y translation to view 63 | public var translationY: CGFloat { 64 | get { translationOffset.y } 65 | set { 66 | translationOffset.y = newValue 67 | setNeedsLayout() 68 | } 69 | } 70 | /// Minimum size of this frameLayout 71 | public var minSize: CGSize = .zero 72 | /// Mininum width of this frameLayout 73 | public var minWidth: CGFloat { 74 | get { minSize.width } 75 | set { minSize.width = newValue } 76 | } 77 | /// Mininum height of this frameLayout 78 | public var minHeight: CGFloat { 79 | get { minSize.height } 80 | set { minSize.height = newValue } 81 | } 82 | /// Maximum size of frameLayout 83 | public var maxSize: CGSize = .zero 84 | /// Maximum width of this frameLayout 85 | public var maxWidth: CGFloat { 86 | get { maxSize.width } 87 | set { maxSize.width = newValue } 88 | } 89 | /// Maximum height of this frameLayout 90 | public var maxHeight: CGFloat { 91 | get { maxSize.height } 92 | set { maxSize.height = newValue } 93 | } 94 | /// Minimum size of `targetView` 95 | public var minContentSize: CGSize = .zero 96 | /// Mininum width of `targetView` 97 | public var minContentWidth: CGFloat { 98 | get { minContentSize.width } 99 | set { minContentSize.width = newValue } 100 | } 101 | /// Mininum height of `targetView` 102 | public var minContentHeight: CGFloat { 103 | get { minContentSize.height } 104 | set { minContentSize.height = newValue } 105 | } 106 | /// Maximum size of targetView 107 | public var maxContentSize: CGSize = .zero 108 | /// Maximum width of `targetView` 109 | public var maxContentWidth: CGFloat { 110 | get { maxContentSize.width } 111 | set { maxContentSize.width = newValue } 112 | } 113 | /// Maximum height of `targetView` 114 | public var maxContentHeight: CGFloat { 115 | get { maxContentSize.height } 116 | set { maxContentSize.height = newValue } 117 | } 118 | /// Adding size to content size. `minSize` and `maxSize` is still the limitation. 119 | public var extendSize: CGSize = .zero 120 | /// Extending width to content size 121 | public var extendWidth: CGFloat { 122 | get { extendSize.width } 123 | set { extendSize.width = newValue } 124 | } 125 | /// Extending height to content size 126 | public var extendHeight: CGFloat { 127 | get { extendSize.height } 128 | set { extendSize.height = newValue } 129 | } 130 | /// Width of `targetView` will be stretched out to fill frameLayout if the width of this frameLayout is larger than `targetView`'s width 131 | public var allowContentVerticalGrowing = false 132 | /// Width of `targetView` will be shrinked down to fit frameLayout if the width of this frameLayout is smaller than `targetView`'s width 133 | public var allowContentVerticalShrinking = false 134 | /// Height of `targetView` will be stretched out to fill frameLayout if the height of this frameLayout is larger than `targetView`'s height 135 | public var allowContentHorizontalGrowing = false 136 | /// Height of `targetView` will be shrinked down to fit frameLayout if the height of this frameLayout is smaller than `targetView`'s height 137 | public var allowContentHorizontalShrinking = false 138 | /// Value of `sizeThatFits` will be cached based on `targetView`'s memory address. This is not proved for better performance, use it with care. Default is `false` 139 | public var shouldCacheSize = false 140 | /// Make it flexible in a `StackFrameLayout`, that means when it was added to a stack, this flexible stack will be stretched base on the stack size 141 | public var isFlexible = false 142 | /// Ratio used in `StackFrameLayout` when `isFlexible` = true. Default value is auto (`-1`) 143 | public var flexibleRatio: CGFloat = -1 144 | /// if `true`, `sizeThatFits` will returns the intrinsic width of `targetView` 145 | public var isIntrinsicSizeEnabled = true 146 | /// Returns height from `sizeThatFits` base on ratio of width. For example setting `1.0` will returns a square size from `sizeThatFits` 147 | public var heightRatio: CGFloat = 0 { 148 | didSet { 149 | if heightRatio > 0 { 150 | isIntrinsicSizeEnabled = false 151 | } 152 | } 153 | } 154 | 155 | /// Show the dash line of the frameLayout for debugging. This works in development mode only, released version will ignore this 156 | public var debug: Bool = false { 157 | didSet { 158 | #if DEBUG 159 | setNeedsDisplay() 160 | #endif 161 | } 162 | } 163 | 164 | /// Set the color of debug line 165 | public var debugColor: UIColor? = nil { 166 | didSet { 167 | #if DEBUG 168 | setNeedsDisplay() 169 | #endif 170 | } 171 | } 172 | 173 | /// Set the fixed size of frameLayout 174 | public var fixedSize: CGSize = .zero { 175 | didSet { 176 | minSize = fixedSize 177 | maxSize = fixedSize 178 | } 179 | } 180 | 181 | public var fixedWidth: CGFloat { 182 | get { fixedSize.width } 183 | set { fixedSize.width = newValue } 184 | } 185 | 186 | public var fixedHeight: CGFloat { 187 | get { fixedSize.height } 188 | set { fixedSize.height = newValue } 189 | } 190 | 191 | /// Set the fixed size of targetView 192 | public var fixedContentSize: CGSize = .zero { 193 | didSet { 194 | minContentSize = fixedContentSize 195 | maxContentSize = fixedContentSize 196 | } 197 | } 198 | 199 | /// Set fixed width of targetView 200 | public var fixedContentWidth: CGFloat { 201 | get { fixedContentSize.width } 202 | set { fixedContentSize.width = newValue } 203 | } 204 | 205 | /// Set fixed height of targetView 206 | public var fixedContentHeight: CGFloat { 207 | get { fixedContentSize.height } 208 | set { fixedContentSize.height = newValue } 209 | } 210 | 211 | /// Set the alignment of both axis 212 | public var alignment: (vertical: NKContentVerticalAlignment, horizontal: NKContentHorizontalAlignment) = (.fill, .fill) 213 | 214 | /// Block will be called before calling sizeThatFits 215 | public var willSizeThatFitsBlock: ((FrameLayout, CGSize) -> Void)? 216 | /// Block will be called before calling layoutSubviews 217 | public var willLayoutSubviewsBlock: ((FrameLayout) -> Void)? 218 | /// Block will be called after layoutSubviews finished 219 | public var didLayoutSubviewsBlock: ((FrameLayout) -> Void)? 220 | 221 | override open var frame: CGRect { 222 | get { super.frame } 223 | set { 224 | if newValue.isInfinite || newValue.isNull || newValue.minX.isNaN || newValue.minY.isNaN || newValue.width.isNaN || newValue.height.isNaN { return } 225 | 226 | super.frame = newValue 227 | setNeedsLayout() 228 | 229 | #if DEBUG 230 | if debug { 231 | setNeedsDisplay() 232 | } 233 | #endif 234 | 235 | if superview == nil { 236 | layoutIfNeeded() 237 | } 238 | } 239 | } 240 | 241 | override open var bounds: CGRect { 242 | get { super.bounds } 243 | set { 244 | if newValue.isInfinite || newValue.isNull || newValue.minX.isNaN || newValue.minY.isNaN || newValue.width.isNaN || newValue.height.isNaN { return } 245 | 246 | super.bounds = newValue 247 | setNeedsLayout() 248 | 249 | #if DEBUG 250 | if debug { 251 | setNeedsDisplay() 252 | } 253 | #endif 254 | 255 | if superview == nil { 256 | layoutIfNeeded() 257 | } 258 | } 259 | } 260 | 261 | open override var description: String { 262 | return "[\(super.description)]-targetView: \(String(describing: targetView))" 263 | } 264 | 265 | lazy fileprivate var sizeCacheData: [String: CGSize] = { 266 | return [:] 267 | }() 268 | 269 | /// Returns `true` if `targetView` is nil or hidden. And if `ignoreHiddenView` is `true` 270 | public var isEmpty: Bool { 271 | return ((targetView?.isHidden ?? false || isHidden) && ignoreHiddenView) 272 | } 273 | 274 | /// Returns intrinsic content size 275 | open override var intrinsicContentSize: CGSize { 276 | #if os(visionOS) 277 | let scenes = UIApplication.shared.connectedScenes 278 | let windowScene = scenes.first as? UIWindowScene 279 | let window = windowScene?.windows.first 280 | let width: CGFloat = window?.bounds.width ?? 0 281 | return contentSizeThatFits(size: CGSize(width: width, height: .greatestFiniteMagnitude)) 282 | #else 283 | return contentSizeThatFits(size: CGSize(width: UIScreen.main.nativeBounds.width, height: .greatestFiniteMagnitude)) 284 | #endif 285 | } 286 | 287 | // Skeleton 288 | 289 | public var skeletonView: FLSkeletonView? 290 | /// set color for skeleton mode 291 | public var skeletonColor: UIColor = UIColor(white: 0.8, alpha: 1.0) 292 | public var skeletonMinSize: CGSize = .zero 293 | public var skeletonMaxSize: CGSize = .zero 294 | public var isSkeletonMode: Bool = false { 295 | didSet { 296 | if isSkeletonMode { 297 | skeletonView = FLSkeletonView() 298 | skeletonView!.backgroundColor = skeletonColor 299 | addSubview(skeletonView!) 300 | setNeedsLayout() 301 | } 302 | else { 303 | skeletonView?.removeFromSuperview() 304 | skeletonView = nil 305 | } 306 | } 307 | } 308 | 309 | // MARK: - 310 | 311 | @discardableResult 312 | public convenience init(_ block: (FrameLayout) throws -> Void) rethrows { 313 | self.init() 314 | try block(self) 315 | } 316 | 317 | convenience public init(targetView: UIView? = nil) { 318 | self.init() 319 | self.targetView = targetView 320 | } 321 | 322 | public required init() { 323 | super.init(frame: .zero) 324 | 325 | backgroundColor = .clear 326 | isUserInteractionEnabled = false 327 | isIntrinsicSizeEnabled = true 328 | } 329 | 330 | public required init?(coder aDecoder: NSCoder) { 331 | super.init(coder: aDecoder) 332 | } 333 | 334 | #if DEBUG 335 | override open func draw(_ rect: CGRect) { 336 | guard debug, !isEmpty, bounds != .zero else { 337 | super.draw(rect) 338 | return 339 | } 340 | 341 | if debugColor == nil { 342 | debugColor = randomColor() 343 | } 344 | 345 | guard let context = UIGraphicsGetCurrentContext() else { return } 346 | context.saveGState() 347 | context.setStrokeColor(debugColor!.cgColor) 348 | context.setLineDash(phase: 0, lengths: [4.0, 2.0]) 349 | context.stroke(bounds) 350 | context.restoreGState() 351 | } 352 | 353 | fileprivate func randomColor() -> UIColor { 354 | let colors: [UIColor] = [.red, .green, .blue, .brown, .gray, .yellow, .magenta, .black, .orange, .purple, .cyan] 355 | let randomIndex = Int(arc4random()) % colors.count 356 | return colors[randomIndex] 357 | } 358 | #endif 359 | 360 | open func sizeThatFits(_ size: CGSize, intrinsic: Bool = true) -> CGSize { 361 | isIntrinsicSizeEnabled = intrinsic 362 | return sizeThatFits(size) 363 | } 364 | 365 | override open func sizeThatFits(_ size: CGSize) -> CGSize { 366 | return sizeThatFits(size, ignoreHiddenView: ignoreHiddenView) 367 | } 368 | 369 | open func sizeThatFits(_ size: CGSize, ignoreHiddenView: Bool) -> CGSize { 370 | if !isEnabled { return .zero } 371 | 372 | willSizeThatFitsBlock?(self, size) 373 | if isEmpty && ignoreHiddenView { return .zero } 374 | 375 | if minSize == maxSize && minSize.width > 0 && minSize.height > 0 { return minSize } 376 | 377 | var result: CGSize = .zero 378 | let verticalEdgeValues = edgeInsets.left + edgeInsets.right 379 | let horizontalEdgeValues = edgeInsets.top + edgeInsets.bottom 380 | let contentSize = CGSize(width: max(size.width - verticalEdgeValues, 0), height: max(size.height - horizontalEdgeValues, 0)) 381 | 382 | if heightRatio > 0 { 383 | result.width = isIntrinsicSizeEnabled ? contentSizeThatFits(size: contentSize).width : contentSize.width 384 | result.height = result.width * heightRatio 385 | } 386 | else { 387 | result = contentSizeThatFits(size: contentSize) 388 | 389 | if !isIntrinsicSizeEnabled { 390 | result.width = contentSize.width 391 | } 392 | } 393 | 394 | result.limitedTo(minSize: minSize, maxSize: maxSize) 395 | 396 | if result.width > 0 { result.width += verticalEdgeValues } 397 | if result.height > 0 { result.height += horizontalEdgeValues } 398 | 399 | result.width = min(result.width, size.width) 400 | result.height = min(result.height, size.height) 401 | 402 | return result 403 | } 404 | 405 | override open func layoutSubviews() { 406 | if !isEnabled { return } 407 | 408 | willLayoutSubviewsBlock?(self) 409 | super.layoutSubviews() 410 | 411 | defer { 412 | if let skeletonView { 413 | var skeletonFrame: CGRect = targetView != nil ? convert(targetView!.frame, from: targetView!.superview) : bounds.inset(by: edgeInsets) 414 | skeletonFrame.size.limitedTo(minSize: skeletonMinSize, maxSize: skeletonMaxSize) 415 | skeletonView.frame = skeletonFrame 416 | skeletonView.isHidden = targetView == nil || !isEnabled || isEmpty 417 | } 418 | 419 | didLayoutSubviewsBlock?(self) 420 | } 421 | 422 | guard let targetView = targetView, !bounds.isEmpty else { 423 | bindViews(to: self) 424 | return 425 | } 426 | 427 | var targetFrame: CGRect = .zero 428 | #if swift(>=4.2) 429 | let containerFrame = bounds.inset(by: edgeInsets) 430 | #else 431 | let containerFrame = UIEdgeInsetsInsetRect(bounds, edgeInsets) 432 | #endif 433 | let contentSize = (alignment.horizontal != .fill || alignment.vertical != .fill) || (minContentSize != .zero || maxContentSize != .zero) ? contentSizeThatFits(size: containerFrame.size) : .zero 434 | 435 | switch alignment.horizontal { 436 | case .left: 437 | if allowContentHorizontalGrowing { 438 | targetFrame.size.width = max(containerFrame.width, contentSize.width) 439 | } 440 | else { 441 | targetFrame.size.width = allowContentHorizontalShrinking ? min(containerFrame.width, contentSize.width) : contentSize.width 442 | } 443 | 444 | targetFrame.origin.x = containerFrame.minX 445 | break 446 | 447 | case .right: 448 | if allowContentHorizontalGrowing { 449 | targetFrame.size.width = max(containerFrame.width, contentSize.width) 450 | } 451 | else { 452 | targetFrame.size.width = allowContentHorizontalShrinking ? min(containerFrame.width, contentSize.width) : contentSize.width 453 | } 454 | 455 | targetFrame.origin.x = containerFrame.maxX - targetFrame.width 456 | break 457 | 458 | case .center: 459 | if allowContentHorizontalGrowing { 460 | targetFrame.size.width = max(containerFrame.width, contentSize.width) 461 | } 462 | else { 463 | targetFrame.size.width = allowContentHorizontalShrinking ? min(containerFrame.width, contentSize.width) : contentSize.width 464 | } 465 | 466 | targetFrame.origin.x = containerFrame.minX + (containerFrame.width - targetFrame.width) / 2 467 | break 468 | 469 | case .fill: 470 | targetFrame.origin.x = containerFrame.minX 471 | targetFrame.size.width = containerFrame.width 472 | break 473 | 474 | case .fit: 475 | if allowContentHorizontalGrowing { 476 | targetFrame.size.width = max(containerFrame.width, contentSize.width) 477 | } 478 | else { 479 | targetFrame.size.width = min(containerFrame.width, contentSize.width) 480 | } 481 | 482 | targetFrame.origin.x = containerFrame.minX + (containerFrame.width - targetFrame.width) / 2 483 | break 484 | } 485 | 486 | switch alignment.vertical { 487 | case .top: 488 | if allowContentVerticalGrowing { 489 | targetFrame.size.height = max(containerFrame.height, contentSize.height) 490 | } 491 | else if allowContentVerticalShrinking { 492 | targetFrame.size.height = min(containerFrame.height, contentSize.height) 493 | } 494 | else { 495 | targetFrame.size.height = contentSize.height 496 | } 497 | 498 | targetFrame.origin.y = containerFrame.minY 499 | break 500 | 501 | case .bottom: 502 | if allowContentVerticalGrowing { 503 | targetFrame.size.height = max(containerFrame.height, contentSize.height) 504 | } 505 | else if allowContentVerticalShrinking { 506 | targetFrame.size.height = min(containerFrame.height, contentSize.height) 507 | } 508 | else { 509 | targetFrame.size.height = contentSize.height 510 | } 511 | 512 | targetFrame.origin.y = containerFrame.maxY - contentSize.height 513 | break 514 | 515 | case .center: 516 | if allowContentVerticalGrowing { 517 | targetFrame.size.height = max(containerFrame.height, contentSize.height) 518 | } 519 | else if allowContentVerticalShrinking { 520 | targetFrame.size.height = min(containerFrame.height, contentSize.height) 521 | } 522 | else { 523 | targetFrame.size.height = contentSize.height 524 | } 525 | 526 | targetFrame.origin.y = containerFrame.minY + (containerFrame.height - contentSize.height) / 2 527 | break 528 | 529 | case .fill: 530 | targetFrame.origin.y = containerFrame.minY 531 | targetFrame.size.height = containerFrame.height 532 | break 533 | 534 | case .fit: 535 | if allowContentVerticalGrowing { 536 | targetFrame.size.height = max(containerFrame.height, contentSize.height) 537 | } 538 | else { 539 | targetFrame.size.height = min(containerFrame.height, contentSize.height) 540 | } 541 | 542 | targetFrame.origin.y = containerFrame.minY + (containerFrame.height - targetFrame.height) / 2 543 | break 544 | } 545 | 546 | targetFrame.size.limitedTo(minSize: minContentSize, maxSize: maxContentSize) 547 | targetFrame = targetFrame.integral 548 | targetFrame = targetFrame.offsetBy(dx: translationOffset.x, dy: translationOffset.y) 549 | 550 | if targetView.superview == self { 551 | targetView.frame = targetFrame 552 | } 553 | else { 554 | if superview == nil || window == nil { 555 | targetFrame.origin.x = frame.minX 556 | targetFrame.origin.y = frame.minY 557 | 558 | var superView: UIView? = superview 559 | while superView != nil && (superView is FrameLayout) { 560 | targetFrame.origin.x += superView!.frame.minX 561 | targetFrame.origin.y += superView!.frame.minY 562 | superView = superView!.superview 563 | } 564 | 565 | targetView.frame = targetFrame.offsetBy(dx: translationOffset.x, dy: translationOffset.y) 566 | } 567 | else { 568 | targetView.frame = convert(targetFrame, to: targetView.superview) 569 | } 570 | } 571 | 572 | bindViews(to: targetView) 573 | } 574 | 575 | func bindViews(to targetView: UIView) { 576 | var bindViews = bindingViews ?? [] 577 | if let views = lazyBindingViews?() { bindViews.append(contentsOf: views.compactMap {$0}) } 578 | guard !bindViews.isEmpty else { return } 579 | #if swift(>=4.2) 580 | let targetFrame = targetView.frame.inset(by: bindingEdgeInsets) 581 | #else 582 | let targetFrame = UIEdgeInsetsInsetRect(targetView.frame, bindingEdgeInsets) 583 | #endif 584 | bindViews.forEach { 585 | if $0.superview == targetView { 586 | $0.frame = CGRect(origin: .zero, size: targetFrame.size) 587 | } 588 | else if $0.superview != targetView.superview, let superView1 = $0.superview, let superView2 = targetView.superview { 589 | $0.frame = superView2.convert(targetFrame, to: superView1) 590 | } 591 | else { 592 | $0.frame = targetFrame 593 | } 594 | } 595 | } 596 | 597 | open override func didMoveToWindow() { 598 | super.didMoveToWindow() 599 | setNeedsLayout() 600 | } 601 | 602 | open override func didMoveToSuperview() { 603 | super.didMoveToSuperview() 604 | setNeedsLayout() 605 | } 606 | 607 | override open func setNeedsLayout() { 608 | super.setNeedsLayout() 609 | targetView?.setNeedsLayout() 610 | } 611 | 612 | override open func layoutIfNeeded() { 613 | super.layoutIfNeeded() 614 | targetView?.layoutIfNeeded() 615 | } 616 | 617 | // MARK: - 618 | 619 | fileprivate func addressOf(_ o: T) -> String { 620 | let addr = unsafeBitCast(o, to: Int.self) 621 | return String(format: "%p", addr) 622 | } 623 | 624 | fileprivate func contentSizeThatFits(size: CGSize) -> CGSize { 625 | guard let targetView = targetView else { return .zero } 626 | 627 | if minContentSize == maxContentSize && minContentSize.width > 0 && minContentSize.height > 0 { return minContentSize } 628 | 629 | var result: CGSize 630 | 631 | if minSize == maxSize && minSize.width > 0 && minSize.height > 0 { 632 | result = minSize // fixedSize 633 | } 634 | else { 635 | if shouldCacheSize { 636 | let key = "\(addressOf(targetView))_\(size)" 637 | if let value = sizeCacheData[key] { 638 | return value 639 | } 640 | else { 641 | result = targetView.sizeThatFits(size) 642 | sizeCacheData[key] = result 643 | } 644 | } 645 | else { 646 | result = targetView.sizeThatFits(size) 647 | } 648 | 649 | result.width += extendSize.width 650 | result.height += extendSize.height 651 | 652 | result.limitedTo(minSize: minSize, maxSize: maxSize) 653 | } 654 | 655 | result.limitedTo(minSize: minContentSize, maxSize: maxContentSize) 656 | return result 657 | } 658 | 659 | } 660 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/GridFrameLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridFrameLayout.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 5/8/20. 6 | // 7 | 8 | import UIKit 9 | 10 | open class GridFrameLayout: FrameLayout { 11 | public var axis: NKLayoutAxis = .horizontal { 12 | didSet { 13 | arrangeViews() 14 | } 15 | } 16 | 17 | /// Auto set number of columns or rows base on its size 18 | public var isAutoSize = false 19 | 20 | /* 21 | public override var isUserInteractionEnabled: Bool { 22 | didSet { 23 | stackLayout.frameLayouts.forEach { $0.isUserInteractionEnabled = isUserInteractionEnabled } 24 | } 25 | } 26 | */ 27 | 28 | public override var isIntrinsicSizeEnabled: Bool { 29 | get { stackLayout.isIntrinsicSizeEnabled } 30 | set { 31 | stackLayout.isIntrinsicSizeEnabled = newValue 32 | setNeedsLayout() 33 | } 34 | } 35 | 36 | override public var edgeInsets: UIEdgeInsets { 37 | get { stackLayout.edgeInsets } 38 | set { 39 | stackLayout.edgeInsets = newValue 40 | setNeedsLayout() 41 | } 42 | } 43 | 44 | override public var minSize: CGSize { 45 | didSet { 46 | stackLayout.minSize = minSize 47 | setNeedsLayout() 48 | } 49 | } 50 | 51 | override public var maxSize: CGSize { 52 | didSet { 53 | stackLayout.maxSize = minSize 54 | setNeedsLayout() 55 | } 56 | } 57 | 58 | override public var fixedSize: CGSize { 59 | didSet { 60 | stackLayout.fixedSize = fixedSize 61 | setNeedsLayout() 62 | } 63 | } 64 | 65 | public var minRowHeight: CGFloat = 0 { 66 | didSet { 67 | stackLayout.frameLayouts.forEach { $0.minSize = CGSize(width: $0.minSize.width, height: minRowHeight) } 68 | setNeedsLayout() 69 | } 70 | } 71 | 72 | public var maxRowHeight: CGFloat = 0 { 73 | didSet { 74 | stackLayout.frameLayouts.forEach { $0.maxSize = CGSize(width: $0.maxSize.width, height: maxRowHeight) } 75 | setNeedsLayout() 76 | } 77 | } 78 | 79 | public var fixedRowHeight: CGFloat = 0 { 80 | didSet { 81 | stackLayout.frameLayouts.forEach { $0.fixedSize = CGSize(width: $0.fixedSize.width, height: fixedRowHeight) } 82 | setNeedsLayout() 83 | } 84 | } 85 | 86 | public var minColumnWidth: CGFloat = 0 { 87 | didSet { 88 | stackLayout.frameLayouts.filter { $0 is StackFrameLayout }.forEach { $0.minSize = CGSize(width: minColumnWidth, height: $0.minSize.height) } 89 | setNeedsLayout() 90 | } 91 | } 92 | 93 | public var maxColumnWidth: CGFloat = 0 { 94 | didSet { 95 | stackLayout.frameLayouts.filter { $0 is StackFrameLayout }.forEach { $0.maxSize = CGSize(width: maxColumnWidth, height: $0.maxSize.height) } 96 | setNeedsLayout() 97 | } 98 | } 99 | 100 | public var fixedColumnWidth: CGFloat = 0 { 101 | didSet { 102 | stackLayout.frameLayouts.filter { $0 is StackFrameLayout }.forEach { $0.fixedSize = CGSize(width: fixedColumnWidth, height: $0.fixedSize.height) } 103 | setNeedsLayout() 104 | } 105 | } 106 | 107 | override public var heightRatio: CGFloat { 108 | didSet { 109 | stackLayout.heightRatio = heightRatio 110 | setNeedsLayout() 111 | } 112 | } 113 | 114 | override public var debug: Bool { 115 | didSet { stackLayout.debug = debug } 116 | } 117 | 118 | override public var debugColor: UIColor? { 119 | didSet { stackLayout.debugColor = debugColor } 120 | } 121 | 122 | public var verticalSpacing: CGFloat { 123 | get { stackLayout.spacing } 124 | set { 125 | stackLayout.spacing = newValue 126 | setNeedsLayout() 127 | } 128 | } 129 | 130 | public var horizontalSpacing: CGFloat = 0 { 131 | didSet { 132 | stackLayout.frameLayouts.filter { $0 is StackFrameLayout }.forEach { ($0 as? StackFrameLayout)?.spacing = horizontalSpacing } 133 | setNeedsLayout() 134 | } 135 | } 136 | 137 | // Skeleton 138 | 139 | /// set color for skeleton mode 140 | override public var skeletonColor: UIColor { 141 | didSet { 142 | stackLayout.skeletonColor = skeletonColor 143 | } 144 | } 145 | override public var skeletonMinSize: CGSize { 146 | didSet { 147 | stackLayout.skeletonMinSize = skeletonMinSize 148 | } 149 | } 150 | override public var skeletonMaxSize: CGSize { 151 | didSet { 152 | stackLayout.skeletonMaxSize = skeletonMaxSize 153 | } 154 | } 155 | override public var isSkeletonMode: Bool { 156 | didSet { 157 | stackLayout.isSkeletonMode = isSkeletonMode 158 | setNeedsLayout() 159 | } 160 | } 161 | 162 | // MARK: - 163 | 164 | public var rows: Int { 165 | get { stackLayout.frameLayouts.count } 166 | set { 167 | let count = stackLayout.frameLayouts.count 168 | 169 | if newValue == 0 { 170 | removeAllCells() 171 | return 172 | } 173 | 174 | if newValue < count { 175 | while stackLayout.frameLayouts.count > newValue { 176 | removeRow(at: stackLayout.frameLayouts.count - 1) 177 | } 178 | } 179 | else if newValue > count { 180 | while stackLayout.frameLayouts.count < newValue { 181 | addRow() 182 | } 183 | } 184 | } 185 | } 186 | 187 | private var initColumns: Int = 0 188 | public var columns: Int = 0 { 189 | didSet { 190 | stackLayout.frameLayouts.forEach { (layout) in 191 | if let layout = layout as? StackFrameLayout { 192 | layout.numberOfFrameLayouts = columns 193 | layout.frameLayouts.forEach { 194 | if fixedColumnWidth > 0 { 195 | $0.fixedSize = CGSize(width: fixedColumnWidth, height: $0.fixedSize.height) 196 | } 197 | else { 198 | $0.minSize = CGSize(width: minColumnWidth, height: $0.minSize.height) 199 | $0.maxSize = CGSize(width: maxColumnWidth, height: $0.maxSize.height) 200 | } 201 | } 202 | } 203 | } 204 | } 205 | } 206 | 207 | public fileprivate(set) var viewCount: Int = 0 208 | public var views: [UIView] = [] { 209 | didSet { 210 | views.forEach { 211 | if $0.superview == nil { 212 | addSubview($0) 213 | } 214 | } 215 | 216 | viewCount = views.count 217 | arrangeViews() 218 | } 219 | } 220 | 221 | public var firstRowLayout: StackFrameLayout? { 222 | return stackLayout.firstFrameLayout as? StackFrameLayout 223 | } 224 | 225 | public var lastRowLayout: StackFrameLayout? { 226 | return stackLayout.lastFrameLayout as? StackFrameLayout 227 | } 228 | 229 | let stackLayout = StackFrameLayout(axis: .vertical, distribution: .equal) 230 | 231 | // MARK: - 232 | 233 | public convenience init(axis: NKLayoutAxis, column: Int = 0, rows: Int = 0) { 234 | self.init() 235 | 236 | self.axis = axis 237 | defer { 238 | self.rows = rows 239 | self.columns = column 240 | self.initColumns = column 241 | } 242 | } 243 | 244 | public required init() { 245 | super.init() 246 | 247 | axis = .horizontal 248 | isIntrinsicSizeEnabled = true 249 | addSubview(stackLayout) 250 | } 251 | 252 | public required init?(coder aDecoder: NSCoder) { 253 | super.init(coder: aDecoder) 254 | } 255 | 256 | @discardableResult 257 | public init(_ block: (GridFrameLayout) throws -> Void) rethrows { 258 | super.init() 259 | try block(self) 260 | } 261 | 262 | // MARK: - 263 | 264 | public func viewAt(row: Int, column: Int) -> UIView? { 265 | return frameLayout(row: row, column: column)?.targetView 266 | } 267 | 268 | public func viewsAt(row: Int) -> [UIView]? { 269 | return rows(at: row)?.frameLayouts.compactMap( { return $0.targetView } ) 270 | } 271 | 272 | public func viewsAt(column: Int) -> [UIView]? { 273 | var results = [UIView]() 274 | for r in 0.. StackFrameLayout? { 284 | guard index > -1, index < stackLayout.frameLayouts.count, let frameLayout = stackLayout.frameLayouts[index] as? StackFrameLayout else { return nil } 285 | return frameLayout 286 | } 287 | 288 | public func frameLayout(row: Int, column: Int) -> FrameLayout? { 289 | guard row > -1, row < stackLayout.frameLayouts.count else { return nil } 290 | guard let rowLayout = stackLayout.frameLayouts[row] as? StackFrameLayout else { return nil } 291 | return rowLayout.frameLayout(at: column) 292 | } 293 | 294 | public func allFrameLayouts() -> [FrameLayout] { 295 | return stackLayout.frameLayouts.compactMap { $0 as? StackFrameLayout }.flatMap { $0.frameLayouts } 296 | } 297 | 298 | public func lastFrameLayout(containsView: Bool = false) -> FrameLayout? { 299 | guard let lastRows = lastRowLayout else { return nil } 300 | 301 | if containsView { 302 | return lastRows.frameLayouts.last(where: { $0.targetView != nil }) 303 | } 304 | else { 305 | return lastRows.frameLayouts.last 306 | } 307 | } 308 | 309 | // MARK: - 310 | 311 | fileprivate func newRow() -> StackFrameLayout { 312 | let layout = StackFrameLayout(axis: .horizontal, distribution: .equal) 313 | layout.parent = self 314 | layout.numberOfFrameLayouts = columns 315 | layout.spacing = horizontalSpacing 316 | layout.debug = debug 317 | layout.isSkeletonMode = isSkeletonMode || layout.isSkeletonMode 318 | layout.skeletonColor = skeletonColor 319 | 320 | if fixedRowHeight > 0 { 321 | layout.fixedSize = CGSize(width: 0, height: fixedRowHeight) 322 | } 323 | else { 324 | layout.minSize = CGSize(width: 0, height: minRowHeight) 325 | layout.maxSize = CGSize(width: 0, height: maxRowHeight) 326 | } 327 | 328 | layout.frameLayouts.forEach { 329 | if fixedColumnWidth > 0 { 330 | $0.fixedSize = CGSize(width: fixedColumnWidth, height: $0.fixedSize.height) 331 | } 332 | else { 333 | $0.minSize = CGSize(width: minColumnWidth, height: $0.minSize.height) 334 | $0.maxSize = CGSize(width: maxColumnWidth, height: $0.maxSize.height) 335 | } 336 | } 337 | 338 | return layout 339 | } 340 | 341 | @discardableResult 342 | open func addRow() -> StackFrameLayout { 343 | let layout = newRow() 344 | stackLayout.add(layout) 345 | setNeedsLayout() 346 | return layout 347 | } 348 | 349 | @discardableResult 350 | open func insertRow(at index: Int, invert: Bool = false) -> StackFrameLayout { 351 | let layout = newRow() 352 | stackLayout.insert(layout, at: index, invert: invert) 353 | setNeedsLayout() 354 | return layout 355 | } 356 | 357 | open func removeRow(at index: Int) { 358 | stackLayout.removeFrameLayout(at: index) 359 | setNeedsLayout() 360 | } 361 | 362 | open func removeLastRow() { 363 | guard stackLayout.frameLayouts.count > 0 else { return } 364 | stackLayout.removeFrameLayout(at: stackLayout.frameLayouts.count - 1) 365 | setNeedsLayout() 366 | } 367 | 368 | // MARK: - 369 | 370 | open func addColumn() { 371 | stackLayout.frameLayouts.forEach { 372 | if let rowLayout = $0 as? StackFrameLayout { 373 | let row = rowLayout.add() 374 | row.debug = debug 375 | row.isSkeletonMode = isSkeletonMode || row.isSkeletonMode 376 | row.skeletonColor = skeletonColor 377 | 378 | if fixedColumnWidth > 0 { 379 | row.fixedSize = CGSize(width: fixedColumnWidth, height: fixedRowHeight) 380 | } 381 | else { 382 | row.minSize = CGSize(width: minColumnWidth, height: minRowHeight) 383 | row.maxSize = CGSize(width: maxColumnWidth, height: maxRowHeight) 384 | } 385 | } 386 | } 387 | setNeedsLayout() 388 | } 389 | 390 | open func insertColumn(at index: Int) { 391 | stackLayout.frameLayouts.forEach { 392 | if let rowLayout = $0 as? StackFrameLayout { 393 | let row = rowLayout.insert(nil, at: index) 394 | row.debug = debug 395 | row.isSkeletonMode = isSkeletonMode 396 | row.skeletonColor = skeletonColor 397 | 398 | if fixedColumnWidth > 0 { 399 | row.fixedSize = CGSize(width: fixedColumnWidth, height: fixedRowHeight) 400 | } 401 | else { 402 | row.minSize = CGSize(width: minColumnWidth, height: minRowHeight) 403 | row.maxSize = CGSize(width: maxColumnWidth, height: maxRowHeight) 404 | } 405 | } 406 | } 407 | setNeedsLayout() 408 | } 409 | 410 | open func removeColumn(at index: Int) { 411 | stackLayout.frameLayouts.forEach { ($0 as? StackFrameLayout)?.removeFrameLayout(at: index) } 412 | setNeedsLayout() 413 | } 414 | 415 | open func removeLastColumn() { 416 | stackLayout.frameLayouts.forEach { 417 | if let rowLayout = $0 as? StackFrameLayout { 418 | rowLayout.removeFrameLayout(at: rowLayout.frameLayouts.count - 1) 419 | } 420 | } 421 | setNeedsLayout() 422 | } 423 | 424 | open func removeAllCells() { 425 | stackLayout.removeAll() 426 | } 427 | 428 | // MARK: - 429 | 430 | func arrangeViews(autoColumns: Bool = true) { 431 | guard viewCount > 0 else { return } 432 | 433 | var numberOfRows = stackLayout.frameLayouts.count 434 | if isAutoSize { 435 | if axis == .horizontal, columns > 0 { 436 | let fitRows = max(Int(ceil(Double(viewCount) / Double(columns))), 1) 437 | if fitRows != rows { 438 | rows = fitRows 439 | numberOfRows = fitRows 440 | } 441 | } 442 | else if axis == .vertical, rows > 0 { 443 | let fitColumn = max(Int(ceil(Double(viewCount) / Double(rows))), 1) 444 | if fitColumn != columns { 445 | columns = fitColumn 446 | } 447 | } 448 | } 449 | 450 | var i: Int = 0 451 | 452 | if axis == .horizontal { 453 | if autoColumns, maxColumnWidth > 0 { 454 | var viewSize = stackLayout.bounds.size 455 | if viewSize == .zero { viewSize = bounds.size } 456 | let fitColumns = max(Int(viewSize.width / maxColumnWidth), max(initColumns, 1)) 457 | if columns != fitColumns { 458 | columns = fitColumns 459 | arrangeViews(autoColumns: false) 460 | return 461 | } 462 | } 463 | 464 | for r in 0.. Self { 495 | isUserInteractionEnabled = enabled 496 | stackLayout.frameLayouts.forEach { $0.isUserInteractionEnabled = enabled } 497 | return self 498 | } 499 | 500 | // MARK: - 501 | 502 | fileprivate var lastSize: CGSize = .zero 503 | open override func layoutSubviews() { 504 | super.layoutSubviews() 505 | if !isEnabled { return } 506 | 507 | if maxColumnWidth > 0, lastSize != bounds.size { 508 | lastSize = bounds.size 509 | arrangeViews() 510 | } 511 | 512 | if stackLayout.frame != bounds { 513 | stackLayout.frame = bounds 514 | } 515 | } 516 | 517 | open override func sizeThatFits(_ size: CGSize) -> CGSize { 518 | if !isEnabled { return .zero } 519 | return stackLayout.sizeThatFits(size) 520 | } 521 | 522 | } 523 | -------------------------------------------------------------------------------- /FrameLayoutKit/Classes/ScrollStackView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollStackView.swift 3 | // FrameLayoutKit 4 | // 5 | // Created by Nam Kennic on 6/23/20. 6 | // 7 | 8 | import UIKit 9 | 10 | open class ScrollStackView: UIView { 11 | 12 | open var views: [UIView] { 13 | get { frameLayouts.compactMap { $0.targetView } } 14 | set { _views = newValue } 15 | } 16 | 17 | fileprivate var _views: [UIView] = [] { 18 | didSet { 19 | updateLayout() 20 | setNeedsLayout() 21 | } 22 | } 23 | 24 | open var spacing: CGFloat { 25 | get { frameLayout.spacing } 26 | set { 27 | frameLayout.spacing = newValue 28 | setNeedsLayout() 29 | } 30 | } 31 | 32 | open var edgeInsets: UIEdgeInsets { 33 | get { frameLayout.edgeInsets } 34 | set { 35 | frameLayout.edgeInsets = newValue 36 | setNeedsLayout() 37 | } 38 | } 39 | 40 | open var isDirectionalLockEnabled: Bool { 41 | get { scrollView.isDirectionalLockEnabled } 42 | set { 43 | scrollView.isDirectionalLockEnabled = newValue 44 | setNeedsLayout() 45 | } 46 | } 47 | 48 | override open var frame: CGRect { 49 | didSet { setNeedsLayout() } 50 | } 51 | 52 | override open var bounds: CGRect { 53 | didSet { setNeedsLayout() } 54 | } 55 | 56 | public var axis: NKLayoutAxis { 57 | get { frameLayout.axis } 58 | set { 59 | frameLayout.axis = newValue 60 | setNeedsLayout() 61 | } 62 | } 63 | 64 | public var distribution: NKLayoutDistribution { 65 | get { frameLayout.distribution } 66 | set { 67 | frameLayout.distribution = newValue 68 | setNeedsLayout() 69 | } 70 | } 71 | 72 | public var debug: Bool { 73 | get { frameLayout.debug } 74 | set { frameLayout.debug = newValue } 75 | } 76 | 77 | public var debugColor: UIColor? { 78 | get { frameLayout.debugColor } 79 | set { frameLayout.debugColor = newValue } 80 | } 81 | 82 | public var isOverlapped: Bool { 83 | get { frameLayout.isOverlapped } 84 | set { frameLayout.isOverlapped = newValue } 85 | } 86 | 87 | public var fixedContentSize: CGSize { 88 | get { frameLayout.fixedContentSize } 89 | set { 90 | frameLayout.fixedContentSize = newValue 91 | setNeedsLayout() 92 | } 93 | } 94 | 95 | public var fixedContentWidth: CGFloat { 96 | get { frameLayout.fixedContentWidth } 97 | set { 98 | frameLayout.fixedContentWidth = newValue 99 | setNeedsLayout() 100 | } 101 | } 102 | 103 | public var fixedContentHeight: CGFloat { 104 | get { frameLayout.fixedContentHeight } 105 | set { 106 | frameLayout.fixedContentHeight = newValue 107 | setNeedsLayout() 108 | } 109 | } 110 | 111 | public var minContentSize: CGSize { 112 | get { frameLayout.minContentSize } 113 | set { 114 | frameLayout.minContentSize = newValue 115 | setNeedsLayout() 116 | } 117 | } 118 | 119 | public var minContentWidth: CGFloat { 120 | get { frameLayout.minContentWidth } 121 | set { 122 | frameLayout.minContentWidth = newValue 123 | setNeedsLayout() 124 | } 125 | } 126 | 127 | public var minContentHeight: CGFloat { 128 | get { frameLayout.minContentHeight } 129 | set { 130 | frameLayout.minContentHeight = newValue 131 | setNeedsLayout() 132 | } 133 | } 134 | 135 | public var maxContentSize: CGSize { 136 | get { frameLayout.maxContentSize } 137 | set { 138 | frameLayout.maxContentSize = newValue 139 | setNeedsLayout() 140 | } 141 | } 142 | 143 | public var maxContentWidth: CGFloat { 144 | get { frameLayout.maxContentWidth } 145 | set { 146 | frameLayout.maxContentWidth = newValue 147 | setNeedsLayout() 148 | } 149 | } 150 | 151 | public var maxContentHeight: CGFloat { 152 | get { frameLayout.maxContentHeight } 153 | set { 154 | frameLayout.maxContentHeight = newValue 155 | setNeedsLayout() 156 | } 157 | } 158 | 159 | public var minSize: CGSize { 160 | get { frameLayout.minSize } 161 | set { 162 | frameLayout.minSize = newValue 163 | setNeedsLayout() 164 | } 165 | } 166 | 167 | public var minWidth: CGFloat { 168 | get { frameLayout.minWidth } 169 | set { 170 | frameLayout.minWidth = newValue 171 | setNeedsLayout() 172 | } 173 | } 174 | 175 | public var minHeight: CGFloat { 176 | get { frameLayout.minHeight } 177 | set { 178 | frameLayout.minHeight = newValue 179 | setNeedsLayout() 180 | } 181 | } 182 | 183 | public var maxSize: CGSize { 184 | get { frameLayout.maxSize } 185 | set { 186 | frameLayout.maxSize = newValue 187 | setNeedsLayout() 188 | } 189 | } 190 | 191 | public var maxWidth: CGFloat { 192 | get { frameLayout.maxWidth } 193 | set { 194 | frameLayout.maxWidth = newValue 195 | setNeedsLayout() 196 | } 197 | } 198 | 199 | public var maxHeight: CGFloat { 200 | get { frameLayout.maxHeight } 201 | set { 202 | frameLayout.maxHeight = newValue 203 | setNeedsLayout() 204 | } 205 | } 206 | 207 | public var fixedSize: CGSize { 208 | get { frameLayout.fixedSize } 209 | set { 210 | frameLayout.fixedSize = newValue 211 | setNeedsLayout() 212 | } 213 | } 214 | 215 | public var fixedWidth: CGFloat { 216 | get { frameLayout.fixedWidth } 217 | set { 218 | frameLayout.fixedWidth = newValue 219 | setNeedsLayout() 220 | } 221 | } 222 | 223 | public var fixedHeight: CGFloat { 224 | get { frameLayout.fixedHeight } 225 | set { 226 | frameLayout.fixedHeight = newValue 227 | setNeedsLayout() 228 | } 229 | } 230 | 231 | /// Set minContentSize for every FrameLayout inside 232 | open var minItemSize: CGSize { 233 | get { frameLayout.minItemSize } 234 | set { 235 | frameLayout.minItemSize = newValue 236 | setNeedsLayout() 237 | } 238 | } 239 | 240 | /// Set maxContentSize for every FrameLayout inside 241 | open var maxItemSize: CGSize { 242 | get { frameLayout.maxItemSize } 243 | set { 244 | frameLayout.maxItemSize = newValue 245 | setNeedsLayout() 246 | } 247 | } 248 | 249 | /// Set fixedContentSize for every FrameLayout inside 250 | open var fixedItemSize: CGSize { 251 | get { frameLayout.fixedItemSize } 252 | set { 253 | frameLayout.fixedItemSize = newValue 254 | setNeedsLayout() 255 | } 256 | } 257 | 258 | public var extendSize: CGSize { 259 | get { frameLayout.extendSize } 260 | set { 261 | frameLayout.extendSize = newValue 262 | setNeedsLayout() 263 | } 264 | } 265 | 266 | public var extendWidth: CGFloat { 267 | get { frameLayout.extendWidth } 268 | set { 269 | frameLayout.extendWidth = newValue 270 | setNeedsLayout() 271 | } 272 | } 273 | 274 | public var extendHeight: CGFloat { 275 | get { frameLayout.extendHeight } 276 | set { 277 | frameLayout.extendHeight = newValue 278 | setNeedsLayout() 279 | } 280 | } 281 | 282 | public var contentFitSize: CGSize = CGSize(width: CGFloat.infinity, height: CGFloat.infinity) { 283 | didSet { setNeedsLayout() } 284 | } 285 | 286 | public var ignoreHiddenView: Bool { 287 | get { frameLayout.ignoreHiddenView } 288 | set { 289 | frameLayout.ignoreHiddenView = newValue 290 | setNeedsLayout() 291 | } 292 | } 293 | 294 | public var isIntrinsicSizeEnabled: Bool { 295 | get { frameLayout.isIntrinsicSizeEnabled } 296 | set { 297 | frameLayout.isIntrinsicSizeEnabled = newValue 298 | setNeedsLayout() 299 | } 300 | } 301 | 302 | public var isFlexible: Bool { 303 | get { frameLayout.isFlexible } 304 | set { 305 | frameLayout.isFlexible = newValue 306 | setNeedsLayout() 307 | } 308 | } 309 | 310 | public var isEnabled = true 311 | 312 | public var heightRatio: CGFloat { 313 | get { frameLayout.heightRatio } 314 | set { 315 | frameLayout.heightRatio = newValue 316 | setNeedsLayout() 317 | } 318 | } 319 | 320 | // Skeleton 321 | 322 | /// set color for skeleton mode 323 | public var skeletonColor: UIColor { 324 | get { frameLayout.skeletonColor } 325 | set { 326 | frameLayout.skeletonColor = newValue 327 | setNeedsLayout() 328 | } 329 | } 330 | public var skeletonMinSize: CGSize { 331 | get { frameLayout.skeletonMinSize } 332 | set { 333 | frameLayout.skeletonMinSize = newValue 334 | setNeedsLayout() 335 | } 336 | } 337 | public var skeletonMaxSize: CGSize { 338 | get { frameLayout.skeletonMaxSize } 339 | set { 340 | frameLayout.skeletonMaxSize = newValue 341 | setNeedsLayout() 342 | } 343 | } 344 | public var isSkeletonMode: Bool { 345 | get { frameLayout.isSkeletonMode } 346 | set { 347 | frameLayout.isSkeletonMode = newValue 348 | setNeedsLayout() 349 | } 350 | } 351 | 352 | public var frameLayouts: [FrameLayout] { 353 | get { frameLayout.frameLayouts } 354 | set { frameLayout.frameLayouts = newValue } 355 | } 356 | 357 | public var firstFrameLayout: FrameLayout? { frameLayout.firstFrameLayout } 358 | public var lastFrameLayout: FrameLayout? { frameLayout.lastFrameLayout } 359 | 360 | /// Block will be called before calling sizeThatFits 361 | @available(*, deprecated, renamed: "willSizeThatFitsBlock") 362 | public var preSizeThatFitsConfigurationBlock: ((ScrollStackView, CGSize) -> Void)? { 363 | get { willSizeThatFitsBlock } 364 | set { willSizeThatFitsBlock = newValue } 365 | } 366 | 367 | @available(*, deprecated, renamed: "willLayoutSubviewsBlock") 368 | public var preLayoutConfigurationBlock: ((ScrollStackView) -> Void)? { 369 | get { willLayoutSubviewsBlock } 370 | set { willLayoutSubviewsBlock = newValue } 371 | } 372 | 373 | /// Block will be called before calling sizeThatFits 374 | public var willSizeThatFitsBlock: ((ScrollStackView, CGSize) -> Void)? 375 | /// Block will be called before calling layoutSubviews 376 | public var willLayoutSubviewsBlock: ((ScrollStackView) -> Void)? 377 | /// Block will be called at the end of layoutSubviews function 378 | public var didLayoutSubviewsBlock: ((ScrollStackView) -> Void)? 379 | 380 | public let scrollView = UIScrollView() 381 | public let frameLayout = StackFrameLayout(axis: .vertical, distribution: .top) 382 | 383 | // MARK: - 384 | 385 | convenience public init(axis: NKLayoutAxis = .vertical, distribution: NKLayoutDistribution = .top, views: [UIView]? = nil) { 386 | self.init() 387 | 388 | self.axis = axis 389 | self.distribution = distribution 390 | 391 | defer { 392 | if let views, !views.isEmpty { 393 | self.views = views 394 | } 395 | } 396 | } 397 | 398 | @discardableResult 399 | public convenience init(_ block: (ScrollStackView) throws -> Void) rethrows { 400 | self.init() 401 | try block(self) 402 | } 403 | 404 | public required init() { 405 | super.init(frame: .zero) 406 | 407 | scrollView.bounces = true 408 | scrollView.alwaysBounceHorizontal = false 409 | scrollView.alwaysBounceVertical = false 410 | scrollView.isDirectionalLockEnabled = true 411 | scrollView.showsVerticalScrollIndicator = false 412 | scrollView.showsHorizontalScrollIndicator = false 413 | scrollView.clipsToBounds = false 414 | scrollView.delaysContentTouches = false 415 | 416 | #if os(iOS) 417 | if #available(iOS 11.0, *) { scrollView.contentInsetAdjustmentBehavior = .never } 418 | if #available(iOS 13.0, *) { scrollView.automaticallyAdjustsScrollIndicatorInsets = false } 419 | #endif 420 | 421 | frameLayout.spacing = 0.0 422 | frameLayout.isIntrinsicSizeEnabled = true 423 | frameLayout.shouldCacheSize = false 424 | scrollView.addSubview(frameLayout) 425 | addSubview(scrollView) 426 | } 427 | 428 | required public init?(coder aDecoder: NSCoder) { 429 | super.init(coder: aDecoder) 430 | } 431 | 432 | override open func sizeThatFits(_ size: CGSize) -> CGSize { 433 | if !isEnabled { return .zero } 434 | willSizeThatFitsBlock?(self, size) 435 | return frameLayout.sizeThatFits(size) 436 | } 437 | 438 | override open func layoutSubviews() { 439 | if !isEnabled { return } 440 | 441 | willLayoutSubviewsBlock?(self) 442 | super.layoutSubviews() 443 | 444 | let viewSize = bounds.size 445 | let sizeToFit = !isDirectionalLockEnabled ? contentFitSize : (axis == .horizontal ? CGSize(width: contentFitSize.width, height: viewSize.height) : CGSize(width: viewSize.width, height: contentFitSize.height)) 446 | let contentSize = frameLayout.sizeThatFits(sizeToFit, intrinsic: true) 447 | scrollView.contentSize = contentSize 448 | scrollView.frame = bounds 449 | 450 | var contentFrame = bounds 451 | if axis == .horizontal { 452 | contentFrame.size.width = max(viewSize.width, contentSize.width) 453 | if isDirectionalLockEnabled { 454 | scrollView.contentSize.height = min(viewSize.height, contentSize.height) 455 | } 456 | else { 457 | contentFrame.size.height = max(viewSize.height, contentSize.height) 458 | } 459 | } 460 | else { 461 | contentFrame.size.height = max(viewSize.height, contentSize.height) 462 | if isDirectionalLockEnabled { 463 | scrollView.contentSize.width = min(viewSize.width, contentSize.width) 464 | } 465 | else { 466 | contentFrame.size.width = max(viewSize.width, contentSize.width) 467 | } 468 | } 469 | 470 | frameLayout.frame = contentFrame 471 | didLayoutSubviewsBlock?(self) 472 | } 473 | 474 | // MARK: - 475 | 476 | public func view(at index: Int) -> UIView? { frameLayout.frameLayout(at: index)?.targetView } 477 | public func frameLayout(at index: Int) -> FrameLayout? { frameLayout.frameLayout(at: index) } 478 | public func frameLayout(with view: UIView) -> FrameLayout? { frameLayout.frameLayout(with: view) } 479 | public func enumerate(_ block: (FrameLayout, Int, inout Bool) -> Void) { frameLayout.enumerate(block) } 480 | 481 | @discardableResult 482 | public func flexible(ratio: CGFloat = -1) -> Self { 483 | frameLayout.flexible(ratio: ratio) 484 | return self 485 | } 486 | 487 | @discardableResult 488 | public func invert() -> Self { 489 | frameLayout.invert() 490 | return self 491 | } 492 | 493 | @discardableResult 494 | open func add(_ view: UIView?) -> FrameLayout { 495 | let layout = frameLayout.add(view) 496 | if let view { scrollView.addSubview(view) } 497 | setNeedsLayout() 498 | return layout 499 | } 500 | 501 | @discardableResult 502 | open func add(_ views: [UIView]) -> [FrameLayout] { 503 | return views.map { add($0) } 504 | } 505 | 506 | @discardableResult 507 | open func insert(_ view: UIView?, at index: Int, invert: Bool = false) -> FrameLayout { 508 | let targetIndex = invert ? max(frameLayouts.count - index, 0) : index 509 | let layout = frameLayout.insert(view, at: targetIndex) 510 | if let view { scrollView.insertSubview(view, at: targetIndex) } 511 | setNeedsLayout() 512 | return layout 513 | } 514 | 515 | @discardableResult 516 | open func addSpace(_ size: CGFloat = 0) -> FrameLayout { 517 | let layout = add(UIView()) 518 | layout.minSize = CGSize(width: axis == .horizontal ? size : 0, height: axis == .vertical ? size : 0) 519 | return layout 520 | } 521 | 522 | @discardableResult 523 | open func replace(view: UIView, at index: Int) -> Self { 524 | self.view(at: index)?.removeFromSuperview() 525 | scrollView.addSubview(view) 526 | frameLayout.frameLayout(at: index)?.targetView = view 527 | setNeedsLayout() 528 | return self 529 | } 530 | 531 | @discardableResult 532 | open func removeView(at index: Int) -> Self { 533 | frameLayout.removeFrameLayout(at: index, autoRemoveTargetView: true) 534 | setNeedsLayout() 535 | return self 536 | } 537 | 538 | @discardableResult 539 | open func removeAll() -> Self { 540 | frameLayout.removeAll(autoRemoveTargetView: true) 541 | setNeedsLayout() 542 | return self 543 | } 544 | 545 | open func relayoutSubviews(animateDuration: TimeInterval = 0.35, options: UIView.AnimationOptions = .curveEaseInOut, completion: ((Bool) -> Void)? = nil) { 546 | setNeedsLayout() 547 | 548 | UIView.animate(withDuration: animateDuration, delay: 0.0, options: options, animations: { 549 | self.layoutIfNeeded() 550 | }, completion: completion) 551 | } 552 | 553 | @discardableResult 554 | open func padding(top: CGFloat = 0, left: CGFloat = 0, bottom: CGFloat = 0, right: CGFloat = 0) -> Self { 555 | edgeInsets = UIEdgeInsets(top: top, left: left, bottom: bottom, right: right) 556 | return self 557 | } 558 | 559 | @discardableResult 560 | open func addPadding(top: CGFloat = 0, left: CGFloat = 0, bottom: CGFloat = 0, right: CGFloat = 0) -> Self { 561 | edgeInsets = UIEdgeInsets(top: edgeInsets.top + top, left: edgeInsets.left + left, bottom: edgeInsets.bottom + bottom, right: edgeInsets.right + right) 562 | return self 563 | } 564 | 565 | override open func setNeedsLayout() { 566 | super.setNeedsLayout() 567 | frameLayout.setNeedsLayout() 568 | } 569 | 570 | open override func layoutIfNeeded() { 571 | super.layoutIfNeeded() 572 | frameLayout.layoutIfNeeded() 573 | } 574 | 575 | /** 576 | This will set `isUserInteractionEnabled` as well as all sub-frameLayouts to the same value. 577 | - parameter enabled: The name says it all 578 | */ 579 | @discardableResult 580 | public func setUserInteraction(enabled: Bool) -> Self { 581 | isUserInteractionEnabled = enabled 582 | frameLayouts.forEach { $0.isUserInteractionEnabled = enabled } 583 | return self 584 | } 585 | 586 | // MARK: - 587 | 588 | fileprivate func updateLayout() { 589 | if _views.isEmpty { 590 | frameLayout.removeAll(autoRemoveTargetView: true) 591 | } 592 | else { 593 | let total = _views.count 594 | 595 | if frameLayout.frameLayouts.count > total { 596 | frameLayout.enumerate { layout, index, _ in 597 | if Int(index) >= Int(total) { 598 | layout.targetView?.removeFromSuperview() 599 | } 600 | } 601 | } 602 | 603 | frameLayout.numberOfFrameLayouts = total 604 | 605 | frameLayout.enumerate { layout, idx, _ in 606 | let view = views[idx] 607 | scrollView.addSubview(view) 608 | layout.targetView = view 609 | } 610 | } 611 | 612 | setNeedsLayout() 613 | } 614 | 615 | } 616 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Nam Kennic 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "FrameLayoutKit", 8 | platforms: [.iOS(.v9), .tvOS(.v9)], 9 | products: [ 10 | .library( 11 | name: "FrameLayoutKit", 12 | targets: ["FrameLayoutKit"]), 13 | ], 14 | targets: [ 15 | .target( 16 | name: "FrameLayoutKit", 17 | path: "FrameLayoutKit/Classes", 18 | exclude: ["Example"]) 19 | ], 20 | swiftLanguageVersions: [.v5] 21 | ) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FrameLayoutKit 2 | 3 | [![Platform](https://img.shields.io/cocoapods/p/FrameLayoutKit.svg?style=flat)](http://cocoapods.org/pods/FrameLayoutKit) 4 | [![Language](http://img.shields.io/badge/language-Swift-brightgreen.svg?style=flat)](https://developer.apple.com/swift) 5 | [![Version](https://img.shields.io/cocoapods/v/FrameLayoutKit.svg?style=flat-square)](http://cocoapods.org/pods/FrameLayoutKit) 6 | [![SwiftPM Compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 7 | [![License](https://img.shields.io/cocoapods/l/FrameLayoutKit.svg?style=flat-square)](http://cocoapods.org/pods/FrameLayoutKit) 8 | 9 | ![image](images/banner.jpg) 10 | 11 | A super fast and easy-to-use layout library for iOS. FrameLayoutKit supports complex layouts, including chaining and nesting layout with simple and intuitive operand syntax. 12 | 13 | It simplifies the UI creation process, resulting in cleaner and more maintainable code. 14 | 15 | ## Why Use FrameLayoutKit? 16 | 17 | Say NO to autolayout constraint nightmare: 18 | 19 | 20 | 21 | 22 | 25 | 28 | 29 |
Autolayout FrameLayoutKit
23 | No 24 | 26 | Yes!!! 27 |
30 | 31 | ## Table of Contents 32 | 33 | - [Installation](#installation) 34 | - [Core Components](#core-components) 35 | - [Basic Usage](#basic-usage) 36 | - [DSL Syntax](#dsl-syntax) 37 | - [Examples](#examples) 38 | - [Performance](#performance) 39 | - [Requirements](#requirements) 40 | - [Author](#author) 41 | - [License](#license) 42 | 43 | ## Installation 44 | 45 | FrameLayoutKit is available through `Swift Package Manager` (Recommended) and [CocoaPods](http://cocoapods.org): 46 | 47 | Regardless of the method, make sure to import the framework into your project: 48 | 49 | ```swift 50 | import FrameLayoutKit 51 | ``` 52 | 53 | ### Swift Package Manager (Recommended) 54 | 55 | [Swift Package Manager](https://swift.org/package-manager/) is recommended to install FrameLayoutKit. 56 | 57 | 1. Click `File` 58 | 2. `Add Packages...` 59 | 3. Enter the git URL for FrameLayoutKit: 60 | 61 | ```swift 62 | https://github.com/kennic/FrameLayoutKit.git 63 | ``` 64 | 65 | ### CocoaPods 66 | 67 | FrameLayoutKit can also be installed as a [CocoaPod](https://cocoapods.org/). To install, add the following line to your Podfile: 68 | 69 | ```ruby 70 | pod "FrameLayoutKit" 71 | ``` 72 | 73 | ## Core Components 74 | 75 | ![image](images/FrameLayoutKit.png) 76 | 77 | FrameLayoutKit includes the following core components: 78 | 79 | ### FrameLayout 80 | 81 | The most basic class, manages a single view and adjusts its size and position based on configured properties. 82 | 83 | ### StackFrameLayout 84 | 85 | Manages multiple views in rows (horizontal) or columns (vertical), similar to `UIStackView` but with higher performance and more options. 86 | 87 | - **HStackLayout**: Horizontal layout 88 | - **VStackLayout**: Vertical layout 89 | - **ZStackLayout**: Stacked layout (z-index) 90 | 91 | ### GridFrameLayout 92 | 93 | Arranges views in a grid, with customizable number of columns and rows. 94 | 95 | ### FlowFrameLayout 96 | 97 | Arranges views in a flow, automatically wrapping to the next line when there's not enough space. 98 | 99 | ### DoubleFrameLayout 100 | 101 | Manages two views with various layout options. 102 | 103 | ### ScrollStackView 104 | 105 | Combines `UIScrollView` with `StackFrameLayout` to create a scrollview that can automatically layout its child views. 106 | 107 | ## Full Documentation: 108 | [Read Full Documentation here](https://deepwiki.com/kennic/FrameLayoutKit) 109 | 110 | ## Basic Usage 111 | 112 | ### Creating and Configuring Layouts 113 | 114 | ```swift 115 | // Create a vertical layout 116 | let vStackLayout = VStackLayout() 117 | vStackLayout.spacing = 10 118 | vStackLayout.distribution = .center 119 | vStackLayout.padding(top: 20, left: 20, bottom: 20, right: 20) 120 | 121 | // Add views to the layout 122 | vStackLayout.add(view1) 123 | vStackLayout.add(view2) 124 | vStackLayout.add(view3) 125 | 126 | // Add the layout to a parent view 127 | parentView.addSubview(vStackLayout) 128 | 129 | // Update the layout's frame 130 | vStackLayout.frame = parentView.bounds 131 | ``` 132 | 133 | ### Using Operator Syntax (Recommended) 134 | 135 | FrameLayoutKit provides the `+` operator syntax to easily add views to layouts: 136 | 137 | ```swift 138 | // Add a single view 139 | vStackLayout + view1 140 | 141 | // Add an array of views 142 | vStackLayout + [view1, view2, view3] 143 | 144 | // Add spacing 145 | vStackLayout + 10 // Add 10pt spacing 146 | 147 | // Add a child layout 148 | vStackLayout + hStackLayout 149 | ``` 150 | 151 | ### Configuring View Properties 152 | 153 | ```swift 154 | // Configure alignment 155 | (vStackLayout + view1).alignment = (.center, .fill) 156 | 157 | // Configure fixed size 158 | (vStackLayout + view2).fixedSize = CGSize(width: 100, height: 50) 159 | 160 | // Add a flexible view (can expand) 161 | (vStackLayout + view3).flexible() 162 | ``` 163 | 164 | ### Chained Syntax (Recommended) 165 | 166 | ```swift 167 | vStackLayout 168 | .distribution(.center) 169 | .spacing(16) 170 | .flexible() 171 | .fixedHeight(50) 172 | .aligns(.top, .center) 173 | .padding(top: 20, left: 20, bottom: 20, right: 20) 174 | ``` 175 | 176 | ## DSL Syntax (Experimental) 177 | 178 | FrameLayoutKit provides a DSL (Domain Specific Language) syntax similar to SwiftUI, making layout creation more intuitive and readable: 179 | 180 | ```swift 181 | // Create VStackLayout with DSL syntax 182 | let vStackLayout = VStackView { 183 | titleLabel 184 | descriptionLabel 185 | SpaceItem(20) // Add a 20pt space 186 | Item(actionButton).minWidth(120) // Customize the button's minimum width 187 | } 188 | 189 | // Create HStackLayout with DSL syntax 190 | let hStackLayout = HStackView { 191 | StackItem(imageView).fixedSize(width: 50, height: 50) 192 | VStackView { 193 | titleLabel 194 | subtitleLabel 195 | }.spacing(5) 196 | FlexibleSpace() // Add flexible space 197 | StackItem(button).align(vertical: .center, horizontal: .right) 198 | } 199 | ``` 200 | 201 | ### Main DSL Components 202 | 203 | - **StackItem**: Wraps a view to add to a stack with additional options 204 | - **SpaceItem**: Adds fixed spacing 205 | - **FlexibleSpace**: Adds flexible spacing (can expand) 206 | - **Item**: Similar to StackItem but with more options 207 | 208 | ## Examples 209 | 210 | Here are some examples of how FrameLayoutKit works: 211 | 212 | 213 | 214 | 215 | 237 | 240 | 241 |
Source Result
216 | 217 | ```swift 218 | let frameLayout = HStackLayout() 219 | frameLayout + VStackLayout { 220 | ($0 + earthImageView).alignment = (.top, .center) 221 | ($0 + 0).flexible() // add a flexible space 222 | ($0 + rocketImageView).alignment = (.center, .center) 223 | } 224 | frameLayout + VStackLayout { 225 | $0 + [nameLabel, dateLabel] // add an array of views 226 | $0 + 10 // add a space with a minimum of 10 pixels 227 | $0 + messageLabel // add a single view 228 | }.spacing(5.0) 229 | 230 | frameLayout 231 | .spacing(15) 232 | .padding(top: 15, left: 15, bottom: 15, right: 15) 233 | .debug(true) // show dashed lines to visualize the layout 234 | 235 | ```` 236 | 238 | result 1 239 |
242 | 243 | 244 | 245 | 246 | 261 | 264 | 265 |
Source Result
247 | 248 | ```swift 249 | let frameLayout = VStackLayout { 250 | ($0 + imageView).flexible() 251 | $0 + VStackLayout { 252 | $0 + titleLabel 253 | $0 + ratingLabel 254 | } 255 | }.padding(top: 12, left: 12, bottom: 12, right: 12) 256 | .distribution(.bottom) 257 | .spacing(5) 258 | ```` 259 | 260 | 262 | result 1 263 |
266 | 267 | 268 | 269 | 270 | 288 | 291 | 292 |
Source Result
271 | 272 | ```swift 273 | let posterSize = CGSize(width: 100, height: 150) 274 | let frameLayout = ZStackLayout() 275 | frameLayout + backdropImageView 276 | frameLayout + VStackLayout { 277 | $0 + HStackLayout { 278 | ($0 + posterImageView).fixedSize(posterSize) 279 | $0 + VStackLayout { 280 | $0 + titleLabel 281 | $0 + subtitleLabel 282 | }.padding(bottom: 5).flexible().distribution(.bottom) 283 | }.spacing(12).padding(top: 0, left: 12, bottom: 12, right: 12) 284 | }.distribution(.bottom) 285 | ``` 286 | 287 | 289 | result 2 290 |
293 | 294 | 295 | 296 | 297 | 319 | 322 | 323 |
Source Result
298 | 299 | ```swift 300 | let buttonSize = CGSize(width: 45, height: 45) 301 | let cardView = VStackLayout() 302 | .spacing(10) 303 | .padding(top: 24, left: 24, bottom: 24, right: 24) 304 | 305 | cardView + titleLabel 306 | (cardView + emailField).minHeight = 50 307 | (cardView + passwordField).minHeight = 50 308 | (cardView + nextButton).fixedHeight = 45 309 | (cardView + separateLine) 310 | .fixedContentHeight(1) 311 | .padding(top: 4, left: 0, bottom: 4, right: 40) 312 | cardView + HStackLayout { 313 | ($0 + [facebookButton, googleButton, appleButton]) 314 | .forEach { $0.fixedContentSize(buttonSize) } 315 | }.distribution(.center).spacing(10) 316 | ``` 317 | 318 | 320 | result 2 321 |
324 | 325 | ## Key Properties 326 | 327 | ### FrameLayout 328 | 329 | - **targetView**: The view managed by this layout 330 | - **edgeInsets**: Padding around the view 331 | - **minSize/maxSize**: Minimum/maximum size of the layout 332 | - **minContentSize/maxContentSize**: Minimum/maximum size of the child view 333 | - **fixedSize/fixedContentSize**: Fixed size of the layout/child view 334 | - **contentAlignment**: Content alignment (top, center, bottom, left, right, fill, fit) 335 | - **isFlexible**: Allows the layout to expand to fill available space 336 | 337 | ### StackFrameLayout 338 | 339 | - **axis**: Direction of the stack (vertical, horizontal) 340 | - **distribution**: How child views are distributed (top, center, bottom, left, right, fill, fit, justified) 341 | - **spacing**: Space between child views 342 | - **isJustified**: Evenly distributes child views 343 | - **isOverlapped**: Allows child views to overlap 344 | 345 | ## Performance 346 | 347 | FrameLayoutKit is one of the fastest layout libraries. 348 | ![Benchmark Results](images/bechmark.png "Benchmark results") 349 | 350 | See: [Layout libraries benchmark's project](https://github.com/layoutBox/LayoutFrameworkBenchmark) 351 | 352 | ## Requirements 353 | 354 | - iOS 11.0+ 355 | - Swift 5.0+ 356 | 357 | ## Author 358 | 359 | Nam Kennic, namkennic@me.com 360 | 361 | ## License 362 | 363 | FrameLayoutKit is available under the MIT license. See the LICENSE file for more info. 364 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj -------------------------------------------------------------------------------- /images/FrameLayoutKit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/images/FrameLayoutKit.png -------------------------------------------------------------------------------- /images/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/images/banner.jpg -------------------------------------------------------------------------------- /images/bechmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/images/bechmark.png -------------------------------------------------------------------------------- /images/example_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/images/example_1.png -------------------------------------------------------------------------------- /images/example_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/images/example_2.png -------------------------------------------------------------------------------- /images/example_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/images/example_3.png -------------------------------------------------------------------------------- /images/frameLayoutSyntax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/images/frameLayoutSyntax.png -------------------------------------------------------------------------------- /images/helloWorld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/images/helloWorld.png -------------------------------------------------------------------------------- /images/no_constraint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/FrameLayoutKit/2a70bb0ef0f5d064db367277ff2a54d5f22c0a5e/images/no_constraint.png --------------------------------------------------------------------------------