├── .github └── workflows │ ├── ci.yml │ ├── documentation.yml │ ├── prepare_release.yml │ └── release.yml ├── .gitignore ├── .ruby-version ├── .spi.yml ├── .swiftlint.yml ├── CHANGELOG.md ├── CompositionalLayoutDSL.podspec ├── CompositionalLayoutDSL.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── CompositionalLayoutDSL.xcscheme │ └── CompositionalLayoutDSLApp.xcscheme ├── CompositionalLayoutDSL.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CompositionalLayoutDSLApp ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── SceneDelegate.swift └── ViewController.swift ├── CompositionalLayoutDSLTests ├── Info.plist ├── LayoutTests │ ├── DecorationItemDSLTests.swift │ ├── GroupDSLTests.swift │ ├── SectionDSLTests.swift │ ├── SupplementaryItemDSLTests.swift │ └── __Snapshots__ │ │ ├── DecorationItemDSLTests │ │ ├── testDecorationItem.1.png │ │ ├── testDecorationItem.2.png │ │ ├── testDecorationItem.3.png │ │ ├── testDecorationItem.4.png │ │ └── testDecorationItem.5.png │ │ ├── GroupDSLTests │ │ ├── testInnerGroups.1.png │ │ ├── testInnerGroups.2.png │ │ ├── testInnerGroups.3.png │ │ ├── testInnerGroups.4.png │ │ └── testInnerGroups.5.png │ │ ├── SectionDSLTests │ │ ├── testListSection.1.png │ │ ├── testListSection.2.png │ │ ├── testListSection.3.png │ │ ├── testListSection.4.png │ │ └── testListSection.5.png │ │ └── SupplementaryItemDSLTests │ │ ├── testSupplementaryItem.1.png │ │ ├── testSupplementaryItem.2.png │ │ ├── testSupplementaryItem.3.png │ │ ├── testSupplementaryItem.4.png │ │ └── testSupplementaryItem.5.png ├── TestingCollectionView │ ├── TestingCellView.swift │ ├── TestingCollectionViewController.swift │ ├── TestingCollectionViewModel.swift │ ├── TestingDecorationView.swift │ └── TestingSupplementaryView.swift └── Utils │ ├── NSCollectionLayoutSize+Utils.swift │ └── assertLayouts.swift ├── Dangerfile ├── Example ├── CompositionalLayoutDSL_Example.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── CompositionalLayoutDSL_Example_iOS.xcscheme ├── CompositionalLayoutDSL_Example_iOS │ ├── App │ │ ├── DemoCollectionViewController │ │ │ ├── DemoCellView.swift │ │ │ ├── DemoCollectionViewController.swift │ │ │ └── DemoSupplementaryView.swift │ │ ├── ShowcaseViewController │ │ │ ├── AppStoreLayoutLike │ │ │ │ ├── AdaptativeColumnLaneSection.swift │ │ │ │ ├── AppStoreNewContentSection.swift │ │ │ │ ├── AppStoreTopContentSection.swift │ │ │ │ └── AppStoreTrendingContentSection.swift │ │ │ ├── CompositionalLayout │ │ │ │ ├── CompositionalLayoutWithSupplementaryView.swift │ │ │ │ └── GettingStartedCompositionalLayout.swift │ │ │ ├── Group │ │ │ │ └── FractalGroup.swift │ │ │ ├── Section │ │ │ │ ├── ListSection.swift │ │ │ │ └── SectionWithHeader.swift │ │ │ └── ShowcaseViewController.swift │ │ └── Utils │ │ │ └── Section │ │ │ ├── ColumnLaneSection.swift │ │ │ ├── LaneSection.swift │ │ │ └── SectionWithEnvironmentInsets.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ └── SceneDelegate.swift └── CompositionalLayoutDSL_Example_macOS │ ├── App │ ├── DemoCollectionViewController │ │ ├── DemoCellView.swift │ │ ├── DemoCollectionViewController.swift │ │ └── DemoSupplementaryView.swift │ └── ShowcaseViewController │ │ └── ShowcaseViewController.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── Main.storyboard │ ├── CompositionalLayoutDSL_Example_macOS.entitlements │ └── Info.plist ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.resolved ├── Package.swift ├── Podfile ├── Podfile.lock ├── README.md ├── Sources └── CompositionalLayoutDSL │ ├── Internal │ ├── Builders │ │ ├── BoundarySupplementaryItemBuilder.swift │ │ ├── ConfigurationBuilder.swift │ │ ├── DecorationItemBuilder.swift │ │ ├── GroupBuilder.swift │ │ ├── ItemBuilder.swift │ │ ├── SectionBuilder.swift │ │ └── SupplementaryItemBuilder.swift │ └── ModifiedLayout │ │ ├── ModifiedLayoutBoundarySupplementaryItem.swift │ │ ├── ModifiedLayoutConfiguration.swift │ │ ├── ModifiedLayoutDecorationItem.swift │ │ ├── ModifiedLayoutGroup.swift │ │ ├── ModifiedLayoutItem.swift │ │ ├── ModifiedLayoutSection.swift │ │ └── ModifiedLayoutSupplementaryItem.swift │ └── Public │ ├── BoundarySupplementaryItem │ ├── BoundarySupplementaryItem.swift │ └── LayoutBoundarySupplementaryItem.swift │ ├── CompositionalLayout.swift │ ├── CompositionalLayoutDSL.swift │ ├── Configuration │ ├── Configuration.swift │ └── LayoutConfiguration.swift │ ├── DecorationItem │ ├── DecorationItem.swift │ └── LayoutDecorationItem.swift │ ├── Group │ ├── CustomGroup.swift │ ├── HGroup.swift │ ├── LayoutGroup.swift │ └── VGroup.swift │ ├── Item │ ├── Item.swift │ └── LayoutItem.swift │ ├── ResizableItem.swift │ ├── ResultBuilders.swift │ ├── Section │ ├── LayoutSection.swift │ ├── ListSection.swift │ ├── RawSection.swift │ └── Section.swift │ ├── SupplementaryItem │ ├── LayoutSupplementaryItem.swift │ └── SupplementaryItem.swift │ └── Utils.swift ├── fastlane ├── .env.default ├── Fastfile └── Pluginfile └── images └── GettingStartedExample.jpg /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: pull_request 4 | 5 | concurrency: 6 | group: build-${{ github.event.pull_request.number || github.ref }} 7 | cancel-in-progress: true 8 | 9 | env: 10 | DEVELOPER_DIR: /Applications/Xcode_15.0.app/Contents/Developer 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: macOS-13 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Bundle install 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | bundler: "Gemfile.lock" 26 | bundler-cache: true 27 | 28 | - name: Pods cache 29 | uses: actions/cache@v2 30 | with: 31 | path: Pods 32 | key: ${{ runner.os }}-cocoapods-${{ hashFiles('**/Podfile.lock') }} 33 | 34 | - name: Pod install 35 | run: bundle exec pod install 36 | 37 | - name: Build and test 38 | env: 39 | GITHUB_API_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN_CI }} 40 | run: bundle exec fastlane ci_check 41 | 42 | - uses: actions/upload-artifact@v2 43 | if: failure() 44 | with: 45 | name: test-artifacts 46 | path: tests_derived_data/Logs/Test/*.xcresult 47 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | DEVELOPER_DIR: /Applications/Xcode_15.0.app/Contents/Developer 10 | 11 | jobs: 12 | build: 13 | runs-on: macOS-13 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Generate Documentation 18 | run: | 19 | swift package --allow-writing-to-directory ./Documentation \ 20 | generate-documentation --target CompositionalLayoutDSL \ 21 | --disable-indexing \ 22 | --transform-for-static-hosting \ 23 | --hosting-base-path /CompositionalLayoutDSL \ 24 | --output-path ./Documentation 25 | - name: Deploy to GitHub Pages 26 | uses: peaceiris/actions-gh-pages@v3 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | publish_branch: docs 30 | publish_dir: ./Documentation 31 | user_name: 'github-actions[bot]' 32 | user_email: 'github-actions[bot]@users.noreply.github.com' 33 | -------------------------------------------------------------------------------- /.github/workflows/prepare_release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare a new release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | name: 7 | description: 'Version number' 8 | required: true 9 | 10 | env: 11 | DEVELOPER_DIR: /Applications/Xcode_15.0.app/Contents/Developer 12 | 13 | jobs: 14 | build: 15 | runs-on: macOS-13 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Bundle install 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | bundler: "Gemfile.lock" 23 | bundler-cache: true 24 | 25 | - name: Create release branch 26 | env: 27 | LC_ALL: en_US.UTF-8 28 | LANG: en_US.UTF-8 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | GIT_COMMITTER_NAME: Bot Fabernovel 31 | GIT_AUTHOR_NAME: Bot Fabernovel 32 | GIT_COMMITTER_EMAIL: ci@fabernovel.com 33 | GIT_AUTHOR_EMAIL: ci@fabernovel.com 34 | run: bundle exec fastlane create_release_branch version:${{ github.event.inputs.name }} 35 | 36 | - name: Prepate release 37 | env: 38 | LC_ALL: en_US.UTF-8 39 | LANG: en_US.UTF-8 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | GIT_COMMITTER_NAME: Bot Fabernovel 42 | GIT_AUTHOR_NAME: Bot Fabernovel 43 | GIT_COMMITTER_EMAIL: ci@fabernovel.com 44 | GIT_AUTHOR_EMAIL: ci@fabernovel.com 45 | run: bundle exec fastlane prepare_release bypass_confirmations:true 46 | 47 | - name: Create release pull requests 48 | env: 49 | LC_ALL: en_US.UTF-8 50 | LANG: en_US.UTF-8 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | GIT_COMMITTER_NAME: Bot Fabernovel 53 | GIT_AUTHOR_NAME: Bot Fabernovel 54 | GIT_COMMITTER_EMAIL: ci@fabernovel.com 55 | GIT_AUTHOR_EMAIL: ci@fabernovel.com 56 | run: bundle exec fastlane create_release_pr 57 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release a new version 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | env: 7 | DEVELOPER_DIR: /Applications/Xcode_15.0.app/Contents/Developer 8 | 9 | jobs: 10 | build: 11 | runs-on: macOS-13 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Bundle install 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | bundler: "Gemfile.lock" 19 | bundler-cache: true 20 | 21 | - name: Prepare release 22 | env: 23 | LC_ALL: en_US.UTF-8 24 | LANG: en_US.UTF-8 25 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TOKEN_CI }} 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | GIT_COMMITTER_NAME: Bot Fabernovel 28 | GIT_AUTHOR_NAME: Bot Fabernovel 29 | GIT_COMMITTER_EMAIL: ci@fabernovel.com 30 | GIT_AUTHOR_EMAIL: ci@fabernovel.com 31 | run: bundle exec fastlane publish_release 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | tests_derived_data/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata 20 | 21 | ## Other 22 | *.xccheckout 23 | *.moved-aside 24 | *.xcuserstate 25 | *.xcscmblueprint 26 | *.DS_Store 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.xcarchive 32 | *.app.dSYM.zip 33 | 34 | # CocoaPods 35 | # 36 | # We recommend against adding the Pods directory to your .gitignore. However 37 | # you should judge for yourself, the pros and cons are mentioned at: 38 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 39 | 40 | Pods/ 41 | 42 | ## SPM 43 | /.build 44 | /Packages 45 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 46 | 47 | ##Fastlane 48 | # fastlane specific 49 | fastlane/report.xml 50 | 51 | # deliver temporary files 52 | fastlane/Preview.html 53 | 54 | # snapshot generated screenshots 55 | fastlane/screenshots 56 | 57 | # scan temporary files 58 | fastlane/test_output 59 | 60 | #fastlane bundler 61 | fastlane/Gemfile 62 | fastlane/Gemfile.lock 63 | fastlane/.bundle/config 64 | 65 | #Fastlane Plugins 66 | Pluginfile_fastlane 67 | Gemfile_fastlane 68 | Gemfile_fastlane.lock 69 | 70 | ##AppCode 71 | .idea/ 72 | 73 | ## Ruby 74 | .bundle/ 75 | vendor/ 76 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.2 2 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | external_links: 3 | documentation: "https://fabernovel.github.io/CompositionalLayoutDSL/documentation/CompositionalLayoutDSL" 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | `CompositionalLayoutDSL` adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased] 6 | 7 | ## [0.2.0] - 2023-09-29 8 | 9 | ### Updated 10 | - Documentation will now use DocC 11 | 12 | ### Removed 13 | - Drop support for Swift 5.3, we now use `@resultBuilder` instead of `@_functionBuilder` 14 | - Drop support for Carthage 15 | 16 | ## [0.1.0] - 2021-04-28 17 | 18 | ### Created 19 | 20 | #### Structures 21 | - `CompositionalLayout` 22 | - `Configuration` 23 | - `Section` 24 | - `ListSection` 25 | - `RawSection` 26 | - `HGroup` 27 | - `VGroup` 28 | - `CustomGroup` 29 | - `Item` 30 | - `DecorationItem` 31 | - `SupplementaryItem` 32 | - `BoundarySupplementaryItem` 33 | 34 | #### Enumerations 35 | - `SupplementaryItem.AnchorOffset` 36 | - `ListResultBuilder` 37 | 38 | #### Protocols 39 | - `LayoutConfiguration` 40 | - `LayoutSection` 41 | - `LayoutGroup` 42 | - `LayoutItem` 43 | - `LayoutDecorationItem` 44 | - `LayoutSupplementaryItem` 45 | - `LayoutBoundarySupplementaryItem` 46 | - `ResizableItem` 47 | 48 | #### Type aliases 49 | - `LayoutItemBuilder` 50 | - `LayoutBoundarySupplementaryItemBuilder` 51 | - `LayoutSupplementaryItemBuilder` 52 | - `LayoutDecorationItemBuilder` 53 | 54 | #### Functions 55 | - `LayoutSectionBuilder(layoutSection:) -> NSCollectionLayoutSection` 56 | - `LayoutBuilder(configuration:layoutSection:) -> NSCollectionViewCompositionalLayout` 57 | - `LayoutBuilder(configuration:layoutSection:) -> UICollectionViewCompositionalLayout` 58 | - `LayoutBuilder(compositionalLayout:) -> NSCollectionViewCompositionalLayout` 59 | - `LayoutBuilder(compositionalLayout:) -> UICollectionViewCompositionalLayout` 60 | 61 | #### External extensions 62 | 63 | - `NSCollectionView.setCollectionViewLayout(_ layout: CompositionalLayout)` 64 | - `UICollectionView.setCollectionViewLayout(_ layout: CompositionalLayout)` 65 | -------------------------------------------------------------------------------- /CompositionalLayoutDSL.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'CompositionalLayoutDSL' 3 | spec.version = '0.2.0' 4 | spec.summary = 'library to ease the creation of UICollectionViewCompositionalLayout' 5 | spec.homepage = 'https://github.com/faberNovel/CompositionalLayoutDSL' 6 | spec.license = { :type => 'MIT', :file => 'LICENSE' } 7 | spec.author = { 'Alexandre Podlewski' => 'alexandre.podlewski@fabernovel.com' } 8 | spec.source = { :git => 'https://github.com/faberNovel/CompositionalLayoutDSL', :tag => "v#{spec.version}" } 9 | spec.social_media_url = 'https://twitter.com/fabernovel' 10 | spec.ios.deployment_target = '13.0' 11 | spec.tvos.deployment_target = '13.0' 12 | spec.osx.deployment_target = '10.15' 13 | spec.framework = 'Foundation' 14 | spec.ios.framework = 'UIKit' 15 | spec.osx.framework = 'AppKit' 16 | spec.swift_versions = ['5.1', '5.2', '5.3', '5.4'] 17 | spec.source_files = 'Sources/CompositionalLayoutDSL/**/*' 18 | end 19 | -------------------------------------------------------------------------------- /CompositionalLayoutDSL.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CompositionalLayoutDSL.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CompositionalLayoutDSL.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CompositionalLayoutDSL", 6 | "repositoryURL": "git@github.com:faberNovel/CompositionalLayoutDSL.git", 7 | "state": { 8 | "branch": "develop", 9 | "revision": "007263619d6213ffe8a5462372d404ed4b833e61", 10 | "version": null 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /CompositionalLayoutDSL.xcodeproj/xcshareddata/xcschemes/CompositionalLayoutDSL.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /CompositionalLayoutDSL.xcodeproj/xcshareddata/xcschemes/CompositionalLayoutDSLApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /CompositionalLayoutDSL.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /CompositionalLayoutDSL.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 13/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | func application( 15 | _ application: UIApplication, 16 | // swiftlint:disable:next discouraged_optional_collection 17 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 18 | ) -> Bool { 19 | return true 20 | } 21 | 22 | // MARK: UISceneSession Lifecycle 23 | 24 | func application(_ application: UIApplication, 25 | configurationForConnecting connectingSceneSession: UISceneSession, 26 | options: UIScene.ConnectionOptions) -> UISceneConfiguration { 27 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 28 | } 29 | 30 | func application(_ application: UIApplication, 31 | didDiscardSceneSessions sceneSessions: Set) { 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLApp/Base.lproj/LaunchScreen.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 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UIApplicationSupportsIndirectInputEvents 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLApp/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 13/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func scene( 16 | _ scene: UIScene, 17 | willConnectTo session: UISceneSession, 18 | options connectionOptions: UIScene.ConnectionOptions 19 | ) { 20 | guard let windowScene = (scene as? UIWindowScene) else { return } 21 | let window = UIWindow(windowScene: windowScene) 22 | self.window = window 23 | window.rootViewController = ViewController() 24 | window.makeKeyAndVisible() 25 | } 26 | 27 | func sceneDidDisconnect(_ scene: UIScene) {} 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) {} 30 | 31 | func sceneWillResignActive(_ scene: UIScene) {} 32 | 33 | func sceneWillEnterForeground(_ scene: UIScene) {} 34 | 35 | func sceneDidEnterBackground(_ scene: UIScene) {} 36 | } 37 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLApp/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 13/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController {} 12 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/DecorationItemDSLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DecorationItemDSLTests.swift 3 | // CompositionalLayoutDSLTests 4 | // 5 | // Created by Alexandre Podlewski on 14/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import CompositionalLayoutDSL 11 | import SnapshotTesting 12 | 13 | class DecorationItemDSLTests: XCTestCase { 14 | 15 | func testDecorationItem() throws { 16 | let traditionalLayout = TestTraditionalListSectionLayout() 17 | let dslLayout = TestListSectionSection() 18 | 19 | assertLayouts( 20 | layout1: UICollectionViewCompositionalLayout( 21 | section: traditionalLayout.section 22 | ).registeringDecorationView(), 23 | layout2: LayoutBuilder { dslLayout.layoutSection }.registeringDecorationView(), 24 | as: .image(on: .iPhoneX, traits: UITraitCollection(userInterfaceStyle: .light)), 25 | named: "testDecorationItem", 26 | maxTestsCount: 5 27 | ) 28 | } 29 | } 30 | 31 | private extension UICollectionViewLayout { 32 | func registeringDecorationView() -> UICollectionViewLayout { 33 | self.register(TestingDecorationView.self, forDecorationViewOfKind: TestingDecorationView.kind) 34 | return self 35 | } 36 | } 37 | 38 | private struct TestListSectionSection: LayoutSection { 39 | 40 | var layoutSection: LayoutSection { 41 | Section { 42 | HGroup(count: 4) { Item() } 43 | .height(.absolute(40)) 44 | .interItemSpacing(.fixed(8)) 45 | } 46 | .interGroupSpacing(8) 47 | .orthogonalScrollingBehavior(.continuous) 48 | .decorationItems { 49 | DecorationItem(elementKind: TestingDecorationView.kind) 50 | } 51 | .contentInsets(value: 16) 52 | } 53 | } 54 | 55 | private struct TestTraditionalListSectionLayout { 56 | 57 | var section: NSCollectionLayoutSection { 58 | let group = NSCollectionLayoutGroup.horizontal( 59 | layoutSize: .absoluteHeight(40), 60 | subitem: NSCollectionLayoutItem(layoutSize: .fractional()), 61 | count: 4 62 | ) 63 | group.interItemSpacing = .fixed(8) 64 | 65 | let section = NSCollectionLayoutSection(group: group) 66 | section.interGroupSpacing = 8 67 | section.orthogonalScrollingBehavior = .continuous 68 | section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16) 69 | section.decorationItems = [ 70 | .background(elementKind: TestingDecorationView.kind) 71 | ] 72 | return section 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/GroupDSLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupDSLTests.swift 3 | // CompositionalLayoutDSLTests 4 | // 5 | // Created by Alexandre Podlewski on 13/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import CompositionalLayoutDSL 11 | import SnapshotTesting 12 | 13 | class CompositionalLayoutDSLTests: XCTestCase { 14 | 15 | func testInnerGroups() throws { 16 | let traditionalLayout = TestInnerGroupsTraditionalLayout() 17 | let dslLayout = testInnerGroupsLayout() 18 | assertLayouts( 19 | layout1: UICollectionViewCompositionalLayout( 20 | section: traditionalLayout.section, 21 | configuration: traditionalLayout.configuration 22 | ), 23 | layout2: LayoutBuilder { dslLayout }, 24 | as: .image(on: .iPhoneX, traits: UITraitCollection(userInterfaceStyle: .light)), 25 | named: "InnerGroups", 26 | maxTestsCount: 5 27 | ) 28 | } 29 | } 30 | 31 | func testInnerGroupsLayout() -> CompositionalLayout { 32 | CompositionalLayout { _, _ in 33 | Section { 34 | HGroup { 35 | Item(width: .fractionalWidth(1 / 3)) 36 | .contentInsets(trailing: 4) 37 | VGroup(count: 2) { Item() } 38 | .width(.fractionalWidth(1 / 3)) 39 | .interItemSpacing(.fixed(8)) 40 | .contentInsets(horizontal: 4) 41 | VGroup(count: 3) { Item() } 42 | .width(.fractionalWidth(1 / 3)) 43 | .interItemSpacing(.fixed(8)) 44 | .contentInsets(leading: 4) 45 | } 46 | .height(.absolute(100)) 47 | .contentInsets(horizontal: 16) 48 | } 49 | .interGroupSpacing(8) 50 | } 51 | .interSectionSpacing(8) 52 | } 53 | 54 | private struct TestInnerGroupsTraditionalLayout { 55 | 56 | var section: NSCollectionLayoutSection { 57 | let item = NSCollectionLayoutItem(layoutSize: .fractional(width: 1 / 3)) 58 | item.contentInsets.trailing = 4 59 | 60 | let innerGroup1 = NSCollectionLayoutGroup.vertical( 61 | layoutSize: .fractional(width: 1 / 3), 62 | subitem: NSCollectionLayoutItem(layoutSize: .fractional()), 63 | count: 2 64 | ) 65 | innerGroup1.interItemSpacing = .fixed(8) 66 | innerGroup1.contentInsets.leading = 4 67 | innerGroup1.contentInsets.trailing = 4 68 | 69 | let innerGroup2 = NSCollectionLayoutGroup.vertical( 70 | layoutSize: .fractional(width: 1 / 3), 71 | subitem: NSCollectionLayoutItem(layoutSize: .fractional()), 72 | count: 3 73 | ) 74 | innerGroup2.interItemSpacing = .fixed(8) 75 | innerGroup2.contentInsets.leading = 4 76 | 77 | let group = NSCollectionLayoutGroup.horizontal( 78 | layoutSize: .absoluteHeight(100), 79 | subitems: [item, innerGroup1, innerGroup2] 80 | ) 81 | group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) 82 | 83 | let section = NSCollectionLayoutSection(group: group) 84 | section.interGroupSpacing = 8 85 | 86 | return section 87 | } 88 | 89 | var configuration: UICollectionViewCompositionalLayoutConfiguration { 90 | let configuration = UICollectionViewCompositionalLayoutConfiguration() 91 | configuration.interSectionSpacing = 8 92 | return configuration 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/SectionDSLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionDSLTests.swift 3 | // CompositionalLayoutDSLTests 4 | // 5 | // Created by Alexandre Podlewski on 13/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import CompositionalLayoutDSL 11 | import SnapshotTesting 12 | 13 | class SectionDSLTests: XCTestCase { 14 | func testListSection() throws { 15 | let traditionalLayout = TestTraditionalListSectionLayout() 16 | let dslLayout = TestListSectionSection() 17 | 18 | assertLayouts( 19 | layout1: UICollectionViewCompositionalLayout( 20 | section: traditionalLayout.section 21 | ), 22 | layout2: LayoutBuilder { dslLayout.layoutSection }, 23 | as: .image(on: .iPhoneX, traits: UITraitCollection(userInterfaceStyle: .light)), 24 | named: "ListSection", 25 | maxTestsCount: 5 26 | ) 27 | } 28 | } 29 | 30 | private struct TestListSectionSection: LayoutSection { 31 | 32 | var layoutSection: LayoutSection { 33 | Section { 34 | HGroup(count: 1) { Item() } 35 | .height(.absolute(40)) 36 | } 37 | .interGroupSpacing(2) 38 | .boundarySupplementaryItems { 39 | BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) 40 | .height(.absolute(24)) 41 | .alignment(.top) 42 | .pinToVisibleBounds(true) 43 | BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionFooter) 44 | .height(.absolute(24)) 45 | .alignment(.bottom) 46 | .pinToVisibleBounds(true) 47 | } 48 | } 49 | } 50 | 51 | private struct TestTraditionalListSectionLayout { 52 | 53 | var section: NSCollectionLayoutSection { 54 | let group = NSCollectionLayoutGroup.horizontal( 55 | layoutSize: .absoluteHeight(40), 56 | subitem: NSCollectionLayoutItem(layoutSize: .fractional()), 57 | count: 1 58 | ) 59 | 60 | let header = NSCollectionLayoutBoundarySupplementaryItem( 61 | layoutSize: .absoluteHeight(24), 62 | elementKind: UICollectionView.elementKindSectionHeader, 63 | alignment: .top 64 | ) 65 | header.pinToVisibleBounds = true 66 | let footer = NSCollectionLayoutBoundarySupplementaryItem( 67 | layoutSize: .absoluteHeight(24), 68 | elementKind: UICollectionView.elementKindSectionFooter, 69 | alignment: .bottom 70 | ) 71 | footer.pinToVisibleBounds = true 72 | 73 | let section = NSCollectionLayoutSection(group: group) 74 | section.interGroupSpacing = 2 75 | section.boundarySupplementaryItems = [header, footer] 76 | 77 | return section 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/SupplementaryItemDSLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SupplementaryItemDSLTests.swift 3 | // CompositionalLayoutDSLTests 4 | // 5 | // Created by Alexandre Podlewski on 14/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import CompositionalLayoutDSL 11 | import SnapshotTesting 12 | 13 | class SupplementaryItemDSLTests: XCTestCase { 14 | func testSupplementaryItem() throws { 15 | let traditionalLayout = TestTraditionalSectionLayout() 16 | let dslLayout = TestSectionSection() 17 | 18 | assertLayouts( 19 | layout1: UICollectionViewCompositionalLayout( 20 | section: traditionalLayout.section 21 | ), 22 | layout2: LayoutBuilder { dslLayout.layoutSection }, 23 | as: .image(on: .iPhoneX, traits: UITraitCollection(userInterfaceStyle: .light)), 24 | named: "testSupplementaryItem", 25 | maxTestsCount: 5 26 | ) 27 | } 28 | } 29 | 30 | private struct TestSectionSection: LayoutSection { 31 | 32 | var layoutSection: LayoutSection { 33 | Section { 34 | HGroup(count: 4) { 35 | Item { 36 | SupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) 37 | .height(.absolute(15)) 38 | .width(.absolute(15)) 39 | .containerAnchor( 40 | edges: [.top, .trailing], 41 | offset: .fractional(x: 0.5, y: -0.5) 42 | ) 43 | } 44 | .height(.fractionalWidth(1 / 4)) 45 | } 46 | .height(.absolute(80)) 47 | .interItemSpacing(.fixed(16)) 48 | .supplementaryItems { 49 | SupplementaryItem(elementKind: UICollectionView.elementKindSectionFooter) 50 | .height(.absolute(60)) 51 | .width(.absolute(20)) 52 | .containerAnchor(edges: .trailing, offset: .fractional(x: 1, y: 0)) 53 | } 54 | .contentInsets(trailing: 20) 55 | } 56 | .interGroupSpacing(16) 57 | .contentInsets(bottom: 16) 58 | } 59 | } 60 | 61 | private struct TestTraditionalSectionLayout { 62 | 63 | var section: NSCollectionLayoutSection { 64 | let badgeItemSize = NSCollectionLayoutSize( 65 | widthDimension: .absolute(15), 66 | heightDimension: .absolute(15) 67 | ) 68 | let badgeItem = NSCollectionLayoutSupplementaryItem( 69 | layoutSize: badgeItemSize, 70 | elementKind: UICollectionView.elementKindSectionHeader, 71 | containerAnchor: NSCollectionLayoutAnchor( 72 | edges: [.top, .trailing], 73 | fractionalOffset: CGPoint(x: 0.5, y: -0.5) 74 | ) 75 | ) 76 | 77 | let itemSize = NSCollectionLayoutSize( 78 | widthDimension: .fractionalWidth(1), 79 | heightDimension: .fractionalWidth(1 / 4) 80 | ) 81 | let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [badgeItem]) 82 | let group = NSCollectionLayoutGroup.horizontal( 83 | layoutSize: .absoluteHeight(80), 84 | subitem: item, 85 | count: 4 86 | ) 87 | group.interItemSpacing = .fixed(16) 88 | 89 | let groupTrailingItemSize = NSCollectionLayoutSize( 90 | widthDimension: .absolute(20), 91 | heightDimension: .absolute(60) 92 | ) 93 | let groupTrailingItem = NSCollectionLayoutSupplementaryItem( 94 | layoutSize: groupTrailingItemSize, 95 | elementKind: UICollectionView.elementKindSectionFooter, 96 | containerAnchor: NSCollectionLayoutAnchor( 97 | edges: [.trailing], 98 | fractionalOffset: CGPoint(x: 1, y: 0) 99 | ) 100 | ) 101 | group.supplementaryItems = [groupTrailingItem] 102 | group.contentInsets.trailing = 20 103 | 104 | let section = NSCollectionLayoutSection(group: group) 105 | section.interGroupSpacing = 16 106 | section.contentInsets.bottom = 16 107 | 108 | return section 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.1.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.2.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.3.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.4.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.5.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.1.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.2.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.3.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.4.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.5.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.1.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.2.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.3.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.4.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.5.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.1.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.2.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.3.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.4.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.5.png -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/TestingCollectionView/TestingCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestingCellView.swift 3 | // CompositionalLayoutDSLTests 4 | // 5 | // Created by Alexandre Podlewski on 13/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TestingCellView: UICollectionViewCell { 12 | 13 | private let label = UILabel() 14 | 15 | // MARK: - Life cycle 16 | 17 | override init(frame: CGRect) { 18 | super.init(frame: frame) 19 | setup() 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | super.init(coder: coder) 24 | setup() 25 | } 26 | 27 | override func prepareForReuse() { 28 | super.prepareForReuse() 29 | label.text = nil 30 | } 31 | 32 | // MARK: - CellView 33 | 34 | func configure(with text: String) { 35 | label.text = text 36 | } 37 | 38 | // MARK: - Private 39 | 40 | private func setup() { 41 | backgroundColor = .gray 42 | layer.cornerRadius = 6 43 | contentView.addSubview(label) 44 | label.translatesAutoresizingMaskIntoConstraints = false 45 | label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true 46 | label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/TestingCollectionView/TestingCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestingCollectionViewController.swift 3 | // CompositionalLayoutDSLTests 4 | // 5 | // Created by Alexandre Podlewski on 13/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TestingCollectionViewController: UICollectionViewController { 12 | 13 | private static let cellIdentifier = "TestingCellView" 14 | private static let supplementaryIdentifier = "TestingSupplementaryView" 15 | 16 | private var sectionItemsCount: [Int] = [] 17 | 18 | // MARK: - Life cycle 19 | 20 | init() { 21 | super.init(collectionViewLayout: UICollectionViewFlowLayout()) 22 | } 23 | 24 | required init?(coder: NSCoder) { 25 | super.init(coder: coder) 26 | } 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | collectionView.backgroundColor = .systemBackground 31 | collectionView.register(TestingCellView.self, forCellWithReuseIdentifier: Self.cellIdentifier) 32 | collectionView.register( 33 | TestingSupplementaryView.self, 34 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, 35 | withReuseIdentifier: Self.supplementaryIdentifier 36 | ) 37 | collectionView.register( 38 | TestingSupplementaryView.self, 39 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, 40 | withReuseIdentifier: Self.supplementaryIdentifier 41 | ) 42 | } 43 | 44 | // MARK: - TestingCollectionViewController 45 | 46 | func configure(with viewModel: TestingCollectionViewModel) { 47 | sectionItemsCount = viewModel.sectionItemsCount 48 | collectionView.reloadData() 49 | } 50 | 51 | // MARK: - UICollectionViewDataSource 52 | 53 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 54 | return sectionItemsCount.count 55 | } 56 | 57 | override func collectionView(_ collectionView: UICollectionView, 58 | numberOfItemsInSection section: Int) -> Int { 59 | return sectionItemsCount[section] 60 | } 61 | 62 | override func collectionView(_ collectionView: UICollectionView, 63 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 64 | guard 65 | let cell = collectionView.dequeueReusableCell( 66 | withReuseIdentifier: Self.cellIdentifier, 67 | for: indexPath 68 | ) as? TestingCellView 69 | else { fatalError("Unexpected dequeued cell") } 70 | cell.configure(with: "\(indexPath)") 71 | return cell 72 | } 73 | 74 | override func collectionView( 75 | _ collectionView: UICollectionView, 76 | viewForSupplementaryElementOfKind kind: String, 77 | at indexPath: IndexPath 78 | ) -> UICollectionReusableView { 79 | guard 80 | let supplementaryView = collectionView.dequeueReusableSupplementaryView( 81 | ofKind: kind, 82 | withReuseIdentifier: Self.supplementaryIdentifier, 83 | for: indexPath 84 | ) as? TestingSupplementaryView 85 | else { fatalError("Unexpected dequeued supplementary view") } 86 | 87 | supplementaryView.configure(with: "\(indexPath)") 88 | return supplementaryView 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/TestingCollectionView/TestingCollectionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestingCollectionViewModel.swift 3 | // CompositionalLayoutDSLTests 4 | // 5 | // Created by Alexandre Podlewski on 13/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftCheck 11 | 12 | struct TestingCollectionViewModel { 13 | let sectionItemsCount: [Int] 14 | } 15 | 16 | extension TestingCollectionViewModel: Arbitrary { 17 | 18 | static var arbitrary: Gen { 19 | Gen.compose { c in 20 | TestingCollectionViewModel( 21 | sectionItemsCount: c.generate( 22 | using: Gen.choose((0, 40)).proliferateNonEmpty 23 | ) 24 | ) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/TestingCollectionView/TestingDecorationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestingDecorationView.swift 3 | // CompositionalLayoutDSLTests 4 | // 5 | // Created by Alexandre Podlewski on 14/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TestingDecorationView: UICollectionReusableView { 12 | 13 | static let kind = "TestingDecorationView" 14 | 15 | // MARK: - Life cycle 16 | 17 | override init(frame: CGRect) { 18 | super.init(frame: frame) 19 | setup() 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | super.init(coder: coder) 24 | setup() 25 | } 26 | 27 | // MARK: - Private 28 | 29 | private func setup() { 30 | backgroundColor = .systemBackground 31 | layer.borderWidth = 5 32 | layer.borderColor = UIColor.cyan.cgColor 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/TestingCollectionView/TestingSupplementaryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestingSupplementaryView.swift 3 | // CompositionalLayoutDSLTests 4 | // 5 | // Created by Alexandre Podlewski on 13/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TestingSupplementaryView: UICollectionReusableView { 12 | 13 | private let label = UILabel() 14 | 15 | // MARK: - Life cycle 16 | 17 | override init(frame: CGRect) { 18 | super.init(frame: frame) 19 | setup() 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | super.init(coder: coder) 24 | setup() 25 | } 26 | 27 | override func prepareForReuse() { 28 | super.prepareForReuse() 29 | label.text = nil 30 | } 31 | 32 | // MARK: - TestingSupplementaryView 33 | 34 | func configure(with text: String) { 35 | label.text = text 36 | } 37 | 38 | // MARK: - Private 39 | 40 | private func setup() { 41 | backgroundColor = UIColor { $0.userInterfaceStyle == .dark ? .darkGray : .lightGray } 42 | addSubview(label) 43 | label.translatesAutoresizingMaskIntoConstraints = false 44 | label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 45 | label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/Utils/NSCollectionLayoutSize+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSCollectionLayoutSize+Utils.swift 3 | // CompositionalLayoutDSLTests 4 | // 5 | // Created by Alexandre Podlewski on 13/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension NSCollectionLayoutSize { 12 | static func fractional(width: CGFloat = 1.0, height: CGFloat = 1.0) -> NSCollectionLayoutSize { 13 | return NSCollectionLayoutSize( 14 | widthDimension: .fractionalWidth(width), 15 | heightDimension: .fractionalHeight(height) 16 | ) 17 | } 18 | static func absoluteHeight(_ height: CGFloat) -> NSCollectionLayoutSize { 19 | return NSCollectionLayoutSize( 20 | widthDimension: .fractionalWidth(1.0), 21 | heightDimension: .absolute(height) 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CompositionalLayoutDSLTests/Utils/assertLayouts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // assertLayouts.swift 3 | // CompositionalLayoutDSLTests 4 | // 5 | // Created by Alexandre Podlewski on 13/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | import SnapshotTesting 12 | import ADAssertLayout 13 | import ADLayoutTest 14 | 15 | extension XCTestCase { 16 | 17 | func assertLayouts( 18 | layout1: UICollectionViewLayout, 19 | layout2: UICollectionViewLayout, 20 | as snapshotting: Snapshotting, 21 | named name: String, 22 | maxTestsCount: Int, 23 | file: StaticString = #file, 24 | testName: String = #function, 25 | line: UInt = #line 26 | ) { 27 | var counter = 0 28 | runLayoutTests( 29 | named: name, 30 | snapshotStrategy: .failureOnly, 31 | randomStrategy: .consistent, 32 | maxTestsCount: maxTestsCount, 33 | file: file, 34 | line: line, 35 | run: { (viewModel: TestingCollectionViewModel) -> ViewAssertionResult in 36 | counter += 1 37 | return self.compareLayout( 38 | viewModel: viewModel, 39 | layout1: layout1, 40 | layout2: layout2, 41 | as: snapshotting, 42 | counter: counter, 43 | file: file, 44 | testName: testName, 45 | line: line 46 | ) 47 | } 48 | ) 49 | } 50 | 51 | private func compareLayout( 52 | viewModel: TestingCollectionViewModel, 53 | layout1: UICollectionViewLayout, 54 | layout2: UICollectionViewLayout, 55 | as snapshotting: Snapshotting, 56 | counter: Int, 57 | file: StaticString = #file, 58 | testName: String = #function, 59 | line: UInt = #line 60 | ) -> ViewAssertionResult { 61 | let controller = TestingCollectionViewController() 62 | // ???: (Alexandre Podlewski) 13/04/2021 Needed for all cells to be rendered 63 | controller.view.frame.size.height = 3000 64 | controller.view.frame.size.width = 3000 65 | controller.configure(with: viewModel) 66 | 67 | controller.collectionView.collectionViewLayout = layout1 68 | controller.collectionView.collectionViewLayout.invalidateLayout() 69 | controller.collectionView.reloadData() 70 | 71 | assertSnapshot( 72 | matching: controller, 73 | as: snapshotting, 74 | named: String(counter), 75 | file: file, 76 | testName: testName, 77 | line: line 78 | ) 79 | 80 | controller.collectionView.collectionViewLayout = layout2 81 | controller.collectionView.collectionViewLayout.invalidateLayout() 82 | controller.collectionView.reloadData() 83 | 84 | assertSnapshot( 85 | matching: controller, 86 | as: snapshotting, 87 | named: String(counter), 88 | file: file, 89 | testName: testName, 90 | line: line 91 | ) 92 | 93 | return .success 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | ## SwiftLint 2 | 3 | swiftlint.binary_path = 'Pods/SwiftLint/swiftlint' 4 | swiftlint.max_num_violations = 20 5 | swiftlint.lint_files( 6 | fail_on_error: true, 7 | inline_mode: true, 8 | additional_swiftlint_args: "--strict" 9 | ) 10 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CompositionalLayoutDSL", 6 | "repositoryURL": "git@github.com:faberNovel/CompositionalLayoutDSL.git", 7 | "state": { 8 | "branch": "develop", 9 | "revision": "d857d11e58b1f88800740f6b5c8f58127d45d354", 10 | "version": null 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example.xcodeproj/xcshareddata/xcschemes/CompositionalLayoutDSL_Example_iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/DemoCollectionViewController/DemoCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoCellView.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 08/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DemoCellView: UICollectionViewCell { 12 | 13 | private let label = UILabel() 14 | 15 | override var isHighlighted: Bool { 16 | didSet { 17 | alpha = isHighlighted ? 0.7 : 1 18 | } 19 | } 20 | 21 | override var isSelected: Bool { 22 | didSet { 23 | backgroundColor = isSelected ? .lightGray : .gray 24 | } 25 | } 26 | 27 | // MARK: - Life cycle 28 | 29 | override init(frame: CGRect) { 30 | super.init(frame: frame) 31 | setup() 32 | } 33 | 34 | required init?(coder: NSCoder) { 35 | super.init(coder: coder) 36 | setup() 37 | } 38 | 39 | override func prepareForReuse() { 40 | super.prepareForReuse() 41 | label.text = nil 42 | } 43 | 44 | // MARK: - CellView 45 | 46 | func configure(with text: String) { 47 | label.text = text 48 | } 49 | 50 | // MARK: - Private 51 | 52 | private func setup() { 53 | backgroundColor = .gray 54 | layer.cornerRadius = 6 55 | contentView.addSubview(label) 56 | label.translatesAutoresizingMaskIntoConstraints = false 57 | label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true 58 | label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/DemoCollectionViewController/DemoCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoCollectionViewController.swift 3 | // testCollectionSectionLayout 4 | // 5 | // Created by Alexandre Podlewski on 06/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DemoCollectionViewController: UICollectionViewController { 12 | 13 | static let cellIdentifier = "DemoCellView" 14 | static let supplementaryIdentifier = "DemoSupplementaryView" 15 | 16 | var sectionItemsCount: [Int] = [9, 4, 16, 1, 42, 10, 100] 17 | 18 | // MARK: - Life cycle 19 | 20 | init() { 21 | super.init(collectionViewLayout: UICollectionViewFlowLayout()) 22 | } 23 | 24 | required init?(coder: NSCoder) { 25 | super.init(coder: coder) 26 | } 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | collectionView.backgroundColor = .systemBackground 31 | collectionView.register(DemoCellView.self, forCellWithReuseIdentifier: Self.cellIdentifier) 32 | collectionView.register( 33 | DemoSupplementaryView.self, 34 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, 35 | withReuseIdentifier: Self.supplementaryIdentifier 36 | ) 37 | collectionView.register( 38 | DemoSupplementaryView.self, 39 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, 40 | withReuseIdentifier: Self.supplementaryIdentifier 41 | ) 42 | } 43 | 44 | // MARK: - UICollectionViewDataSource 45 | 46 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 47 | return sectionItemsCount.count 48 | } 49 | 50 | override func collectionView(_ collectionView: UICollectionView, 51 | numberOfItemsInSection section: Int) -> Int { 52 | return sectionItemsCount[section] 53 | } 54 | 55 | override func collectionView(_ collectionView: UICollectionView, 56 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 57 | guard 58 | let cell = collectionView.dequeueReusableCell( 59 | withReuseIdentifier: Self.cellIdentifier, 60 | for: indexPath 61 | ) as? DemoCellView 62 | else { fatalError("Unexpected dequeued cell") } 63 | cell.configure(with: "\(indexPath)") 64 | return cell 65 | } 66 | 67 | override func collectionView( 68 | _ collectionView: UICollectionView, 69 | viewForSupplementaryElementOfKind kind: String, 70 | at indexPath: IndexPath 71 | ) -> UICollectionReusableView { 72 | guard 73 | let supplementaryView = collectionView.dequeueReusableSupplementaryView( 74 | ofKind: kind, 75 | withReuseIdentifier: Self.supplementaryIdentifier, 76 | for: indexPath 77 | ) as? DemoSupplementaryView 78 | else { fatalError("Unexpected dequeued supplementary view") } 79 | 80 | supplementaryView.configure(with: "\(indexPath)") 81 | return supplementaryView 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/DemoCollectionViewController/DemoSupplementaryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoSupplementaryView.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 08/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DemoSupplementaryView: UICollectionReusableView { 12 | 13 | private let label = UILabel() 14 | 15 | // MARK: - Life cycle 16 | 17 | override init(frame: CGRect) { 18 | super.init(frame: frame) 19 | setup() 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | super.init(coder: coder) 24 | setup() 25 | } 26 | 27 | override func prepareForReuse() { 28 | super.prepareForReuse() 29 | label.text = nil 30 | } 31 | 32 | // MARK: - DemoSupplementaryView 33 | 34 | func configure(with text: String) { 35 | label.text = text 36 | } 37 | 38 | // MARK: - Private 39 | 40 | private func setup() { 41 | backgroundColor = UIColor { $0.userInterfaceStyle == .dark ? .darkGray : .lightGray } 42 | addSubview(label) 43 | label.translatesAutoresizingMaskIntoConstraints = false 44 | label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 45 | label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AdaptativeColumnLaneSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdaptativeColumnLaneSection.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 09/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CompositionalLayoutDSL 11 | 12 | struct AdaptativeColumnLaneSection: LayoutSection { 13 | 14 | /// Returns cell height from the cell width 15 | let cellHeightProvider: (CGFloat) -> CGFloat 16 | let interItemSpacing: ColumnLaneInterItemSpacing 17 | let environment: NSCollectionLayoutEnvironment 18 | let itemProvider: () -> LayoutItem 19 | 20 | // MARK: - LayoutSection 21 | 22 | var layoutSection: LayoutSection { 23 | ColumnLaneSection( 24 | columns: columns, 25 | cellHeightProvider: cellHeightProvider, 26 | interItemSpacing: interItemSpacing, 27 | environment: environment, 28 | itemProvider: itemProvider 29 | ) 30 | } 31 | 32 | // MARK: - Private 33 | 34 | private var columns: Float { 35 | switch environment.container.effectiveContentSize.width { 36 | case ..<500: 37 | return 1 38 | case 500..<700: 39 | return 1.5 40 | case 700...: 41 | return 2.5 42 | default: 43 | return 2.5 44 | } 45 | } 46 | } 47 | 48 | extension AdaptativeColumnLaneSection { 49 | 50 | init(environment: NSCollectionLayoutEnvironment, 51 | cellHeightProvider: @escaping (CGFloat) -> CGFloat, 52 | itemProvider: @escaping () -> LayoutItem = { Item() }) { 53 | self.init( 54 | cellHeightProvider: cellHeightProvider, 55 | interItemSpacing: .standard, 56 | environment: environment, 57 | itemProvider: itemProvider 58 | ) 59 | } 60 | 61 | init(environment: NSCollectionLayoutEnvironment, 62 | cellHeight: CGFloat, 63 | interItemSpacing: ColumnLaneInterItemSpacing = .standard, 64 | itemProvider: @escaping () -> LayoutItem = { Item() }) { 65 | assert(cellHeight > 0, "A 0 height is not allowed") 66 | self.init( 67 | cellHeightProvider: { _ in cellHeight }, 68 | interItemSpacing: interItemSpacing, 69 | environment: environment, 70 | itemProvider: itemProvider 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AppStoreNewContentSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStoreNewContentSection.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 09/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CompositionalLayoutDSL 11 | 12 | struct AppStoreNewContentSection: LayoutSection { 13 | 14 | let environment: NSCollectionLayoutEnvironment 15 | 16 | // MARK: - LayoutSection 17 | 18 | var layoutSection: LayoutSection { 19 | SectionWithEnvironmentInsets( 20 | insets: NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 0, trailing: 16), 21 | environment: environment 22 | ) { updatedEnvironment in 23 | AdaptativeColumnLaneSection( 24 | environment: updatedEnvironment, 25 | cellHeightProvider: cellHeight 26 | ) { 27 | Item { 28 | SupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) 29 | .height(.absolute(80)) 30 | .containerAnchor( 31 | NSCollectionLayoutAnchor( 32 | edges: .top, 33 | absoluteOffset: CGPoint(x: 0, y: -88) 34 | ) 35 | ) 36 | .zIndex(zIndex: 100) 37 | } 38 | .contentInsets(top: 88) 39 | } 40 | .orthogonalScrollingBehavior(.groupPaging) 41 | } 42 | } 43 | 44 | // MARK: - Private 45 | 46 | private func cellHeight(width: CGFloat) -> CGFloat { 47 | return width * 9 / 16 + 88 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AppStoreTopContentSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStoreTopContentSection.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 09/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CompositionalLayoutDSL 11 | 12 | struct AppStoreTopContentSection: LayoutSection { 13 | 14 | let environment: NSCollectionLayoutEnvironment 15 | 16 | // MARK: - LayoutSection 17 | 18 | var layoutSection: LayoutSection { 19 | SectionWithHeader { 20 | SectionWithEnvironmentInsets( 21 | insets: NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 0, trailing: 16), 22 | environment: environment 23 | ) { _ in 24 | LaneSection( 25 | cellHeight: 300 * 9 / 16 + 30, 26 | cellWidth: 300, 27 | horizontalSpacing: 16 28 | ) { 29 | Item { 30 | SupplementaryItem(elementKind: UICollectionView.elementKindSectionFooter) 31 | .containerAnchor( 32 | NSCollectionLayoutAnchor( 33 | edges: .bottom, 34 | absoluteOffset: CGPoint(x: 0, y: 30) 35 | ) 36 | ) 37 | .height(.absolute(26)) 38 | } 39 | .contentInsets(bottom: 30) 40 | } 41 | .orthogonalScrollingBehavior(.groupPaging) 42 | } 43 | } 44 | .supplementariesFollowContentInsets(false) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AppStoreTrendingContentSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStoreTrendingContentSection.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 09/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CompositionalLayoutDSL 11 | 12 | struct AppStoreTrendingContentSection: LayoutSection { 13 | 14 | let environment: NSCollectionLayoutEnvironment 15 | 16 | // MARK: - LayoutSection 17 | 18 | var layoutSection: LayoutSection { 19 | SectionWithEnvironmentInsets( 20 | insets: NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 0, trailing: 16), 21 | environment: environment 22 | ) { updatedEnvironment in 23 | AdaptativeColumnLaneSection( 24 | environment: updatedEnvironment, 25 | cellHeight: 240 26 | ) { 27 | VGroup(count: 3) { 28 | Item() 29 | } 30 | .interItemSpacing(.fixed(4)) 31 | } 32 | .orthogonalScrollingBehavior(.groupPaging) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/CompositionalLayout/CompositionalLayoutWithSupplementaryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalLayoutWithSupplementaryView.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 12/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CompositionalLayoutDSL 11 | 12 | struct CompositionalLayoutWithSupplementaryView { 13 | func layout() -> UICollectionViewLayout { 14 | LayoutBuilder { 15 | CompositionalLayout { _, _ in 16 | Section { 17 | VGroup(count: 1) { Item() } 18 | .height(.absolute(200)) 19 | .width(.fractionalWidth(0.85)) 20 | .interItemSpacing(.fixed(8)) 21 | } 22 | .interGroupSpacing(8) 23 | .orthogonalScrollingBehavior(.continuous) 24 | } 25 | .interSectionSpacing(20) 26 | .boundarySupplementaryItems { 27 | BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) 28 | .height(.absolute(150)) 29 | .alignment(.top) 30 | .absoluteOffset(CGPoint(x: 0, y: -8)) 31 | BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionFooter) 32 | .height(.absolute(50)) 33 | .alignment(.bottom) 34 | .absoluteOffset(CGPoint(x: 0, y: 8)) 35 | } 36 | } 37 | } 38 | } 39 | 40 | // Same layout with only UIKit APIs 41 | 42 | struct TraditionalCompositionalLayoutWithSupplementaryView { 43 | func layout() -> UICollectionViewLayout { 44 | return UICollectionViewCompositionalLayout(section: section, configuration: configuration) 45 | } 46 | 47 | // MARK: - Private 48 | 49 | private var section: NSCollectionLayoutSection { 50 | let itemSize = NSCollectionLayoutSize( 51 | widthDimension: .fractionalWidth(1), 52 | heightDimension: .fractionalHeight(1) 53 | ) 54 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 55 | let groupSize = NSCollectionLayoutSize( 56 | widthDimension: .fractionalWidth(0.85), 57 | heightDimension: .absolute(200) 58 | ) 59 | let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 1) 60 | return NSCollectionLayoutSection(group: group) 61 | } 62 | 63 | private var configuration: UICollectionViewCompositionalLayoutConfiguration { 64 | let configuration = UICollectionViewCompositionalLayoutConfiguration() 65 | configuration.interSectionSpacing = 20 66 | configuration.boundarySupplementaryItems = [globalHeader, globalFooter] 67 | return configuration 68 | } 69 | 70 | private var globalHeader: NSCollectionLayoutBoundarySupplementaryItem { 71 | let size = NSCollectionLayoutSize( 72 | widthDimension: .fractionalWidth(1), 73 | heightDimension: .absolute(200) 74 | ) 75 | return NSCollectionLayoutBoundarySupplementaryItem( 76 | layoutSize: size, 77 | elementKind: UICollectionView.elementKindSectionHeader, 78 | alignment: .top, 79 | absoluteOffset: CGPoint(x: 0, y: -8) 80 | ) 81 | } 82 | 83 | private var globalFooter: NSCollectionLayoutBoundarySupplementaryItem { 84 | let size = NSCollectionLayoutSize( 85 | widthDimension: .fractionalWidth(1), 86 | heightDimension: .absolute(50) 87 | ) 88 | return NSCollectionLayoutBoundarySupplementaryItem( 89 | layoutSize: size, 90 | elementKind: UICollectionView.elementKindSectionFooter, 91 | alignment: .top, 92 | absoluteOffset: CGPoint(x: 0, y: 8) 93 | ) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/CompositionalLayout/GettingStartedCompositionalLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GettingStartedCompositionalLayout.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 12/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CompositionalLayoutDSL 11 | 12 | struct GettingStartedCompositionalLayout { 13 | func layout() -> UICollectionViewLayout { 14 | return LayoutBuilder { 15 | Section { 16 | VGroup(count: 1) { Item() } 17 | .height(.fractionalWidth(0.3)) 18 | .width(.fractionalWidth(0.3)) 19 | .interItemSpacing(.fixed(8)) 20 | } 21 | .interGroupSpacing(8) 22 | .contentInsets(horizontal: 16, vertical: 8) 23 | .orthogonalScrollingBehavior(.continuous) 24 | .supplementariesFollowContentInsets(false) 25 | .boundarySupplementaryItems { 26 | BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) 27 | .height(.absolute(30)) 28 | .alignment(.top) 29 | .pinToVisibleBounds(true) 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/Section/ListSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListSection.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 09/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CompositionalLayoutDSL 11 | 12 | struct ListSection: LayoutSection { 13 | 14 | var layoutSection: LayoutSection { 15 | Section { 16 | HGroup(count: 5) { Item() } 17 | .height(.fractionalWidth(1 / 5)) 18 | .interItemSpacing(.fixed(4)) 19 | .contentInsets(horizontal: 8, vertical: 0) 20 | } 21 | .interGroupSpacing(4) 22 | .boundarySupplementaryItems { 23 | BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) 24 | .absoluteOffset(CGPoint(x: 0, y: -4)) 25 | .height(.absolute(20)) 26 | .alignment(.top) 27 | .pinToVisibleBounds(true) 28 | BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionFooter) 29 | .absoluteOffset(CGPoint(x: 0, y: 4)) 30 | .height(.absolute(20)) 31 | .alignment(.bottom) 32 | .pinToVisibleBounds(true) 33 | } 34 | } 35 | } 36 | 37 | // Same layout with only UIKit APIs 38 | 39 | struct TraditionalListSection { 40 | 41 | var layoutSection: NSCollectionLayoutSection { 42 | let itemSize = NSCollectionLayoutSize( 43 | widthDimension: .fractionalWidth(1), 44 | heightDimension: .fractionalHeight(1) 45 | ) 46 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 47 | 48 | let groupSize = NSCollectionLayoutSize( 49 | widthDimension: .fractionalWidth(1), 50 | heightDimension: .fractionalWidth(1 / 5) 51 | ) 52 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 5) 53 | group.interItemSpacing = .fixed(4) 54 | group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8) 55 | 56 | let headerFooterItemSize = NSCollectionLayoutSize( 57 | widthDimension: .fractionalWidth(1), 58 | heightDimension: .absolute(20) 59 | ) 60 | let headerItem = NSCollectionLayoutBoundarySupplementaryItem( 61 | layoutSize: headerFooterItemSize, 62 | elementKind: UICollectionView.elementKindSectionHeader, 63 | alignment: .top, 64 | absoluteOffset: CGPoint(x: 0, y: -4) 65 | ) 66 | headerItem.pinToVisibleBounds = true 67 | let footerItem = NSCollectionLayoutBoundarySupplementaryItem( 68 | layoutSize: headerFooterItemSize, 69 | elementKind: UICollectionView.elementKindSectionFooter, 70 | alignment: .bottom, 71 | absoluteOffset: CGPoint(x: 0, y: 4) 72 | ) 73 | footerItem.pinToVisibleBounds = true 74 | 75 | let section = NSCollectionLayoutSection(group: group) 76 | section.interGroupSpacing = 4 77 | section.boundarySupplementaryItems = [ 78 | headerItem, 79 | footerItem 80 | ] 81 | 82 | return section 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/Section/SectionWithHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionWithHeader.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 09/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CompositionalLayoutDSL 11 | 12 | struct SectionWithHeader: LayoutSection { 13 | 14 | var kind: String = UICollectionView.elementKindSectionHeader 15 | var height: NSCollectionLayoutDimension = .absolute(30) 16 | var section: () -> LayoutSection 17 | 18 | var layoutSection: LayoutSection { 19 | section() 20 | .boundarySupplementaryItems { 21 | BoundarySupplementaryItem(elementKind: kind) 22 | .height(height) 23 | .alignment(.top) 24 | } 25 | } 26 | } 27 | 28 | // Same layout with only UIKit APIs 29 | 30 | struct TraditionalSectionWithHeader { 31 | 32 | var kind: String = UICollectionView.elementKindSectionHeader 33 | var height: NSCollectionLayoutDimension = .absolute(30) 34 | var baseSectionLayout: NSCollectionLayoutSection 35 | 36 | var layoutSection: NSCollectionLayoutSection { 37 | let headerItemSize = NSCollectionLayoutSize( 38 | widthDimension: .fractionalWidth(1), 39 | heightDimension: height 40 | ) 41 | let headerItem = NSCollectionLayoutBoundarySupplementaryItem( 42 | layoutSize: headerItemSize, 43 | elementKind: kind, 44 | alignment: .top 45 | ) 46 | baseSectionLayout.boundarySupplementaryItems.append(headerItem) 47 | 48 | return baseSectionLayout 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/ShowcaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowcaseViewController.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 08/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CompositionalLayoutDSL 11 | 12 | class ShowcaseViewController: DemoCollectionViewController { 13 | 14 | private var currentLayoutIndex = 0 { 15 | didSet { 16 | title = "\(currentLayoutIndex + 1) / \(showCaseLayouts.count)" 17 | } 18 | } 19 | 20 | private let nextLayoutButton = UIBarButtonItem() 21 | 22 | // MARK: - Life cycle 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | currentLayoutIndex = 0 28 | nextLayoutButton.target = self 29 | nextLayoutButton.action = #selector(nextLayout) 30 | nextLayoutButton.title = "Next layout" 31 | navigationItem.rightBarButtonItem = nextLayoutButton 32 | 33 | collectionView.collectionViewLayout = showCaseLayouts[currentLayoutIndex] 34 | } 35 | 36 | // MARK: - Private 37 | 38 | @objc private func nextLayout() { 39 | let nextLayoutIndex = (currentLayoutIndex + 1) % showCaseLayouts.count 40 | currentLayoutIndex = nextLayoutIndex 41 | collectionView.setCollectionViewLayout(showCaseLayouts[currentLayoutIndex], animated: false) 42 | collectionView.reloadData() 43 | collectionView.contentOffset = CGPoint(x: 0, y: -collectionView.adjustedContentInset.top) 44 | } 45 | } 46 | 47 | extension ShowcaseViewController { 48 | 49 | // MARK: - Layouts 50 | 51 | private var showCaseLayouts: [UICollectionViewLayout] { 52 | [ 53 | GettingStartedCompositionalLayout().layout(), 54 | LayoutBuilder { ListSection() }, 55 | // LayoutBuilder { TraditionalListSection() }, 56 | LayoutBuilder { 57 | Section { 58 | FractalGroup(ratio: 0.5, depth: 2) 59 | .height(.fractionalWidth(0.5)) 60 | } 61 | }, 62 | // LayoutBuilder { 63 | // Section { 64 | // TraditionalFractalGroup(ratio: 0.5, depth: 2, heightDimension: .fractionalWidth(0.5)) 65 | // } 66 | // }, 67 | LayoutBuilder { 68 | SectionWithHeader { 69 | Section { 70 | FractalGroup(ratio: 0.5, depth: 2) 71 | .height(.fractionalWidth(0.45)) 72 | .width(.fractionalWidth(0.9)) 73 | } 74 | .orthogonalScrollingBehavior(.continuous) 75 | } 76 | }, 77 | LayoutBuilder { 78 | CompositionalLayout(repeatingSections: [ 79 | // swiftlint:disable opening_brace 80 | { AppStoreNewContentSection(environment: $1) }, 81 | { AppStoreTrendingContentSection(environment: $1) }, 82 | { AppStoreTopContentSection(environment: $1) }, 83 | { AppStoreTrendingContentSection(environment: $1) }, 84 | { AppStoreTrendingContentSection(environment: $1) } 85 | // swiftlint:enable opening_brace 86 | ]) 87 | }, 88 | CompositionalLayoutWithSupplementaryView().layout() 89 | // TraditionalCompositionalLayoutWithSupplementaryView().layout() 90 | ] 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/Utils/Section/ColumnLaneSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColumnLaneSection.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 09/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CompositionalLayoutDSL 11 | 12 | enum ColumnLaneInterItemSpacing { 13 | case standard 14 | case custom(spacing: CGFloat) 15 | } 16 | 17 | struct ColumnLaneSection: LayoutSection { 18 | 19 | let columns: Float 20 | /// Returns cell height from the cell width 21 | let cellHeightProvider: (CGFloat) -> CGFloat 22 | let interItemSpacing: ColumnLaneInterItemSpacing 23 | let environment: NSCollectionLayoutEnvironment 24 | let itemProvider: () -> LayoutItem 25 | 26 | // MARK: - LayoutSection 27 | 28 | var layoutSection: LayoutSection { 29 | Section { 30 | VGroup { itemProvider() } 31 | .width(.absolute(cellWidth)) 32 | .height(.absolute(cellHeight)) 33 | } 34 | .interGroupSpacing(horizontalSpacing) 35 | .orthogonalScrollingBehavior(.continuousGroupLeadingBoundary) 36 | } 37 | 38 | // MARK: - Private 39 | 40 | private var cellWidth: CGFloat { 41 | let cumulatedHorizontalSpacing = horizontalSpacing * CGFloat(columns - 1) 42 | let effectiveWidth = environment.container.effectiveContentSize.width 43 | return (effectiveWidth - cumulatedHorizontalSpacing) / CGFloat(columns) 44 | } 45 | 46 | private var cellHeight: CGFloat { 47 | let cellHeight = cellHeightProvider(cellWidth) 48 | assert(cellHeight > 0, "Cell height can not be 0") 49 | return cellHeight 50 | } 51 | 52 | private var horizontalSpacing: CGFloat { 53 | switch interItemSpacing { 54 | case let .custom(spacing: customSpacing): 55 | return customSpacing 56 | case .standard: 57 | return 8 58 | } 59 | } 60 | } 61 | 62 | extension ColumnLaneSection { 63 | 64 | init(columns: Float, 65 | environment: NSCollectionLayoutEnvironment, 66 | cellHeightProvider: @escaping (CGFloat) -> CGFloat, 67 | itemProvider: @escaping () -> LayoutItem = { Item() }) { 68 | self.init( 69 | columns: columns, 70 | cellHeightProvider: cellHeightProvider, 71 | interItemSpacing: .standard, 72 | environment: environment, 73 | itemProvider: itemProvider 74 | ) 75 | } 76 | 77 | init(columns: Float, 78 | environment: NSCollectionLayoutEnvironment, 79 | cellHeight: CGFloat, 80 | interItemSpacing: ColumnLaneInterItemSpacing = .standard, 81 | itemProvider: @escaping () -> LayoutItem = { Item() }) { 82 | assert(cellHeight > 0, "A 0 height is not allowed") 83 | self.init( 84 | columns: columns, 85 | cellHeightProvider: { _ in cellHeight }, 86 | interItemSpacing: interItemSpacing, 87 | environment: environment, 88 | itemProvider: itemProvider 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/Utils/Section/LaneSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LaneSection.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 09/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CompositionalLayoutDSL 11 | 12 | struct LaneSection: LayoutSection { 13 | 14 | let cellHeight: CGFloat 15 | let cellWidth: CGFloat 16 | let horizontalSpacing: CGFloat 17 | let itemProvider: () -> LayoutItem 18 | 19 | init(cellHeight: CGFloat, 20 | cellWidth: CGFloat, 21 | horizontalSpacing: CGFloat, 22 | itemProvider: @escaping () -> LayoutItem) { 23 | precondition(cellWidth > 0, "Cell width cannot be 0") 24 | precondition(cellHeight > 0, "Cell height cannot be 0") 25 | self.cellHeight = cellHeight 26 | self.cellWidth = cellWidth 27 | self.horizontalSpacing = horizontalSpacing 28 | self.itemProvider = itemProvider 29 | } 30 | 31 | var layoutSection: LayoutSection { 32 | Section { 33 | VGroup { itemProvider() } 34 | .width(.absolute(cellWidth)) 35 | .height(.absolute(cellHeight)) 36 | } 37 | .interGroupSpacing(horizontalSpacing) 38 | .orthogonalScrollingBehavior(.continuousGroupLeadingBoundary) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/App/Utils/Section/SectionWithEnvironmentInsets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionWithEnvironmentInsets.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 09/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CompositionalLayoutDSL 11 | 12 | struct SectionWithEnvironmentInsets: LayoutSection { 13 | 14 | let insets: NSDirectionalEdgeInsets 15 | let baseSection: LayoutSection 16 | 17 | init(insets: NSDirectionalEdgeInsets, 18 | environment: NSCollectionLayoutEnvironment, 19 | baseSection: (NSCollectionLayoutEnvironment) -> LayoutSection) { 20 | self.insets = insets 21 | self.baseSection = baseSection( 22 | CustomCollectionLayoutEnvironment( 23 | from: environment, 24 | withAdditionalInsets: insets 25 | ) 26 | ) 27 | } 28 | 29 | // MARK: - LayoutSection 30 | 31 | var layoutSection: LayoutSection { 32 | baseSection.contentInsets(insets) 33 | } 34 | } 35 | 36 | private class CustomCollectionLayoutEnvironment: NSObject, NSCollectionLayoutEnvironment { 37 | 38 | let container: NSCollectionLayoutContainer 39 | let traitCollection: UITraitCollection 40 | 41 | init(container: NSCollectionLayoutContainer, 42 | traitCollection: UITraitCollection, 43 | withAdditionalInsets insets: NSDirectionalEdgeInsets) { 44 | self.container = CustomCollectionLayoutContainer(from: container, withAdditionalInsets: insets) 45 | self.traitCollection = traitCollection 46 | } 47 | 48 | convenience init(from collectionLayoutEnvironment: NSCollectionLayoutEnvironment, 49 | withAdditionalInsets insets: NSDirectionalEdgeInsets) { 50 | self.init( 51 | container: collectionLayoutEnvironment.container, 52 | traitCollection: collectionLayoutEnvironment.traitCollection, 53 | withAdditionalInsets: insets 54 | ) 55 | } 56 | } 57 | 58 | private class CustomCollectionLayoutContainer: NSObject, NSCollectionLayoutContainer { 59 | let contentSize: CGSize 60 | let effectiveContentSize: CGSize 61 | let contentInsets: NSDirectionalEdgeInsets 62 | let effectiveContentInsets: NSDirectionalEdgeInsets 63 | 64 | init(contentSize: CGSize, 65 | effectiveContentSize: CGSize, 66 | contentInsets: NSDirectionalEdgeInsets, 67 | effectiveContentInsets: NSDirectionalEdgeInsets, 68 | withAdditionalInsets insets: NSDirectionalEdgeInsets = .zero) { 69 | self.contentSize = contentSize 70 | self.effectiveContentSize = effectiveContentSize.inseted(by: insets) 71 | self.contentInsets = contentInsets.adding(insets) 72 | self.effectiveContentInsets = effectiveContentInsets.adding(insets) 73 | } 74 | 75 | convenience init(from collectionLayoutContainer: NSCollectionLayoutContainer, 76 | withAdditionalInsets insets: NSDirectionalEdgeInsets) { 77 | self.init( 78 | contentSize: collectionLayoutContainer.contentSize, 79 | effectiveContentSize: collectionLayoutContainer.effectiveContentSize, 80 | contentInsets: collectionLayoutContainer.contentInsets, 81 | effectiveContentInsets: collectionLayoutContainer.effectiveContentInsets, 82 | withAdditionalInsets: insets 83 | ) 84 | } 85 | } 86 | 87 | private extension NSDirectionalEdgeInsets { 88 | func adding(_ insets: NSDirectionalEdgeInsets) -> NSDirectionalEdgeInsets { 89 | var copy = self 90 | copy.trailing += insets.trailing 91 | copy.leading += insets.leading 92 | copy.bottom += insets.bottom 93 | copy.top += insets.top 94 | return copy 95 | } 96 | } 97 | 98 | private extension CGSize { 99 | func inseted(by insets: NSDirectionalEdgeInsets) -> CGSize { 100 | var copy = self 101 | copy.width = max(0, width - insets.leading - insets.trailing) 102 | copy.height = max(0, height - insets.top - insets.bottom) 103 | return copy 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 08/04/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | func application( 14 | _ application: UIApplication, 15 | // swiftlint:disable:next discouraged_optional_collection 16 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 17 | ) -> Bool { 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, 24 | configurationForConnecting connectingSceneSession: UISceneSession, 25 | options: UIScene.ConnectionOptions) -> UISceneConfiguration { 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/Base.lproj/LaunchScreen.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 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UIApplicationSupportsIndirectInputEvents 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_iOS/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // CompositionalLayoutDSL_Example_iOS 4 | // 5 | // Created by Alexandre Podlewski on 08/04/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, 15 | willConnectTo session: UISceneSession, 16 | options connectionOptions: UIScene.ConnectionOptions) { 17 | guard let windowScene = (scene as? UIWindowScene) else { return } 18 | let window = UIWindow(windowScene: windowScene) 19 | self.window = window 20 | window.rootViewController = UINavigationController(rootViewController: ShowcaseViewController()) 21 | window.makeKeyAndVisible() 22 | } 23 | 24 | func sceneDidDisconnect(_ scene: UIScene) { 25 | } 26 | 27 | func sceneDidBecomeActive(_ scene: UIScene) { 28 | } 29 | 30 | func sceneWillResignActive(_ scene: UIScene) { 31 | } 32 | 33 | func sceneWillEnterForeground(_ scene: UIScene) { 34 | } 35 | 36 | func sceneDidEnterBackground(_ scene: UIScene) { 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_macOS/App/DemoCollectionViewController/DemoCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoCellView.swift 3 | // CompositionalLayoutDSL_Example_macOS 4 | // 5 | // Created by Alexandre Podlewski on 21/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | class DemoCellView: NSCollectionViewItem { 12 | 13 | private let label = NSText() 14 | 15 | // MARK: - Life cycle 16 | 17 | override func loadView() { 18 | self.view = NSView() 19 | self.view.wantsLayer = true 20 | } 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | setup() 25 | } 26 | 27 | override func prepareForReuse() { 28 | super.prepareForReuse() 29 | label.string = "" 30 | } 31 | 32 | // MARK: - CellView 33 | 34 | func configure(with text: String) { 35 | label.string = text 36 | } 37 | 38 | // MARK: - Private 39 | 40 | private func setup() { 41 | view.layer?.backgroundColor = NSColor.gray.cgColor 42 | view.layer?.cornerRadius = 6 43 | view.addSubview(label) 44 | label.translatesAutoresizingMaskIntoConstraints = false 45 | label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 46 | label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true 47 | label.widthAnchor.constraint(equalToConstant: 50).isActive = true 48 | label.heightAnchor.constraint(equalToConstant: 15).isActive = true 49 | label.alignment = .center 50 | label.backgroundColor = NSColor.gray 51 | label.isEditable = false 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_macOS/App/DemoCollectionViewController/DemoCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoCollectionViewController.swift 3 | // CompositionalLayoutDSL_Example_macOS 4 | // 5 | // Created by Alexandre Podlewski on 21/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | class DemoCollectionViewController: NSViewController, NSCollectionViewDataSource, NSCollectionViewDelegate { 12 | 13 | static let cellIdentifier = NSUserInterfaceItemIdentifier(rawValue: "DemoCellView") 14 | static let supplementaryIdentifier = NSUserInterfaceItemIdentifier(rawValue: "DemoSupplementaryView") 15 | 16 | lazy var scrollView = NSScrollView() 17 | lazy var collectionView = NSCollectionView() 18 | 19 | var sectionItemsCount: [Int] = [9, 4, 16, 1, 42, 10, 100] 20 | 21 | // MARK: - Life cycle 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | view.addSubview(scrollView) 26 | scrollView.translatesAutoresizingMaskIntoConstraints = false 27 | scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true 28 | scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true 29 | scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true 30 | scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true 31 | 32 | scrollView.documentView = collectionView 33 | collectionView.backgroundColors = [NSColor.red] 34 | setup() 35 | } 36 | 37 | // MARK: - NSCollectionViewDataSource 38 | 39 | func numberOfSections(in collectionView: NSCollectionView) -> Int { 40 | return sectionItemsCount.count 41 | } 42 | 43 | func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { 44 | return sectionItemsCount[section] 45 | } 46 | 47 | func collectionView( 48 | _ collectionView: NSCollectionView, 49 | itemForRepresentedObjectAt indexPath: IndexPath 50 | ) -> NSCollectionViewItem { 51 | guard 52 | let cell = collectionView.makeItem( 53 | withIdentifier: Self.cellIdentifier, 54 | for: indexPath 55 | ) as? DemoCellView 56 | else { fatalError("Unexpected dequeued cell") } 57 | cell.configure(with: "\(indexPath)") 58 | return cell 59 | } 60 | 61 | func collectionView( 62 | _ collectionView: NSCollectionView, 63 | viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, 64 | at indexPath: IndexPath 65 | ) -> NSView { 66 | guard 67 | let supplementaryView = collectionView.makeSupplementaryView( 68 | ofKind: kind, 69 | withIdentifier: Self.supplementaryIdentifier, 70 | for: indexPath 71 | ) as? DemoSupplementaryView 72 | else { fatalError("Unexpected dequeued supplementary view") } 73 | 74 | supplementaryView.configure(with: "\(indexPath)") 75 | return supplementaryView 76 | } 77 | 78 | // MARK: - Private 79 | 80 | private func setup() { 81 | collectionView.dataSource = self 82 | collectionView.delegate = self 83 | collectionView.collectionViewLayout = NSCollectionViewFlowLayout() 84 | collectionView.backgroundColors = [.windowBackgroundColor] 85 | 86 | collectionView.register(DemoCellView.self, forItemWithIdentifier: Self.cellIdentifier) 87 | collectionView.register( 88 | DemoSupplementaryView.self, 89 | forSupplementaryViewOfKind: NSCollectionView.elementKindSectionHeader, 90 | withIdentifier: Self.supplementaryIdentifier 91 | ) 92 | collectionView.register( 93 | DemoSupplementaryView.self, 94 | forSupplementaryViewOfKind: NSCollectionView.elementKindSectionFooter, 95 | withIdentifier: Self.supplementaryIdentifier 96 | ) 97 | 98 | collectionView.reloadData() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_macOS/App/DemoCollectionViewController/DemoSupplementaryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoSupplementaryView.swift 3 | // CompositionalLayoutDSL_Example_macOS 4 | // 5 | // Created by Alexandre Podlewski on 21/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | final class DemoSupplementaryView: NSView, NSCollectionViewElement { 12 | 13 | private let label = NSText() 14 | 15 | // MARK: - Life cycle 16 | 17 | override init(frame frameRect: NSRect) { 18 | super.init(frame: frameRect) 19 | setup() 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | super.init(coder: coder) 24 | setup() 25 | } 26 | 27 | override func prepareForReuse() { 28 | super.prepareForReuse() 29 | label.string = "" 30 | } 31 | 32 | // MARK: - DemoSupplementaryView 33 | 34 | func configure(with text: String) { 35 | label.string = text 36 | } 37 | 38 | // MARK: - Private 39 | 40 | private func setup() { 41 | self.wantsLayer = true 42 | self.layer?.backgroundColor = NSColor.darkGray.cgColor 43 | addSubview(label) 44 | label.translatesAutoresizingMaskIntoConstraints = false 45 | label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 46 | label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 47 | label.widthAnchor.constraint(equalToConstant: 50).isActive = true 48 | label.heightAnchor.constraint(equalToConstant: 15).isActive = true 49 | label.alignment = .center 50 | label.backgroundColor = NSColor.darkGray 51 | label.isEditable = false 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_macOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CompositionalLayoutDSL_Example_macOS 4 | // 5 | // Created by Alexandre Podlewski on 21/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @main 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | func applicationDidFinishLaunching(_ aNotification: Notification) { 15 | // Insert code here to initialize your application 16 | } 17 | 18 | func applicationWillTerminate(_ aNotification: Notification) { 19 | // Insert code here to tear down your application 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_macOS/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_macOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_macOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_macOS/CompositionalLayoutDSL_Example_macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/CompositionalLayoutDSL_Example_macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2021 Fabernovel. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby '3.2.2' 3 | 4 | gem 'cocoapods', '1.13.0' 5 | gem 'fastlane', '<3.0' 6 | gem 'danger' 7 | gem 'danger-swiftlint' 8 | 9 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 10 | eval_gemfile(plugins_path) if File.exist?(plugins_path) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Fabernovel 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.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SwiftDocCPlugin", 6 | "repositoryURL": "https://github.com/apple/swift-docc-plugin", 7 | "state": { 8 | "branch": null, 9 | "revision": "26ac5758409154cc448d7ab82389c520fa8a8247", 10 | "version": "1.3.0" 11 | } 12 | }, 13 | { 14 | "package": "SymbolKit", 15 | "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", 16 | "state": { 17 | "branch": null, 18 | "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", 19 | "version": "1.0.0" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 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: "CompositionalLayoutDSL", 8 | platforms: [ 9 | .iOS(.v13), 10 | .tvOS(.v13), 11 | .macOS(.v10_15) 12 | ], 13 | products: [ 14 | .library( 15 | name: "CompositionalLayoutDSL", 16 | targets: ["CompositionalLayoutDSL"] 17 | ) 18 | ], 19 | dependencies: [ 20 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), 21 | ], 22 | targets: [ 23 | .target( 24 | name: "CompositionalLayoutDSL", 25 | dependencies: [] 26 | ) 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '13.0' 2 | use_frameworks! 3 | 4 | target 'CompositionalLayoutDSLApp' do 5 | pod 'SwiftLint', '~> 0.42.0' 6 | pod 'CompositionalLayoutDSL', :path => './' 7 | end 8 | 9 | target 'CompositionalLayoutDSLTests' do 10 | pod 'ADLayoutTest', '~> 1.0' 11 | pod 'SnapshotTesting', '~> 1.8' 12 | pod 'CompositionalLayoutDSL', :path => './' 13 | end 14 | 15 | post_install do |installer| 16 | installer.pods_project.targets.each do |target| 17 | target.build_configurations.each do |config| 18 | 19 | # Change the Optimization level for each target/configuration 20 | if !config.name.include?("Distribution") 21 | config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '0' 22 | end 23 | 24 | # Disable Pod Codesign 25 | config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = "" 26 | config.build_settings['CODE_SIGNING_REQUIRED'] = "NO" 27 | config.build_settings['CODE_SIGNING_ALLOWED'] = "NO" 28 | 29 | # Fix build issue on Xcode 15 30 | if ["SwiftCheck", "ADLayoutTest", "ADAssertLayout"].include? target.name 31 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = 11.0 32 | config.build_settings['TVOS_DEPLOYMENT_TARGET'] = 11.0 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - ADAssertLayout (1.0.1) 3 | - ADLayoutTest (1.0.1): 4 | - ADAssertLayout (~> 1.0) 5 | - SwiftCheck (~> 0.12) 6 | - CompositionalLayoutDSL (0.2.0) 7 | - SnapshotTesting (1.9.0) 8 | - SwiftCheck (0.12.0) 9 | - SwiftLint (0.42.0) 10 | 11 | DEPENDENCIES: 12 | - ADLayoutTest (~> 1.0) 13 | - CompositionalLayoutDSL (from `./`) 14 | - SnapshotTesting (~> 1.8) 15 | - SwiftLint (~> 0.42.0) 16 | 17 | SPEC REPOS: 18 | trunk: 19 | - ADAssertLayout 20 | - ADLayoutTest 21 | - SnapshotTesting 22 | - SwiftCheck 23 | - SwiftLint 24 | 25 | EXTERNAL SOURCES: 26 | CompositionalLayoutDSL: 27 | :path: "./" 28 | 29 | SPEC CHECKSUMS: 30 | ADAssertLayout: 9848d3544bc5531bef0271a9de53114f215d36ee 31 | ADLayoutTest: 2a5675b3fa446be1fc331696f2bd480ab20c2677 32 | CompositionalLayoutDSL: 1173bbdf7da894b335a49370699ee73cc0396262 33 | SnapshotTesting: 6141c48b6aa76ead61431ca665c14ab9a066c53b 34 | SwiftCheck: d1dd04d955cc76620b0f84e087536bd059a11c24 35 | SwiftLint: 4fa9579c63416865179bc416f0a92d55f009600d 36 | 37 | PODFILE CHECKSUM: 38d89a9438d967956f96db57415e9bc928624d0d 38 | 39 | COCOAPODS: 1.13.0 40 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/Builders/BoundarySupplementaryItemBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoundarySupplementaryItemBuilder.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | internal protocol BuildableBoundarySupplementaryItem: BuildableSupplementaryItem { 16 | func makeBoundarySupplementaryItem() -> NSCollectionLayoutBoundarySupplementaryItem 17 | } 18 | 19 | extension BuildableBoundarySupplementaryItem { 20 | func makeSupplementaryItem() -> NSCollectionLayoutSupplementaryItem { 21 | makeBoundarySupplementaryItem() 22 | } 23 | } 24 | 25 | internal enum BoundarySupplementaryItemBuilder { 26 | static func make( 27 | from layoutBoundarySupplementaryItem: LayoutBoundarySupplementaryItem 28 | ) -> NSCollectionLayoutBoundarySupplementaryItem { 29 | guard let buildable = getBuildableBoundarySupplementaryItem(from: layoutBoundarySupplementaryItem) else { 30 | // swiftlint:disable:next line_length 31 | fatalError("Unable to convert the given LayoutBoundarySupplementaryItem to NSCollectionLayoutBoundarySupplementaryItem") 32 | } 33 | return buildable.makeBoundarySupplementaryItem() 34 | } 35 | 36 | private static func getBuildableBoundarySupplementaryItem( 37 | from layoutBoundarySupplementaryItem: LayoutBoundarySupplementaryItem 38 | ) -> BuildableBoundarySupplementaryItem? { 39 | var currentBoundarySupplementaryItem = layoutBoundarySupplementaryItem 40 | while !(currentBoundarySupplementaryItem is BuildableBoundarySupplementaryItem) { 41 | currentBoundarySupplementaryItem = currentBoundarySupplementaryItem.layoutBoundarySupplementaryItem 42 | } 43 | return currentBoundarySupplementaryItem as? BuildableBoundarySupplementaryItem 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/Builders/ConfigurationBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationBuilder.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | internal protocol BuildableConfiguration { 16 | func makeConfiguration() -> ConfigurationBuilder.TransformedType 17 | } 18 | 19 | internal enum ConfigurationBuilder { 20 | 21 | #if os(macOS) 22 | typealias TransformedType = NSCollectionViewCompositionalLayoutConfiguration 23 | #else 24 | typealias TransformedType = UICollectionViewCompositionalLayoutConfiguration 25 | #endif 26 | 27 | static func make( 28 | from layoutConfiguration: LayoutConfiguration 29 | ) -> ConfigurationBuilder.TransformedType { 30 | guard let buildableConfiguration = getBuildableConfiguration(from: layoutConfiguration) else { 31 | // swiftlint:disable:next line_length 32 | fatalError("Unable to convert the given LayoutConfiguration to UICollectionViewCompositionalLayoutConfiguration") 33 | } 34 | return buildableConfiguration.makeConfiguration() 35 | } 36 | 37 | private static func getBuildableConfiguration( 38 | from layoutConfiguration: LayoutConfiguration 39 | ) -> BuildableConfiguration? { 40 | var currentConfiguration = layoutConfiguration 41 | while !(currentConfiguration is BuildableConfiguration) { 42 | currentConfiguration = currentConfiguration.layoutConfiguration 43 | } 44 | return currentConfiguration as? BuildableConfiguration 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/Builders/DecorationItemBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DecorationItemBuilder.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | internal protocol BuildableDecorationItem: BuildableItem { 16 | func makeDecorationItem() -> NSCollectionLayoutDecorationItem 17 | } 18 | 19 | extension BuildableDecorationItem { 20 | func makeItem() -> NSCollectionLayoutItem { 21 | makeDecorationItem() 22 | } 23 | } 24 | 25 | internal enum DecorationItemBuilder { 26 | static func make( 27 | from layoutDecorationItem: LayoutDecorationItem 28 | ) -> NSCollectionLayoutDecorationItem { 29 | guard let buildable = getBuildableDecorationItem(from: layoutDecorationItem) else { 30 | fatalError("Unable to convert the given LayoutDecorationItem to NSCollectionLayoutDecorationItem") 31 | } 32 | return buildable.makeDecorationItem() 33 | } 34 | 35 | private static func getBuildableDecorationItem( 36 | from layoutDecorationItem: LayoutDecorationItem 37 | ) -> BuildableDecorationItem? { 38 | var currentDecorationItem = layoutDecorationItem 39 | while !(currentDecorationItem is BuildableDecorationItem) { 40 | currentDecorationItem = currentDecorationItem.layoutDecorationItem 41 | } 42 | return currentDecorationItem as? BuildableDecorationItem 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/Builders/GroupBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupBuilder.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | internal protocol BuildableGroup: BuildableItem { 16 | func makeGroup() -> NSCollectionLayoutGroup 17 | } 18 | 19 | extension BuildableGroup { 20 | func makeItem() -> NSCollectionLayoutItem { 21 | makeGroup() 22 | } 23 | } 24 | 25 | internal enum GroupBuilder { 26 | static func make(from layoutGroup: LayoutGroup) -> NSCollectionLayoutGroup { 27 | guard let buildableGroup = getBuildableGroup(from: layoutGroup) else { 28 | fatalError("Unable to convert the given LayoutGroup to NSCollectionLayoutGroup") 29 | } 30 | return buildableGroup.makeGroup() 31 | } 32 | 33 | private static func getBuildableGroup(from layoutGroup: LayoutGroup) -> BuildableGroup? { 34 | var currentGroup = layoutGroup 35 | while !(currentGroup is BuildableGroup) { 36 | currentGroup = currentGroup.layoutGroup 37 | } 38 | return currentGroup as? BuildableGroup 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/Builders/ItemBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemBuilder.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | internal protocol BuildableItem { 16 | func makeItem() -> NSCollectionLayoutItem 17 | } 18 | 19 | internal enum ItemBuilder { 20 | static func make(from layoutItem: LayoutItem) -> NSCollectionLayoutItem { 21 | guard let buildableItem = getBuildableItem(from: layoutItem) else { 22 | fatalError("Unable to convert the given LayoutItem to NSCollectionLayoutItem") 23 | } 24 | return buildableItem.makeItem() 25 | } 26 | 27 | private static func getBuildableItem(from layoutItem: LayoutItem) -> BuildableItem? { 28 | var currentItem = layoutItem 29 | while !(currentItem is BuildableItem) { 30 | currentItem = currentItem.layoutItem 31 | } 32 | return currentItem as? BuildableItem 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/Builders/SectionBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionBuilder.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | internal protocol BuildableSection { 16 | func makeSection() -> NSCollectionLayoutSection 17 | } 18 | 19 | internal enum SectionBuilder { 20 | static func make(from layoutSection: LayoutSection) -> NSCollectionLayoutSection { 21 | guard let buildableSection = getBuildableSection(from: layoutSection) else { 22 | fatalError("Unable to convert the given LayoutSection to NSCollectionLayoutSection") 23 | } 24 | return buildableSection.makeSection() 25 | } 26 | 27 | private static func getBuildableSection(from layoutSection: LayoutSection) -> BuildableSection? { 28 | var currentSection = layoutSection 29 | while !(currentSection is BuildableSection) { 30 | currentSection = currentSection.layoutSection 31 | } 32 | return currentSection as? BuildableSection 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/Builders/SupplementaryItemBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SupplementaryItemBuilder.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | internal protocol BuildableSupplementaryItem: BuildableItem { 16 | func makeSupplementaryItem() -> NSCollectionLayoutSupplementaryItem 17 | } 18 | 19 | extension BuildableSupplementaryItem { 20 | func makeItem() -> NSCollectionLayoutItem { 21 | makeSupplementaryItem() 22 | } 23 | } 24 | 25 | internal enum SupplementaryItemBuilder { 26 | static func make( 27 | from layoutSupplementaryItem: LayoutSupplementaryItem 28 | ) -> NSCollectionLayoutSupplementaryItem { 29 | guard let buildable = getBuildableSupplementaryItem(from: layoutSupplementaryItem) else { 30 | fatalError("Unable to convert the given LayoutSupplementaryItem to NSCollectionLayoutSupplementaryItem") 31 | } 32 | return buildable.makeSupplementaryItem() 33 | } 34 | 35 | private static func getBuildableSupplementaryItem( 36 | from layoutSupplementaryItem: LayoutSupplementaryItem 37 | ) -> BuildableSupplementaryItem? { 38 | var currentSupplementaryItem = layoutSupplementaryItem 39 | while !(currentSupplementaryItem is BuildableSupplementaryItem) { 40 | currentSupplementaryItem = currentSupplementaryItem.layoutSupplementaryItem 41 | } 42 | return currentSupplementaryItem as? BuildableSupplementaryItem 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutBoundarySupplementaryItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifiedLayoutBoundarySupplementaryItem.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | struct ValueModifiedLayoutBoundarySupplementaryItem: LayoutBoundarySupplementaryItem, 16 | BuildableBoundarySupplementaryItem { 17 | let boundarySupplementaryItem: LayoutBoundarySupplementaryItem 18 | let valueModifier: (inout NSCollectionLayoutBoundarySupplementaryItem) -> Void 19 | 20 | var layoutBoundarySupplementaryItem: LayoutBoundarySupplementaryItem { self } 21 | 22 | func makeBoundarySupplementaryItem() -> NSCollectionLayoutBoundarySupplementaryItem { 23 | var collectionLayoutBoundarySupplementaryItem = BoundarySupplementaryItemBuilder 24 | .make(from: boundarySupplementaryItem) 25 | valueModifier(&collectionLayoutBoundarySupplementaryItem) 26 | return collectionLayoutBoundarySupplementaryItem 27 | } 28 | } 29 | 30 | extension LayoutBoundarySupplementaryItem { 31 | 32 | func valueModifier( 33 | _ value: T, 34 | keyPath: WritableKeyPath 35 | ) -> LayoutBoundarySupplementaryItem { 36 | ValueModifiedLayoutBoundarySupplementaryItem(boundarySupplementaryItem: self) { 37 | $0[keyPath: keyPath] = value 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifiedLayoutConfiguration.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | struct ValueModifiedLayoutConfiguration: LayoutConfiguration, BuildableConfiguration { 16 | let configuration: LayoutConfiguration 17 | let valueModifier: (inout ConfigurationBuilder.TransformedType) -> Void 18 | 19 | var layoutConfiguration: LayoutConfiguration { self } 20 | 21 | func makeConfiguration() -> ConfigurationBuilder.TransformedType { 22 | var collectionLayoutConfiguration = ConfigurationBuilder.make(from: configuration) 23 | valueModifier(&collectionLayoutConfiguration) 24 | return collectionLayoutConfiguration 25 | } 26 | } 27 | 28 | extension LayoutConfiguration { 29 | 30 | func valueModifier( 31 | _ value: T, 32 | keyPath: WritableKeyPath 33 | ) -> LayoutConfiguration { 34 | ValueModifiedLayoutConfiguration(configuration: self) { $0[keyPath: keyPath] = value } 35 | } 36 | 37 | func valueModifier( 38 | modifier: @escaping (inout ConfigurationBuilder.TransformedType) -> Void 39 | ) -> LayoutConfiguration { 40 | ValueModifiedLayoutConfiguration(configuration: self, valueModifier: modifier) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutDecorationItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifiedLayoutDecorationItem.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | struct ValueModifiedLayoutDecorationItem: LayoutDecorationItem, BuildableDecorationItem { 16 | let decorationItem: LayoutDecorationItem 17 | let valueModifier: (inout NSCollectionLayoutDecorationItem) -> Void 18 | 19 | var layoutDecorationItem: LayoutDecorationItem { self } 20 | 21 | func makeDecorationItem() -> NSCollectionLayoutDecorationItem { 22 | var collectionLayoutDecorationItem = DecorationItemBuilder.make(from: decorationItem) 23 | valueModifier(&collectionLayoutDecorationItem) 24 | return collectionLayoutDecorationItem 25 | } 26 | } 27 | 28 | extension LayoutDecorationItem { 29 | func valueModifier( 30 | _ value: T, 31 | keyPath: WritableKeyPath 32 | ) -> LayoutDecorationItem { 33 | ValueModifiedLayoutDecorationItem(decorationItem: self) { $0[keyPath: keyPath] = value } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifiedLayoutGroup.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | struct ValueModifiedLayoutGroup: LayoutGroup, BuildableGroup { 16 | let group: LayoutGroup 17 | let valueModifier: (inout NSCollectionLayoutGroup) -> Void 18 | 19 | var layoutGroup: LayoutGroup { self } 20 | 21 | func makeGroup() -> NSCollectionLayoutGroup { 22 | var collectionLayoutGroup = GroupBuilder.make(from: group) 23 | valueModifier(&collectionLayoutGroup) 24 | return collectionLayoutGroup 25 | } 26 | } 27 | 28 | extension LayoutGroup { 29 | 30 | func valueModifier(_ value: T, keyPath: WritableKeyPath) -> LayoutGroup { 31 | ValueModifiedLayoutGroup(group: self) { $0[keyPath: keyPath] = value } 32 | } 33 | 34 | func valueModifier( 35 | modifier: @escaping (inout NSCollectionLayoutGroup) -> Void 36 | ) -> LayoutGroup { 37 | ValueModifiedLayoutGroup(group: self, valueModifier: modifier) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifiedLayoutItem.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | struct ValueModifiedLayoutItem: LayoutItem, BuildableItem { 16 | let item: LayoutItem 17 | let valueModifier: (inout NSCollectionLayoutItem) -> Void 18 | 19 | var layoutItem: LayoutItem { self } 20 | 21 | func makeItem() -> NSCollectionLayoutItem { 22 | var collectionLayoutItem = ItemBuilder.make(from: item) 23 | valueModifier(&collectionLayoutItem) 24 | return collectionLayoutItem 25 | } 26 | } 27 | 28 | extension LayoutItem { 29 | func valueModifier(_ value: T, keyPath: WritableKeyPath) -> LayoutItem { 30 | ValueModifiedLayoutItem(item: self) { $0[keyPath: keyPath] = value } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifiedLayoutSection.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | struct ValueModifiedLayoutSection: LayoutSection, BuildableSection { 16 | let section: LayoutSection 17 | let valueModifier: (inout NSCollectionLayoutSection) -> Void 18 | 19 | var layoutSection: LayoutSection { self } 20 | 21 | func makeSection() -> NSCollectionLayoutSection { 22 | var collectionLayoutSection = SectionBuilder.make(from: section) 23 | valueModifier(&collectionLayoutSection) 24 | return collectionLayoutSection 25 | } 26 | } 27 | 28 | extension LayoutSection { 29 | 30 | func valueModifier(_ value: T, keyPath: WritableKeyPath) -> LayoutSection { 31 | ValueModifiedLayoutSection(section: self) { $0[keyPath: keyPath] = value } 32 | } 33 | 34 | func valueModifier( 35 | modifier: @escaping (inout NSCollectionLayoutSection) -> Void 36 | ) -> LayoutSection { 37 | ValueModifiedLayoutSection(section: self, valueModifier: modifier) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutSupplementaryItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifiedLayoutSupplementaryItem.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | struct ValueModifiedLayoutSupplementaryItem: LayoutSupplementaryItem, BuildableSupplementaryItem { 16 | let supplementaryItem: LayoutSupplementaryItem 17 | let valueModifier: (inout NSCollectionLayoutSupplementaryItem) -> Void 18 | 19 | var layoutSupplementaryItem: LayoutSupplementaryItem { self } 20 | 21 | func makeSupplementaryItem() -> NSCollectionLayoutSupplementaryItem { 22 | var collectionLayoutSupplementaryItem = SupplementaryItemBuilder.make(from: supplementaryItem) 23 | valueModifier(&collectionLayoutSupplementaryItem) 24 | return collectionLayoutSupplementaryItem 25 | } 26 | } 27 | 28 | extension LayoutSupplementaryItem { 29 | 30 | func valueModifier( 31 | _ value: T, 32 | keyPath: WritableKeyPath 33 | ) -> LayoutSupplementaryItem { 34 | ValueModifiedLayoutSupplementaryItem(supplementaryItem: self) { $0[keyPath: keyPath] = value } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/BoundarySupplementaryItem/BoundarySupplementaryItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoundarySupplementaryItem.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// An object used to add headers or footers to a collection view. 16 | public struct BoundarySupplementaryItem: LayoutBoundarySupplementaryItem, ResizableItem { 17 | 18 | private var widthDimension: NSCollectionLayoutDimension 19 | private var heightDimension: NSCollectionLayoutDimension 20 | private var elementKind: String 21 | 22 | private var alignment: NSRectAlignment = .top 23 | private var absoluteOffset: CGPoint = .zero 24 | 25 | // MARK: - Life cycle 26 | 27 | /// Creates a boundary supplementary item of the specified size, with a string to identify the 28 | /// element kind. 29 | public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), 30 | height: NSCollectionLayoutDimension = .fractionalHeight(1), 31 | elementKind: String) { 32 | self.widthDimension = width 33 | self.heightDimension = height 34 | self.elementKind = elementKind 35 | } 36 | 37 | /// Creates a boundary supplementary item of the specified size, with a string to identify the 38 | /// element kind. 39 | public init(size: NSCollectionLayoutSize, 40 | elementKind: String) { 41 | self.widthDimension = size.widthDimension 42 | self.heightDimension = size.heightDimension 43 | self.elementKind = elementKind 44 | } 45 | 46 | // MARK: - BoundarySupplementaryItem 47 | 48 | /// The alignment of the boundary supplementary item relative to the section or layout it's attached to. 49 | /// 50 | /// The default value for this property is `NSRectAlignment.top` 51 | public func alignment(_ alignment: NSRectAlignment) -> Self { 52 | with(self) { $0.alignment = alignment } 53 | } 54 | 55 | public func absoluteOffset(_ absoluteOffset: CGPoint) -> Self { 56 | with(self) { $0.absoluteOffset = absoluteOffset } 57 | } 58 | 59 | // MARK: - LayoutBoundarySupplementaryItem 60 | 61 | public var layoutBoundarySupplementaryItem: LayoutBoundarySupplementaryItem { 62 | self 63 | } 64 | 65 | // MARK: - ResizableItem 66 | 67 | /// Configure the width of the boundary supplementary item 68 | /// 69 | /// The default value is `.fractionalWidth(1.0)` 70 | public func width(_ width: NSCollectionLayoutDimension) -> Self { 71 | with(self) { $0.widthDimension = width } 72 | } 73 | 74 | /// Configure the height of the boundary supplementary item 75 | /// 76 | /// The default value is `.fractionalHeight(1.0)` 77 | public func height(_ height: NSCollectionLayoutDimension) -> Self { 78 | with(self) { $0.heightDimension = height } 79 | } 80 | } 81 | 82 | extension BoundarySupplementaryItem: BuildableBoundarySupplementaryItem { 83 | func makeBoundarySupplementaryItem() -> NSCollectionLayoutBoundarySupplementaryItem { 84 | let boundarySupplementaryItem = NSCollectionLayoutBoundarySupplementaryItem( 85 | layoutSize: NSCollectionLayoutSize( 86 | widthDimension: widthDimension, 87 | heightDimension: heightDimension 88 | ), 89 | elementKind: elementKind, 90 | alignment: alignment, 91 | absoluteOffset: absoluteOffset 92 | ) 93 | return boundarySupplementaryItem 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/CompositionalLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalLayoutDSL.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 12/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// An object that completely represent a compositional layout. 16 | /// 17 | /// You can create a fully configured layout like showed in the example below 18 | /// 19 | /// ```swift 20 | /// let compositionalLayout = CompositionalLayout { (_, _) in 21 | /// Section { 22 | /// VGroup { Item() } 23 | /// .width(.fractionalWidth(1/3)) 24 | /// .interItemSpacing(.fixed(8)) 25 | /// } 26 | /// .interGroupSpacing(8) 27 | /// .contentInsets(horizontal: 20) 28 | /// } 29 | /// .interSectionSpacing(16) 30 | /// .boundarySupplementaryItems { 31 | /// BoundarySupplementaryItem(elementKind: "globalHeader") 32 | /// .alignment(.top) 33 | /// } 34 | /// ``` 35 | /// 36 | public struct CompositionalLayout { 37 | 38 | public typealias SectionProvider = (Int, NSCollectionLayoutEnvironment) -> LayoutSection? 39 | 40 | private let sectionBuilder: SectionProvider 41 | private var configuration: LayoutConfiguration 42 | 43 | // MARK: - Life cycle 44 | 45 | public init(configuration: LayoutConfiguration = Configuration(), 46 | sectionsBuilder: @escaping SectionProvider) { 47 | self.sectionBuilder = sectionsBuilder 48 | self.configuration = configuration 49 | } 50 | 51 | // MARK: - CompositionalLayout 52 | 53 | #if os(macOS) 54 | /// Configure the axis that the content in the collection view layout scrolls along. 55 | /// 56 | /// The default value of this property is `UICollectionView.ScrollDirection.vertical`. 57 | public func scrollDirection( 58 | _ scrollDirection: NSCollectionView.ScrollDirection 59 | ) -> Self { 60 | with(self) { $0.configuration = $0.configuration.scrollDirection(scrollDirection) } 61 | } 62 | #else 63 | /// Configure the axis that the content in the collection view layout scrolls along. 64 | /// 65 | /// The default value of this property is `UICollectionView.ScrollDirection.vertical`. 66 | public func scrollDirection( 67 | _ scrollDirection: UICollectionView.ScrollDirection 68 | ) -> Self { 69 | with(self) { $0.configuration = $0.configuration.scrollDirection(scrollDirection) } 70 | } 71 | #endif 72 | 73 | /// Configure the amount of space between the sections in the layout. 74 | /// 75 | /// The default value of this property is `0.0`. 76 | public func interSectionSpacing(_ interSectionSpacing: CGFloat) -> Self { 77 | with(self) { $0.configuration = $0.configuration.interSectionSpacing(interSectionSpacing) } 78 | } 79 | 80 | /// Add an array of the supplementary items that are associated with the boundary edges 81 | /// of the entire layout, such as global headers and footers. 82 | public func boundarySupplementaryItems( 83 | @LayoutBoundarySupplementaryItemBuilder 84 | _ boundarySupplementaryItems: () -> [LayoutBoundarySupplementaryItem] 85 | ) -> Self { 86 | with(self) { 87 | $0.configuration = $0.configuration.boundarySupplementaryItems(boundarySupplementaryItems) 88 | } 89 | } 90 | 91 | #if !os(macOS) 92 | /// Configure the boundary to reference when defining content insets. 93 | /// 94 | /// The default value of this property is ``UIContentInsetsReference.safeArea`` 95 | @available(iOS 14.0, tvOS 14.0, *) 96 | public func contentInsetsReference( 97 | _ contentInsetsReference: UIContentInsetsReference 98 | ) -> Self { 99 | with(self) { 100 | $0.configuration = $0.configuration.contentInsetsReference(contentInsetsReference) 101 | } 102 | } 103 | #endif 104 | } 105 | 106 | public extension CompositionalLayout { 107 | 108 | init(configuration: LayoutConfiguration = Configuration(), 109 | repeatingSections sectionsBuilder: [SectionProvider]) { 110 | self.init(configuration: configuration) { section, environment in 111 | guard !sectionsBuilder.isEmpty else { return nil } 112 | let sectionBuilder = sectionsBuilder[section % sectionsBuilder.count] 113 | return sectionBuilder(section, environment) 114 | } 115 | } 116 | } 117 | 118 | extension CompositionalLayout { 119 | #if os(macOS) 120 | func makeCollectionViewCompositionalLayout() -> NSCollectionViewCompositionalLayout { 121 | return NSCollectionViewCompositionalLayout( 122 | sectionProvider: { section, environment in 123 | return sectionBuilder(section, environment).map(SectionBuilder.make(from:)) 124 | }, 125 | configuration: ConfigurationBuilder.make(from: configuration) 126 | ) 127 | } 128 | #else 129 | func makeCollectionViewCompositionalLayout() -> UICollectionViewCompositionalLayout { 130 | return UICollectionViewCompositionalLayout( 131 | sectionProvider: { section, environment in 132 | return self.sectionBuilder(section, environment).map(SectionBuilder.make(from:)) 133 | }, 134 | configuration: ConfigurationBuilder.make(from: configuration) 135 | ) 136 | } 137 | #endif 138 | } 139 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/CompositionalLayoutDSL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalLayoutDSL.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 06/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | // swiftlint:disable identifier_name 16 | 17 | /// Converts a layout section into a `NSCollectionLayoutSection` 18 | public func LayoutSectionBuilder( 19 | layoutSection: () -> LayoutSection 20 | ) -> NSCollectionLayoutSection { 21 | SectionBuilder.make(from: layoutSection()) 22 | } 23 | 24 | #if os(macOS) 25 | /// Converts a layout configuration and a layout section into an `NSCollectionViewCompositionalLayout` 26 | public func LayoutBuilder( 27 | configuration: LayoutConfiguration = Configuration(), 28 | layoutSection: () -> LayoutSection 29 | ) -> NSCollectionViewCompositionalLayout { 30 | return NSCollectionViewCompositionalLayout( 31 | section: SectionBuilder.make(from: layoutSection()), 32 | configuration: ConfigurationBuilder.make(from: configuration) 33 | ) 34 | } 35 | #else 36 | /// Converts a layout configuration and a layout section into a `UICollectionViewCompositionalLayout` 37 | public func LayoutBuilder( 38 | configuration: LayoutConfiguration = Configuration(), 39 | layoutSection: () -> LayoutSection 40 | ) -> UICollectionViewCompositionalLayout { 41 | return UICollectionViewCompositionalLayout( 42 | section: SectionBuilder.make(from: layoutSection()), 43 | configuration: ConfigurationBuilder.make(from: configuration) 44 | ) 45 | } 46 | #endif 47 | 48 | #if os(macOS) 49 | /// Converts a compositionalLayout into an `NSCollectionViewCompositionalLayout` 50 | public func LayoutBuilder( 51 | compositionalLayout: () -> CompositionalLayout 52 | ) -> NSCollectionViewCompositionalLayout { 53 | compositionalLayout().makeCollectionViewCompositionalLayout() 54 | } 55 | #else 56 | /// Converts a compositionalLayout into a `UICollectionViewCompositionalLayout` 57 | public func LayoutBuilder( 58 | compositionalLayout: () -> CompositionalLayout 59 | ) -> UICollectionViewCompositionalLayout { 60 | compositionalLayout().makeCollectionViewCompositionalLayout() 61 | } 62 | #endif 63 | 64 | #if os(macOS) 65 | extension NSCollectionView { 66 | /// Configure a UICollectionView layout with a CompositionalLayout 67 | public func setCollectionViewLayout( 68 | _ layout: CompositionalLayout 69 | ) { 70 | self.collectionViewLayout = LayoutBuilder { layout } 71 | } 72 | } 73 | #else 74 | extension UICollectionView { 75 | /// Configure a UICollectionView layout with a CompositionalLayout 76 | public func setCollectionViewLayout( 77 | _ layout: CompositionalLayout, 78 | animated: Bool, 79 | completion: ((Bool) -> Void)? = nil 80 | ) { 81 | self.setCollectionViewLayout( 82 | LayoutBuilder { layout }, 83 | animated: animated, 84 | completion: completion 85 | ) 86 | } 87 | } 88 | #endif 89 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/Configuration/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalConfiguration.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 12/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// An object that defines scroll direction, section spacing, and headers or footers for the layout. 16 | public struct Configuration: LayoutConfiguration { 17 | 18 | // MARK: - Life cycle 19 | 20 | public init() {} 21 | 22 | // MARK: - CompositionalLayoutConfiguration 23 | 24 | public var layoutConfiguration: LayoutConfiguration { 25 | return self 26 | } 27 | } 28 | 29 | extension Configuration: BuildableConfiguration { 30 | func makeConfiguration() -> ConfigurationBuilder.TransformedType { 31 | #if os(macOS) 32 | return NSCollectionViewCompositionalLayoutConfiguration() 33 | #else 34 | return UICollectionViewCompositionalLayoutConfiguration() 35 | #endif 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/Configuration/LayoutConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalLayoutConfiguration.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 12/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// A type that represents a compositional layout configuration and provides 16 | /// modifiers to change the configuration. 17 | /// 18 | /// You create custom configuration by declaring types that conform to the 19 | /// ``LayoutConfiguration`` protocol. Implement the required ``layoutConfiguration`` 20 | /// computed property to provide your customized settings. 21 | /// 22 | /// ```swift 23 | /// struct MyConfiguration: LayoutConfiguration { 24 | /// var layoutConfiguration: LayoutConfiguration { 25 | /// Configuration() 26 | /// .scrollDirection(.horizontal) 27 | /// .interSectionSpacing(16) 28 | /// .contentInsetsReference(.readableContent) 29 | /// } 30 | /// } 31 | /// ``` 32 | /// 33 | public protocol LayoutConfiguration { 34 | var layoutConfiguration: LayoutConfiguration { get } 35 | } 36 | 37 | extension LayoutConfiguration { 38 | 39 | // MARK: - Mutable properties 40 | 41 | #if os(macOS) 42 | /// Configure the axis that the content in the collection view layout scrolls along. 43 | /// 44 | /// The default value of this property is `NSCollectionView.ScrollDirection.vertical`. 45 | @warn_unqualified_access 46 | public func scrollDirection( 47 | _ scrollDirection: NSCollectionView.ScrollDirection 48 | ) -> LayoutConfiguration { 49 | valueModifier(scrollDirection, keyPath: \.scrollDirection) 50 | } 51 | #else 52 | /// Configure the axis that the content in the collection view layout scrolls along. 53 | /// 54 | /// The default value of this property is `UICollectionView.ScrollDirection.vertical`. 55 | @warn_unqualified_access 56 | public func scrollDirection( 57 | _ scrollDirection: UICollectionView.ScrollDirection 58 | ) -> LayoutConfiguration { 59 | valueModifier(scrollDirection, keyPath: \.scrollDirection) 60 | } 61 | #endif 62 | 63 | /// Configure the amount of space between the sections in the layout. 64 | /// 65 | /// The default value of this property is `0.0`. 66 | @warn_unqualified_access 67 | public func interSectionSpacing(_ interSectionSpacing: CGFloat) -> LayoutConfiguration { 68 | valueModifier(interSectionSpacing, keyPath: \.interSectionSpacing) 69 | } 70 | 71 | /// Add an array of the supplementary items that are associated with the boundary edges 72 | /// of the entire layout, such as global headers and footers. 73 | @warn_unqualified_access 74 | public func boundarySupplementaryItems( 75 | @LayoutBoundarySupplementaryItemBuilder 76 | _ boundarySupplementaryItems: () -> [LayoutBoundarySupplementaryItem] 77 | ) -> LayoutConfiguration { 78 | let boundarySupplementaryItems = boundarySupplementaryItems() 79 | .map(BoundarySupplementaryItemBuilder.make(from:)) 80 | return valueModifier { 81 | $0.boundarySupplementaryItems.append(contentsOf: boundarySupplementaryItems) 82 | } 83 | } 84 | 85 | #if !os(macOS) 86 | /// Configure the boundary to reference when defining content insets. 87 | /// 88 | /// The default value of this property is ``UIContentInsetsReference.safeArea`` 89 | @available(iOS 14.0, tvOS 14.0, *) 90 | @warn_unqualified_access 91 | public func contentInsetsReference( 92 | _ contentInsetsReference: UIContentInsetsReference 93 | ) -> LayoutConfiguration { 94 | valueModifier(contentInsetsReference, keyPath: \.contentInsetsReference) 95 | } 96 | #endif 97 | } 98 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/DecorationItem/DecorationItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DecorationItem.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// An object used to add a background to a section of a collection view. 16 | public struct DecorationItem: LayoutDecorationItem { 17 | 18 | private var elementKind: String 19 | 20 | // MARK: - Life cycle 21 | 22 | public init(elementKind: String) { 23 | self.elementKind = elementKind 24 | } 25 | 26 | // MARK: - LayoutDecorationItem 27 | 28 | public var layoutDecorationItem: LayoutDecorationItem { 29 | return self 30 | } 31 | } 32 | 33 | extension DecorationItem: BuildableDecorationItem { 34 | func makeDecorationItem() -> NSCollectionLayoutDecorationItem { 35 | let decorationItem = NSCollectionLayoutDecorationItem.background(elementKind: elementKind) 36 | return decorationItem 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/DecorationItem/LayoutDecorationItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutDecorationItem.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// A type that represents a decoration item in a compositional layout and provides 16 | /// modifiers to configure decoration items. 17 | /// 18 | /// You create custom decoration items by declaring types that conform to the 19 | /// ``LayoutDecorationItem`` protocol. Implement the required ``layoutDecorationItem`` 20 | /// computed property to provide the content and configuration for your custom decoration item. 21 | /// 22 | /// ```swift 23 | /// struct MyDecorationItem: LayoutDecorationItem { 24 | /// var layoutDecorationItem: LayoutDecorationItem { 25 | /// DecorationItem(elementKind: "backgroundKind") 26 | /// .contentInsets(value: 4) 27 | /// } 28 | /// } 29 | /// ``` 30 | /// 31 | public protocol LayoutDecorationItem: LayoutItem { 32 | var layoutDecorationItem: LayoutDecorationItem { get } 33 | } 34 | 35 | public extension LayoutDecorationItem { 36 | 37 | // MARK: - LayoutItem 38 | 39 | var layoutItem: LayoutItem { self } 40 | } 41 | 42 | extension LayoutDecorationItem { 43 | 44 | // MARK: - Decoration Item mutable properties 45 | 46 | /// Configure the vertical stacking order of the decoration item in relation to other items in the section. 47 | /// 48 | /// The default value of this property is 0, which means the decoration item appears below all 49 | /// other items in the section. 50 | @warn_unqualified_access 51 | public func zIndex(zIndex: Int) -> LayoutDecorationItem { 52 | valueModifier(zIndex, keyPath: \.zIndex) 53 | } 54 | } 55 | 56 | extension LayoutDecorationItem { 57 | 58 | // MARK: - Content Insets 59 | 60 | /// Configure the amount of space added around the content of the item to adjust its final 61 | /// size after its position is computed. 62 | @warn_unqualified_access 63 | public func contentInsets(value: CGFloat) -> LayoutDecorationItem { 64 | return self.contentInsets(top: value, leading: value, bottom: value, trailing: value) 65 | } 66 | 67 | /// Configure the amount of space added around the content of the item to adjust its final 68 | /// size after its position is computed. 69 | @warn_unqualified_access 70 | public func contentInsets(horizontal: CGFloat = 0, vertical: CGFloat = 0) -> LayoutDecorationItem { 71 | return self.contentInsets(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) 72 | } 73 | 74 | /// Configure the amount of space added around the content of the item to adjust its final 75 | /// size after its position is computed. 76 | @warn_unqualified_access 77 | public func contentInsets( 78 | top: CGFloat = 0, 79 | leading: CGFloat = 0, 80 | bottom: CGFloat = 0, 81 | trailing: CGFloat = 0 82 | ) -> LayoutDecorationItem { 83 | return self.contentInsets( 84 | NSDirectionalEdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing) 85 | ) 86 | } 87 | 88 | /// Configure the amount of space added around the content of the item to adjust its final 89 | /// size after its position is computed. 90 | @warn_unqualified_access 91 | public func contentInsets(_ insets: NSDirectionalEdgeInsets) -> LayoutDecorationItem { 92 | valueModifier(insets, keyPath: \.contentInsets) 93 | } 94 | } 95 | 96 | extension LayoutDecorationItem { 97 | 98 | // MARK: - Edge Spacing 99 | 100 | /// Configure the amount of space added around the boundaries of the item between other items 101 | /// and this item's container. 102 | @warn_unqualified_access 103 | public func edgeSpacing(value: NSCollectionLayoutSpacing?) -> LayoutDecorationItem { 104 | return self.edgeSpacing(top: value, leading: value, bottom: value, trailing: value) 105 | } 106 | 107 | /// Configure the amount of space added around the boundaries of the item between other items 108 | /// and this item's container. 109 | @warn_unqualified_access 110 | public func edgeSpacing( 111 | horizontal: NSCollectionLayoutSpacing? = nil, 112 | vertical: NSCollectionLayoutSpacing? = nil 113 | ) -> LayoutDecorationItem { 114 | return self.edgeSpacing(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) 115 | } 116 | 117 | /// Configure the amount of space added around the boundaries of the item between other items 118 | /// and this item's container. 119 | @warn_unqualified_access 120 | public func edgeSpacing( 121 | top: NSCollectionLayoutSpacing? = nil, 122 | leading: NSCollectionLayoutSpacing? = nil, 123 | bottom: NSCollectionLayoutSpacing? = nil, 124 | trailing: NSCollectionLayoutSpacing? = nil 125 | ) -> LayoutDecorationItem { 126 | return self.edgeSpacing( 127 | NSCollectionLayoutEdgeSpacing(leading: leading, top: top, trailing: trailing, bottom: bottom) 128 | ) 129 | } 130 | 131 | /// Configure the amount of space added around the boundaries of the item between other items 132 | /// and this item's container. 133 | @warn_unqualified_access 134 | public func edgeSpacing(_ edgeSpacing: NSCollectionLayoutEdgeSpacing) -> LayoutDecorationItem { 135 | valueModifier(edgeSpacing, keyPath: \.edgeSpacing) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/Group/CustomGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomGroup.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// A customizable container for a set of items. 16 | public struct CustomGroup: LayoutGroup, ResizableItem { 17 | 18 | private var widthDimension: NSCollectionLayoutDimension 19 | private var heightDimension: NSCollectionLayoutDimension 20 | private let itemProvider: NSCollectionLayoutGroupCustomItemProvider 21 | 22 | // MARK: - Life cycle 23 | 24 | /// Creates a group of the specified size, with an item provider that creates a custom 25 | /// arrangement for those items. 26 | public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), 27 | height: NSCollectionLayoutDimension = .fractionalHeight(1), 28 | itemProvider: @escaping NSCollectionLayoutGroupCustomItemProvider) { 29 | self.widthDimension = width 30 | self.heightDimension = height 31 | self.itemProvider = itemProvider 32 | } 33 | 34 | /// Creates a group of the specified size, with an item provider that creates a custom 35 | /// arrangement for those items. 36 | public init(size: NSCollectionLayoutSize, 37 | itemProvider: @escaping NSCollectionLayoutGroupCustomItemProvider) { 38 | self.widthDimension = size.widthDimension 39 | self.heightDimension = size.heightDimension 40 | self.itemProvider = itemProvider 41 | } 42 | 43 | /// Creates a group with an item provider that creates a custom arrangement for those items. 44 | public init(itemProvider: @escaping NSCollectionLayoutGroupCustomItemProvider) { 45 | self.init(width: .fractionalWidth(1), height: .fractionalHeight(1), itemProvider: itemProvider) 46 | } 47 | 48 | // MARK: - LayoutGroup 49 | 50 | public var layoutGroup: LayoutGroup { 51 | return self 52 | } 53 | 54 | // MARK: - ResizableItem 55 | 56 | /// Configure the width of the group 57 | /// 58 | /// The default value is `.fractionalWidth(1.0)` 59 | public func width(_ width: NSCollectionLayoutDimension) -> Self { 60 | with(self) { $0.widthDimension = width } 61 | } 62 | 63 | /// Configure the height of the group 64 | /// 65 | /// The default value is `.fractionalHeight(1.0)` 66 | public func height(_ height: NSCollectionLayoutDimension) -> Self { 67 | with(self) { $0.heightDimension = height } 68 | } 69 | } 70 | 71 | extension CustomGroup: BuildableGroup { 72 | func makeGroup() -> NSCollectionLayoutGroup { 73 | let size = NSCollectionLayoutSize( 74 | widthDimension: widthDimension, 75 | heightDimension: heightDimension 76 | ) 77 | let group = NSCollectionLayoutGroup.custom(layoutSize: size, itemProvider: itemProvider) 78 | return group 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/Group/HGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HGroup.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// A container for a set of items that lays out the items horizontally. 16 | public struct HGroup: LayoutGroup, ResizableItem { 17 | 18 | enum SubItems { 19 | case list([LayoutItem]) 20 | case repeated(LayoutItem, count: Int) 21 | } 22 | 23 | private var widthDimension: NSCollectionLayoutDimension 24 | private var heightDimension: NSCollectionLayoutDimension 25 | private var subItems: SubItems 26 | 27 | // MARK: - Life cycle 28 | 29 | /// Creates a group of the specified size, containing an array of items arranged in a horizontal line. 30 | public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), 31 | height: NSCollectionLayoutDimension = .fractionalHeight(1), 32 | @LayoutItemBuilder subItems: () -> [LayoutItem]) { 33 | self.widthDimension = width 34 | self.heightDimension = height 35 | self.subItems = .list(subItems()) 36 | } 37 | 38 | /// Creates a group of the specified size, containing an array of items arranged in a horizontal line. 39 | public init(size: NSCollectionLayoutSize, 40 | @LayoutItemBuilder subItems: () -> [LayoutItem]) { 41 | self.widthDimension = size.widthDimension 42 | self.heightDimension = size.heightDimension 43 | self.subItems = .list(subItems()) 44 | } 45 | 46 | /// Creates a group of the specified size, containing an array of equally sized items arranged 47 | /// in a horizontal line up to the number specified by count. 48 | public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), 49 | height: NSCollectionLayoutDimension = .fractionalHeight(1), 50 | count: Int, 51 | subItem: () -> LayoutItem) { 52 | self.widthDimension = width 53 | self.heightDimension = height 54 | self.subItems = .repeated(subItem(), count: count) 55 | } 56 | 57 | /// Creates a group of the specified size, containing an array of equally sized items arranged 58 | /// in a horizontal line up to the number specified by count. 59 | public init(size: NSCollectionLayoutSize, 60 | count: Int, 61 | subItem: () -> LayoutItem) { 62 | self.widthDimension = size.widthDimension 63 | self.heightDimension = size.heightDimension 64 | self.subItems = .repeated(subItem(), count: count) 65 | } 66 | 67 | /// Creates a group containing an array of items arranged in a horizontal line. 68 | public init(@LayoutItemBuilder subItems: () -> [LayoutItem]) { 69 | self.init(width: .fractionalWidth(1), height: .fractionalHeight(1), subItems: subItems) 70 | } 71 | 72 | // MARK: - LayoutGroup 73 | 74 | public var layoutGroup: LayoutGroup { 75 | return self 76 | } 77 | 78 | // MARK: - ResizableItem 79 | 80 | /// Configure the width of the group 81 | /// 82 | /// The default value is `.fractionalWidth(1.0)` 83 | public func width(_ width: NSCollectionLayoutDimension) -> Self { 84 | with(self) { $0.widthDimension = width } 85 | } 86 | 87 | /// Configure the height of the group 88 | /// 89 | /// The default value is `.fractionalHeight(1.0)` 90 | public func height(_ height: NSCollectionLayoutDimension) -> Self { 91 | with(self) { $0.heightDimension = height } 92 | } 93 | } 94 | 95 | extension HGroup: BuildableGroup { 96 | func makeGroup() -> NSCollectionLayoutGroup { 97 | let size = NSCollectionLayoutSize( 98 | widthDimension: widthDimension, 99 | heightDimension: heightDimension 100 | ) 101 | let group: NSCollectionLayoutGroup 102 | switch subItems { 103 | case let .list(items): 104 | group = NSCollectionLayoutGroup.horizontal( 105 | layoutSize: size, 106 | subitems: items.map(ItemBuilder.make(from:)) 107 | ) 108 | case let .repeated(item, count): 109 | group = NSCollectionLayoutGroup.horizontal( 110 | layoutSize: size, 111 | subitem: ItemBuilder.make(from: item), 112 | count: count 113 | ) 114 | } 115 | return group 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/Group/LayoutGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutGroup.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// A type that represents a group in a compositional layout and provides 16 | /// modifiers to configure groups. 17 | /// 18 | /// You create custom groups by declaring types that conform to the 19 | /// ``LayoutGroup`` protocol. Implement the required ``layoutGroup`` 20 | /// computed property to provide the content and configuration for your custom group. 21 | /// 22 | /// ```swift 23 | /// struct MyGroup: LayoutGroup { 24 | /// var layoutGroup: LayoutGroup { 25 | /// HGroup(count: 4) { 26 | /// Item() 27 | /// } 28 | /// .height(.absolute(300)) 29 | /// .interItemSpacing(.fixed(8)) 30 | /// } 31 | /// } 32 | /// ``` 33 | /// 34 | public protocol LayoutGroup: LayoutItem { 35 | var layoutGroup: LayoutGroup { get } 36 | } 37 | 38 | public extension LayoutGroup { 39 | 40 | // MARK: - LayoutItem 41 | 42 | var layoutItem: LayoutItem { layoutGroup } 43 | } 44 | 45 | extension LayoutGroup { 46 | 47 | // MARK: - Group Mutable properties 48 | 49 | /// Add an array of the supplementary items that are anchored to the group. 50 | @warn_unqualified_access 51 | public func supplementaryItems( 52 | @LayoutSupplementaryItemBuilder _ supplementaryItems: () -> [LayoutSupplementaryItem] 53 | ) -> LayoutGroup { 54 | let supplementaryItems = supplementaryItems().map(SupplementaryItemBuilder.make(from:)) 55 | return valueModifier { $0.supplementaryItems.append(contentsOf: supplementaryItems) } 56 | } 57 | 58 | /// Configure the amount of space between the items along the layout axis of the group. 59 | @warn_unqualified_access 60 | public func interItemSpacing(_ interItemSpacing: NSCollectionLayoutSpacing?) -> LayoutGroup { 61 | valueModifier(interItemSpacing, keyPath: \.interItemSpacing) 62 | } 63 | } 64 | 65 | extension LayoutGroup { 66 | 67 | // MARK: - Content Insets 68 | 69 | /// Configure the amount of space added around the content of the item to adjust its final 70 | /// size after its position is computed. 71 | @warn_unqualified_access 72 | public func contentInsets(value: CGFloat) -> LayoutGroup { 73 | return self.contentInsets(top: value, leading: value, bottom: value, trailing: value) 74 | } 75 | 76 | /// Configure the amount of space added around the content of the item to adjust its final 77 | /// size after its position is computed. 78 | @warn_unqualified_access 79 | public func contentInsets(horizontal: CGFloat = 0, vertical: CGFloat = 0) -> LayoutGroup { 80 | return self.contentInsets(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) 81 | } 82 | 83 | /// Configure the amount of space added around the content of the item to adjust its final 84 | /// size after its position is computed. 85 | @warn_unqualified_access 86 | public func contentInsets( 87 | top: CGFloat = 0, 88 | leading: CGFloat = 0, 89 | bottom: CGFloat = 0, 90 | trailing: CGFloat = 0 91 | ) -> LayoutGroup { 92 | return self.contentInsets( 93 | NSDirectionalEdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing) 94 | ) 95 | } 96 | 97 | /// Configure the amount of space added around the content of the item to adjust its final 98 | /// size after its position is computed. 99 | @warn_unqualified_access 100 | public func contentInsets(_ insets: NSDirectionalEdgeInsets) -> LayoutGroup { 101 | valueModifier(insets, keyPath: \.contentInsets) 102 | } 103 | } 104 | 105 | extension LayoutGroup { 106 | 107 | // MARK: - Edge Spacing 108 | 109 | /// Configure the amount of space added around the boundaries of the item between other items 110 | /// and this item's container. 111 | @warn_unqualified_access 112 | public func edgeSpacing(value: NSCollectionLayoutSpacing?) -> LayoutGroup { 113 | return self.edgeSpacing(top: value, leading: value, bottom: value, trailing: value) 114 | } 115 | 116 | /// Configure the amount of space added around the boundaries of the item between other items 117 | /// and this item's container. 118 | @warn_unqualified_access 119 | public func edgeSpacing( 120 | horizontal: NSCollectionLayoutSpacing? = nil, 121 | vertical: NSCollectionLayoutSpacing? = nil 122 | ) -> LayoutGroup { 123 | return self.edgeSpacing(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) 124 | } 125 | 126 | /// Configure the amount of space added around the boundaries of the item between other items 127 | /// and this item's container. 128 | @warn_unqualified_access 129 | public func edgeSpacing( 130 | top: NSCollectionLayoutSpacing? = nil, 131 | leading: NSCollectionLayoutSpacing? = nil, 132 | bottom: NSCollectionLayoutSpacing? = nil, 133 | trailing: NSCollectionLayoutSpacing? = nil 134 | ) -> LayoutGroup { 135 | return self.edgeSpacing( 136 | NSCollectionLayoutEdgeSpacing(leading: leading, top: top, trailing: trailing, bottom: bottom) 137 | ) 138 | } 139 | 140 | /// Configure the amount of space added around the boundaries of the item between other items 141 | /// and this item's container. 142 | @warn_unqualified_access 143 | public func edgeSpacing(_ edgeSpacing: NSCollectionLayoutEdgeSpacing) -> LayoutGroup { 144 | valueModifier(edgeSpacing, keyPath: \.edgeSpacing) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/Group/VGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VGroup.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// A container for a set of items that lays out the items vertically. 16 | public struct VGroup: LayoutGroup, ResizableItem { 17 | 18 | private enum SubItems { 19 | case list([LayoutItem]) 20 | case repeated(LayoutItem, count: Int) 21 | } 22 | 23 | private var widthDimension: NSCollectionLayoutDimension 24 | private var heightDimension: NSCollectionLayoutDimension 25 | private var subItems: SubItems 26 | 27 | // MARK: - Life cycle 28 | 29 | /// Creates a group of the specified size, containing an array of items arranged in a vertical line. 30 | public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), 31 | height: NSCollectionLayoutDimension = .fractionalHeight(1), 32 | @LayoutItemBuilder subItems: () -> [LayoutItem]) { 33 | self.widthDimension = width 34 | self.heightDimension = height 35 | self.subItems = .list(subItems()) 36 | } 37 | 38 | /// Creates a group of the specified size, containing an array of items arranged in a vertical line. 39 | public init(size: NSCollectionLayoutSize, 40 | @LayoutItemBuilder subItems: () -> [LayoutItem]) { 41 | self.widthDimension = size.widthDimension 42 | self.heightDimension = size.heightDimension 43 | self.subItems = .list(subItems()) 44 | } 45 | 46 | /// Creates a group of the specified size, containing an array of equally sized items arranged 47 | /// in a horizontal line up to the number specified by count. 48 | public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), 49 | height: NSCollectionLayoutDimension = .fractionalHeight(1), 50 | count: Int, 51 | subItem: () -> LayoutItem) { 52 | self.widthDimension = width 53 | self.heightDimension = height 54 | self.subItems = .repeated(subItem(), count: count) 55 | } 56 | 57 | /// Creates a group of the specified size, containing an array of equally sized items arranged 58 | /// in a horizontal line up to the number specified by count. 59 | public init(size: NSCollectionLayoutSize, 60 | count: Int, 61 | subItem: () -> LayoutItem) { 62 | self.widthDimension = size.widthDimension 63 | self.heightDimension = size.heightDimension 64 | self.subItems = .repeated(subItem(), count: count) 65 | } 66 | 67 | /// Creates a group containing an array of items arranged in a vertical line. 68 | public init(@LayoutItemBuilder subItems: () -> [LayoutItem]) { 69 | self.init(width: .fractionalWidth(1), height: .fractionalHeight(1), subItems: subItems) 70 | } 71 | 72 | // MARK: - LayoutGroup 73 | 74 | public var layoutGroup: LayoutGroup { 75 | return self 76 | } 77 | 78 | // MARK: - ResizableItem 79 | 80 | /// Configure the width of the group 81 | /// 82 | /// The default value is `.fractionalWidth(1.0)` 83 | public func width(_ width: NSCollectionLayoutDimension) -> Self { 84 | with(self) { $0.widthDimension = width } 85 | } 86 | 87 | /// Configure the height of the group 88 | /// 89 | /// The default value is `.fractionalHeight(1.0)` 90 | public func height(_ height: NSCollectionLayoutDimension) -> Self { 91 | with(self) { $0.heightDimension = height } 92 | } 93 | } 94 | 95 | extension VGroup: BuildableGroup { 96 | func makeGroup() -> NSCollectionLayoutGroup { 97 | let size = NSCollectionLayoutSize( 98 | widthDimension: widthDimension, 99 | heightDimension: heightDimension 100 | ) 101 | let group: NSCollectionLayoutGroup 102 | switch subItems { 103 | case let .list(items): 104 | group = NSCollectionLayoutGroup.vertical( 105 | layoutSize: size, 106 | subitems: items.map(ItemBuilder.make(from:)) 107 | ) 108 | case let .repeated(item, count): 109 | group = NSCollectionLayoutGroup.vertical( 110 | layoutSize: size, 111 | subitem: ItemBuilder.make(from: item), 112 | count: count 113 | ) 114 | } 115 | return group 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/Item/Item.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Item.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// The most basic component of a collection view's layout. 16 | public struct Item: LayoutItem, ResizableItem { 17 | 18 | private var widthDimension: NSCollectionLayoutDimension 19 | private var heightDimension: NSCollectionLayoutDimension 20 | private var supplementaryItems: [LayoutSupplementaryItem] 21 | 22 | // MARK: - Life cycle 23 | 24 | /// Creates an item of the specified size with an array of supplementary items to attach to the item. 25 | public init(width: NSCollectionLayoutDimension, 26 | height: NSCollectionLayoutDimension, 27 | @LayoutSupplementaryItemBuilder supplementaryItems: () -> [LayoutSupplementaryItem]) { 28 | self.widthDimension = width 29 | self.heightDimension = height 30 | self.supplementaryItems = supplementaryItems() 31 | } 32 | 33 | /// Creates an item of the specified size 34 | public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), 35 | height: NSCollectionLayoutDimension = .fractionalHeight(1)) { 36 | self.widthDimension = width 37 | self.heightDimension = height 38 | self.supplementaryItems = [] 39 | } 40 | 41 | /// Creates an item of the specified size 42 | public init(size: NSCollectionLayoutSize) { 43 | self.widthDimension = size.widthDimension 44 | self.heightDimension = size.heightDimension 45 | self.supplementaryItems = [] 46 | } 47 | 48 | /// Creates an item with an array of supplementary items to attach to the item. 49 | public init(@LayoutSupplementaryItemBuilder supplementaryItems: () -> [LayoutSupplementaryItem]) { 50 | self.init(width: .fractionalWidth(1), height: .fractionalHeight(1), supplementaryItems: supplementaryItems) 51 | } 52 | 53 | public init() { 54 | self.init(width: .fractionalWidth(1), height: .fractionalHeight(1), supplementaryItems: { }) 55 | } 56 | 57 | // MARK: - LayoutItem 58 | 59 | public var layoutItem: LayoutItem { 60 | return self 61 | } 62 | 63 | // MARK: - ResizableItem 64 | 65 | /// Configure the width of the item 66 | /// 67 | /// The default value is `.fractionalWidth(1.0)` 68 | public func width(_ width: NSCollectionLayoutDimension) -> Self { 69 | with(self) { $0.widthDimension = width } 70 | } 71 | 72 | /// Configure the height of the item 73 | /// 74 | /// The default value is `.fractionalHeight(1.0)` 75 | public func height(_ height: NSCollectionLayoutDimension) -> Self { 76 | with(self) { $0.heightDimension = height } 77 | } 78 | } 79 | 80 | extension Item: BuildableItem { 81 | func makeItem() -> NSCollectionLayoutItem { 82 | let item = NSCollectionLayoutItem( 83 | layoutSize: NSCollectionLayoutSize(widthDimension: widthDimension, heightDimension: heightDimension), 84 | supplementaryItems: supplementaryItems.map(SupplementaryItemBuilder.make(from:)) 85 | ) 86 | return item 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/Item/LayoutItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutItem.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// A type that represents an item in a compositional layout and provides 16 | /// modifiers to configure items. 17 | /// 18 | /// You create custom items by declaring types that conform to the ``LayoutItem`` 19 | /// protocol. Implement the required ``layoutItem`` computed property to 20 | /// provide the content and configuration for your custom item. 21 | /// 22 | /// ```swift 23 | /// struct MyItem: LayoutItem { 24 | /// var layoutItem: LayoutItem { 25 | /// Item { 26 | /// SupplementaryItem(elementKind: "badge") 27 | /// .containerAnchor( 28 | /// edges: [.top, .trailing], 29 | /// offset: .fractional(x: 0.5, y: -0.5) 30 | /// ) 31 | /// .height(.absolute(20)) 32 | /// .width(.absolute(20)) 33 | /// } 34 | /// } 35 | /// } 36 | /// ``` 37 | /// 38 | public protocol LayoutItem { 39 | var layoutItem: LayoutItem { get } 40 | } 41 | 42 | extension LayoutItem { 43 | 44 | // MARK: - Content Insets 45 | 46 | /// Configure the amount of space between the content of the section and its boundaries. 47 | @warn_unqualified_access 48 | public func contentInsets(value: CGFloat) -> LayoutItem { 49 | return self.contentInsets(top: value, leading: value, bottom: value, trailing: value) 50 | } 51 | 52 | /// Configure the amount of space between the content of the section and its boundaries. 53 | @warn_unqualified_access 54 | public func contentInsets(horizontal: CGFloat = 0, vertical: CGFloat = 0) -> LayoutItem { 55 | return self.contentInsets(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) 56 | } 57 | 58 | /// Configure the amount of space between the content of the section and its boundaries. 59 | @warn_unqualified_access 60 | public func contentInsets( 61 | top: CGFloat = 0, 62 | leading: CGFloat = 0, 63 | bottom: CGFloat = 0, 64 | trailing: CGFloat = 0 65 | ) -> LayoutItem { 66 | return self.contentInsets( 67 | NSDirectionalEdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing) 68 | ) 69 | } 70 | 71 | /// Configure the amount of space between the content of the section and its boundaries. 72 | @warn_unqualified_access 73 | public func contentInsets(_ insets: NSDirectionalEdgeInsets) -> LayoutItem { 74 | valueModifier(insets, keyPath: \.contentInsets) 75 | } 76 | } 77 | 78 | extension LayoutItem { 79 | 80 | // MARK: - Edge Spacing 81 | 82 | /// Configure the amount of space added around the boundaries of the item between other items 83 | /// and this item's container. 84 | @warn_unqualified_access 85 | public func edgeSpacing(value: NSCollectionLayoutSpacing?) -> LayoutItem { 86 | return self.edgeSpacing(top: value, leading: value, bottom: value, trailing: value) 87 | } 88 | 89 | /// Configure the amount of space added around the boundaries of the item between other items 90 | /// and this item's container. 91 | @warn_unqualified_access 92 | public func edgeSpacing( 93 | horizontal: NSCollectionLayoutSpacing? = nil, 94 | vertical: NSCollectionLayoutSpacing? = nil 95 | ) -> LayoutItem { 96 | return self.edgeSpacing(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) 97 | } 98 | 99 | /// Configure the amount of space added around the boundaries of the item between other items 100 | /// and this item's container. 101 | @warn_unqualified_access 102 | public func edgeSpacing( 103 | top: NSCollectionLayoutSpacing? = nil, 104 | leading: NSCollectionLayoutSpacing? = nil, 105 | bottom: NSCollectionLayoutSpacing? = nil, 106 | trailing: NSCollectionLayoutSpacing? = nil 107 | ) -> LayoutItem { 108 | return self.edgeSpacing( 109 | NSCollectionLayoutEdgeSpacing(leading: leading, top: top, trailing: trailing, bottom: bottom) 110 | ) 111 | } 112 | 113 | /// Configure the amount of space added around the boundaries of the item between other items 114 | /// and this item's container. 115 | @warn_unqualified_access 116 | public func edgeSpacing(_ edgeSpacing: NSCollectionLayoutEdgeSpacing) -> LayoutItem { 117 | valueModifier(edgeSpacing, keyPath: \.edgeSpacing) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/ResizableItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResizableItem.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | public protocol ResizableItem { 16 | func size(_ size: NSCollectionLayoutSize) -> Self 17 | func width(_ width: NSCollectionLayoutDimension) -> Self 18 | func height(_ height: NSCollectionLayoutDimension) -> Self 19 | } 20 | 21 | public extension ResizableItem { 22 | func size(_ size: NSCollectionLayoutSize) -> Self { 23 | self.width(size.widthDimension).height(size.heightDimension) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/ResultBuilders.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResultBuilders.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | public typealias LayoutItemBuilder = ListResultBuilder 10 | public typealias LayoutBoundarySupplementaryItemBuilder = ListResultBuilder 11 | public typealias LayoutSupplementaryItemBuilder = ListResultBuilder 12 | public typealias LayoutDecorationItemBuilder = ListResultBuilder 13 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/Section/ListSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListSection.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 20/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if !os(macOS) 10 | import UIKit 11 | 12 | @available(iOS 14.0, tvOS 14, *) 13 | public struct ListSection: LayoutSection { 14 | 15 | private var configuration: UICollectionLayoutListConfiguration 16 | private let layoutEnvironment: NSCollectionLayoutEnvironment 17 | 18 | // MARK: - Life cycle 19 | 20 | /// Creates a list section with the specified list configuration and layout environment. 21 | public init( 22 | configuration: UICollectionLayoutListConfiguration, 23 | layoutEnvironment: NSCollectionLayoutEnvironment 24 | ) { 25 | self.configuration = configuration 26 | self.layoutEnvironment = layoutEnvironment 27 | } 28 | 29 | /// Creates a list section with the specified list appearance and layout environment. 30 | public init( 31 | appearance: UICollectionLayoutListConfiguration.Appearance, 32 | layoutEnvironment: NSCollectionLayoutEnvironment 33 | ) { 34 | self.configuration = UICollectionLayoutListConfiguration(appearance: appearance) 35 | self.layoutEnvironment = layoutEnvironment 36 | } 37 | 38 | // MARK: - ListSection 39 | 40 | /// A Boolean value that determines whether the list shows separators between cells. 41 | @available(tvOS, unavailable) 42 | public func showsSeparators(_ showsSeparators: Bool) -> Self { 43 | with(self) { $0.configuration.showsSeparators = showsSeparators } 44 | } 45 | 46 | /// The background color of the list. 47 | /// 48 | /// The default value is nil, which means that the configuration uses the system background 49 | /// color for the specified appearance. 50 | public func backgroundColor(_ backgroundColor: UIColor?) -> Self { 51 | with(self) { $0.configuration.backgroundColor = backgroundColor } 52 | } 53 | 54 | @available(tvOS, unavailable) 55 | public func trailingSwipeActionsConfigurationProvider( 56 | // swiftlint:disable:next line_length 57 | _ trailingSwipeActionsConfigurationProvider: UICollectionLayoutListConfiguration.SwipeActionsConfigurationProvider? 58 | ) -> Self { 59 | // swiftlint:disable:next line_length 60 | with(self) { $0.configuration.trailingSwipeActionsConfigurationProvider = trailingSwipeActionsConfigurationProvider } 61 | } 62 | 63 | /// The type of header to use for the list. 64 | /// 65 | /// The default value is `UICollectionLayoutListConfiguration.HeaderMode.none`. 66 | public func headerMode(_ headerMode: UICollectionLayoutListConfiguration.HeaderMode) -> Self { 67 | with(self) { $0.configuration.headerMode = headerMode } 68 | } 69 | 70 | /// The type of footer to use for the list. 71 | /// 72 | /// The default value is `UICollectionLayoutListConfiguration.FooterMode.none`. 73 | public func footerMode(_ footerMode: UICollectionLayoutListConfiguration.FooterMode) -> Self { 74 | with(self) { $0.configuration.footerMode = footerMode } 75 | } 76 | 77 | // MARK: - LayoutSection 78 | 79 | public var layoutSection: LayoutSection { 80 | return self 81 | } 82 | } 83 | 84 | @available(iOS 14.0, tvOS 14, *) 85 | extension ListSection: BuildableSection { 86 | func makeSection() -> NSCollectionLayoutSection { 87 | return .list(using: configuration, layoutEnvironment: layoutEnvironment) 88 | } 89 | } 90 | #endif 91 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/Section/RawSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawSection.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 27/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// A container to allow usage of `NSCollectionLayoutSection` with this library 16 | public struct RawSection: LayoutSection { 17 | 18 | private let rawLayoutSection: NSCollectionLayoutSection 19 | 20 | // MARK: - Life cycle 21 | 22 | public init(rawLayoutSection: NSCollectionLayoutSection) { 23 | self.rawLayoutSection = rawLayoutSection 24 | } 25 | 26 | // MARK: - LayoutSection 27 | 28 | public var layoutSection: LayoutSection { 29 | return self 30 | } 31 | } 32 | 33 | extension RawSection: BuildableSection { 34 | 35 | func makeSection() -> NSCollectionLayoutSection { 36 | return rawLayoutSection 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/Section/Section.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Section.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// A container that combines a set of groups into distinct visual groupings. 16 | public struct Section: LayoutSection { 17 | 18 | private let group: LayoutGroup 19 | 20 | // MARK: - Life cycle 21 | 22 | public init(group: () -> LayoutGroup) { 23 | self.group = group() 24 | } 25 | 26 | // MARK: - LayoutSection 27 | 28 | public var layoutSection: LayoutSection { 29 | return self 30 | } 31 | } 32 | 33 | extension Section: BuildableSection { 34 | 35 | func makeSection() -> NSCollectionLayoutSection { 36 | return NSCollectionLayoutSection(group: GroupBuilder.make(from: group)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/SupplementaryItem/LayoutSupplementaryItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutSupplementaryItem.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 07/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// A type that represents a supplementary item in a compositional layout and provides 16 | /// modifiers to configure supplementary items. 17 | /// 18 | /// You create custom supplementary items by declaring types that conform to the 19 | /// ``LayoutSupplementaryItem`` protocol. Implement the required ``layoutSupplementaryItem`` 20 | /// computed property to provide the content and configuration for your custom supplementary item. 21 | /// 22 | /// ```swift 23 | /// struct MySupplementaryItem: LayoutSupplementaryItem { 24 | /// var layoutSupplementaryItem: LayoutSupplementaryItem { 25 | /// SupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) 26 | /// .height(.absolute(40)) 27 | /// .containerAnchor(edges: .top) 28 | /// .zIndex(zIndex: 10) 29 | /// } 30 | /// } 31 | /// ``` 32 | /// 33 | public protocol LayoutSupplementaryItem: LayoutItem { 34 | var layoutSupplementaryItem: LayoutSupplementaryItem { get } 35 | } 36 | 37 | public extension LayoutSupplementaryItem { 38 | // MARK: - LayoutItem 39 | 40 | var layoutItem: LayoutItem { self } 41 | } 42 | 43 | extension LayoutSupplementaryItem { 44 | 45 | // MARK: - Supplementary Item mutable properties 46 | 47 | /// Configure the vertical stacking order of the decoration item in relation to other items in the section. 48 | /// 49 | /// The default value of this property is 0, which means the decoration item appears below all 50 | /// other items in the section. 51 | @warn_unqualified_access 52 | public func zIndex(zIndex: Int) -> LayoutSupplementaryItem { 53 | valueModifier(zIndex, keyPath: \.zIndex) 54 | } 55 | } 56 | 57 | extension LayoutSupplementaryItem { 58 | 59 | // MARK: - Content Insets 60 | 61 | /// Configure the amount of space added around the content of the item to adjust its final 62 | /// size after its position is computed. 63 | @warn_unqualified_access 64 | public func contentInsets(value: CGFloat) -> LayoutSupplementaryItem { 65 | return self.contentInsets(top: value, leading: value, bottom: value, trailing: value) 66 | } 67 | 68 | /// Configure the amount of space added around the content of the item to adjust its final 69 | /// size after its position is computed. 70 | @warn_unqualified_access 71 | public func contentInsets(horizontal: CGFloat = 0, vertical: CGFloat = 0) -> LayoutSupplementaryItem { 72 | return self.contentInsets(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) 73 | } 74 | 75 | /// Configure the amount of space added around the content of the item to adjust its final 76 | /// size after its position is computed. 77 | @warn_unqualified_access 78 | public func contentInsets( 79 | top: CGFloat = 0, 80 | leading: CGFloat = 0, 81 | bottom: CGFloat = 0, 82 | trailing: CGFloat = 0 83 | ) -> LayoutSupplementaryItem { 84 | return self.contentInsets( 85 | NSDirectionalEdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing) 86 | ) 87 | } 88 | 89 | /// Configure the amount of space added around the content of the item to adjust its final 90 | /// size after its position is computed. 91 | @warn_unqualified_access 92 | public func contentInsets(_ insets: NSDirectionalEdgeInsets) -> LayoutSupplementaryItem { 93 | valueModifier(insets, keyPath: \.contentInsets) 94 | } 95 | } 96 | 97 | extension LayoutSupplementaryItem { 98 | 99 | // MARK: - Edge Spacing 100 | 101 | /// Configure the amount of space added around the boundaries of the item between other items 102 | /// and this item's container. 103 | @warn_unqualified_access 104 | public func edgeSpacing(value: NSCollectionLayoutSpacing?) -> LayoutSupplementaryItem { 105 | return self.edgeSpacing(top: value, leading: value, bottom: value, trailing: value) 106 | } 107 | 108 | /// Configure the amount of space added around the boundaries of the item between other items 109 | /// and this item's container. 110 | @warn_unqualified_access 111 | public func edgeSpacing( 112 | horizontal: NSCollectionLayoutSpacing? = nil, 113 | vertical: NSCollectionLayoutSpacing? = nil 114 | ) -> LayoutSupplementaryItem { 115 | return self.edgeSpacing(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) 116 | } 117 | 118 | /// Configure the amount of space added around the boundaries of the item between other items 119 | /// and this item's container. 120 | @warn_unqualified_access 121 | public func edgeSpacing( 122 | top: NSCollectionLayoutSpacing? = nil, 123 | leading: NSCollectionLayoutSpacing? = nil, 124 | bottom: NSCollectionLayoutSpacing? = nil, 125 | trailing: NSCollectionLayoutSpacing? = nil 126 | ) -> LayoutSupplementaryItem { 127 | return self.edgeSpacing( 128 | NSCollectionLayoutEdgeSpacing(leading: leading, top: top, trailing: trailing, bottom: bottom) 129 | ) 130 | } 131 | 132 | /// Configure the amount of space added around the boundaries of the item between other items 133 | /// and this item's container. 134 | @warn_unqualified_access 135 | public func edgeSpacing(_ edgeSpacing: NSCollectionLayoutEdgeSpacing) -> LayoutSupplementaryItem { 136 | valueModifier(edgeSpacing, keyPath: \.edgeSpacing) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutDSL/Public/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // CompositionalLayoutDSL 4 | // 5 | // Created by Alexandre Podlewski on 06/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func with(_ object: T, modifier: (inout T) -> Void) -> T { 12 | var copy = object 13 | modifier(©) 14 | return copy 15 | } 16 | 17 | /// A custom parameter attribute that constructs list of `Element` from closures. 18 | /// 19 | /// You typically use ``ListResultBuilder`` (or a typealias of it) as a parameter attribute for 20 | /// elements producing closure parameters, allowing those closures to provide multiple elements 21 | /// For example, the following `HGroup` initialiser accepts a closure that produces 22 | /// one or more items via the view builder. 23 | /// 24 | /// ```swift 25 | /// struct HGroup { 26 | /// init(@ListResultBuilder subItems: () -> [LayoutItem]) { /* ... */ } 27 | /// } 28 | /// ``` 29 | /// 30 | /// Clients of this function can use multiple-statement closures to provide 31 | /// several elements, as shown in the following example: 32 | /// 33 | /// ```swift 34 | /// HGroup { 35 | /// Item().width(.fractionalWidth(0.5)) 36 | /// if condition { 37 | /// VGroup(count: 3) { Item() } 38 | /// .width(.fractionalWidth(0.5)) 39 | /// } else { 40 | /// Item().width(.fractionalWidth(0.5)) 41 | /// } 42 | /// } 43 | /// ``` 44 | /// 45 | @resultBuilder 46 | public enum ListResultBuilder { 47 | 48 | public static func buildBlock(_ components: [Element]...) -> [Element] { 49 | return components.flatMap { $0 } 50 | } 51 | 52 | public static func buildExpression(_ expression: Element) -> [Element] { 53 | return [expression] 54 | } 55 | 56 | /// Provides support for “if” statements in multi-statement closures, 57 | /// producing an optional view that is visible only when the condition 58 | /// evaluates to `true`. 59 | // swiftlint:disable:next discouraged_optional_collection 60 | public static func buildOptional(_ component: [Element]?) -> [Element] { 61 | return component ?? [] 62 | } 63 | 64 | /// Provides support for "if" and "switch" statements in multi-statement closures, 65 | /// producing conditional content for the "then" branch. 66 | public static func buildEither(first component: [Element]) -> [Element] { 67 | return component 68 | } 69 | 70 | /// Provides support for "if-else" and "switch" statements in multi-statement closures, 71 | /// producing conditional content for the "else" branch. 72 | public static func buildEither(second component: [Element]) -> [Element] { 73 | return component 74 | } 75 | 76 | public static func buildArray(_ components: [[Element]]) -> [Element] { 77 | return components.flatMap { $0 } 78 | } 79 | } 80 | 81 | @available(iOS 14.0, tvOS 14.0, *) 82 | extension ListResultBuilder { 83 | 84 | /// Provides support for "if" statements with `#available()` clauses in 85 | /// multi-statement closures, producing conditional content for the "then" 86 | /// branch, i.e. the conditionally-available branch. 87 | public static func buildLimitedAvailability(_ component: [Element]) -> [Element] { 88 | return component 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /fastlane/.env.default: -------------------------------------------------------------------------------- 1 | PODSPEC = "CompositionalLayoutDSL.podspec" 2 | CHANGELOG = "CHANGELOG.md" 3 | REPO = "faberNovel/CompositionalLayoutDSL" -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | skip_docs 2 | 3 | # CI 4 | 5 | ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "60" 6 | 7 | desc "Run all unit tests" 8 | lane :tests do 9 | scan( 10 | workspace: "CompositionalLayoutDSL.xcworkspace", 11 | scheme: "CompositionalLayoutDSLApp", 12 | derived_data_path: "tests_derived_data", 13 | clean: true, 14 | devices: ["iPhone 15"] 15 | ) 16 | end 17 | 18 | desc "Run CI check for a commit" 19 | lane :ci_check do 20 | tests 21 | danger( 22 | github_api_token: ENV["GITHUB_API_TOKEN"], 23 | verbose: true, 24 | fail_on_errors: true 25 | ) 26 | pod_lib_lint( 27 | use_bundle_exec: true, 28 | allow_warnings: true 29 | ) 30 | end 31 | 32 | # Peepare release 33 | 34 | desc "Create release branch" 35 | lane :create_release_branch do |options| 36 | target_version = options[:version] 37 | raise "The version is missing. Use `fastlane create_release_pr version:{version_number}`.`" if target_version.nil? 38 | release_branch = "release/v" + target_version 39 | sh("git", "checkout", "-b", release_branch) 40 | push_to_git_remote( 41 | local_branch: release_branch, 42 | remote_branch: release_branch, 43 | set_upstream: true 44 | ) 45 | end 46 | 47 | desc "Prepare release of a new version" 48 | lane :prepare_release do |options| 49 | ensure_git_branch(branch: 'release/*') 50 | ensure_git_status_clean 51 | 52 | bypass_confirmations = options[:bypass_confirmations] 53 | target_version = target_version_from_branch 54 | 55 | next unless bypass_confirmations || UI.confirm("Is your CHANGELOG up to date?") 56 | bump_version(target_version) 57 | update_changelog(target_version) 58 | 59 | ensure_git_status_clean 60 | 61 | if bypass_confirmations || UI.confirm("Push?") 62 | push_to_git_remote 63 | UI.success "Release preparation pushed" 64 | end 65 | end 66 | 67 | desc "Create release PR" 68 | lane :create_release_pr do 69 | ensure_git_branch(branch: 'release/*') 70 | ensure_git_status_clean 71 | 72 | ["main", "develop"].each do |base| 73 | create_pull_request( 74 | api_bearer: ENV["GITHUB_TOKEN"], 75 | repo: ENV["REPO"], 76 | title: "Release #{target_version_from_branch}", 77 | base: base 78 | ) 79 | end 80 | end 81 | 82 | # Release 83 | 84 | desc "Publish release" 85 | lane :publish_release do 86 | ensure_git_branch(branch: 'main') 87 | 88 | target_version = version_get_podspec(path: ENV["PODSPEC"]) 89 | changelog = read_changelog( 90 | changelog_path: ENV["CHANGELOG"], 91 | section_identifier: "[#{target_version}]" 92 | ) 93 | 94 | set_github_release( 95 | repository_name: ENV["REPO"], 96 | api_bearer: ENV["GITHUB_TOKEN"], 97 | name: "v#{target_version}", 98 | tag_name: "v#{target_version}", 99 | description: changelog, 100 | commitish: "main" 101 | ) 102 | 103 | pod_push(allow_warnings: true) 104 | end 105 | 106 | ##################################################### 107 | # Private 108 | ##################################################### 109 | 110 | def update_changelog(target_version) 111 | changelog_path = ENV["CHANGELOG"] 112 | stamp_changelog( 113 | changelog_path: changelog_path, 114 | section_identifier: "#{target_version}", 115 | stamp_datetime_format: '%F' 116 | ) 117 | 118 | git_add(path: changelog_path) 119 | git_commit( 120 | path: changelog_path, 121 | message: "Update CHANGELOG" 122 | ) 123 | end 124 | 125 | def bump_version(target_version) 126 | podspec_path = ENV["PODSPEC"] 127 | version_bump_podspec( 128 | path: podspec_path, 129 | version_number: target_version 130 | ) 131 | 132 | pod_install # update the Podfile.lock with the new version 133 | 134 | path = [podspec_path, "Podfile.lock"] 135 | git_add(path: path) 136 | git_commit( 137 | path: path, 138 | message: "Bump to #{target_version}" 139 | ) 140 | end 141 | 142 | def target_version_from_branch 143 | git_branch.gsub(/release\/[A-Za-z]*/, "") 144 | end 145 | 146 | def pod_install 147 | sh "bundle exec pod install" 148 | end -------------------------------------------------------------------------------- /fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'fastlane-plugin-changelog' 6 | -------------------------------------------------------------------------------- /images/GettingStartedExample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/CompositionalLayoutDSL/3b1c5adad64f031e28417ee9460dd58556cb6e33/images/GettingStartedExample.jpg --------------------------------------------------------------------------------