├── .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
--------------------------------------------------------------------------------