diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8d78f07 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: [push, pull_request] + +jobs: + build: + + runs-on: macOS-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6.5 + bundler-cache: true + + - name: Pods cache + uses: actions/cache@v2 + with: + path: Pods + key: ${{ runner.os }}-cocoapods-${{ hashFiles('**/Podfile.lock') }} + + - name: Pod install + run: bundle exec pod install + + - name: Build and test + env: + GITHUB_API_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN_CI }} + run: bundle exec fastlane ci_check + + - uses: actions/upload-artifact@v2 + if: failure() + with: + name: test-artifacts + path: tests_derived_data/Logs/Test/*.xcresult diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..d045bb5 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,32 @@ +name: Documentation + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Generate Documentation + uses: SwiftDocOrg/swift-doc@master + with: + inputs: "Sources" + format: html + module-name: CompositionalLayoutDSL + output: "Documentation" + base-url: /CompositionalLayoutDSL/ + # The documentation step is generating files as root, and so are not readable by the runner without that fix + - name: Fix owner of generated files + run: sudo chown $USER -R ./Documentation + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_branch: docs + publish_dir: ./Documentation + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' diff --git a/.github/workflows/prepare_release.yml b/.github/workflows/prepare_release.yml new file mode 100644 index 0000000..363de5d --- /dev/null +++ b/.github/workflows/prepare_release.yml @@ -0,0 +1,54 @@ +name: Prepare a new release + +on: + workflow_dispatch: + inputs: + name: + description: 'Version number' + required: true + +jobs: + build: + runs-on: macOS-latest + steps: + - uses: actions/checkout@v2 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6.5 + + - name: Bundle install + run: bundle install + + - name: Create release branch + env: + LC_ALL: en_US.UTF-8 + LANG: en_US.UTF-8 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_COMMITTER_NAME: Bot Fabernovel + GIT_AUTHOR_NAME: Bot Fabernovel + GIT_COMMITTER_EMAIL: ci@fabernovel.com + GIT_AUTHOR_EMAIL: ci@fabernovel.com + run: bundle exec fastlane create_release_branch version:${{ github.event.inputs.name }} + + - name: Prepate release + env: + LC_ALL: en_US.UTF-8 + LANG: en_US.UTF-8 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_COMMITTER_NAME: Bot Fabernovel + GIT_AUTHOR_NAME: Bot Fabernovel + GIT_COMMITTER_EMAIL: ci@fabernovel.com + GIT_AUTHOR_EMAIL: ci@fabernovel.com + run: bundle exec fastlane prepare_release bypass_confirmations:true + + - name: Create release pull requests + env: + LC_ALL: en_US.UTF-8 + LANG: en_US.UTF-8 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_COMMITTER_NAME: Bot Fabernovel + GIT_AUTHOR_NAME: Bot Fabernovel + GIT_COMMITTER_EMAIL: ci@fabernovel.com + GIT_AUTHOR_EMAIL: ci@fabernovel.com + run: bundle exec fastlane create_release_pr diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6ad3ea0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: Release a new version + +on: + workflow_dispatch + +jobs: + build: + runs-on: macOS-latest + steps: + - uses: actions/checkout@v2 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6.5 + + - name: Bundle install + run: bundle install + + - name: Prepare release + env: + LC_ALL: en_US.UTF-8 + LANG: en_US.UTF-8 + COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_AP_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_COMMITTER_NAME: Bot Fabernovel + GIT_AUTHOR_NAME: Bot Fabernovel + GIT_COMMITTER_EMAIL: ci@fabernovel.com + GIT_AUTHOR_EMAIL: ci@fabernovel.com + run: bundle exec fastlane publish_release diff --git a/.gitignore b/.gitignore index 7711386..fadbbf5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ ## Build generated build/ DerivedData +tests_derived_data/ ## Various settings *.pbxuser diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..69a99e3 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,260 @@ +disabled_rules: # rule identifiers to exclude from running + # - block_based_kvo + # - class_delegate_protocol + # - closing_brace + # - closure_parameter_position + # - colon + # - comma + # - comment_spacing + # - compiler_protocol_init + # - computed_accessors_order + # - control_statement + # - cyclomatic_complexity + # - deployment_target + # - discarded_notification_center_observer + # - discouraged_direct_init + # - duplicate_imports + # - duplicate_enum_cases + # - dynamic_inline + # - empty_enum_arguments + # - empty_parameters + # - empty_parentheses_with_trailing_closure + # - fallthrough + # - file_length + # - for_where + # - force_cast + # - force_try + # - function_body_length + # - function_parameter_count + # - generic_type_name + # - identifier_name + # - implicit_getter + # - inclusive_language + # - inert_defer + # - is_disjoint + # - large_tuple + # - leading_whitespace + # - legacy_cggeometry_functions + # - legacy_constant + # - legacy_constructor + # - legacy_nsgeometry_functions + # - legacy_hashing + # - line_length + # - mark + # - multiple_closures_with_trailing_closure + # - nesting + - no_fallthrough_only + # - no_space_in_method_call + # - notification_center_detachment + # - nsobject_prefer_isequal + # - opening_brace + # - operator_whitespace + # - orphaned_doc_comment + # - private_over_fileprivate + # - private_unit_test + # - protocol_property_accessors_order + # - reduce_boolean + # - redundant_discardable_let + # - redundant_optional_initialization + # - redundant_string_enum_value + # - redundant_void_return + # - return_arrow_whitespace + # - shorthand_operator + # - statement_position + # - superfluous_disable_command + # - switch_case_alignment + # - syntactic_sugar + - todo + - trailing_comma + # - trailing_newline + # - trailing_semicolon + # - trailing_whitespace + # - type_body_length + # - type_name + # - unneeded_break_in_switch + # - unused_capture_list + # - unused_closure_parameter + # - unused_control_flow_label + # - unused_enumerated + # - unused_optional_binding + # - unused_setter_value + # - valid_ibinspectable + # - vertical_parameter_alignment + # - vertical_whitespace + # - void_return + # - weak_delegate + # - xctfail_message + +opt_in_rules: + - anyobject_protocol + # - attributes + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + # - conditional_returns_on_newline + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discouraged_object_literal + - discouraged_optional_boolean + - discouraged_optional_collection + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + # - expiring_todo + # - explicit_acl + # - explicit_enum_raw_value + - explicit_init + # - explicit_self + # - explicit_top_level_acl + # - explicit_type_interface + # - extension_access_modifier + - fatal_error_message + # - file_header + # - file_name + # - file_types_order + - first_where + - force_unwrapping + # - function_default_parameter_at_end + - ibinspectable_in_extension + - identical_operands + # - implicit_return + - implicitly_unwrapped_optional + # - indentation_width + # - joined_default_parameter + - last_where + - legacy_multiple + - legacy_random + # - let_var_whitespace + - literal_expression_end_indentation + - lower_acl_than_parent + # - missing_docs + - modifier_order + - multiline_arguments + # - multiline_arguments_brackets + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + # - multiline_parameters_brackets + # - nimble_operator + # - no_extension_access_modifier + # - no_grouping_extension + # - nslocalizedstring_key + # - nslocalizedstring_require_bundle + # - number_separator + # - object_literal + - operator_usage_whitespace + - optional_enum_case_matching + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + # - prefer_nimble + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + # - prefixed_toplevel_constant + - private_action + - private_outlet + # - prohibited_interface_builder + # - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + # - raw_value_for_camel_cased_codable_enum + - reduce_into + - redundant_nil_coalescing + - redundant_objc_attribute + - redundant_set_access_control + - redundant_type_annotation + # - required_deinit + # - required_enum_case + - single_test_class + - sorted_first_last + # - sorted_imports + - static_operator + # - strict_fileprivate + - strong_iboutlet + - switch_case_on_newline + # - test_case_accessibility + - toggle_bool + # - trailing_closure + # - type_contents_order + - unavailable_function + # - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - unused_declaration + - unused_import + - vertical_parameter_alignment_on_call + # - vertical_whitespace_between_cases + # - vertical_whitespace_closing_braces # Waiting for https://github.com/realm/SwiftLint/issues/2322 + # - vertical_whitespace_opening_braces # Waiting for https://github.com/realm/SwiftLint/issues/2322 + - xct_specific_matcher + - yoda_condition + +# rule configuration +closure_body_length: + warning: 20 + error: 35 +cyclomatic_complexity: + warning: 10 + error: 20 +force_cast: warning +force_try: warning +function_body_length: + warning: 35 + error: 50 +deployment_target: + iOS_deployment_target: 10 + # macOS_deployment_target: + # watchOS_deployment_target: + # tvOS_deployment_target: +identifier_name: + min_length: + warning: 0 + error: 0 + max_length: + warning: 100 + error: 200 + allowed_symbols: '_' +line_length: + warning: 120 + error: 160 + ignores_urls: true +type_name: + min_length: + warning: 0 + error: 0 + max_length: + warning: 100 + error: 200 + +custom_rules: + double_space: + include: "*.swift" + name: "Double space" + regex: '([a-z,A-Z] \s+)' + message: "Double space between keywords" + match_kinds: keyword + severity: warning + boolean_operators_end_of_line: + include: "*.swift" + name: "Boolean Operators" + regex: '((?:&&|\|\|)$)' + message: "Boolean operators should not be at the end of line" + severity: warning + explicit_failure_calls: + name: "Avoid asserting 'false'" + regex: '((assert|precondition)\(false)' + message: "Use assertionFailure() or preconditionFailure() instead." + severity: warning + +excluded: # paths to ignore during linting. Takes precedence over `included`. + - Carthage + - Pods + - ADUtilsTests + - vendor \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3edd65d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,55 @@ +# Change Log +All notable changes to this project will be documented in this file. +`CompositionalLayoutDSL` adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased] + +## [0.1.0] - 2021-04-28Z + +### Created + +#### Structures +- `CompositionalLayout` +- `Configuration` +- `Section` +- `ListSection` +- `RawSection` +- `HGroup` +- `VGroup` +- `CustomGroup` +- `Item` +- `DecorationItem` +- `SupplementaryItem` +- `BoundarySupplementaryItem` + +#### Enumerations +- `SupplementaryItem.AnchorOffset` +- `ListResultBuilder` + +#### Protocols +- `LayoutConfiguration` +- `LayoutSection` +- `LayoutGroup` +- `LayoutItem` +- `LayoutDecorationItem` +- `LayoutSupplementaryItem` +- `LayoutBoundarySupplementaryItem` +- `ResizableItem` + +#### Type aliases +- `LayoutItemBuilder` +- `LayoutBoundarySupplementaryItemBuilder` +- `LayoutSupplementaryItemBuilder` +- `LayoutDecorationItemBuilder` + +#### Functions +- `LayoutSectionBuilder(layoutSection:) -> NSCollectionLayoutSection` +- `LayoutBuilder(configuration:layoutSection:) -> NSCollectionViewCompositionalLayout` +- `LayoutBuilder(configuration:layoutSection:) -> UICollectionViewCompositionalLayout` +- `LayoutBuilder(compositionalLayout:) -> NSCollectionViewCompositionalLayout` +- `LayoutBuilder(compositionalLayout:) -> UICollectionViewCompositionalLayout` + +#### External extensions + +- `NSCollectionView.setCollectionViewLayout(_ layout: CompositionalLayout)` +- `UICollectionView.setCollectionViewLayout(_ layout: CompositionalLayout)` diff --git a/CompositionalLayoutDSL.podspec b/CompositionalLayoutDSL.podspec index b62520e..a77ff9d 100644 --- a/CompositionalLayoutDSL.podspec +++ b/CompositionalLayoutDSL.podspec @@ -9,7 +9,10 @@ Pod::Spec.new do |spec| spec.social_media_url = 'https://twitter.com/fabernovel' spec.ios.deployment_target = '13.0' spec.tvos.deployment_target = '13.0' - spec.framework = 'Foundation', 'UIKit' - spec.swift_versions = '5.1' - s.source_files = 'Sources/CompositionalLayoutDSL/**/*' + spec.osx.deployment_target = '10.15' + spec.framework = 'Foundation' + spec.ios.framework = 'UIKit' + spec.osx.framework = 'AppKit' + spec.swift_versions = ['5.1', '5.2', '5.3', '5.4'] + spec.source_files = 'Sources/CompositionalLayoutDSL/**/*' end diff --git a/CompositionalLayoutDSL.xcodeproj/project.pbxproj b/CompositionalLayoutDSL.xcodeproj/project.pbxproj new file mode 100644 index 0000000..2c8680f --- /dev/null +++ b/CompositionalLayoutDSL.xcodeproj/project.pbxproj @@ -0,0 +1,1078 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 066BE791BC5D7FF45980A612 /* Pods_CompositionalLayoutDSLApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA0209AFE87AE39112E94214 /* Pods_CompositionalLayoutDSLApp.framework */; }; + 6D508F071546CF8D53B32BE2 /* Pods_CompositionalLayoutDSLTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB84120BE9B1E0297035ABEF /* Pods_CompositionalLayoutDSLTests.framework */; }; + ACD60C7D26331A35006F6462 /* CompositionalLayoutDSL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ACD60C7626331A35006F6462 /* CompositionalLayoutDSL.framework */; }; + ACD60C7E26331A35006F6462 /* CompositionalLayoutDSL.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = ACD60C7626331A35006F6462 /* CompositionalLayoutDSL.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + ACD60C8626331A65006F6462 /* UniversalFramework.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = ACD60C8426331A65006F6462 /* UniversalFramework.xcconfig */; }; + ACD60CB726331B67006F6462 /* ItemBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C8A26331B67006F6462 /* ItemBuilder.swift */; }; + ACD60CB826331B67006F6462 /* BoundarySupplementaryItemBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C8B26331B67006F6462 /* BoundarySupplementaryItemBuilder.swift */; }; + ACD60CB926331B67006F6462 /* DecorationItemBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C8C26331B67006F6462 /* DecorationItemBuilder.swift */; }; + ACD60CBA26331B67006F6462 /* GroupBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C8D26331B67006F6462 /* GroupBuilder.swift */; }; + ACD60CBB26331B67006F6462 /* SectionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C8E26331B67006F6462 /* SectionBuilder.swift */; }; + ACD60CBC26331B67006F6462 /* ConfigurationBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C8F26331B67006F6462 /* ConfigurationBuilder.swift */; }; + ACD60CBD26331B67006F6462 /* SupplementaryItemBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C9026331B67006F6462 /* SupplementaryItemBuilder.swift */; }; + ACD60CBE26331B67006F6462 /* ModifiedLayoutSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C9226331B67006F6462 /* ModifiedLayoutSection.swift */; }; + ACD60CBF26331B67006F6462 /* ModifiedLayoutItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C9326331B67006F6462 /* ModifiedLayoutItem.swift */; }; + ACD60CC026331B67006F6462 /* ModifiedLayoutConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C9426331B67006F6462 /* ModifiedLayoutConfiguration.swift */; }; + ACD60CC126331B67006F6462 /* ModifiedLayoutDecorationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C9526331B67006F6462 /* ModifiedLayoutDecorationItem.swift */; }; + ACD60CC226331B67006F6462 /* ModifiedLayoutBoundarySupplementaryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C9626331B67006F6462 /* ModifiedLayoutBoundarySupplementaryItem.swift */; }; + ACD60CC326331B67006F6462 /* ModifiedLayoutSupplementaryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C9726331B67006F6462 /* ModifiedLayoutSupplementaryItem.swift */; }; + ACD60CC426331B67006F6462 /* ModifiedLayoutGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C9826331B67006F6462 /* ModifiedLayoutGroup.swift */; }; + ACD60CC526331B67006F6462 /* LayoutConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C9B26331B67006F6462 /* LayoutConfiguration.swift */; }; + ACD60CC626331B67006F6462 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C9C26331B67006F6462 /* Configuration.swift */; }; + ACD60CC726331B67006F6462 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C9E26331B67006F6462 /* Section.swift */; }; + ACD60CC826331B67006F6462 /* LayoutSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60C9F26331B67006F6462 /* LayoutSection.swift */; }; + ACD60CC926331B67006F6462 /* ListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CA026331B67006F6462 /* ListSection.swift */; }; + ACD60CCA26331B67006F6462 /* ResultBuilders.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CA126331B67006F6462 /* ResultBuilders.swift */; }; + ACD60CCB26331B67006F6462 /* LayoutDecorationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CA326331B67006F6462 /* LayoutDecorationItem.swift */; }; + ACD60CCC26331B67006F6462 /* DecorationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CA426331B67006F6462 /* DecorationItem.swift */; }; + ACD60CCD26331B67006F6462 /* LayoutGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CA626331B67006F6462 /* LayoutGroup.swift */; }; + ACD60CCE26331B67006F6462 /* CustomGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CA726331B67006F6462 /* CustomGroup.swift */; }; + ACD60CCF26331B67006F6462 /* VGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CA826331B67006F6462 /* VGroup.swift */; }; + ACD60CD026331B67006F6462 /* HGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CA926331B67006F6462 /* HGroup.swift */; }; + ACD60CD126331B67006F6462 /* CompositionalLayoutDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CAA26331B67006F6462 /* CompositionalLayoutDSL.swift */; }; + ACD60CD226331B67006F6462 /* ResizableItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CAB26331B67006F6462 /* ResizableItem.swift */; }; + ACD60CD326331B67006F6462 /* SupplementaryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CAD26331B67006F6462 /* SupplementaryItem.swift */; }; + ACD60CD426331B67006F6462 /* LayoutSupplementaryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CAE26331B67006F6462 /* LayoutSupplementaryItem.swift */; }; + ACD60CD526331B67006F6462 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CB026331B67006F6462 /* Item.swift */; }; + ACD60CD626331B67006F6462 /* LayoutItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CB126331B67006F6462 /* LayoutItem.swift */; }; + ACD60CD726331B67006F6462 /* LayoutBoundarySupplementaryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CB326331B67006F6462 /* LayoutBoundarySupplementaryItem.swift */; }; + ACD60CD826331B67006F6462 /* BoundarySupplementaryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CB426331B67006F6462 /* BoundarySupplementaryItem.swift */; }; + ACD60CD926331B67006F6462 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CB526331B67006F6462 /* Utils.swift */; }; + ACD60CDA26331B67006F6462 /* CompositionalLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD60CB626331B67006F6462 /* CompositionalLayout.swift */; }; + ACDE9DC92626E1750033F0B0 /* SupplementaryItemDSLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACDE9DC82626E1750033F0B0 /* SupplementaryItemDSLTests.swift */; }; + ACDE9DCB2626EC0B0033F0B0 /* DecorationItemDSLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACDE9DCA2626EC0B0033F0B0 /* DecorationItemDSLTests.swift */; }; + ACDE9DCD2626ECCE0033F0B0 /* TestingDecorationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACDE9DCC2626ECCE0033F0B0 /* TestingDecorationView.swift */; }; + ACF2C1BF2625D01400E06BA7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF2C1BE2625D01400E06BA7 /* AppDelegate.swift */; }; + ACF2C1C12625D01400E06BA7 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF2C1C02625D01400E06BA7 /* SceneDelegate.swift */; }; + ACF2C1C32625D01400E06BA7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF2C1C22625D01400E06BA7 /* ViewController.swift */; }; + ACF2C1C82625D01500E06BA7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = ACF2C1C72625D01500E06BA7 /* Assets.xcassets */; }; + ACF2C1CB2625D01500E06BA7 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ACF2C1C92625D01500E06BA7 /* LaunchScreen.storyboard */; }; + ACF2C1D62625D01600E06BA7 /* GroupDSLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF2C1D52625D01600E06BA7 /* GroupDSLTests.swift */; }; + ACF2C1F52625E79C00E06BA7 /* TestingCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF2C1F42625E79C00E06BA7 /* TestingCollectionViewController.swift */; }; + ACF2C1F82625E89300E06BA7 /* TestingSupplementaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF2C1F72625E89300E06BA7 /* TestingSupplementaryView.swift */; }; + ACF2C1FA2625E8A500E06BA7 /* TestingCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF2C1F92625E8A500E06BA7 /* TestingCellView.swift */; }; + ACF2C1FD2625FC6E00E06BA7 /* NSCollectionLayoutSize+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF2C1FC2625FC6E00E06BA7 /* NSCollectionLayoutSize+Utils.swift */; }; + ACF2C1FF2625FC8E00E06BA7 /* assertLayouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF2C1FE2625FC8E00E06BA7 /* assertLayouts.swift */; }; + ACF2C2012625FF3A00E06BA7 /* TestingCollectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF2C2002625FF3A00E06BA7 /* TestingCollectionViewModel.swift */; }; + ACF2C20426260A9D00E06BA7 /* SectionDSLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF2C20326260A9D00E06BA7 /* SectionDSLTests.swift */; }; + ACFB76482638188E00B00E56 /* RawSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACFB76472638188E00B00E56 /* RawSection.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + ACD60C7B26331A35006F6462 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = ACF2C1B32625D01400E06BA7 /* Project object */; + proxyType = 1; + remoteGlobalIDString = ACD60C7526331A35006F6462; + remoteInfo = CompositionalLayoutDSL; + }; + ACF2C1D22625D01600E06BA7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = ACF2C1B32625D01400E06BA7 /* Project object */; + proxyType = 1; + remoteGlobalIDString = ACF2C1BA2625D01400E06BA7; + remoteInfo = CompositionalLayoutDSL; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + ACD60C8226331A35006F6462 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ACD60C7E26331A35006F6462 /* CompositionalLayoutDSL.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 000E561C82B4F1F842B27FB8 /* Pods-CompositionalLayoutDSLApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CompositionalLayoutDSLApp.release.xcconfig"; path = "Target Support Files/Pods-CompositionalLayoutDSLApp/Pods-CompositionalLayoutDSLApp.release.xcconfig"; sourceTree = ""; }; + 74E75573EC429923E664F111 /* Pods-CompositionalLayoutDSLApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CompositionalLayoutDSLApp.debug.xcconfig"; path = "Target Support Files/Pods-CompositionalLayoutDSLApp/Pods-CompositionalLayoutDSLApp.debug.xcconfig"; sourceTree = ""; }; + ACD60C7626331A35006F6462 /* CompositionalLayoutDSL.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CompositionalLayoutDSL.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + ACD60C7926331A35006F6462 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + ACD60C8426331A65006F6462 /* UniversalFramework.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework.xcconfig; sourceTree = ""; }; + ACD60C8A26331B67006F6462 /* ItemBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemBuilder.swift; sourceTree = ""; }; + ACD60C8B26331B67006F6462 /* BoundarySupplementaryItemBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoundarySupplementaryItemBuilder.swift; sourceTree = ""; }; + ACD60C8C26331B67006F6462 /* DecorationItemBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecorationItemBuilder.swift; sourceTree = ""; }; + ACD60C8D26331B67006F6462 /* GroupBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupBuilder.swift; sourceTree = ""; }; + ACD60C8E26331B67006F6462 /* SectionBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SectionBuilder.swift; sourceTree = ""; }; + ACD60C8F26331B67006F6462 /* ConfigurationBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationBuilder.swift; sourceTree = ""; }; + ACD60C9026331B67006F6462 /* SupplementaryItemBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SupplementaryItemBuilder.swift; sourceTree = ""; }; + ACD60C9226331B67006F6462 /* ModifiedLayoutSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModifiedLayoutSection.swift; sourceTree = ""; }; + ACD60C9326331B67006F6462 /* ModifiedLayoutItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModifiedLayoutItem.swift; sourceTree = ""; }; + ACD60C9426331B67006F6462 /* ModifiedLayoutConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModifiedLayoutConfiguration.swift; sourceTree = ""; }; + ACD60C9526331B67006F6462 /* ModifiedLayoutDecorationItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModifiedLayoutDecorationItem.swift; sourceTree = ""; }; + ACD60C9626331B67006F6462 /* ModifiedLayoutBoundarySupplementaryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModifiedLayoutBoundarySupplementaryItem.swift; sourceTree = ""; }; + ACD60C9726331B67006F6462 /* ModifiedLayoutSupplementaryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModifiedLayoutSupplementaryItem.swift; sourceTree = ""; }; + ACD60C9826331B67006F6462 /* ModifiedLayoutGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModifiedLayoutGroup.swift; sourceTree = ""; }; + ACD60C9B26331B67006F6462 /* LayoutConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutConfiguration.swift; sourceTree = ""; }; + ACD60C9C26331B67006F6462 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; + ACD60C9E26331B67006F6462 /* Section.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; + ACD60C9F26331B67006F6462 /* LayoutSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutSection.swift; sourceTree = ""; }; + ACD60CA026331B67006F6462 /* ListSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListSection.swift; sourceTree = ""; }; + ACD60CA126331B67006F6462 /* ResultBuilders.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultBuilders.swift; sourceTree = ""; }; + ACD60CA326331B67006F6462 /* LayoutDecorationItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutDecorationItem.swift; sourceTree = ""; }; + ACD60CA426331B67006F6462 /* DecorationItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecorationItem.swift; sourceTree = ""; }; + ACD60CA626331B67006F6462 /* LayoutGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutGroup.swift; sourceTree = ""; }; + ACD60CA726331B67006F6462 /* CustomGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomGroup.swift; sourceTree = ""; }; + ACD60CA826331B67006F6462 /* VGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VGroup.swift; sourceTree = ""; }; + ACD60CA926331B67006F6462 /* HGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HGroup.swift; sourceTree = ""; }; + ACD60CAA26331B67006F6462 /* CompositionalLayoutDSL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompositionalLayoutDSL.swift; sourceTree = ""; }; + ACD60CAB26331B67006F6462 /* ResizableItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResizableItem.swift; sourceTree = ""; }; + ACD60CAD26331B67006F6462 /* SupplementaryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SupplementaryItem.swift; sourceTree = ""; }; + ACD60CAE26331B67006F6462 /* LayoutSupplementaryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutSupplementaryItem.swift; sourceTree = ""; }; + ACD60CB026331B67006F6462 /* Item.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; + ACD60CB126331B67006F6462 /* LayoutItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutItem.swift; sourceTree = ""; }; + ACD60CB326331B67006F6462 /* LayoutBoundarySupplementaryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutBoundarySupplementaryItem.swift; sourceTree = ""; }; + ACD60CB426331B67006F6462 /* BoundarySupplementaryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoundarySupplementaryItem.swift; sourceTree = ""; }; + ACD60CB526331B67006F6462 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; + ACD60CB626331B67006F6462 /* CompositionalLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompositionalLayout.swift; sourceTree = ""; }; + ACDE9DC82626E1750033F0B0 /* SupplementaryItemDSLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupplementaryItemDSLTests.swift; sourceTree = ""; }; + ACDE9DCA2626EC0B0033F0B0 /* DecorationItemDSLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationItemDSLTests.swift; sourceTree = ""; }; + ACDE9DCC2626ECCE0033F0B0 /* TestingDecorationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingDecorationView.swift; sourceTree = ""; }; + ACF2C1BB2625D01400E06BA7 /* CompositionalLayoutDSLApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CompositionalLayoutDSLApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + ACF2C1BE2625D01400E06BA7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + ACF2C1C02625D01400E06BA7 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + ACF2C1C22625D01400E06BA7 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + ACF2C1C72625D01500E06BA7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + ACF2C1CA2625D01500E06BA7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + ACF2C1CC2625D01500E06BA7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + ACF2C1D12625D01600E06BA7 /* CompositionalLayoutDSLTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CompositionalLayoutDSLTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + ACF2C1D52625D01600E06BA7 /* GroupDSLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupDSLTests.swift; sourceTree = ""; }; + ACF2C1D72625D01600E06BA7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + ACF2C1F42625E79C00E06BA7 /* TestingCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingCollectionViewController.swift; sourceTree = ""; }; + ACF2C1F72625E89300E06BA7 /* TestingSupplementaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingSupplementaryView.swift; sourceTree = ""; }; + ACF2C1F92625E8A500E06BA7 /* TestingCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingCellView.swift; sourceTree = ""; }; + ACF2C1FC2625FC6E00E06BA7 /* NSCollectionLayoutSize+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSCollectionLayoutSize+Utils.swift"; sourceTree = ""; }; + ACF2C1FE2625FC8E00E06BA7 /* assertLayouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = assertLayouts.swift; sourceTree = ""; }; + ACF2C2002625FF3A00E06BA7 /* TestingCollectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingCollectionViewModel.swift; sourceTree = ""; }; + ACF2C20326260A9D00E06BA7 /* SectionDSLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionDSLTests.swift; sourceTree = ""; }; + ACFB76472638188E00B00E56 /* RawSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawSection.swift; sourceTree = ""; }; + BB84120BE9B1E0297035ABEF /* Pods_CompositionalLayoutDSLTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CompositionalLayoutDSLTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DA0209AFE87AE39112E94214 /* Pods_CompositionalLayoutDSLApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CompositionalLayoutDSLApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E711B7B3BE01BEA25E83E41C /* Pods-CompositionalLayoutDSLTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CompositionalLayoutDSLTests.debug.xcconfig"; path = "Target Support Files/Pods-CompositionalLayoutDSLTests/Pods-CompositionalLayoutDSLTests.debug.xcconfig"; sourceTree = ""; }; + F40EB76D180E510B0ADF8C8F /* Pods-CompositionalLayoutDSLTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CompositionalLayoutDSLTests.release.xcconfig"; path = "Target Support Files/Pods-CompositionalLayoutDSLTests/Pods-CompositionalLayoutDSLTests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + ACD60C7326331A35006F6462 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + ACF2C1B82625D01400E06BA7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ACD60C7D26331A35006F6462 /* CompositionalLayoutDSL.framework in Frameworks */, + 066BE791BC5D7FF45980A612 /* Pods_CompositionalLayoutDSLApp.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + ACF2C1CE2625D01600E06BA7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6D508F071546CF8D53B32BE2 /* Pods_CompositionalLayoutDSLTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 58D896CBF2AB2622C57D76B7 /* Pods */ = { + isa = PBXGroup; + children = ( + 74E75573EC429923E664F111 /* Pods-CompositionalLayoutDSLApp.debug.xcconfig */, + 000E561C82B4F1F842B27FB8 /* Pods-CompositionalLayoutDSLApp.release.xcconfig */, + E711B7B3BE01BEA25E83E41C /* Pods-CompositionalLayoutDSLTests.debug.xcconfig */, + F40EB76D180E510B0ADF8C8F /* Pods-CompositionalLayoutDSLTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 5D68F9AF3C47D2C4B092DDE9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + DA0209AFE87AE39112E94214 /* Pods_CompositionalLayoutDSLApp.framework */, + BB84120BE9B1E0297035ABEF /* Pods_CompositionalLayoutDSLTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + ACD60C7726331A35006F6462 /* CompositionalLayoutDSL */ = { + isa = PBXGroup; + children = ( + ACD60C7926331A35006F6462 /* Info.plist */, + ACD60C8426331A65006F6462 /* UniversalFramework.xcconfig */, + ACD60C8726331B67006F6462 /* CompositionalLayoutDSL */, + ); + path = CompositionalLayoutDSL; + sourceTree = ""; + }; + ACD60C8726331B67006F6462 /* CompositionalLayoutDSL */ = { + isa = PBXGroup; + children = ( + ACD60C8826331B67006F6462 /* Internal */, + ACD60C9926331B67006F6462 /* Public */, + ); + name = CompositionalLayoutDSL; + path = Sources/CompositionalLayoutDSL; + sourceTree = SOURCE_ROOT; + }; + ACD60C8826331B67006F6462 /* Internal */ = { + isa = PBXGroup; + children = ( + ACD60C8926331B67006F6462 /* Builders */, + ACD60C9126331B67006F6462 /* ModifiedLayout */, + ); + path = Internal; + sourceTree = ""; + }; + ACD60C8926331B67006F6462 /* Builders */ = { + isa = PBXGroup; + children = ( + ACD60C8A26331B67006F6462 /* ItemBuilder.swift */, + ACD60C8B26331B67006F6462 /* BoundarySupplementaryItemBuilder.swift */, + ACD60C8C26331B67006F6462 /* DecorationItemBuilder.swift */, + ACD60C8D26331B67006F6462 /* GroupBuilder.swift */, + ACD60C8E26331B67006F6462 /* SectionBuilder.swift */, + ACD60C8F26331B67006F6462 /* ConfigurationBuilder.swift */, + ACD60C9026331B67006F6462 /* SupplementaryItemBuilder.swift */, + ); + path = Builders; + sourceTree = ""; + }; + ACD60C9126331B67006F6462 /* ModifiedLayout */ = { + isa = PBXGroup; + children = ( + ACD60C9226331B67006F6462 /* ModifiedLayoutSection.swift */, + ACD60C9326331B67006F6462 /* ModifiedLayoutItem.swift */, + ACD60C9426331B67006F6462 /* ModifiedLayoutConfiguration.swift */, + ACD60C9526331B67006F6462 /* ModifiedLayoutDecorationItem.swift */, + ACD60C9626331B67006F6462 /* ModifiedLayoutBoundarySupplementaryItem.swift */, + ACD60C9726331B67006F6462 /* ModifiedLayoutSupplementaryItem.swift */, + ACD60C9826331B67006F6462 /* ModifiedLayoutGroup.swift */, + ); + path = ModifiedLayout; + sourceTree = ""; + }; + ACD60C9926331B67006F6462 /* Public */ = { + isa = PBXGroup; + children = ( + ACD60C9A26331B67006F6462 /* Configuration */, + ACD60C9D26331B67006F6462 /* Section */, + ACD60CA226331B67006F6462 /* DecorationItem */, + ACD60CA526331B67006F6462 /* Group */, + ACD60CAC26331B67006F6462 /* SupplementaryItem */, + ACD60CAF26331B67006F6462 /* Item */, + ACD60CB226331B67006F6462 /* BoundarySupplementaryItem */, + ACD60CA126331B67006F6462 /* ResultBuilders.swift */, + ACD60CAA26331B67006F6462 /* CompositionalLayoutDSL.swift */, + ACD60CAB26331B67006F6462 /* ResizableItem.swift */, + ACD60CB526331B67006F6462 /* Utils.swift */, + ACD60CB626331B67006F6462 /* CompositionalLayout.swift */, + ); + path = Public; + sourceTree = ""; + }; + ACD60C9A26331B67006F6462 /* Configuration */ = { + isa = PBXGroup; + children = ( + ACD60C9B26331B67006F6462 /* LayoutConfiguration.swift */, + ACD60C9C26331B67006F6462 /* Configuration.swift */, + ); + path = Configuration; + sourceTree = ""; + }; + ACD60C9D26331B67006F6462 /* Section */ = { + isa = PBXGroup; + children = ( + ACD60C9E26331B67006F6462 /* Section.swift */, + ACFB76472638188E00B00E56 /* RawSection.swift */, + ACD60C9F26331B67006F6462 /* LayoutSection.swift */, + ACD60CA026331B67006F6462 /* ListSection.swift */, + ); + path = Section; + sourceTree = ""; + }; + ACD60CA226331B67006F6462 /* DecorationItem */ = { + isa = PBXGroup; + children = ( + ACD60CA326331B67006F6462 /* LayoutDecorationItem.swift */, + ACD60CA426331B67006F6462 /* DecorationItem.swift */, + ); + path = DecorationItem; + sourceTree = ""; + }; + ACD60CA526331B67006F6462 /* Group */ = { + isa = PBXGroup; + children = ( + ACD60CA626331B67006F6462 /* LayoutGroup.swift */, + ACD60CA726331B67006F6462 /* CustomGroup.swift */, + ACD60CA826331B67006F6462 /* VGroup.swift */, + ACD60CA926331B67006F6462 /* HGroup.swift */, + ); + path = Group; + sourceTree = ""; + }; + ACD60CAC26331B67006F6462 /* SupplementaryItem */ = { + isa = PBXGroup; + children = ( + ACD60CAD26331B67006F6462 /* SupplementaryItem.swift */, + ACD60CAE26331B67006F6462 /* LayoutSupplementaryItem.swift */, + ); + path = SupplementaryItem; + sourceTree = ""; + }; + ACD60CAF26331B67006F6462 /* Item */ = { + isa = PBXGroup; + children = ( + ACD60CB026331B67006F6462 /* Item.swift */, + ACD60CB126331B67006F6462 /* LayoutItem.swift */, + ); + path = Item; + sourceTree = ""; + }; + ACD60CB226331B67006F6462 /* BoundarySupplementaryItem */ = { + isa = PBXGroup; + children = ( + ACD60CB326331B67006F6462 /* LayoutBoundarySupplementaryItem.swift */, + ACD60CB426331B67006F6462 /* BoundarySupplementaryItem.swift */, + ); + path = BoundarySupplementaryItem; + sourceTree = ""; + }; + ACF2C1B22625D01400E06BA7 = { + isa = PBXGroup; + children = ( + ACF2C1BD2625D01400E06BA7 /* CompositionalLayoutDSLApp */, + ACF2C1D42625D01600E06BA7 /* CompositionalLayoutDSLTests */, + ACD60C7726331A35006F6462 /* CompositionalLayoutDSL */, + ACF2C1BC2625D01400E06BA7 /* Products */, + 58D896CBF2AB2622C57D76B7 /* Pods */, + 5D68F9AF3C47D2C4B092DDE9 /* Frameworks */, + ); + sourceTree = ""; + }; + ACF2C1BC2625D01400E06BA7 /* Products */ = { + isa = PBXGroup; + children = ( + ACF2C1BB2625D01400E06BA7 /* CompositionalLayoutDSLApp.app */, + ACF2C1D12625D01600E06BA7 /* CompositionalLayoutDSLTests.xctest */, + ACD60C7626331A35006F6462 /* CompositionalLayoutDSL.framework */, + ); + name = Products; + sourceTree = ""; + }; + ACF2C1BD2625D01400E06BA7 /* CompositionalLayoutDSLApp */ = { + isa = PBXGroup; + children = ( + ACF2C1BE2625D01400E06BA7 /* AppDelegate.swift */, + ACF2C1C02625D01400E06BA7 /* SceneDelegate.swift */, + ACF2C1C22625D01400E06BA7 /* ViewController.swift */, + ACF2C1C72625D01500E06BA7 /* Assets.xcassets */, + ACF2C1C92625D01500E06BA7 /* LaunchScreen.storyboard */, + ACF2C1CC2625D01500E06BA7 /* Info.plist */, + ); + path = CompositionalLayoutDSLApp; + sourceTree = ""; + }; + ACF2C1D42625D01600E06BA7 /* CompositionalLayoutDSLTests */ = { + isa = PBXGroup; + children = ( + ACF2C1FB2625FC5F00E06BA7 /* Utils */, + ACF2C1F62625E83B00E06BA7 /* TestingCollectionView */, + ACF2C202262607AB00E06BA7 /* LayoutTests */, + ACF2C1D72625D01600E06BA7 /* Info.plist */, + ); + path = CompositionalLayoutDSLTests; + sourceTree = ""; + }; + ACF2C1F62625E83B00E06BA7 /* TestingCollectionView */ = { + isa = PBXGroup; + children = ( + ACF2C1F42625E79C00E06BA7 /* TestingCollectionViewController.swift */, + ACF2C1F72625E89300E06BA7 /* TestingSupplementaryView.swift */, + ACF2C1F92625E8A500E06BA7 /* TestingCellView.swift */, + ACF2C2002625FF3A00E06BA7 /* TestingCollectionViewModel.swift */, + ACDE9DCC2626ECCE0033F0B0 /* TestingDecorationView.swift */, + ); + path = TestingCollectionView; + sourceTree = ""; + }; + ACF2C1FB2625FC5F00E06BA7 /* Utils */ = { + isa = PBXGroup; + children = ( + ACF2C1FC2625FC6E00E06BA7 /* NSCollectionLayoutSize+Utils.swift */, + ACF2C1FE2625FC8E00E06BA7 /* assertLayouts.swift */, + ); + path = Utils; + sourceTree = ""; + }; + ACF2C202262607AB00E06BA7 /* LayoutTests */ = { + isa = PBXGroup; + children = ( + ACF2C1D52625D01600E06BA7 /* GroupDSLTests.swift */, + ACF2C20326260A9D00E06BA7 /* SectionDSLTests.swift */, + ACDE9DC82626E1750033F0B0 /* SupplementaryItemDSLTests.swift */, + ACDE9DCA2626EC0B0033F0B0 /* DecorationItemDSLTests.swift */, + ); + path = LayoutTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + ACD60C7126331A35006F6462 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + ACD60C7526331A35006F6462 /* CompositionalLayoutDSL */ = { + isa = PBXNativeTarget; + buildConfigurationList = ACD60C7F26331A35006F6462 /* Build configuration list for PBXNativeTarget "CompositionalLayoutDSL" */; + buildPhases = ( + ACD60C7126331A35006F6462 /* Headers */, + ACD60C7226331A35006F6462 /* Sources */, + ACD60C7326331A35006F6462 /* Frameworks */, + ACD60C7426331A35006F6462 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CompositionalLayoutDSL; + productName = CompositionalLayoutDSL; + productReference = ACD60C7626331A35006F6462 /* CompositionalLayoutDSL.framework */; + productType = "com.apple.product-type.framework"; + }; + ACF2C1BA2625D01400E06BA7 /* CompositionalLayoutDSLApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = ACF2C1E52625D01600E06BA7 /* Build configuration list for PBXNativeTarget "CompositionalLayoutDSLApp" */; + buildPhases = ( + 6974283F3F678F46FB543FE1 /* [CP] Check Pods Manifest.lock */, + ACF2C1F32625D97E00E06BA7 /* SwiftLint */, + ACF2C1B72625D01400E06BA7 /* Sources */, + ACF2C1B82625D01400E06BA7 /* Frameworks */, + ACF2C1B92625D01400E06BA7 /* Resources */, + ACD60C8226331A35006F6462 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ACD60C7C26331A35006F6462 /* PBXTargetDependency */, + ); + name = CompositionalLayoutDSLApp; + packageProductDependencies = ( + ); + productName = CompositionalLayoutDSL; + productReference = ACF2C1BB2625D01400E06BA7 /* CompositionalLayoutDSLApp.app */; + productType = "com.apple.product-type.application"; + }; + ACF2C1D02625D01600E06BA7 /* CompositionalLayoutDSLTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = ACF2C1E82625D01600E06BA7 /* Build configuration list for PBXNativeTarget "CompositionalLayoutDSLTests" */; + buildPhases = ( + 7E289FDB5F73CF7CAF411E33 /* [CP] Check Pods Manifest.lock */, + ACF2C1CD2625D01600E06BA7 /* Sources */, + ACF2C1CE2625D01600E06BA7 /* Frameworks */, + ACF2C1CF2625D01600E06BA7 /* Resources */, + E0690740A50C436D27025851 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ACF2C1D32625D01600E06BA7 /* PBXTargetDependency */, + ); + name = CompositionalLayoutDSLTests; + productName = CompositionalLayoutDSLTests; + productReference = ACF2C1D12625D01600E06BA7 /* CompositionalLayoutDSLTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + ACF2C1B32625D01400E06BA7 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1250; + LastUpgradeCheck = 1250; + ORGANIZATIONNAME = Fabernovel; + TargetAttributes = { + ACD60C7526331A35006F6462 = { + CreatedOnToolsVersion = 12.5; + }; + ACF2C1BA2625D01400E06BA7 = { + CreatedOnToolsVersion = 12.5; + }; + ACF2C1D02625D01600E06BA7 = { + CreatedOnToolsVersion = 12.5; + TestTargetID = ACF2C1BA2625D01400E06BA7; + }; + }; + }; + buildConfigurationList = ACF2C1B62625D01400E06BA7 /* Build configuration list for PBXProject "CompositionalLayoutDSL" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = ACF2C1B22625D01400E06BA7; + packageReferences = ( + ); + productRefGroup = ACF2C1BC2625D01400E06BA7 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + ACF2C1BA2625D01400E06BA7 /* CompositionalLayoutDSLApp */, + ACF2C1D02625D01600E06BA7 /* CompositionalLayoutDSLTests */, + ACD60C7526331A35006F6462 /* CompositionalLayoutDSL */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + ACD60C7426331A35006F6462 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ACD60C8626331A65006F6462 /* UniversalFramework.xcconfig in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + ACF2C1B92625D01400E06BA7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ACF2C1CB2625D01500E06BA7 /* LaunchScreen.storyboard in Resources */, + ACF2C1C82625D01500E06BA7 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + ACF2C1CF2625D01600E06BA7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6974283F3F678F46FB543FE1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-CompositionalLayoutDSLApp-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 7E289FDB5F73CF7CAF411E33 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-CompositionalLayoutDSLTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + ACF2C1F32625D97E00E06BA7 /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = SwiftLint; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; + }; + E0690740A50C436D27025851 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-CompositionalLayoutDSLTests/Pods-CompositionalLayoutDSLTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-CompositionalLayoutDSLTests/Pods-CompositionalLayoutDSLTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-CompositionalLayoutDSLTests/Pods-CompositionalLayoutDSLTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + ACD60C7226331A35006F6462 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ACD60CCA26331B67006F6462 /* ResultBuilders.swift in Sources */, + ACD60CC726331B67006F6462 /* Section.swift in Sources */, + ACD60CB826331B67006F6462 /* BoundarySupplementaryItemBuilder.swift in Sources */, + ACD60CC126331B67006F6462 /* ModifiedLayoutDecorationItem.swift in Sources */, + ACD60CBA26331B67006F6462 /* GroupBuilder.swift in Sources */, + ACD60CBE26331B67006F6462 /* ModifiedLayoutSection.swift in Sources */, + ACD60CD526331B67006F6462 /* Item.swift in Sources */, + ACD60CCC26331B67006F6462 /* DecorationItem.swift in Sources */, + ACD60CBC26331B67006F6462 /* ConfigurationBuilder.swift in Sources */, + ACD60CC926331B67006F6462 /* ListSection.swift in Sources */, + ACD60CD926331B67006F6462 /* Utils.swift in Sources */, + ACD60CC326331B67006F6462 /* ModifiedLayoutSupplementaryItem.swift in Sources */, + ACD60CC626331B67006F6462 /* Configuration.swift in Sources */, + ACD60CB726331B67006F6462 /* ItemBuilder.swift in Sources */, + ACD60CC526331B67006F6462 /* LayoutConfiguration.swift in Sources */, + ACD60CB926331B67006F6462 /* DecorationItemBuilder.swift in Sources */, + ACD60CD226331B67006F6462 /* ResizableItem.swift in Sources */, + ACD60CC826331B67006F6462 /* LayoutSection.swift in Sources */, + ACD60CCB26331B67006F6462 /* LayoutDecorationItem.swift in Sources */, + ACD60CD826331B67006F6462 /* BoundarySupplementaryItem.swift in Sources */, + ACD60CD026331B67006F6462 /* HGroup.swift in Sources */, + ACD60CCD26331B67006F6462 /* LayoutGroup.swift in Sources */, + ACD60CC426331B67006F6462 /* ModifiedLayoutGroup.swift in Sources */, + ACD60CBF26331B67006F6462 /* ModifiedLayoutItem.swift in Sources */, + ACD60CBB26331B67006F6462 /* SectionBuilder.swift in Sources */, + ACFB76482638188E00B00E56 /* RawSection.swift in Sources */, + ACD60CD726331B67006F6462 /* LayoutBoundarySupplementaryItem.swift in Sources */, + ACD60CCE26331B67006F6462 /* CustomGroup.swift in Sources */, + ACD60CD626331B67006F6462 /* LayoutItem.swift in Sources */, + ACD60CBD26331B67006F6462 /* SupplementaryItemBuilder.swift in Sources */, + ACD60CD126331B67006F6462 /* CompositionalLayoutDSL.swift in Sources */, + ACD60CC026331B67006F6462 /* ModifiedLayoutConfiguration.swift in Sources */, + ACD60CD426331B67006F6462 /* LayoutSupplementaryItem.swift in Sources */, + ACD60CD326331B67006F6462 /* SupplementaryItem.swift in Sources */, + ACD60CCF26331B67006F6462 /* VGroup.swift in Sources */, + ACD60CC226331B67006F6462 /* ModifiedLayoutBoundarySupplementaryItem.swift in Sources */, + ACD60CDA26331B67006F6462 /* CompositionalLayout.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + ACF2C1B72625D01400E06BA7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ACF2C1C32625D01400E06BA7 /* ViewController.swift in Sources */, + ACF2C1BF2625D01400E06BA7 /* AppDelegate.swift in Sources */, + ACF2C1C12625D01400E06BA7 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + ACF2C1CD2625D01600E06BA7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ACF2C1D62625D01600E06BA7 /* GroupDSLTests.swift in Sources */, + ACF2C1FF2625FC8E00E06BA7 /* assertLayouts.swift in Sources */, + ACDE9DC92626E1750033F0B0 /* SupplementaryItemDSLTests.swift in Sources */, + ACF2C1FA2625E8A500E06BA7 /* TestingCellView.swift in Sources */, + ACF2C20426260A9D00E06BA7 /* SectionDSLTests.swift in Sources */, + ACDE9DCD2626ECCE0033F0B0 /* TestingDecorationView.swift in Sources */, + ACF2C1F52625E79C00E06BA7 /* TestingCollectionViewController.swift in Sources */, + ACDE9DCB2626EC0B0033F0B0 /* DecorationItemDSLTests.swift in Sources */, + ACF2C2012625FF3A00E06BA7 /* TestingCollectionViewModel.swift in Sources */, + ACF2C1F82625E89300E06BA7 /* TestingSupplementaryView.swift in Sources */, + ACF2C1FD2625FC6E00E06BA7 /* NSCollectionLayoutSize+Utils.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + ACD60C7C26331A35006F6462 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = ACD60C7526331A35006F6462 /* CompositionalLayoutDSL */; + targetProxy = ACD60C7B26331A35006F6462 /* PBXContainerItemProxy */; + }; + ACF2C1D32625D01600E06BA7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = ACF2C1BA2625D01400E06BA7 /* CompositionalLayoutDSLApp */; + targetProxy = ACF2C1D22625D01600E06BA7 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + ACF2C1C92625D01500E06BA7 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + ACF2C1CA2625D01500E06BA7 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + ACD60C8026331A35006F6462 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = ACD60C8426331A65006F6462 /* UniversalFramework.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = LL36D6ZV57; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = CompositionalLayoutDSL/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.fabernovel.CompositionalLayoutDSL; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3,6"; + TVOS_DEPLOYMENT_TARGET = 13.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + ACD60C8126331A35006F6462 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = ACD60C8426331A65006F6462 /* UniversalFramework.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = LL36D6ZV57; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = CompositionalLayoutDSL/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.fabernovel.CompositionalLayoutDSL; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3,6"; + TVOS_DEPLOYMENT_TARGET = 13.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + ACF2C1E32625D01600E06BA7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + ACF2C1E42625D01600E06BA7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + ACF2C1E62625D01600E06BA7 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 74E75573EC429923E664F111 /* Pods-CompositionalLayoutDSLApp.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = LL36D6ZV57; + INFOPLIST_FILE = CompositionalLayoutDSLApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.fabernovel.CompositionalLayoutDSLApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + ACF2C1E72625D01600E06BA7 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 000E561C82B4F1F842B27FB8 /* Pods-CompositionalLayoutDSLApp.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = LL36D6ZV57; + INFOPLIST_FILE = CompositionalLayoutDSLApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.fabernovel.CompositionalLayoutDSLApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + ACF2C1E92625D01600E06BA7 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E711B7B3BE01BEA25E83E41C /* Pods-CompositionalLayoutDSLTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = LL36D6ZV57; + INFOPLIST_FILE = CompositionalLayoutDSLTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.fabernovel.CompositionalLayoutDSLTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CompositionalLayoutDSLApp.app/CompositionalLayoutDSLApp"; + }; + name = Debug; + }; + ACF2C1EA2625D01600E06BA7 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F40EB76D180E510B0ADF8C8F /* Pods-CompositionalLayoutDSLTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = LL36D6ZV57; + INFOPLIST_FILE = CompositionalLayoutDSLTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.fabernovel.CompositionalLayoutDSLTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CompositionalLayoutDSLApp.app/CompositionalLayoutDSLApp"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + ACD60C7F26331A35006F6462 /* Build configuration list for PBXNativeTarget "CompositionalLayoutDSL" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + ACD60C8026331A35006F6462 /* Debug */, + ACD60C8126331A35006F6462 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + ACF2C1B62625D01400E06BA7 /* Build configuration list for PBXProject "CompositionalLayoutDSL" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + ACF2C1E32625D01600E06BA7 /* Debug */, + ACF2C1E42625D01600E06BA7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + ACF2C1E52625D01600E06BA7 /* Build configuration list for PBXNativeTarget "CompositionalLayoutDSLApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + ACF2C1E62625D01600E06BA7 /* Debug */, + ACF2C1E72625D01600E06BA7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + ACF2C1E82625D01600E06BA7 /* Build configuration list for PBXNativeTarget "CompositionalLayoutDSLTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + ACF2C1E92625D01600E06BA7 /* Debug */, + ACF2C1EA2625D01600E06BA7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = ACF2C1B32625D01400E06BA7 /* Project object */; +} diff --git a/CompositionalLayoutDSL.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/CompositionalLayoutDSL.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/CompositionalLayoutDSL.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/CompositionalLayoutDSL.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/CompositionalLayoutDSL.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/CompositionalLayoutDSL.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/CompositionalLayoutDSL.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CompositionalLayoutDSL.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..69315f2 --- /dev/null +++ b/CompositionalLayoutDSL.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "CompositionalLayoutDSL", + "repositoryURL": "git@github.com:faberNovel/CompositionalLayoutDSL.git", + "state": { + "branch": "develop", + "revision": "007263619d6213ffe8a5462372d404ed4b833e61", + "version": null + } + } + ] + }, + "version": 1 +} diff --git a/CompositionalLayoutDSL.xcodeproj/xcshareddata/xcschemes/CompositionalLayoutDSL.xcscheme b/CompositionalLayoutDSL.xcodeproj/xcshareddata/xcschemes/CompositionalLayoutDSL.xcscheme new file mode 100644 index 0000000..5683963 --- /dev/null +++ b/CompositionalLayoutDSL.xcodeproj/xcshareddata/xcschemes/CompositionalLayoutDSL.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CompositionalLayoutDSL.xcodeproj/xcshareddata/xcschemes/CompositionalLayoutDSLApp.xcscheme b/CompositionalLayoutDSL.xcodeproj/xcshareddata/xcschemes/CompositionalLayoutDSLApp.xcscheme new file mode 100644 index 0000000..befdf9c --- /dev/null +++ b/CompositionalLayoutDSL.xcodeproj/xcshareddata/xcschemes/CompositionalLayoutDSLApp.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CompositionalLayoutDSL.xcworkspace/contents.xcworkspacedata b/CompositionalLayoutDSL.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..042380b --- /dev/null +++ b/CompositionalLayoutDSL.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/CompositionalLayoutDSL.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/CompositionalLayoutDSL.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/CompositionalLayoutDSL.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/CompositionalLayoutDSL/Info.plist b/CompositionalLayoutDSL/Info.plist new file mode 100644 index 0000000..9bcb244 --- /dev/null +++ b/CompositionalLayoutDSL/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/CompositionalLayoutDSL/UniversalFramework.xcconfig b/CompositionalLayoutDSL/UniversalFramework.xcconfig new file mode 100644 index 0000000..ba4f450 --- /dev/null +++ b/CompositionalLayoutDSL/UniversalFramework.xcconfig @@ -0,0 +1,38 @@ +// +// Source: https://github.com/mrackwitz/xcconfigs +// Copyright (c) 2014-2015 Marius Rackwitz. All rights reserved. +// + +// Make it universal +SUPPORTED_PLATFORMS = macosx iphonesimulator iphoneos appletvos appletvsimulator +VALID_ARCHS[sdk=macosx*] = i386 x86_64 +VALID_ARCHS[sdk=iphoneos*] = arm64 armv7 armv7s +VALID_ARCHS[sdk=iphonesimulator*] = i386 x86_64 +VALID_ARCHS[sdk=appletvos*] = arm64 +VALID_ARCHS[sdk=appletvsimulator*] = x86_64 + +// Dynamic linking uses different default copy paths +LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' +LD_RUNPATH_SEARCH_PATHS[sdk=iphoneos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +LD_RUNPATH_SEARCH_PATHS[sdk=iphonesimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +LD_RUNPATH_SEARCH_PATHS[sdk=appletvos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +LD_RUNPATH_SEARCH_PATHS[sdk=appletvsimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' + +// OSX-specific default settings +FRAMEWORK_VERSION[sdk=macosx*] = A +COMBINE_HIDPI_IMAGES[sdk=macosx*] = YES + +// iOS-specific default settings +CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer +TARGETED_DEVICE_FAMILY[sdk=iphonesimulator*] = 1,2 +TARGETED_DEVICE_FAMILY[sdk=iphone*] = 1,2 + +// TV-specific default settings +TARGETED_DEVICE_FAMILY[sdk=appletvsimulator*] = 3 +TARGETED_DEVICE_FAMILY[sdk=appletv*] = 3 + +ENABLE_BITCODE[sdk=macosx*] = NO +ENABLE_BITCODE[sdk=iphonesimulator*] = YES +ENABLE_BITCODE[sdk=iphone*] = YES +ENABLE_BITCODE[sdk=appletvsimulator*] = YES +ENABLE_BITCODE[sdk=appletv*] = YES diff --git a/CompositionalLayoutDSLApp/AppDelegate.swift b/CompositionalLayoutDSLApp/AppDelegate.swift new file mode 100644 index 0000000..5a5deb3 --- /dev/null +++ b/CompositionalLayoutDSLApp/AppDelegate.swift @@ -0,0 +1,33 @@ +// +// AppDelegate.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 13/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application( + _ application: UIApplication, + // swiftlint:disable:next discouraged_optional_collection + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, + didDiscardSceneSessions sceneSessions: Set) { + } +} diff --git a/CompositionalLayoutDSLApp/Assets.xcassets/AccentColor.colorset/Contents.json b/CompositionalLayoutDSLApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/CompositionalLayoutDSLApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CompositionalLayoutDSLApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/CompositionalLayoutDSLApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/CompositionalLayoutDSLApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CompositionalLayoutDSLApp/Assets.xcassets/Contents.json b/CompositionalLayoutDSLApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/CompositionalLayoutDSLApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CompositionalLayoutDSLApp/Base.lproj/LaunchScreen.storyboard b/CompositionalLayoutDSLApp/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/CompositionalLayoutDSLApp/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CompositionalLayoutDSLApp/Info.plist b/CompositionalLayoutDSLApp/Info.plist new file mode 100644 index 0000000..2688b32 --- /dev/null +++ b/CompositionalLayoutDSLApp/Info.plist @@ -0,0 +1,62 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/CompositionalLayoutDSLApp/SceneDelegate.swift b/CompositionalLayoutDSLApp/SceneDelegate.swift new file mode 100644 index 0000000..1e00416 --- /dev/null +++ b/CompositionalLayoutDSLApp/SceneDelegate.swift @@ -0,0 +1,36 @@ +// +// SceneDelegate.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 13/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let windowScene = (scene as? UIWindowScene) else { return } + let window = UIWindow(windowScene: windowScene) + self.window = window + window.rootViewController = ViewController() + window.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) {} + + func sceneDidBecomeActive(_ scene: UIScene) {} + + func sceneWillResignActive(_ scene: UIScene) {} + + func sceneWillEnterForeground(_ scene: UIScene) {} + + func sceneDidEnterBackground(_ scene: UIScene) {} +} diff --git a/CompositionalLayoutDSLApp/ViewController.swift b/CompositionalLayoutDSLApp/ViewController.swift new file mode 100644 index 0000000..13e1ceb --- /dev/null +++ b/CompositionalLayoutDSLApp/ViewController.swift @@ -0,0 +1,11 @@ +// +// ViewController.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 13/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit + +class ViewController: UIViewController {} diff --git a/CompositionalLayoutDSLTests/Info.plist b/CompositionalLayoutDSLTests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/CompositionalLayoutDSLTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/CompositionalLayoutDSLTests/LayoutTests/DecorationItemDSLTests.swift b/CompositionalLayoutDSLTests/LayoutTests/DecorationItemDSLTests.swift new file mode 100644 index 0000000..61dc671 --- /dev/null +++ b/CompositionalLayoutDSLTests/LayoutTests/DecorationItemDSLTests.swift @@ -0,0 +1,74 @@ +// +// DecorationItemDSLTests.swift +// CompositionalLayoutDSLTests +// +// Created by Alexandre Podlewski on 14/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import XCTest +@testable import CompositionalLayoutDSL +import SnapshotTesting + +class DecorationItemDSLTests: XCTestCase { + + func testDecorationItem() throws { + let traditionalLayout = TestTraditionalListSectionLayout() + let dslLayout = TestListSectionSection() + + assertLayouts( + layout1: UICollectionViewCompositionalLayout( + section: traditionalLayout.section + ).registeringDecorationView(), + layout2: LayoutBuilder { dslLayout.layoutSection }.registeringDecorationView(), + as: .image(on: .iPhoneX, traits: UITraitCollection(userInterfaceStyle: .light)), + named: "testDecorationItem", + maxTestsCount: 5 + ) + } +} + +private extension UICollectionViewLayout { + func registeringDecorationView() -> UICollectionViewLayout { + self.register(TestingDecorationView.self, forDecorationViewOfKind: TestingDecorationView.kind) + return self + } +} + +private struct TestListSectionSection: LayoutSection { + + var layoutSection: LayoutSection { + Section { + HGroup(count: 4) { Item() } + .height(.absolute(40)) + .interItemSpacing(.fixed(8)) + } + .interGroupSpacing(8) + .orthogonalScrollingBehavior(.continuous) + .decorationItems { + DecorationItem(elementKind: TestingDecorationView.kind) + } + .contentInsets(value: 16) + } +} + +private struct TestTraditionalListSectionLayout { + + var section: NSCollectionLayoutSection { + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: .absoluteHeight(40), + subitem: NSCollectionLayoutItem(layoutSize: .fractional()), + count: 4 + ) + group.interItemSpacing = .fixed(8) + + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = 8 + section.orthogonalScrollingBehavior = .continuous + section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16) + section.decorationItems = [ + .background(elementKind: TestingDecorationView.kind) + ] + return section + } +} diff --git a/CompositionalLayoutDSLTests/LayoutTests/GroupDSLTests.swift b/CompositionalLayoutDSLTests/LayoutTests/GroupDSLTests.swift new file mode 100644 index 0000000..8a3afea --- /dev/null +++ b/CompositionalLayoutDSLTests/LayoutTests/GroupDSLTests.swift @@ -0,0 +1,94 @@ +// +// GroupDSLTests.swift +// CompositionalLayoutDSLTests +// +// Created by Alexandre Podlewski on 13/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import XCTest +@testable import CompositionalLayoutDSL +import SnapshotTesting + +class CompositionalLayoutDSLTests: XCTestCase { + + func testInnerGroups() throws { + let traditionalLayout = TestInnerGroupsTraditionalLayout() + let dslLayout = testInnerGroupsLayout() + assertLayouts( + layout1: UICollectionViewCompositionalLayout( + section: traditionalLayout.section, + configuration: traditionalLayout.configuration + ), + layout2: LayoutBuilder { dslLayout }, + as: .image(on: .iPhoneX, traits: UITraitCollection(userInterfaceStyle: .light)), + named: "InnerGroups", + maxTestsCount: 5 + ) + } +} + +func testInnerGroupsLayout() -> CompositionalLayout { + CompositionalLayout { _, _ in + Section { + HGroup { + Item(width: .fractionalWidth(1 / 3)) + .contentInsets(trailing: 4) + VGroup(count: 2) { Item() } + .width(.fractionalWidth(1 / 3)) + .interItemSpacing(.fixed(8)) + .contentInsets(horizontal: 4) + VGroup(count: 3) { Item() } + .width(.fractionalWidth(1 / 3)) + .interItemSpacing(.fixed(8)) + .contentInsets(leading: 4) + } + .height(.absolute(100)) + .contentInsets(horizontal: 16) + } + .interGroupSpacing(8) + } + .interSectionSpacing(8) +} + +private struct TestInnerGroupsTraditionalLayout { + + var section: NSCollectionLayoutSection { + let item = NSCollectionLayoutItem(layoutSize: .fractional(width: 1 / 3)) + item.contentInsets.trailing = 4 + + let innerGroup1 = NSCollectionLayoutGroup.vertical( + layoutSize: .fractional(width: 1 / 3), + subitem: NSCollectionLayoutItem(layoutSize: .fractional()), + count: 2 + ) + innerGroup1.interItemSpacing = .fixed(8) + innerGroup1.contentInsets.leading = 4 + innerGroup1.contentInsets.trailing = 4 + + let innerGroup2 = NSCollectionLayoutGroup.vertical( + layoutSize: .fractional(width: 1 / 3), + subitem: NSCollectionLayoutItem(layoutSize: .fractional()), + count: 3 + ) + innerGroup2.interItemSpacing = .fixed(8) + innerGroup2.contentInsets.leading = 4 + + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: .absoluteHeight(100), + subitems: [item, innerGroup1, innerGroup2] + ) + group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) + + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = 8 + + return section + } + + var configuration: UICollectionViewCompositionalLayoutConfiguration { + let configuration = UICollectionViewCompositionalLayoutConfiguration() + configuration.interSectionSpacing = 8 + return configuration + } +} diff --git a/CompositionalLayoutDSLTests/LayoutTests/SectionDSLTests.swift b/CompositionalLayoutDSLTests/LayoutTests/SectionDSLTests.swift new file mode 100644 index 0000000..fd938fb --- /dev/null +++ b/CompositionalLayoutDSLTests/LayoutTests/SectionDSLTests.swift @@ -0,0 +1,79 @@ +// +// SectionDSLTests.swift +// CompositionalLayoutDSLTests +// +// Created by Alexandre Podlewski on 13/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import XCTest +@testable import CompositionalLayoutDSL +import SnapshotTesting + +class SectionDSLTests: XCTestCase { + func testListSection() throws { + let traditionalLayout = TestTraditionalListSectionLayout() + let dslLayout = TestListSectionSection() + + assertLayouts( + layout1: UICollectionViewCompositionalLayout( + section: traditionalLayout.section + ), + layout2: LayoutBuilder { dslLayout.layoutSection }, + as: .image(on: .iPhoneX, traits: UITraitCollection(userInterfaceStyle: .light)), + named: "ListSection", + maxTestsCount: 5 + ) + } +} + +private struct TestListSectionSection: LayoutSection { + + var layoutSection: LayoutSection { + Section { + HGroup(count: 1) { Item() } + .height(.absolute(40)) + } + .interGroupSpacing(2) + .boundarySupplementaryItems { + BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) + .height(.absolute(24)) + .alignment(.top) + .pinToVisibleBounds(true) + BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionFooter) + .height(.absolute(24)) + .alignment(.bottom) + .pinToVisibleBounds(true) + } + } +} + +private struct TestTraditionalListSectionLayout { + + var section: NSCollectionLayoutSection { + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: .absoluteHeight(40), + subitem: NSCollectionLayoutItem(layoutSize: .fractional()), + count: 1 + ) + + let header = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: .absoluteHeight(24), + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top + ) + header.pinToVisibleBounds = true + let footer = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: .absoluteHeight(24), + elementKind: UICollectionView.elementKindSectionFooter, + alignment: .bottom + ) + footer.pinToVisibleBounds = true + + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = 2 + section.boundarySupplementaryItems = [header, footer] + + return section + } +} diff --git a/CompositionalLayoutDSLTests/LayoutTests/SupplementaryItemDSLTests.swift b/CompositionalLayoutDSLTests/LayoutTests/SupplementaryItemDSLTests.swift new file mode 100644 index 0000000..183be37 --- /dev/null +++ b/CompositionalLayoutDSLTests/LayoutTests/SupplementaryItemDSLTests.swift @@ -0,0 +1,110 @@ +// +// SupplementaryItemDSLTests.swift +// CompositionalLayoutDSLTests +// +// Created by Alexandre Podlewski on 14/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import XCTest +@testable import CompositionalLayoutDSL +import SnapshotTesting + +class SupplementaryItemDSLTests: XCTestCase { + func testSupplementaryItem() throws { + let traditionalLayout = TestTraditionalSectionLayout() + let dslLayout = TestSectionSection() + + assertLayouts( + layout1: UICollectionViewCompositionalLayout( + section: traditionalLayout.section + ), + layout2: LayoutBuilder { dslLayout.layoutSection }, + as: .image(on: .iPhoneX, traits: UITraitCollection(userInterfaceStyle: .light)), + named: "testSupplementaryItem", + maxTestsCount: 5 + ) + } +} + +private struct TestSectionSection: LayoutSection { + + var layoutSection: LayoutSection { + Section { + HGroup(count: 4) { + Item { + SupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) + .height(.absolute(15)) + .width(.absolute(15)) + .containerAnchor( + edges: [.top, .trailing], + offset: .fractional(x: 0.5, y: -0.5) + ) + } + .height(.fractionalWidth(1 / 4)) + } + .height(.absolute(80)) + .interItemSpacing(.fixed(16)) + .supplementaryItems { + SupplementaryItem(elementKind: UICollectionView.elementKindSectionFooter) + .height(.absolute(60)) + .width(.absolute(20)) + .containerAnchor(edges: .trailing, offset: .fractional(x: 1, y: 0)) + } + .contentInsets(trailing: 20) + } + .interGroupSpacing(16) + .contentInsets(bottom: 16) + } +} + +private struct TestTraditionalSectionLayout { + + var section: NSCollectionLayoutSection { + let badgeItemSize = NSCollectionLayoutSize( + widthDimension: .absolute(15), + heightDimension: .absolute(15) + ) + let badgeItem = NSCollectionLayoutSupplementaryItem( + layoutSize: badgeItemSize, + elementKind: UICollectionView.elementKindSectionHeader, + containerAnchor: NSCollectionLayoutAnchor( + edges: [.top, .trailing], + fractionalOffset: CGPoint(x: 0.5, y: -0.5) + ) + ) + + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalWidth(1 / 4) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [badgeItem]) + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: .absoluteHeight(80), + subitem: item, + count: 4 + ) + group.interItemSpacing = .fixed(16) + + let groupTrailingItemSize = NSCollectionLayoutSize( + widthDimension: .absolute(20), + heightDimension: .absolute(60) + ) + let groupTrailingItem = NSCollectionLayoutSupplementaryItem( + layoutSize: groupTrailingItemSize, + elementKind: UICollectionView.elementKindSectionFooter, + containerAnchor: NSCollectionLayoutAnchor( + edges: [.trailing], + fractionalOffset: CGPoint(x: 1, y: 0) + ) + ) + group.supplementaryItems = [groupTrailingItem] + group.contentInsets.trailing = 20 + + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = 16 + section.contentInsets.bottom = 16 + + return section + } +} diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.1.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.1.png new file mode 100644 index 0000000..fcf5d68 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.1.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.2.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.2.png new file mode 100644 index 0000000..57463c6 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.2.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.3.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.3.png new file mode 100644 index 0000000..c2de1b8 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.3.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.4.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.4.png new file mode 100644 index 0000000..0ccd807 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.4.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.5.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.5.png new file mode 100644 index 0000000..187ac56 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/DecorationItemDSLTests/testDecorationItem.5.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.1.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.1.png new file mode 100644 index 0000000..620ccd3 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.1.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.2.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.2.png new file mode 100644 index 0000000..c6238b7 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.2.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.3.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.3.png new file mode 100644 index 0000000..f4bcf7c Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.3.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.4.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.4.png new file mode 100644 index 0000000..f44eebb Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.4.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.5.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.5.png new file mode 100644 index 0000000..be71d4b Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/GroupDSLTests/testInnerGroups.5.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.1.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.1.png new file mode 100644 index 0000000..76678f3 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.1.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.2.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.2.png new file mode 100644 index 0000000..c71fc93 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.2.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.3.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.3.png new file mode 100644 index 0000000..887b120 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.3.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.4.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.4.png new file mode 100644 index 0000000..f0896d0 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.4.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.5.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.5.png new file mode 100644 index 0000000..c71fc93 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SectionDSLTests/testListSection.5.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.1.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.1.png new file mode 100644 index 0000000..fb759e9 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.1.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.2.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.2.png new file mode 100644 index 0000000..dad3882 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.2.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.3.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.3.png new file mode 100644 index 0000000..c82ed4a Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.3.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.4.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.4.png new file mode 100644 index 0000000..096ae61 Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.4.png differ diff --git a/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.5.png b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.5.png new file mode 100644 index 0000000..617aadf Binary files /dev/null and b/CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/SupplementaryItemDSLTests/testSupplementaryItem.5.png differ diff --git a/CompositionalLayoutDSLTests/TestingCollectionView/TestingCellView.swift b/CompositionalLayoutDSLTests/TestingCollectionView/TestingCellView.swift new file mode 100644 index 0000000..050c856 --- /dev/null +++ b/CompositionalLayoutDSLTests/TestingCollectionView/TestingCellView.swift @@ -0,0 +1,48 @@ +// +// TestingCellView.swift +// CompositionalLayoutDSLTests +// +// Created by Alexandre Podlewski on 13/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit + +class TestingCellView: UICollectionViewCell { + + private let label = UILabel() + + // MARK: - Life cycle + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + override func prepareForReuse() { + super.prepareForReuse() + label.text = nil + } + + // MARK: - CellView + + func configure(with text: String) { + label.text = text + } + + // MARK: - Private + + private func setup() { + backgroundColor = .gray + layer.cornerRadius = 6 + contentView.addSubview(label) + label.translatesAutoresizingMaskIntoConstraints = false + label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true + label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true + } +} diff --git a/CompositionalLayoutDSLTests/TestingCollectionView/TestingCollectionViewController.swift b/CompositionalLayoutDSLTests/TestingCollectionView/TestingCollectionViewController.swift new file mode 100644 index 0000000..8e413b2 --- /dev/null +++ b/CompositionalLayoutDSLTests/TestingCollectionView/TestingCollectionViewController.swift @@ -0,0 +1,90 @@ +// +// TestingCollectionViewController.swift +// CompositionalLayoutDSLTests +// +// Created by Alexandre Podlewski on 13/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit + +class TestingCollectionViewController: UICollectionViewController { + + private static let cellIdentifier = "TestingCellView" + private static let supplementaryIdentifier = "TestingSupplementaryView" + + private var sectionItemsCount: [Int] = [] + + // MARK: - Life cycle + + init() { + super.init(collectionViewLayout: UICollectionViewFlowLayout()) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func viewDidLoad() { + super.viewDidLoad() + collectionView.backgroundColor = .systemBackground + collectionView.register(TestingCellView.self, forCellWithReuseIdentifier: Self.cellIdentifier) + collectionView.register( + TestingSupplementaryView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: Self.supplementaryIdentifier + ) + collectionView.register( + TestingSupplementaryView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, + withReuseIdentifier: Self.supplementaryIdentifier + ) + } + + // MARK: - TestingCollectionViewController + + func configure(with viewModel: TestingCollectionViewModel) { + sectionItemsCount = viewModel.sectionItemsCount + collectionView.reloadData() + } + + // MARK: - UICollectionViewDataSource + + override func numberOfSections(in collectionView: UICollectionView) -> Int { + return sectionItemsCount.count + } + + override func collectionView(_ collectionView: UICollectionView, + numberOfItemsInSection section: Int) -> Int { + return sectionItemsCount[section] + } + + override func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: Self.cellIdentifier, + for: indexPath + ) as? TestingCellView + else { fatalError("Unexpected dequeued cell") } + cell.configure(with: "\(indexPath)") + return cell + } + + override func collectionView( + _ collectionView: UICollectionView, + viewForSupplementaryElementOfKind kind: String, + at indexPath: IndexPath + ) -> UICollectionReusableView { + guard + let supplementaryView = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: Self.supplementaryIdentifier, + for: indexPath + ) as? TestingSupplementaryView + else { fatalError("Unexpected dequeued supplementary view") } + + supplementaryView.configure(with: "\(indexPath)") + return supplementaryView + } +} diff --git a/CompositionalLayoutDSLTests/TestingCollectionView/TestingCollectionViewModel.swift b/CompositionalLayoutDSLTests/TestingCollectionView/TestingCollectionViewModel.swift new file mode 100644 index 0000000..a48b228 --- /dev/null +++ b/CompositionalLayoutDSLTests/TestingCollectionView/TestingCollectionViewModel.swift @@ -0,0 +1,27 @@ +// +// TestingCollectionViewModel.swift +// CompositionalLayoutDSLTests +// +// Created by Alexandre Podlewski on 13/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import Foundation +import SwiftCheck + +struct TestingCollectionViewModel { + let sectionItemsCount: [Int] +} + +extension TestingCollectionViewModel: Arbitrary { + + static var arbitrary: Gen { + Gen.compose { c in + TestingCollectionViewModel( + sectionItemsCount: c.generate( + using: Gen.choose((0, 40)).proliferateNonEmpty + ) + ) + } + } +} diff --git a/CompositionalLayoutDSLTests/TestingCollectionView/TestingDecorationView.swift b/CompositionalLayoutDSLTests/TestingCollectionView/TestingDecorationView.swift new file mode 100644 index 0000000..0f7f90a --- /dev/null +++ b/CompositionalLayoutDSLTests/TestingCollectionView/TestingDecorationView.swift @@ -0,0 +1,34 @@ +// +// TestingDecorationView.swift +// CompositionalLayoutDSLTests +// +// Created by Alexandre Podlewski on 14/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit + +class TestingDecorationView: UICollectionReusableView { + + static let kind = "TestingDecorationView" + + // MARK: - Life cycle + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + // MARK: - Private + + private func setup() { + backgroundColor = .systemBackground + layer.borderWidth = 5 + layer.borderColor = UIColor.cyan.cgColor + } +} diff --git a/CompositionalLayoutDSLTests/TestingCollectionView/TestingSupplementaryView.swift b/CompositionalLayoutDSLTests/TestingCollectionView/TestingSupplementaryView.swift new file mode 100644 index 0000000..8f6314e --- /dev/null +++ b/CompositionalLayoutDSLTests/TestingCollectionView/TestingSupplementaryView.swift @@ -0,0 +1,47 @@ +// +// TestingSupplementaryView.swift +// CompositionalLayoutDSLTests +// +// Created by Alexandre Podlewski on 13/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit + +class TestingSupplementaryView: UICollectionReusableView { + + private let label = UILabel() + + // MARK: - Life cycle + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + override func prepareForReuse() { + super.prepareForReuse() + label.text = nil + } + + // MARK: - TestingSupplementaryView + + func configure(with text: String) { + label.text = text + } + + // MARK: - Private + + private func setup() { + backgroundColor = UIColor { $0.userInterfaceStyle == .dark ? .darkGray : .lightGray } + addSubview(label) + label.translatesAutoresizingMaskIntoConstraints = false + label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true + label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + } +} diff --git a/CompositionalLayoutDSLTests/Utils/NSCollectionLayoutSize+Utils.swift b/CompositionalLayoutDSLTests/Utils/NSCollectionLayoutSize+Utils.swift new file mode 100644 index 0000000..611b720 --- /dev/null +++ b/CompositionalLayoutDSLTests/Utils/NSCollectionLayoutSize+Utils.swift @@ -0,0 +1,24 @@ +// +// NSCollectionLayoutSize+Utils.swift +// CompositionalLayoutDSLTests +// +// Created by Alexandre Podlewski on 13/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit + +extension NSCollectionLayoutSize { + static func fractional(width: CGFloat = 1.0, height: CGFloat = 1.0) -> NSCollectionLayoutSize { + return NSCollectionLayoutSize( + widthDimension: .fractionalWidth(width), + heightDimension: .fractionalHeight(height) + ) + } + static func absoluteHeight(_ height: CGFloat) -> NSCollectionLayoutSize { + return NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .absolute(height) + ) + } +} diff --git a/CompositionalLayoutDSLTests/Utils/assertLayouts.swift b/CompositionalLayoutDSLTests/Utils/assertLayouts.swift new file mode 100644 index 0000000..7b8b835 --- /dev/null +++ b/CompositionalLayoutDSLTests/Utils/assertLayouts.swift @@ -0,0 +1,95 @@ +// +// assertLayouts.swift +// CompositionalLayoutDSLTests +// +// Created by Alexandre Podlewski on 13/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import XCTest +import SnapshotTesting +import ADAssertLayout +import ADLayoutTest + +extension XCTestCase { + + func assertLayouts( + layout1: UICollectionViewLayout, + layout2: UICollectionViewLayout, + as snapshotting: Snapshotting, + named name: String, + maxTestsCount: Int, + file: StaticString = #file, + testName: String = #function, + line: UInt = #line + ) { + var counter = 0 + runLayoutTests( + named: name, + snapshotStrategy: .failureOnly, + randomStrategy: .consistent, + maxTestsCount: maxTestsCount, + file: file, + line: line, + run: { (viewModel: TestingCollectionViewModel) -> ViewAssertionResult in + counter += 1 + return self.compareLayout( + viewModel: viewModel, + layout1: layout1, + layout2: layout2, + as: snapshotting, + counter: counter, + file: file, + testName: testName, + line: line + ) + } + ) + } + + private func compareLayout( + viewModel: TestingCollectionViewModel, + layout1: UICollectionViewLayout, + layout2: UICollectionViewLayout, + as snapshotting: Snapshotting, + counter: Int, + file: StaticString = #file, + testName: String = #function, + line: UInt = #line + ) -> ViewAssertionResult { + let controller = TestingCollectionViewController() + // ???: (Alexandre Podlewski) 13/04/2021 Needed for all cells to be rendered + controller.view.frame.size.height = 3000 + controller.view.frame.size.width = 3000 + controller.configure(with: viewModel) + + controller.collectionView.collectionViewLayout = layout1 + controller.collectionView.collectionViewLayout.invalidateLayout() + controller.collectionView.reloadData() + + assertSnapshot( + matching: controller, + as: snapshotting, + named: String(counter), + file: file, + testName: testName, + line: line + ) + + controller.collectionView.collectionViewLayout = layout2 + controller.collectionView.collectionViewLayout.invalidateLayout() + controller.collectionView.reloadData() + + assertSnapshot( + matching: controller, + as: snapshotting, + named: String(counter), + file: file, + testName: testName, + line: line + ) + + return .success + } +} diff --git a/Dangerfile b/Dangerfile new file mode 100644 index 0000000..83477ae --- /dev/null +++ b/Dangerfile @@ -0,0 +1,9 @@ +## SwiftLint + +swiftlint.binary_path = 'Pods/SwiftLint/swiftlint' +swiftlint.max_num_violations = 20 +swiftlint.lint_files( + fail_on_error: true, + inline_mode: true, + additional_swiftlint_args: "--strict" +) diff --git a/Example/CompositionalLayoutDSL_Example.xcodeproj/project.pbxproj b/Example/CompositionalLayoutDSL_Example.xcodeproj/project.pbxproj new file mode 100644 index 0000000..227f169 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example.xcodeproj/project.pbxproj @@ -0,0 +1,686 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + AC2AB9C0261F647200FB0857 /* DemoCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC622CE7261F63970058EEE3 /* DemoCollectionViewController.swift */; }; + AC2AB9C2261F64B600FB0857 /* DemoSupplementaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9C1261F64B600FB0857 /* DemoSupplementaryView.swift */; }; + AC2AB9C4261F64CE00FB0857 /* DemoCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9C3261F64CE00FB0857 /* DemoCellView.swift */; }; + AC2AB9C62620520200FB0857 /* ShowcaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9C52620520200FB0857 /* ShowcaseViewController.swift */; }; + AC2AB9CB2620529000FB0857 /* FractalGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9CA2620529000FB0857 /* FractalGroup.swift */; }; + AC2AB9CD262052B700FB0857 /* SectionWithHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9CC262052B700FB0857 /* SectionWithHeader.swift */; }; + AC2AB9CF262052D000FB0857 /* ListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9CE262052D000FB0857 /* ListSection.swift */; }; + AC2AB9D1262083DE00FB0857 /* SectionWithEnvironmentInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9D0262083DE00FB0857 /* SectionWithEnvironmentInsets.swift */; }; + AC2AB9D32620850D00FB0857 /* ColumnLaneSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9D22620850D00FB0857 /* ColumnLaneSection.swift */; }; + AC2AB9D52620852300FB0857 /* LaneSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9D42620852300FB0857 /* LaneSection.swift */; }; + AC2AB9DA2620BD7100FB0857 /* AppStoreNewContentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9D92620BD7100FB0857 /* AppStoreNewContentSection.swift */; }; + AC2AB9DC2620BDCA00FB0857 /* AppStoreTrendingContentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9DB2620BDCA00FB0857 /* AppStoreTrendingContentSection.swift */; }; + AC2AB9DE2620BDE800FB0857 /* AppStoreTopContentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9DD2620BDE800FB0857 /* AppStoreTopContentSection.swift */; }; + AC2AB9E02620BF4900FB0857 /* AdaptativeColumnLaneSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9DF2620BF4900FB0857 /* AdaptativeColumnLaneSection.swift */; }; + AC2AB9E32624A08400FB0857 /* CompositionalLayoutWithSupplementaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AB9E22624A08300FB0857 /* CompositionalLayoutWithSupplementaryView.swift */; }; + AC34ECC3261F5CE800DF2D74 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC34ECC2261F5CE800DF2D74 /* AppDelegate.swift */; }; + AC34ECC5261F5CE800DF2D74 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC34ECC4261F5CE800DF2D74 /* SceneDelegate.swift */; }; + AC34ECCC261F5CED00DF2D74 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC34ECCB261F5CED00DF2D74 /* Assets.xcassets */; }; + AC34ECCF261F5CED00DF2D74 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AC34ECCD261F5CED00DF2D74 /* LaunchScreen.storyboard */; }; + AC4BFFF32638626A00142A9E /* CompositionalLayoutDSL in Frameworks */ = {isa = PBXBuildFile; productRef = AC4BFFF22638626A00142A9E /* CompositionalLayoutDSL */; }; + AC622CE2261F606C0058EEE3 /* CompositionalLayoutDSL in Frameworks */ = {isa = PBXBuildFile; productRef = AC622CE1261F606C0058EEE3 /* CompositionalLayoutDSL */; }; + AC6292EA26303724005B5D3A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC6292E926303724005B5D3A /* AppDelegate.swift */; }; + AC6292EC26303724005B5D3A /* ShowcaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC6292EB26303724005B5D3A /* ShowcaseViewController.swift */; }; + AC6292EE26303727005B5D3A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC6292ED26303727005B5D3A /* Assets.xcassets */; }; + AC6292F126303727005B5D3A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AC6292EF26303727005B5D3A /* Main.storyboard */; }; + AC62930626303F38005B5D3A /* DemoSupplementaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC62930326303F38005B5D3A /* DemoSupplementaryView.swift */; }; + AC62930726303F38005B5D3A /* DemoCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC62930426303F38005B5D3A /* DemoCollectionViewController.swift */; }; + AC62930826303F38005B5D3A /* DemoCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC62930526303F38005B5D3A /* DemoCellView.swift */; }; + ACF2C1682624B00700E06BA7 /* GettingStartedCompositionalLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF2C1672624B00700E06BA7 /* GettingStartedCompositionalLayout.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + AC2AB9C1261F64B600FB0857 /* DemoSupplementaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoSupplementaryView.swift; sourceTree = ""; }; + AC2AB9C3261F64CE00FB0857 /* DemoCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoCellView.swift; sourceTree = ""; }; + AC2AB9C52620520200FB0857 /* ShowcaseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShowcaseViewController.swift; sourceTree = ""; }; + AC2AB9CA2620529000FB0857 /* FractalGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FractalGroup.swift; sourceTree = ""; }; + AC2AB9CC262052B700FB0857 /* SectionWithHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionWithHeader.swift; sourceTree = ""; }; + AC2AB9CE262052D000FB0857 /* ListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListSection.swift; sourceTree = ""; }; + AC2AB9D0262083DE00FB0857 /* SectionWithEnvironmentInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionWithEnvironmentInsets.swift; sourceTree = ""; }; + AC2AB9D22620850D00FB0857 /* ColumnLaneSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnLaneSection.swift; sourceTree = ""; }; + AC2AB9D42620852300FB0857 /* LaneSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaneSection.swift; sourceTree = ""; }; + AC2AB9D92620BD7100FB0857 /* AppStoreNewContentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreNewContentSection.swift; sourceTree = ""; }; + AC2AB9DB2620BDCA00FB0857 /* AppStoreTrendingContentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreTrendingContentSection.swift; sourceTree = ""; }; + AC2AB9DD2620BDE800FB0857 /* AppStoreTopContentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreTopContentSection.swift; sourceTree = ""; }; + AC2AB9DF2620BF4900FB0857 /* AdaptativeColumnLaneSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptativeColumnLaneSection.swift; sourceTree = ""; }; + AC2AB9E22624A08300FB0857 /* CompositionalLayoutWithSupplementaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionalLayoutWithSupplementaryView.swift; sourceTree = ""; }; + AC34ECBF261F5CE800DF2D74 /* CompositionalLayoutDSL_Example_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CompositionalLayoutDSL_Example_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AC34ECC2261F5CE800DF2D74 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + AC34ECC4261F5CE800DF2D74 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + AC34ECCB261F5CED00DF2D74 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AC34ECCE261F5CED00DF2D74 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + AC34ECD0261F5CED00DF2D74 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AC622CE3261F60740058EEE3 /* CompositionalLayoutDSL */ = {isa = PBXFileReference; lastKnownFileType = folder; name = CompositionalLayoutDSL; path = ..; sourceTree = ""; }; + AC622CE7261F63970058EEE3 /* DemoCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoCollectionViewController.swift; sourceTree = ""; }; + AC6292E726303724005B5D3A /* CompositionalLayoutDSL_Example_macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CompositionalLayoutDSL_Example_macOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AC6292E926303724005B5D3A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + AC6292EB26303724005B5D3A /* ShowcaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowcaseViewController.swift; sourceTree = ""; }; + AC6292ED26303727005B5D3A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AC6292F026303727005B5D3A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + AC6292F226303727005B5D3A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AC6292F326303727005B5D3A /* CompositionalLayoutDSL_Example_macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CompositionalLayoutDSL_Example_macOS.entitlements; sourceTree = ""; }; + AC62930326303F38005B5D3A /* DemoSupplementaryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoSupplementaryView.swift; sourceTree = ""; }; + AC62930426303F38005B5D3A /* DemoCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoCollectionViewController.swift; sourceTree = ""; }; + AC62930526303F38005B5D3A /* DemoCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoCellView.swift; sourceTree = ""; }; + ACF2C1672624B00700E06BA7 /* GettingStartedCompositionalLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedCompositionalLayout.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AC34ECBC261F5CE800DF2D74 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AC622CE2261F606C0058EEE3 /* CompositionalLayoutDSL in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AC6292E426303724005B5D3A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AC4BFFF32638626A00142A9E /* CompositionalLayoutDSL in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + AC2AB9C72620524600FB0857 /* ShowcaseViewController */ = { + isa = PBXGroup; + children = ( + AC2AB9D82620BD5B00FB0857 /* AppStoreLayoutLike */, + AC2AB9C92620528600FB0857 /* Group */, + AC2AB9C82620528200FB0857 /* Section */, + AC2AB9E12624A05D00FB0857 /* CompositionalLayout */, + AC2AB9C52620520200FB0857 /* ShowcaseViewController.swift */, + ); + path = ShowcaseViewController; + sourceTree = ""; + }; + AC2AB9C82620528200FB0857 /* Section */ = { + isa = PBXGroup; + children = ( + AC2AB9CC262052B700FB0857 /* SectionWithHeader.swift */, + AC2AB9CE262052D000FB0857 /* ListSection.swift */, + ); + path = Section; + sourceTree = ""; + }; + AC2AB9C92620528600FB0857 /* Group */ = { + isa = PBXGroup; + children = ( + AC2AB9CA2620529000FB0857 /* FractalGroup.swift */, + ); + path = Group; + sourceTree = ""; + }; + AC2AB9D6262086F000FB0857 /* Utils */ = { + isa = PBXGroup; + children = ( + AC2AB9D72620877E00FB0857 /* Section */, + ); + path = Utils; + sourceTree = ""; + }; + AC2AB9D72620877E00FB0857 /* Section */ = { + isa = PBXGroup; + children = ( + AC2AB9D0262083DE00FB0857 /* SectionWithEnvironmentInsets.swift */, + AC2AB9D22620850D00FB0857 /* ColumnLaneSection.swift */, + AC2AB9D42620852300FB0857 /* LaneSection.swift */, + ); + path = Section; + sourceTree = ""; + }; + AC2AB9D82620BD5B00FB0857 /* AppStoreLayoutLike */ = { + isa = PBXGroup; + children = ( + AC2AB9D92620BD7100FB0857 /* AppStoreNewContentSection.swift */, + AC2AB9DB2620BDCA00FB0857 /* AppStoreTrendingContentSection.swift */, + AC2AB9DD2620BDE800FB0857 /* AppStoreTopContentSection.swift */, + AC2AB9DF2620BF4900FB0857 /* AdaptativeColumnLaneSection.swift */, + ); + path = AppStoreLayoutLike; + sourceTree = ""; + }; + AC2AB9E12624A05D00FB0857 /* CompositionalLayout */ = { + isa = PBXGroup; + children = ( + AC2AB9E22624A08300FB0857 /* CompositionalLayoutWithSupplementaryView.swift */, + ACF2C1672624B00700E06BA7 /* GettingStartedCompositionalLayout.swift */, + ); + path = CompositionalLayout; + sourceTree = ""; + }; + AC34ECB6261F5CE800DF2D74 = { + isa = PBXGroup; + children = ( + AC34ECC1261F5CE800DF2D74 /* CompositionalLayoutDSL_Example_iOS */, + AC6292E826303724005B5D3A /* CompositionalLayoutDSL_Example_macOS */, + AC34ECC0261F5CE800DF2D74 /* Products */, + AC622CE3261F60740058EEE3 /* CompositionalLayoutDSL */, + AC62930C26306DF9005B5D3A /* Frameworks */, + ); + sourceTree = ""; + }; + AC34ECC0261F5CE800DF2D74 /* Products */ = { + isa = PBXGroup; + children = ( + AC34ECBF261F5CE800DF2D74 /* CompositionalLayoutDSL_Example_iOS.app */, + AC6292E726303724005B5D3A /* CompositionalLayoutDSL_Example_macOS.app */, + ); + name = Products; + sourceTree = ""; + }; + AC34ECC1261F5CE800DF2D74 /* CompositionalLayoutDSL_Example_iOS */ = { + isa = PBXGroup; + children = ( + AC34ECC2261F5CE800DF2D74 /* AppDelegate.swift */, + AC34ECC4261F5CE800DF2D74 /* SceneDelegate.swift */, + AC622CE6261F63910058EEE3 /* App */, + AC34ECCB261F5CED00DF2D74 /* Assets.xcassets */, + AC34ECCD261F5CED00DF2D74 /* LaunchScreen.storyboard */, + AC34ECD0261F5CED00DF2D74 /* Info.plist */, + ); + path = CompositionalLayoutDSL_Example_iOS; + sourceTree = ""; + }; + AC622CE6261F63910058EEE3 /* App */ = { + isa = PBXGroup; + children = ( + AC2AB9D6262086F000FB0857 /* Utils */, + AC2AB9C72620524600FB0857 /* ShowcaseViewController */, + AC622CE8261F63C40058EEE3 /* DemoCollectionViewController */, + ); + path = App; + sourceTree = ""; + }; + AC622CE8261F63C40058EEE3 /* DemoCollectionViewController */ = { + isa = PBXGroup; + children = ( + AC622CE7261F63970058EEE3 /* DemoCollectionViewController.swift */, + AC2AB9C1261F64B600FB0857 /* DemoSupplementaryView.swift */, + AC2AB9C3261F64CE00FB0857 /* DemoCellView.swift */, + ); + path = DemoCollectionViewController; + sourceTree = ""; + }; + AC6292E826303724005B5D3A /* CompositionalLayoutDSL_Example_macOS */ = { + isa = PBXGroup; + children = ( + AC6292E926303724005B5D3A /* AppDelegate.swift */, + AC62930126303F27005B5D3A /* App */, + AC6292ED26303727005B5D3A /* Assets.xcassets */, + AC6292EF26303727005B5D3A /* Main.storyboard */, + AC6292F226303727005B5D3A /* Info.plist */, + AC6292F326303727005B5D3A /* CompositionalLayoutDSL_Example_macOS.entitlements */, + ); + path = CompositionalLayoutDSL_Example_macOS; + sourceTree = ""; + }; + AC62930126303F27005B5D3A /* App */ = { + isa = PBXGroup; + children = ( + AC62930926305908005B5D3A /* ShowcaseViewController */, + AC62930226303F38005B5D3A /* DemoCollectionViewController */, + ); + path = App; + sourceTree = ""; + }; + AC62930226303F38005B5D3A /* DemoCollectionViewController */ = { + isa = PBXGroup; + children = ( + AC62930326303F38005B5D3A /* DemoSupplementaryView.swift */, + AC62930426303F38005B5D3A /* DemoCollectionViewController.swift */, + AC62930526303F38005B5D3A /* DemoCellView.swift */, + ); + path = DemoCollectionViewController; + sourceTree = ""; + }; + AC62930926305908005B5D3A /* ShowcaseViewController */ = { + isa = PBXGroup; + children = ( + AC6292EB26303724005B5D3A /* ShowcaseViewController.swift */, + ); + path = ShowcaseViewController; + sourceTree = ""; + }; + AC62930C26306DF9005B5D3A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AC34ECBE261F5CE800DF2D74 /* CompositionalLayoutDSL_Example_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = AC34ECD3261F5CED00DF2D74 /* Build configuration list for PBXNativeTarget "CompositionalLayoutDSL_Example_iOS" */; + buildPhases = ( + AC34ECBB261F5CE800DF2D74 /* Sources */, + AC34ECBC261F5CE800DF2D74 /* Frameworks */, + AC34ECBD261F5CE800DF2D74 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CompositionalLayoutDSL_Example_iOS; + packageProductDependencies = ( + AC622CE1261F606C0058EEE3 /* CompositionalLayoutDSL */, + ); + productName = CompositionalLayoutDSL_Example; + productReference = AC34ECBF261F5CE800DF2D74 /* CompositionalLayoutDSL_Example_iOS.app */; + productType = "com.apple.product-type.application"; + }; + AC6292E626303724005B5D3A /* CompositionalLayoutDSL_Example_macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = AC6292F626303727005B5D3A /* Build configuration list for PBXNativeTarget "CompositionalLayoutDSL_Example_macOS" */; + buildPhases = ( + AC6292E326303724005B5D3A /* Sources */, + AC6292E426303724005B5D3A /* Frameworks */, + AC6292E526303724005B5D3A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CompositionalLayoutDSL_Example_macOS; + packageProductDependencies = ( + AC4BFFF22638626A00142A9E /* CompositionalLayoutDSL */, + ); + productName = CompositionalLayoutDSL_Example_macOS; + productReference = AC6292E726303724005B5D3A /* CompositionalLayoutDSL_Example_macOS.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AC34ECB7261F5CE800DF2D74 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1250; + LastUpgradeCheck = 1250; + ORGANIZATIONNAME = Fabernovel; + TargetAttributes = { + AC34ECBE261F5CE800DF2D74 = { + CreatedOnToolsVersion = 12.5; + }; + AC6292E626303724005B5D3A = { + CreatedOnToolsVersion = 12.5; + }; + }; + }; + buildConfigurationList = AC34ECBA261F5CE800DF2D74 /* Build configuration list for PBXProject "CompositionalLayoutDSL_Example" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AC34ECB6261F5CE800DF2D74; + packageReferences = ( + AC622CE0261F606C0058EEE3 /* XCRemoteSwiftPackageReference "CompositionalLayoutDSL" */, + ); + productRefGroup = AC34ECC0261F5CE800DF2D74 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AC34ECBE261F5CE800DF2D74 /* CompositionalLayoutDSL_Example_iOS */, + AC6292E626303724005B5D3A /* CompositionalLayoutDSL_Example_macOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AC34ECBD261F5CE800DF2D74 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AC34ECCF261F5CED00DF2D74 /* LaunchScreen.storyboard in Resources */, + AC34ECCC261F5CED00DF2D74 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AC6292E526303724005B5D3A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AC6292EE26303727005B5D3A /* Assets.xcassets in Resources */, + AC6292F126303727005B5D3A /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AC34ECBB261F5CE800DF2D74 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AC2AB9C2261F64B600FB0857 /* DemoSupplementaryView.swift in Sources */, + AC2AB9CB2620529000FB0857 /* FractalGroup.swift in Sources */, + AC2AB9C0261F647200FB0857 /* DemoCollectionViewController.swift in Sources */, + AC34ECC3261F5CE800DF2D74 /* AppDelegate.swift in Sources */, + AC2AB9DE2620BDE800FB0857 /* AppStoreTopContentSection.swift in Sources */, + AC2AB9D32620850D00FB0857 /* ColumnLaneSection.swift in Sources */, + AC2AB9E02620BF4900FB0857 /* AdaptativeColumnLaneSection.swift in Sources */, + AC34ECC5261F5CE800DF2D74 /* SceneDelegate.swift in Sources */, + AC2AB9DC2620BDCA00FB0857 /* AppStoreTrendingContentSection.swift in Sources */, + AC2AB9DA2620BD7100FB0857 /* AppStoreNewContentSection.swift in Sources */, + AC2AB9D1262083DE00FB0857 /* SectionWithEnvironmentInsets.swift in Sources */, + AC2AB9E32624A08400FB0857 /* CompositionalLayoutWithSupplementaryView.swift in Sources */, + AC2AB9C4261F64CE00FB0857 /* DemoCellView.swift in Sources */, + AC2AB9CD262052B700FB0857 /* SectionWithHeader.swift in Sources */, + ACF2C1682624B00700E06BA7 /* GettingStartedCompositionalLayout.swift in Sources */, + AC2AB9D52620852300FB0857 /* LaneSection.swift in Sources */, + AC2AB9C62620520200FB0857 /* ShowcaseViewController.swift in Sources */, + AC2AB9CF262052D000FB0857 /* ListSection.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AC6292E326303724005B5D3A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AC6292EC26303724005B5D3A /* ShowcaseViewController.swift in Sources */, + AC62930726303F38005B5D3A /* DemoCollectionViewController.swift in Sources */, + AC6292EA26303724005B5D3A /* AppDelegate.swift in Sources */, + AC62930826303F38005B5D3A /* DemoCellView.swift in Sources */, + AC62930626303F38005B5D3A /* DemoSupplementaryView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + AC34ECCD261F5CED00DF2D74 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AC34ECCE261F5CED00DF2D74 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + AC6292EF26303727005B5D3A /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AC6292F026303727005B5D3A /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + AC34ECD1261F5CED00DF2D74 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + AC34ECD2261F5CED00DF2D74 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AC34ECD4261F5CED00DF2D74 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = LL36D6ZV57; + INFOPLIST_FILE = CompositionalLayoutDSL_Example_iOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.fabernovel.CompositionalLayoutDSL-Example"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AC34ECD5261F5CED00DF2D74 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = LL36D6ZV57; + INFOPLIST_FILE = CompositionalLayoutDSL_Example_iOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.fabernovel.CompositionalLayoutDSL-Example"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + AC6292F426303727005B5D3A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = CompositionalLayoutDSL_Example_macOS/CompositionalLayoutDSL_Example_macOS.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = LL36D6ZV57; + ENABLE_HARDENED_RUNTIME = YES; + INFOPLIST_FILE = CompositionalLayoutDSL_Example_macOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = "com.fabernovel.CompositionalLayoutDSL-Example-macOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + AC6292F526303727005B5D3A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = CompositionalLayoutDSL_Example_macOS/CompositionalLayoutDSL_Example_macOS.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = LL36D6ZV57; + ENABLE_HARDENED_RUNTIME = YES; + INFOPLIST_FILE = CompositionalLayoutDSL_Example_macOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = "com.fabernovel.CompositionalLayoutDSL-Example-macOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AC34ECBA261F5CE800DF2D74 /* Build configuration list for PBXProject "CompositionalLayoutDSL_Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC34ECD1261F5CED00DF2D74 /* Debug */, + AC34ECD2261F5CED00DF2D74 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AC34ECD3261F5CED00DF2D74 /* Build configuration list for PBXNativeTarget "CompositionalLayoutDSL_Example_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC34ECD4261F5CED00DF2D74 /* Debug */, + AC34ECD5261F5CED00DF2D74 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AC6292F626303727005B5D3A /* Build configuration list for PBXNativeTarget "CompositionalLayoutDSL_Example_macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC6292F426303727005B5D3A /* Debug */, + AC6292F526303727005B5D3A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + AC622CE0261F606C0058EEE3 /* XCRemoteSwiftPackageReference "CompositionalLayoutDSL" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "git@github.com:faberNovel/CompositionalLayoutDSL.git"; + requirement = { + branch = develop; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + AC4BFFF22638626A00142A9E /* CompositionalLayoutDSL */ = { + isa = XCSwiftPackageProductDependency; + productName = CompositionalLayoutDSL; + }; + AC622CE1261F606C0058EEE3 /* CompositionalLayoutDSL */ = { + isa = XCSwiftPackageProductDependency; + package = AC622CE0261F606C0058EEE3 /* XCRemoteSwiftPackageReference "CompositionalLayoutDSL" */; + productName = CompositionalLayoutDSL; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = AC34ECB7261F5CE800DF2D74 /* Project object */; +} diff --git a/Example/CompositionalLayoutDSL_Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/CompositionalLayoutDSL_Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Example/CompositionalLayoutDSL_Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/CompositionalLayoutDSL_Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/CompositionalLayoutDSL_Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/CompositionalLayoutDSL_Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..b832b6e --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "CompositionalLayoutDSL", + "repositoryURL": "git@github.com:faberNovel/CompositionalLayoutDSL.git", + "state": { + "branch": "develop", + "revision": "d857d11e58b1f88800740f6b5c8f58127d45d354", + "version": null + } + } + ] + }, + "version": 1 +} diff --git a/Example/CompositionalLayoutDSL_Example.xcodeproj/xcshareddata/xcschemes/CompositionalLayoutDSL_Example_iOS.xcscheme b/Example/CompositionalLayoutDSL_Example.xcodeproj/xcshareddata/xcschemes/CompositionalLayoutDSL_Example_iOS.xcscheme new file mode 100644 index 0000000..0480dfe --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example.xcodeproj/xcshareddata/xcschemes/CompositionalLayoutDSL_Example_iOS.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/DemoCollectionViewController/DemoCellView.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/DemoCollectionViewController/DemoCellView.swift new file mode 100644 index 0000000..161d4d8 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/DemoCollectionViewController/DemoCellView.swift @@ -0,0 +1,60 @@ +// +// DemoCellView.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 08/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit + +class DemoCellView: UICollectionViewCell { + + private let label = UILabel() + + override var isHighlighted: Bool { + didSet { + alpha = isHighlighted ? 0.7 : 1 + } + } + + override var isSelected: Bool { + didSet { + backgroundColor = isSelected ? .lightGray : .gray + } + } + + // MARK: - Life cycle + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + override func prepareForReuse() { + super.prepareForReuse() + label.text = nil + } + + // MARK: - CellView + + func configure(with text: String) { + label.text = text + } + + // MARK: - Private + + private func setup() { + backgroundColor = .gray + layer.cornerRadius = 6 + contentView.addSubview(label) + label.translatesAutoresizingMaskIntoConstraints = false + label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true + label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/DemoCollectionViewController/DemoCollectionViewController.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/DemoCollectionViewController/DemoCollectionViewController.swift new file mode 100644 index 0000000..ddcce82 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/DemoCollectionViewController/DemoCollectionViewController.swift @@ -0,0 +1,83 @@ +// +// DemoCollectionViewController.swift +// testCollectionSectionLayout +// +// Created by Alexandre Podlewski on 06/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit + +class DemoCollectionViewController: UICollectionViewController { + + static let cellIdentifier = "DemoCellView" + static let supplementaryIdentifier = "DemoSupplementaryView" + + var sectionItemsCount: [Int] = [9, 4, 16, 1, 42, 10, 100] + + // MARK: - Life cycle + + init() { + super.init(collectionViewLayout: UICollectionViewFlowLayout()) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func viewDidLoad() { + super.viewDidLoad() + collectionView.backgroundColor = .systemBackground + collectionView.register(DemoCellView.self, forCellWithReuseIdentifier: Self.cellIdentifier) + collectionView.register( + DemoSupplementaryView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: Self.supplementaryIdentifier + ) + collectionView.register( + DemoSupplementaryView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, + withReuseIdentifier: Self.supplementaryIdentifier + ) + } + + // MARK: - UICollectionViewDataSource + + override func numberOfSections(in collectionView: UICollectionView) -> Int { + return sectionItemsCount.count + } + + override func collectionView(_ collectionView: UICollectionView, + numberOfItemsInSection section: Int) -> Int { + return sectionItemsCount[section] + } + + override func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: Self.cellIdentifier, + for: indexPath + ) as? DemoCellView + else { fatalError("Unexpected dequeued cell") } + cell.configure(with: "\(indexPath)") + return cell + } + + override func collectionView( + _ collectionView: UICollectionView, + viewForSupplementaryElementOfKind kind: String, + at indexPath: IndexPath + ) -> UICollectionReusableView { + guard + let supplementaryView = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: Self.supplementaryIdentifier, + for: indexPath + ) as? DemoSupplementaryView + else { fatalError("Unexpected dequeued supplementary view") } + + supplementaryView.configure(with: "\(indexPath)") + return supplementaryView + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/DemoCollectionViewController/DemoSupplementaryView.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/DemoCollectionViewController/DemoSupplementaryView.swift new file mode 100644 index 0000000..65c2fcf --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/DemoCollectionViewController/DemoSupplementaryView.swift @@ -0,0 +1,47 @@ +// +// DemoSupplementaryView.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 08/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit + +class DemoSupplementaryView: UICollectionReusableView { + + private let label = UILabel() + + // MARK: - Life cycle + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + override func prepareForReuse() { + super.prepareForReuse() + label.text = nil + } + + // MARK: - DemoSupplementaryView + + func configure(with text: String) { + label.text = text + } + + // MARK: - Private + + private func setup() { + backgroundColor = UIColor { $0.userInterfaceStyle == .dark ? .darkGray : .lightGray } + addSubview(label) + label.translatesAutoresizingMaskIntoConstraints = false + label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true + label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AdaptativeColumnLaneSection.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AdaptativeColumnLaneSection.swift new file mode 100644 index 0000000..cf4a4bc --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AdaptativeColumnLaneSection.swift @@ -0,0 +1,73 @@ +// +// AdaptativeColumnLaneSection.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 09/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import CompositionalLayoutDSL + +struct AdaptativeColumnLaneSection: LayoutSection { + + /// Returns cell height from the cell width + let cellHeightProvider: (CGFloat) -> CGFloat + let interItemSpacing: ColumnLaneInterItemSpacing + let environment: NSCollectionLayoutEnvironment + let itemProvider: () -> LayoutItem + + // MARK: - LayoutSection + + var layoutSection: LayoutSection { + ColumnLaneSection( + columns: columns, + cellHeightProvider: cellHeightProvider, + interItemSpacing: interItemSpacing, + environment: environment, + itemProvider: itemProvider + ) + } + + // MARK: - Private + + private var columns: Float { + switch environment.container.effectiveContentSize.width { + case ..<500: + return 1 + case 500..<700: + return 1.5 + case 700...: + return 2.5 + default: + return 2.5 + } + } +} + +extension AdaptativeColumnLaneSection { + + init(environment: NSCollectionLayoutEnvironment, + cellHeightProvider: @escaping (CGFloat) -> CGFloat, + itemProvider: @escaping () -> LayoutItem = { Item() }) { + self.init( + cellHeightProvider: cellHeightProvider, + interItemSpacing: .standard, + environment: environment, + itemProvider: itemProvider + ) + } + + init(environment: NSCollectionLayoutEnvironment, + cellHeight: CGFloat, + interItemSpacing: ColumnLaneInterItemSpacing = .standard, + itemProvider: @escaping () -> LayoutItem = { Item() }) { + assert(cellHeight > 0, "A 0 height is not allowed") + self.init( + cellHeightProvider: { _ in cellHeight }, + interItemSpacing: interItemSpacing, + environment: environment, + itemProvider: itemProvider + ) + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AppStoreNewContentSection.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AppStoreNewContentSection.swift new file mode 100644 index 0000000..704d38d --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AppStoreNewContentSection.swift @@ -0,0 +1,49 @@ +// +// AppStoreNewContentSection.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 09/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import CompositionalLayoutDSL + +struct AppStoreNewContentSection: LayoutSection { + + let environment: NSCollectionLayoutEnvironment + + // MARK: - LayoutSection + + var layoutSection: LayoutSection { + SectionWithEnvironmentInsets( + insets: NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 0, trailing: 16), + environment: environment + ) { updatedEnvironment in + AdaptativeColumnLaneSection( + environment: updatedEnvironment, + cellHeightProvider: cellHeight + ) { + Item { + SupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) + .height(.absolute(80)) + .containerAnchor( + NSCollectionLayoutAnchor( + edges: .top, + absoluteOffset: CGPoint(x: 0, y: -88) + ) + ) + .zIndex(zIndex: 100) + } + .contentInsets(top: 88) + } + .orthogonalScrollingBehavior(.groupPaging) + } + } + + // MARK: - Private + + private func cellHeight(width: CGFloat) -> CGFloat { + return width * 9 / 16 + 88 + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AppStoreTopContentSection.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AppStoreTopContentSection.swift new file mode 100644 index 0000000..45f290d --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AppStoreTopContentSection.swift @@ -0,0 +1,46 @@ +// +// AppStoreTopContentSection.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 09/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import CompositionalLayoutDSL + +struct AppStoreTopContentSection: LayoutSection { + + let environment: NSCollectionLayoutEnvironment + + // MARK: - LayoutSection + + var layoutSection: LayoutSection { + SectionWithHeader { + SectionWithEnvironmentInsets( + insets: NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 0, trailing: 16), + environment: environment + ) { _ in + LaneSection( + cellHeight: 300 * 9 / 16 + 30, + cellWidth: 300, + horizontalSpacing: 16 + ) { + Item { + SupplementaryItem(elementKind: UICollectionView.elementKindSectionFooter) + .containerAnchor( + NSCollectionLayoutAnchor( + edges: .bottom, + absoluteOffset: CGPoint(x: 0, y: 30) + ) + ) + .height(.absolute(26)) + } + .contentInsets(bottom: 30) + } + .orthogonalScrollingBehavior(.groupPaging) + } + } + .supplementariesFollowContentInsets(false) + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AppStoreTrendingContentSection.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AppStoreTrendingContentSection.swift new file mode 100644 index 0000000..d79f03b --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/AppStoreLayoutLike/AppStoreTrendingContentSection.swift @@ -0,0 +1,35 @@ +// +// AppStoreTrendingContentSection.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 09/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import CompositionalLayoutDSL + +struct AppStoreTrendingContentSection: LayoutSection { + + let environment: NSCollectionLayoutEnvironment + + // MARK: - LayoutSection + + var layoutSection: LayoutSection { + SectionWithEnvironmentInsets( + insets: NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 0, trailing: 16), + environment: environment + ) { updatedEnvironment in + AdaptativeColumnLaneSection( + environment: updatedEnvironment, + cellHeight: 240 + ) { + VGroup(count: 3) { + Item() + } + .interItemSpacing(.fixed(4)) + } + .orthogonalScrollingBehavior(.groupPaging) + } + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/CompositionalLayout/CompositionalLayoutWithSupplementaryView.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/CompositionalLayout/CompositionalLayoutWithSupplementaryView.swift new file mode 100644 index 0000000..0f7c1e0 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/CompositionalLayout/CompositionalLayoutWithSupplementaryView.swift @@ -0,0 +1,95 @@ +// +// CompositionalLayoutWithSupplementaryView.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 12/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import CompositionalLayoutDSL + +struct CompositionalLayoutWithSupplementaryView { + func layout() -> UICollectionViewLayout { + LayoutBuilder { + CompositionalLayout { _, _ in + Section { + VGroup(count: 1) { Item() } + .height(.absolute(200)) + .width(.fractionalWidth(0.85)) + .interItemSpacing(.fixed(8)) + } + .interGroupSpacing(8) + .orthogonalScrollingBehavior(.continuous) + } + .interSectionSpacing(20) + .boundarySupplementaryItems { + BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) + .height(.absolute(150)) + .alignment(.top) + .absoluteOffset(CGPoint(x: 0, y: -8)) + BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionFooter) + .height(.absolute(50)) + .alignment(.bottom) + .absoluteOffset(CGPoint(x: 0, y: 8)) + } + } + } +} + +// Same layout with only UIKit APIs + +struct TraditionalCompositionalLayoutWithSupplementaryView { + func layout() -> UICollectionViewLayout { + return UICollectionViewCompositionalLayout(section: section, configuration: configuration) + } + + // MARK: - Private + + private var section: NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(1) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(0.85), + heightDimension: .absolute(200) + ) + let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 1) + return NSCollectionLayoutSection(group: group) + } + + private var configuration: UICollectionViewCompositionalLayoutConfiguration { + let configuration = UICollectionViewCompositionalLayoutConfiguration() + configuration.interSectionSpacing = 20 + configuration.boundarySupplementaryItems = [globalHeader, globalFooter] + return configuration + } + + private var globalHeader: NSCollectionLayoutBoundarySupplementaryItem { + let size = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .absolute(200) + ) + return NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: size, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top, + absoluteOffset: CGPoint(x: 0, y: -8) + ) + } + + private var globalFooter: NSCollectionLayoutBoundarySupplementaryItem { + let size = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .absolute(50) + ) + return NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: size, + elementKind: UICollectionView.elementKindSectionFooter, + alignment: .top, + absoluteOffset: CGPoint(x: 0, y: 8) + ) + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/CompositionalLayout/GettingStartedCompositionalLayout.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/CompositionalLayout/GettingStartedCompositionalLayout.swift new file mode 100644 index 0000000..b656ef1 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/CompositionalLayout/GettingStartedCompositionalLayout.swift @@ -0,0 +1,33 @@ +// +// GettingStartedCompositionalLayout.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 12/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import CompositionalLayoutDSL + +struct GettingStartedCompositionalLayout { + func layout() -> UICollectionViewLayout { + return LayoutBuilder { + Section { + VGroup(count: 1) { Item() } + .height(.fractionalWidth(0.3)) + .width(.fractionalWidth(0.3)) + .interItemSpacing(.fixed(8)) + } + .interGroupSpacing(8) + .contentInsets(horizontal: 16, vertical: 8) + .orthogonalScrollingBehavior(.continuous) + .supplementariesFollowContentInsets(false) + .boundarySupplementaryItems { + BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) + .height(.absolute(30)) + .alignment(.top) + .pinToVisibleBounds(true) + } + } + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/Group/FractalGroup.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/Group/FractalGroup.swift new file mode 100644 index 0000000..6b408fc --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/Group/FractalGroup.swift @@ -0,0 +1,144 @@ +// +// FractalGroup.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 09/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import CompositionalLayoutDSL + +struct FractalGroup: LayoutGroup, ResizableItem { + + var ratio: CGFloat + var depth: Int + var insets: CGFloat + + var heightDimension: NSCollectionLayoutDimension = .fractionalHeight(1) + var widthDimension: NSCollectionLayoutDimension = .fractionalWidth(1) + + internal init(ratio: CGFloat, depth: Int, insets: CGFloat = 8) { + self.ratio = ratio + self.depth = depth + self.insets = insets + } + + // MARK: - LayoutGroup + + var layoutGroup: LayoutGroup { + let otherRatio = 1 - ratio + + guard depth > 0 else { + return HGroup { Item().contentInsets(value: insets) } + .height(heightDimension) + .width(widthDimension) + } + return HGroup(width: widthDimension, height: heightDimension) { + Item(width: .fractionalWidth(ratio)).contentInsets(value: insets) + VGroup(width: .fractionalWidth(otherRatio)) { + Item(height: .fractionalHeight(ratio)).contentInsets(value: insets) + HGroup(height: .fractionalHeight(otherRatio)) { + VGroup(width: .fractionalWidth(otherRatio)) { + FractalGroup(ratio: ratio, depth: depth - 1, insets: insets) + .height(.fractionalHeight(otherRatio)) + Item(height: .fractionalHeight(ratio)) + .contentInsets(value: insets) + } + Item(width: .fractionalWidth(ratio)).contentInsets(value: insets) + } + } + } + } + + func width(_ width: NSCollectionLayoutDimension) -> Self { + var copy = self + copy.widthDimension = width + return copy + } + + func height(_ height: NSCollectionLayoutDimension) -> Self { + var copy = self + copy.heightDimension = height + return copy + } +} + +// Same layout with only UIKit APIs + +struct TraditionalFractalGroup { + + var ratio: CGFloat + var depth: Int + var insets: CGFloat = 8 + var heightDimension: NSCollectionLayoutDimension = .fractionalHeight(1) + var widthDimension: NSCollectionLayoutDimension = .fractionalWidth(1) + + // MARK: - TraditionalFractalGroup + + var layoutGroup: NSCollectionLayoutGroup { + return fractalLayoutGroup(depth: depth, height: heightDimension) + } + + // MARK: - Private + + // swiftlint:disable:next function_body_length + private func fractalLayoutGroup(depth: Int, height: NSCollectionLayoutDimension) -> NSCollectionLayoutGroup { + let otherRatio = 1 - ratio + let contentInsets = NSDirectionalEdgeInsets(top: insets, leading: insets, bottom: insets, trailing: insets) + let groupSize = NSCollectionLayoutSize(widthDimension: widthDimension, heightDimension: height) + + guard depth > 0 else { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(1) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + item.contentInsets = contentInsets + return .horizontal(layoutSize: groupSize, subitems: [item]) + } + + let horizontalItemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(ratio), + heightDimension: .fractionalHeight(1) + ) + let verticalItemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(ratio) + ) + let horizontalGroupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(otherRatio) + ) + let verticalGroupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(otherRatio), + heightDimension: .fractionalHeight(1) + ) + let horizontalItem = NSCollectionLayoutItem(layoutSize: horizontalItemSize) + horizontalItem.contentInsets = contentInsets + let verticalItem = NSCollectionLayoutItem(layoutSize: verticalItemSize) + verticalItem.contentInsets = contentInsets + + let reccursiveGroup = fractalLayoutGroup(depth: depth - 1, height: .fractionalHeight(otherRatio)) + + let innerVGroup = NSCollectionLayoutGroup.vertical( + layoutSize: verticalGroupSize, + subitems: [reccursiveGroup, verticalItem] + ) + + let innerHGroup = NSCollectionLayoutGroup.horizontal( + layoutSize: horizontalGroupSize, + subitems: [innerVGroup, horizontalItem] + ) + + let outerVGroup = NSCollectionLayoutGroup.vertical( + layoutSize: verticalGroupSize, + subitems: [verticalItem, innerHGroup] + ) + + return NSCollectionLayoutGroup.horizontal( + layoutSize: groupSize, + subitems: [horizontalItem, outerVGroup] + ) + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/Section/ListSection.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/Section/ListSection.swift new file mode 100644 index 0000000..83392cc --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/Section/ListSection.swift @@ -0,0 +1,84 @@ +// +// ListSection.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 09/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import CompositionalLayoutDSL + +struct ListSection: LayoutSection { + + var layoutSection: LayoutSection { + Section { + HGroup(count: 5) { Item() } + .height(.fractionalWidth(1 / 5)) + .interItemSpacing(.fixed(4)) + .contentInsets(horizontal: 8, vertical: 0) + } + .interGroupSpacing(4) + .boundarySupplementaryItems { + BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) + .absoluteOffset(CGPoint(x: 0, y: -4)) + .height(.absolute(20)) + .alignment(.top) + .pinToVisibleBounds(true) + BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionFooter) + .absoluteOffset(CGPoint(x: 0, y: 4)) + .height(.absolute(20)) + .alignment(.bottom) + .pinToVisibleBounds(true) + } + } +} + +// Same layout with only UIKit APIs + +struct TraditionalListSection { + + var layoutSection: NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(1) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalWidth(1 / 5) + ) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 5) + group.interItemSpacing = .fixed(4) + group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8) + + let headerFooterItemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .absolute(20) + ) + let headerItem = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerFooterItemSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top, + absoluteOffset: CGPoint(x: 0, y: -4) + ) + headerItem.pinToVisibleBounds = true + let footerItem = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerFooterItemSize, + elementKind: UICollectionView.elementKindSectionFooter, + alignment: .bottom, + absoluteOffset: CGPoint(x: 0, y: 4) + ) + footerItem.pinToVisibleBounds = true + + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = 4 + section.boundarySupplementaryItems = [ + headerItem, + footerItem + ] + + return section + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/Section/SectionWithHeader.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/Section/SectionWithHeader.swift new file mode 100644 index 0000000..1752656 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/Section/SectionWithHeader.swift @@ -0,0 +1,50 @@ +// +// SectionWithHeader.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 09/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import CompositionalLayoutDSL + +struct SectionWithHeader: LayoutSection { + + var kind: String = UICollectionView.elementKindSectionHeader + var height: NSCollectionLayoutDimension = .absolute(30) + var section: () -> LayoutSection + + var layoutSection: LayoutSection { + section() + .boundarySupplementaryItems { + BoundarySupplementaryItem(elementKind: kind) + .height(height) + .alignment(.top) + } + } +} + +// Same layout with only UIKit APIs + +struct TraditionalSectionWithHeader { + + var kind: String = UICollectionView.elementKindSectionHeader + var height: NSCollectionLayoutDimension = .absolute(30) + var baseSectionLayout: NSCollectionLayoutSection + + var layoutSection: NSCollectionLayoutSection { + let headerItemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: height + ) + let headerItem = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerItemSize, + elementKind: kind, + alignment: .top + ) + baseSectionLayout.boundarySupplementaryItems.append(headerItem) + + return baseSectionLayout + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/ShowcaseViewController.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/ShowcaseViewController.swift new file mode 100644 index 0000000..29d2640 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/ShowcaseViewController.swift @@ -0,0 +1,92 @@ +// +// ShowcaseViewController.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 08/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import CompositionalLayoutDSL + +class ShowcaseViewController: DemoCollectionViewController { + + private var currentLayoutIndex = 0 { + didSet { + title = "\(currentLayoutIndex + 1) / \(showCaseLayouts.count)" + } + } + + private let nextLayoutButton = UIBarButtonItem() + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + + currentLayoutIndex = 0 + nextLayoutButton.target = self + nextLayoutButton.action = #selector(nextLayout) + nextLayoutButton.title = "Next layout" + navigationItem.rightBarButtonItem = nextLayoutButton + + collectionView.collectionViewLayout = showCaseLayouts[currentLayoutIndex] + } + + // MARK: - Private + + @objc private func nextLayout() { + let nextLayoutIndex = (currentLayoutIndex + 1) % showCaseLayouts.count + currentLayoutIndex = nextLayoutIndex + collectionView.setCollectionViewLayout(showCaseLayouts[currentLayoutIndex], animated: false) + collectionView.reloadData() + collectionView.contentOffset = CGPoint(x: 0, y: -collectionView.adjustedContentInset.top) + } +} + +extension ShowcaseViewController { + + // MARK: - Layouts + + private var showCaseLayouts: [UICollectionViewLayout] { + [ + GettingStartedCompositionalLayout().layout(), + LayoutBuilder { ListSection() }, +// LayoutBuilder { TraditionalListSection() }, + LayoutBuilder { + Section { + FractalGroup(ratio: 0.5, depth: 2) + .height(.fractionalWidth(0.5)) + } + }, +// LayoutBuilder { +// Section { +// TraditionalFractalGroup(ratio: 0.5, depth: 2, heightDimension: .fractionalWidth(0.5)) +// } +// }, + LayoutBuilder { + SectionWithHeader { + Section { + FractalGroup(ratio: 0.5, depth: 2) + .height(.fractionalWidth(0.45)) + .width(.fractionalWidth(0.9)) + } + .orthogonalScrollingBehavior(.continuous) + } + }, + LayoutBuilder { + CompositionalLayout(repeatingSections: [ + // swiftlint:disable opening_brace + { AppStoreNewContentSection(environment: $1) }, + { AppStoreTrendingContentSection(environment: $1) }, + { AppStoreTopContentSection(environment: $1) }, + { AppStoreTrendingContentSection(environment: $1) }, + { AppStoreTrendingContentSection(environment: $1) } + // swiftlint:enable opening_brace + ]) + }, + CompositionalLayoutWithSupplementaryView().layout() +// TraditionalCompositionalLayoutWithSupplementaryView().layout() + ] + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/Utils/Section/ColumnLaneSection.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/Utils/Section/ColumnLaneSection.swift new file mode 100644 index 0000000..92ae6b8 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/Utils/Section/ColumnLaneSection.swift @@ -0,0 +1,91 @@ +// +// ColumnLaneSection.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 09/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import CompositionalLayoutDSL + +enum ColumnLaneInterItemSpacing { + case standard + case custom(spacing: CGFloat) +} + +struct ColumnLaneSection: LayoutSection { + + let columns: Float + /// Returns cell height from the cell width + let cellHeightProvider: (CGFloat) -> CGFloat + let interItemSpacing: ColumnLaneInterItemSpacing + let environment: NSCollectionLayoutEnvironment + let itemProvider: () -> LayoutItem + + // MARK: - LayoutSection + + var layoutSection: LayoutSection { + Section { + VGroup { itemProvider() } + .width(.absolute(cellWidth)) + .height(.absolute(cellHeight)) + } + .interGroupSpacing(horizontalSpacing) + .orthogonalScrollingBehavior(.continuousGroupLeadingBoundary) + } + + // MARK: - Private + + private var cellWidth: CGFloat { + let cumulatedHorizontalSpacing = horizontalSpacing * CGFloat(columns - 1) + let effectiveWidth = environment.container.effectiveContentSize.width + return (effectiveWidth - cumulatedHorizontalSpacing) / CGFloat(columns) + } + + private var cellHeight: CGFloat { + let cellHeight = cellHeightProvider(cellWidth) + assert(cellHeight > 0, "Cell height can not be 0") + return cellHeight + } + + private var horizontalSpacing: CGFloat { + switch interItemSpacing { + case let .custom(spacing: customSpacing): + return customSpacing + case .standard: + return 8 + } + } +} + +extension ColumnLaneSection { + + init(columns: Float, + environment: NSCollectionLayoutEnvironment, + cellHeightProvider: @escaping (CGFloat) -> CGFloat, + itemProvider: @escaping () -> LayoutItem = { Item() }) { + self.init( + columns: columns, + cellHeightProvider: cellHeightProvider, + interItemSpacing: .standard, + environment: environment, + itemProvider: itemProvider + ) + } + + init(columns: Float, + environment: NSCollectionLayoutEnvironment, + cellHeight: CGFloat, + interItemSpacing: ColumnLaneInterItemSpacing = .standard, + itemProvider: @escaping () -> LayoutItem = { Item() }) { + assert(cellHeight > 0, "A 0 height is not allowed") + self.init( + columns: columns, + cellHeightProvider: { _ in cellHeight }, + interItemSpacing: interItemSpacing, + environment: environment, + itemProvider: itemProvider + ) + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/Utils/Section/LaneSection.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/Utils/Section/LaneSection.swift new file mode 100644 index 0000000..c93f553 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/Utils/Section/LaneSection.swift @@ -0,0 +1,40 @@ +// +// LaneSection.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 09/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import CompositionalLayoutDSL + +struct LaneSection: LayoutSection { + + let cellHeight: CGFloat + let cellWidth: CGFloat + let horizontalSpacing: CGFloat + let itemProvider: () -> LayoutItem + + init(cellHeight: CGFloat, + cellWidth: CGFloat, + horizontalSpacing: CGFloat, + itemProvider: @escaping () -> LayoutItem) { + precondition(cellWidth > 0, "Cell width cannot be 0") + precondition(cellHeight > 0, "Cell height cannot be 0") + self.cellHeight = cellHeight + self.cellWidth = cellWidth + self.horizontalSpacing = horizontalSpacing + self.itemProvider = itemProvider + } + + var layoutSection: LayoutSection { + Section { + VGroup { itemProvider() } + .width(.absolute(cellWidth)) + .height(.absolute(cellHeight)) + } + .interGroupSpacing(horizontalSpacing) + .orthogonalScrollingBehavior(.continuousGroupLeadingBoundary) + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/App/Utils/Section/SectionWithEnvironmentInsets.swift b/Example/CompositionalLayoutDSL_Example_iOS/App/Utils/Section/SectionWithEnvironmentInsets.swift new file mode 100644 index 0000000..2b3d439 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/App/Utils/Section/SectionWithEnvironmentInsets.swift @@ -0,0 +1,105 @@ +// +// SectionWithEnvironmentInsets.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 09/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import UIKit +import CompositionalLayoutDSL + +struct SectionWithEnvironmentInsets: LayoutSection { + + let insets: NSDirectionalEdgeInsets + let baseSection: LayoutSection + + init(insets: NSDirectionalEdgeInsets, + environment: NSCollectionLayoutEnvironment, + baseSection: (NSCollectionLayoutEnvironment) -> LayoutSection) { + self.insets = insets + self.baseSection = baseSection( + CustomCollectionLayoutEnvironment( + from: environment, + withAdditionalInsets: insets + ) + ) + } + + // MARK: - LayoutSection + + var layoutSection: LayoutSection { + baseSection.contentInsets(insets) + } +} + +private class CustomCollectionLayoutEnvironment: NSObject, NSCollectionLayoutEnvironment { + + let container: NSCollectionLayoutContainer + let traitCollection: UITraitCollection + + init(container: NSCollectionLayoutContainer, + traitCollection: UITraitCollection, + withAdditionalInsets insets: NSDirectionalEdgeInsets) { + self.container = CustomCollectionLayoutContainer(from: container, withAdditionalInsets: insets) + self.traitCollection = traitCollection + } + + convenience init(from collectionLayoutEnvironment: NSCollectionLayoutEnvironment, + withAdditionalInsets insets: NSDirectionalEdgeInsets) { + self.init( + container: collectionLayoutEnvironment.container, + traitCollection: collectionLayoutEnvironment.traitCollection, + withAdditionalInsets: insets + ) + } +} + +private class CustomCollectionLayoutContainer: NSObject, NSCollectionLayoutContainer { + let contentSize: CGSize + let effectiveContentSize: CGSize + let contentInsets: NSDirectionalEdgeInsets + let effectiveContentInsets: NSDirectionalEdgeInsets + + init(contentSize: CGSize, + effectiveContentSize: CGSize, + contentInsets: NSDirectionalEdgeInsets, + effectiveContentInsets: NSDirectionalEdgeInsets, + withAdditionalInsets insets: NSDirectionalEdgeInsets = .zero) { + self.contentSize = contentSize + self.effectiveContentSize = effectiveContentSize.inseted(by: insets) + self.contentInsets = contentInsets.adding(insets) + self.effectiveContentInsets = effectiveContentInsets.adding(insets) + } + + convenience init(from collectionLayoutContainer: NSCollectionLayoutContainer, + withAdditionalInsets insets: NSDirectionalEdgeInsets) { + self.init( + contentSize: collectionLayoutContainer.contentSize, + effectiveContentSize: collectionLayoutContainer.effectiveContentSize, + contentInsets: collectionLayoutContainer.contentInsets, + effectiveContentInsets: collectionLayoutContainer.effectiveContentInsets, + withAdditionalInsets: insets + ) + } +} + +private extension NSDirectionalEdgeInsets { + func adding(_ insets: NSDirectionalEdgeInsets) -> NSDirectionalEdgeInsets { + var copy = self + copy.trailing += insets.trailing + copy.leading += insets.leading + copy.bottom += insets.bottom + copy.top += insets.top + return copy + } +} + +private extension CGSize { + func inseted(by insets: NSDirectionalEdgeInsets) -> CGSize { + var copy = self + copy.width = max(0, width - insets.leading - insets.trailing) + copy.height = max(0, height - insets.top - insets.bottom) + return copy + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/AppDelegate.swift b/Example/CompositionalLayoutDSL_Example_iOS/AppDelegate.swift new file mode 100644 index 0000000..2956912 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/AppDelegate.swift @@ -0,0 +1,31 @@ +// +// AppDelegate.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 08/04/2021. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application( + _ application: UIApplication, + // swiftlint:disable:next discouraged_optional_collection + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/Assets.xcassets/AccentColor.colorset/Contents.json b/Example/CompositionalLayoutDSL_Example_iOS/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/CompositionalLayoutDSL_Example_iOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/Assets.xcassets/Contents.json b/Example/CompositionalLayoutDSL_Example_iOS/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/CompositionalLayoutDSL_Example_iOS/Base.lproj/LaunchScreen.storyboard b/Example/CompositionalLayoutDSL_Example_iOS/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/CompositionalLayoutDSL_Example_iOS/Info.plist b/Example/CompositionalLayoutDSL_Example_iOS/Info.plist new file mode 100644 index 0000000..2688b32 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/Info.plist @@ -0,0 +1,62 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Example/CompositionalLayoutDSL_Example_iOS/SceneDelegate.swift b/Example/CompositionalLayoutDSL_Example_iOS/SceneDelegate.swift new file mode 100644 index 0000000..af76dd9 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_iOS/SceneDelegate.swift @@ -0,0 +1,38 @@ +// +// SceneDelegate.swift +// CompositionalLayoutDSL_Example_iOS +// +// Created by Alexandre Podlewski on 08/04/2021. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + let window = UIWindow(windowScene: windowScene) + self.window = window + window.rootViewController = UINavigationController(rootViewController: ShowcaseViewController()) + window.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) { + } + + func sceneDidBecomeActive(_ scene: UIScene) { + } + + func sceneWillResignActive(_ scene: UIScene) { + } + + func sceneWillEnterForeground(_ scene: UIScene) { + } + + func sceneDidEnterBackground(_ scene: UIScene) { + } +} diff --git a/Example/CompositionalLayoutDSL_Example_macOS/App/DemoCollectionViewController/DemoCellView.swift b/Example/CompositionalLayoutDSL_Example_macOS/App/DemoCollectionViewController/DemoCellView.swift new file mode 100644 index 0000000..0736658 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_macOS/App/DemoCollectionViewController/DemoCellView.swift @@ -0,0 +1,53 @@ +// +// DemoCellView.swift +// CompositionalLayoutDSL_Example_macOS +// +// Created by Alexandre Podlewski on 21/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import AppKit + +class DemoCellView: NSCollectionViewItem { + + private let label = NSText() + + // MARK: - Life cycle + + override func loadView() { + self.view = NSView() + self.view.wantsLayer = true + } + + override func viewDidLoad() { + super.viewDidLoad() + setup() + } + + override func prepareForReuse() { + super.prepareForReuse() + label.string = "" + } + + // MARK: - CellView + + func configure(with text: String) { + label.string = text + } + + // MARK: - Private + + private func setup() { + view.layer?.backgroundColor = NSColor.gray.cgColor + view.layer?.cornerRadius = 6 + view.addSubview(label) + label.translatesAutoresizingMaskIntoConstraints = false + label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + label.widthAnchor.constraint(equalToConstant: 50).isActive = true + label.heightAnchor.constraint(equalToConstant: 15).isActive = true + label.alignment = .center + label.backgroundColor = NSColor.gray + label.isEditable = false + } +} diff --git a/Example/CompositionalLayoutDSL_Example_macOS/App/DemoCollectionViewController/DemoCollectionViewController.swift b/Example/CompositionalLayoutDSL_Example_macOS/App/DemoCollectionViewController/DemoCollectionViewController.swift new file mode 100644 index 0000000..7536515 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_macOS/App/DemoCollectionViewController/DemoCollectionViewController.swift @@ -0,0 +1,100 @@ +// +// DemoCollectionViewController.swift +// CompositionalLayoutDSL_Example_macOS +// +// Created by Alexandre Podlewski on 21/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import AppKit + +class DemoCollectionViewController: NSViewController, NSCollectionViewDataSource, NSCollectionViewDelegate { + + static let cellIdentifier = NSUserInterfaceItemIdentifier(rawValue: "DemoCellView") + static let supplementaryIdentifier = NSUserInterfaceItemIdentifier(rawValue: "DemoSupplementaryView") + + lazy var scrollView = NSScrollView() + lazy var collectionView = NSCollectionView() + + var sectionItemsCount: [Int] = [9, 4, 16, 1, 42, 10, 100] + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(scrollView) + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + + scrollView.documentView = collectionView + collectionView.backgroundColors = [NSColor.red] + setup() + } + + // MARK: - NSCollectionViewDataSource + + func numberOfSections(in collectionView: NSCollectionView) -> Int { + return sectionItemsCount.count + } + + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + return sectionItemsCount[section] + } + + func collectionView( + _ collectionView: NSCollectionView, + itemForRepresentedObjectAt indexPath: IndexPath + ) -> NSCollectionViewItem { + guard + let cell = collectionView.makeItem( + withIdentifier: Self.cellIdentifier, + for: indexPath + ) as? DemoCellView + else { fatalError("Unexpected dequeued cell") } + cell.configure(with: "\(indexPath)") + return cell + } + + func collectionView( + _ collectionView: NSCollectionView, + viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, + at indexPath: IndexPath + ) -> NSView { + guard + let supplementaryView = collectionView.makeSupplementaryView( + ofKind: kind, + withIdentifier: Self.supplementaryIdentifier, + for: indexPath + ) as? DemoSupplementaryView + else { fatalError("Unexpected dequeued supplementary view") } + + supplementaryView.configure(with: "\(indexPath)") + return supplementaryView + } + + // MARK: - Private + + private func setup() { + collectionView.dataSource = self + collectionView.delegate = self + collectionView.collectionViewLayout = NSCollectionViewFlowLayout() + collectionView.backgroundColors = [.windowBackgroundColor] + + collectionView.register(DemoCellView.self, forItemWithIdentifier: Self.cellIdentifier) + collectionView.register( + DemoSupplementaryView.self, + forSupplementaryViewOfKind: NSCollectionView.elementKindSectionHeader, + withIdentifier: Self.supplementaryIdentifier + ) + collectionView.register( + DemoSupplementaryView.self, + forSupplementaryViewOfKind: NSCollectionView.elementKindSectionFooter, + withIdentifier: Self.supplementaryIdentifier + ) + + collectionView.reloadData() + } +} diff --git a/Example/CompositionalLayoutDSL_Example_macOS/App/DemoCollectionViewController/DemoSupplementaryView.swift b/Example/CompositionalLayoutDSL_Example_macOS/App/DemoCollectionViewController/DemoSupplementaryView.swift new file mode 100644 index 0000000..355b19f --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_macOS/App/DemoCollectionViewController/DemoSupplementaryView.swift @@ -0,0 +1,53 @@ +// +// DemoSupplementaryView.swift +// CompositionalLayoutDSL_Example_macOS +// +// Created by Alexandre Podlewski on 21/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import AppKit + +final class DemoSupplementaryView: NSView, NSCollectionViewElement { + + private let label = NSText() + + // MARK: - Life cycle + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + override func prepareForReuse() { + super.prepareForReuse() + label.string = "" + } + + // MARK: - DemoSupplementaryView + + func configure(with text: String) { + label.string = text + } + + // MARK: - Private + + private func setup() { + self.wantsLayer = true + self.layer?.backgroundColor = NSColor.darkGray.cgColor + addSubview(label) + label.translatesAutoresizingMaskIntoConstraints = false + label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true + label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + label.widthAnchor.constraint(equalToConstant: 50).isActive = true + label.heightAnchor.constraint(equalToConstant: 15).isActive = true + label.alignment = .center + label.backgroundColor = NSColor.darkGray + label.isEditable = false + } +} diff --git a/Example/CompositionalLayoutDSL_Example_macOS/App/ShowcaseViewController/ShowcaseViewController.swift b/Example/CompositionalLayoutDSL_Example_macOS/App/ShowcaseViewController/ShowcaseViewController.swift new file mode 100644 index 0000000..7db693d --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_macOS/App/ShowcaseViewController/ShowcaseViewController.swift @@ -0,0 +1,173 @@ +// +// ShowcaseViewController.swift +// CompositionalLayoutDSL_Example_macOS +// +// Created by Alexandre Podlewski on 21/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import Cocoa +import CompositionalLayoutDSL + +class ShowcaseViewController: DemoCollectionViewController { + + private var currentLayoutIndex = 0 { + didSet { + currentLayoutText.string = "\(currentLayoutIndex + 1) / \(showCaseLayouts.count)" + } + } + + private let currentLayoutText = NSTextView() + private let nextLayoutButton = NSButton() + + override func viewDidLoad() { + super.viewDidLoad() + setupControls() + + currentLayoutIndex = 0 + nextLayoutButton.target = self + nextLayoutButton.action = #selector(nextLayout) + nextLayoutButton.title = "Next layout" + nextLayoutButton.bezelStyle = .roundRect + + collectionView.collectionViewLayout = showCaseLayouts[currentLayoutIndex] + collectionView.collectionViewLayout?.invalidateLayout() + } + + // MARK: - Private + + @objc private func nextLayout() { + let nextLayoutIndex = (currentLayoutIndex + 1) % showCaseLayouts.count + currentLayoutIndex = nextLayoutIndex + collectionView.collectionViewLayout = showCaseLayouts[currentLayoutIndex] + collectionView.reloadData() + scrollView.scroll(.zero) + } + + private func setupControls() { + let controlsContainerView = NSVisualEffectView() + controlsContainerView.blendingMode = .withinWindow + controlsContainerView.material = .hudWindow + controlsContainerView.state = .active + let controls = NSStackView(views: [currentLayoutText, nextLayoutButton]) + controls.edgeInsets = NSEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) + controlsContainerView.addSubview(controls) + controlsContainerView.wantsLayer = true + controlsContainerView.layer?.cornerRadius = 8 + controlsContainerView.layer?.maskedCorners = [.layerMinXMinYCorner] + scrollView.scrollerInsets.top += 25 + + currentLayoutText.translatesAutoresizingMaskIntoConstraints = false + currentLayoutText.widthAnchor.constraint(equalToConstant: 50).isActive = true + currentLayoutText.alignment = .center + currentLayoutText.isEditable = false + currentLayoutText.backgroundColor = .clear + + controls.translatesAutoresizingMaskIntoConstraints = false + controls.topAnchor.constraint(equalTo: controlsContainerView.topAnchor).isActive = true + controls.bottomAnchor.constraint(equalTo: controlsContainerView.bottomAnchor).isActive = true + controls.leadingAnchor.constraint(equalTo: controlsContainerView.leadingAnchor).isActive = true + controls.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor).isActive = true + + view.addSubview(controlsContainerView) + controlsContainerView.translatesAutoresizingMaskIntoConstraints = false + controlsContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + controlsContainerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + } +} + +extension ShowcaseViewController { + + // MARK: - Layouts + + private var showCaseLayouts: [NSCollectionViewLayout] { + [ + LayoutBuilder { fourColumnsGroup() }, +// fourColumnsGroupAppKit(), + LayoutBuilder { listLayout() }, + LayoutBuilder { tweetDeckLayout() } + ] + } + + private func fourColumnsGroup() -> CompositionalLayout { + CompositionalLayout { _, _ in + Section { + HGroup(count: 4) { + Item() + } + .height(.absolute(100)) + .interItemSpacing(.fixed(16)) + } + .interGroupSpacing(16) + .contentInsets(horizontal: 16, vertical: 8) + } + } + + private func fourColumnsGroupAppKit() -> NSCollectionViewLayout { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(1) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .absolute(100) + ) + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: groupSize, + subitem: item, + count: 4 + ) + group.interItemSpacing = .fixed(16) + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = 16 + section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16) + return NSCollectionViewCompositionalLayout(section: section) + } + + private func tweetDeckLayout() -> CompositionalLayout { + CompositionalLayout { _, _ in + Section { + HGroup(count: 1) { + Item() + } + .height(.absolute(100)) + .width(.absolute(150)) + } + .interGroupSpacing(4) + .boundarySupplementaryItems { + BoundarySupplementaryItem(elementKind: NSCollectionView.elementKindSectionFooter) + .alignment(.leading) + .width(.absolute(20)) + } + .orthogonalScrollingBehavior(.continuous) + .contentInsets(horizontal: 4) + } + .scrollDirection(.horizontal) + } + + private func listLayout() -> CompositionalLayout { + CompositionalLayout { _, _ in + Section { + HGroup(count: 1) { + Item() + } + .height(.absolute(40)) + .width(.fractionalWidth(1)) + } + .interGroupSpacing(1) + .boundarySupplementaryItems { + BoundarySupplementaryItem(elementKind: NSCollectionView.elementKindSectionHeader) + .alignment(.top) + .height(.absolute(20)) + .pinToVisibleBounds(true) + BoundarySupplementaryItem(elementKind: NSCollectionView.elementKindSectionFooter) + .alignment(.bottom) + .height(.absolute(20)) + .pinToVisibleBounds(true) + } + .contentInsets(horizontal: 8) + .supplementariesFollowContentInsets(false) + } + } +} diff --git a/Example/CompositionalLayoutDSL_Example_macOS/AppDelegate.swift b/Example/CompositionalLayoutDSL_Example_macOS/AppDelegate.swift new file mode 100644 index 0000000..6294eed --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_macOS/AppDelegate.swift @@ -0,0 +1,21 @@ +// +// AppDelegate.swift +// CompositionalLayoutDSL_Example_macOS +// +// Created by Alexandre Podlewski on 21/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import Cocoa + +@main +class AppDelegate: NSObject, NSApplicationDelegate { + + func applicationDidFinishLaunching(_ aNotification: Notification) { + // Insert code here to initialize your application + } + + func applicationWillTerminate(_ aNotification: Notification) { + // Insert code here to tear down your application + } +} diff --git a/Example/CompositionalLayoutDSL_Example_macOS/Assets.xcassets/AccentColor.colorset/Contents.json b/Example/CompositionalLayoutDSL_Example_macOS/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_macOS/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/CompositionalLayoutDSL_Example_macOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/CompositionalLayoutDSL_Example_macOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_macOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/CompositionalLayoutDSL_Example_macOS/Assets.xcassets/Contents.json b/Example/CompositionalLayoutDSL_Example_macOS/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_macOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/CompositionalLayoutDSL_Example_macOS/Base.lproj/Main.storyboard b/Example/CompositionalLayoutDSL_Example_macOS/Base.lproj/Main.storyboard new file mode 100644 index 0000000..2b2138d --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_macOS/Base.lproj/Main.storyboard @@ -0,0 +1,717 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/CompositionalLayoutDSL_Example_macOS/CompositionalLayoutDSL_Example_macOS.entitlements b/Example/CompositionalLayoutDSL_Example_macOS/CompositionalLayoutDSL_Example_macOS.entitlements new file mode 100644 index 0000000..f2ef3ae --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_macOS/CompositionalLayoutDSL_Example_macOS.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Example/CompositionalLayoutDSL_Example_macOS/Info.plist b/Example/CompositionalLayoutDSL_Example_macOS/Info.plist new file mode 100644 index 0000000..b9877aa --- /dev/null +++ b/Example/CompositionalLayoutDSL_Example_macOS/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2021 Fabernovel. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + + diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..dcd2d93 --- /dev/null +++ b/Gemfile @@ -0,0 +1,10 @@ +source 'https://rubygems.org' +ruby '2.6.5' + +gem 'cocoapods', '1.10.0' +gem 'fastlane', '<3.0' +gem 'danger' +gem 'danger-swiftlint' + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..e973aa1 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,315 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.3) + activesupport (5.2.5) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + artifactory (3.0.15) + atomos (0.1.3) + aws-eventstream (1.1.1) + aws-partitions (1.444.0) + aws-sdk-core (3.113.1) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.43.0) + aws-sdk-core (~> 3, >= 3.112.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.93.1) + aws-sdk-core (~> 3, >= 3.112.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.2.3) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + claide (1.0.3) + claide-plugins (0.9.2) + cork + nap + open4 (~> 1.3) + cocoapods (1.10.0) + addressable (~> 2.6) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.10.0) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 1.4.0, < 2.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.4.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.6.6) + nap (~> 1.0) + ruby-macho (~> 1.4) + xcodeproj (>= 1.19.0, < 2.0) + cocoapods-core (1.10.0) + activesupport (> 5.0, < 6) + addressable (~> 2.6) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.4) + cocoapods-downloader (1.4.0) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.0) + cocoapods-trunk (1.5.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored (1.2) + colored2 (3.1.2) + commander-fastlane (4.4.6) + highline (~> 1.7.2) + concurrent-ruby (1.1.8) + cork (0.3.0) + colored2 (~> 3.1) + danger (8.2.3) + claide (~> 1.0) + claide-plugins (>= 0.9.2) + colored2 (~> 3.1) + cork (~> 0.1) + faraday (>= 0.9.0, < 2.0) + faraday-http-cache (~> 2.0) + git (~> 1.7) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.0) + no_proxy_fix + octokit (~> 4.7) + terminal-table (>= 1, < 4) + danger-swiftlint (0.26.0) + danger + rake (> 10) + thor (~> 0.19) + declarative (0.0.20) + digest-crc (0.6.3) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.7.6) + emoji_regex (3.2.2) + escape (0.0.4) + ethon (0.13.0) + ffi (>= 1.15.0) + excon (0.79.0) + faraday (1.3.0) + faraday-net_http (~> 1.0) + multipart-post (>= 1.2, < 3) + ruby2_keywords + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-http-cache (2.2.0) + faraday (>= 0.8) + faraday-net_http (1.0.1) + faraday_middleware (1.0.0) + faraday (~> 1.0) + fastimage (2.2.3) + fastlane (2.180.1) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.3, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander-fastlane (>= 4.4.6, < 5.0.0) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-api-client (>= 0.37.0, < 0.39.0) + google-cloud-storage (>= 1.15.0, < 2.0.0) + highline (>= 1.7.2, < 2.0.0) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (~> 2.0.0) + naturally (~> 2.2) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + slack-notifier (>= 2.0.0, < 3.0.0) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-changelog (0.16.0) + ffi (1.15.0) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + git (1.8.1) + rchardet (~> 1.8) + google-api-client (0.38.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.9) + httpclient (>= 2.8.1, < 3.0) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + signet (~> 0.12) + google-apis-core (0.3.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.14) + httpclient (>= 2.8.1, < 3.0) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + rexml + signet (~> 0.14) + webrick + google-apis-iamcredentials_v1 (0.3.0) + google-apis-core (~> 0.1) + google-apis-storage_v1 (0.3.0) + google-apis-core (~> 0.1) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.5.0) + faraday (>= 0.17.3, < 2.0) + google-cloud-errors (1.1.0) + google-cloud-storage (1.31.0) + addressable (~> 2.5) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.1) + google-cloud-core (~> 1.2) + googleauth (~> 0.9) + mini_mime (~> 1.0) + googleauth (0.16.1) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (~> 0.14) + highline (1.7.10) + http-cookie (1.0.3) + domain_name (~> 0.5) + httpclient (2.8.3) + i18n (1.8.10) + concurrent-ruby (~> 1.0) + jmespath (1.4.0) + json (2.5.1) + jwt (2.2.2) + kramdown (2.3.1) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + memoist (0.16.2) + mini_magick (4.11.0) + mini_mime (1.1.0) + minitest (5.14.4) + molinillo (0.6.6) + multi_json (1.15.0) + multipart-post (2.0.0) + nanaimo (0.3.0) + nap (1.1.0) + naturally (2.2.1) + netrc (0.11.0) + no_proxy_fix (0.1.2) + octokit (4.20.0) + faraday (>= 0.9) + sawyer (~> 0.8.0, >= 0.5.3) + open4 (1.3.4) + os (1.1.1) + plist (3.6.0) + public_suffix (4.0.6) + rake (13.0.3) + rchardet (1.8.0) + representable (3.1.1) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.5) + rouge (2.0.7) + ruby-macho (1.4.0) + ruby2_keywords (0.0.4) + rubyzip (2.3.0) + sawyer (0.8.2) + addressable (>= 2.3.5) + faraday (> 0.8, < 2.0) + security (0.1.3) + signet (0.15.0) + addressable (~> 2.3) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.8) + CFPropertyList + naturally + slack-notifier (2.3.2) + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + thor (0.20.3) + thread_safe (0.3.6) + trailblazer-option (0.1.1) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + typhoeus (1.4.0) + ethon (>= 0.9.0) + tzinfo (1.2.9) + thread_safe (~> 0.1) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.7) + unicode-display_width (1.7.0) + webrick (1.7.0) + word_wrap (1.0.0) + xcodeproj (1.19.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + cocoapods (= 1.10.0) + danger + danger-swiftlint + fastlane (< 3.0) + fastlane-plugin-changelog + +RUBY VERSION + ruby 2.6.5p114 + +BUNDLED WITH + 2.1.4 diff --git a/Package.swift b/Package.swift index 23c7c12..df069d7 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,8 @@ let package = Package( name: "CompositionalLayoutDSL", platforms: [ .iOS(.v13), - .tvOS(.v13) + .tvOS(.v13), + .macOS(.v10_15) ], products: [ .library( @@ -19,10 +20,7 @@ let package = Package( targets: [ .target( name: "CompositionalLayoutDSL", - dependencies: []), - .testTarget( - name: "CompositionalLayoutDSLTests", - dependencies: ["CompositionalLayoutDSL"] + dependencies: [] ) ] ) diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..894d412 --- /dev/null +++ b/Podfile @@ -0,0 +1,28 @@ +platform :ios, '13.0' +use_frameworks! + +target 'CompositionalLayoutDSLApp' do + pod 'SwiftLint', '~> 0.42.0' +end + +target 'CompositionalLayoutDSLTests' do + pod 'ADLayoutTest', '~> 1.0' + pod 'SnapshotTesting', '~> 1.8' +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + + # Change the Optimization level for each target/configuration + if !config.name.include?("Distribution") + config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '0' + end + + # Disable Pod Codesign + config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = "" + config.build_settings['CODE_SIGNING_REQUIRED'] = "NO" + config.build_settings['CODE_SIGNING_ALLOWED'] = "NO" + end + end +end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000..d3d91f5 --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,32 @@ +PODS: + - ADAssertLayout (1.0.0) + - ADLayoutTest (1.0.0): + - ADAssertLayout (~> 1.0) + - SwiftCheck (~> 0.12) + - SnapshotTesting (1.8.2) + - SwiftCheck (0.12.0) + - SwiftLint (0.42.0) + +DEPENDENCIES: + - ADLayoutTest (~> 1.0) + - SnapshotTesting (~> 1.8) + - SwiftLint (~> 0.42.0) + +SPEC REPOS: + trunk: + - ADAssertLayout + - ADLayoutTest + - SnapshotTesting + - SwiftCheck + - SwiftLint + +SPEC CHECKSUMS: + ADAssertLayout: 76445db92dc9fbdcb4fd2d512d8333e902ae2a65 + ADLayoutTest: 892b38b9d91ee3ddc28511beff514db0f0872213 + SnapshotTesting: 38947050d13960d57a4a9c166fcf51bca7d56970 + SwiftCheck: d1dd04d955cc76620b0f84e087536bd059a11c24 + SwiftLint: 4fa9579c63416865179bc416f0a92d55f009600d + +PODFILE CHECKSUM: de078a0561478ef5ba8fa318ccc513986f05021f + +COCOAPODS: 1.10.0 diff --git a/README.md b/README.md index f0a99c1..2ab2f4a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,194 @@ # CompositionalLayoutDSL -A description of this package. +CompositionalLayoutDSL is a Swift library. It makes easier to create compositional layout for collection view. + +--- + +- [Requirements](#requirements) +- [Documentation](#documentation) +- [Getting started](#getting-started) +- [Installation](#installation) + - [CocoaPods](#cocoapods) + - [Carthage](#carthage) + - [Swift Package Manager](#swift-package-manager) + - [Credits](#credits) +- [Behind the scene](#behind-the-scene) + - [Core structs](#core-structs) + - [Modifiers](#modifiers) + - [DSL to UIKit Conversion](#dsl-to-uikit-conversion) +- [License](#license) + +## Requirements + +`CompositionalLayoutDSL` is written in Swift 5. Compatible with iOS 13.0+, tvOS 13.0+ and macOS 10.15+. + +## Documentation + +An online documentation can be found [here](https://fabernovel.github.io/CompositionalLayoutDSL/). + +## Getting started + +To see some usage of this library you can look to: +- An [example project](./Example/) is available and contains an iOS and a macOS application. +- The [testing target](./CompositionalLayoutDSLTests) which contains snapshot tests. The tests verify that the DSL behave the same way of using UIKit directly, the snapshot can be found [here](./CompositionalLayoutDSLTests/LayoutTests/__Snapshots__/). + +Here some layout examples: + + + + + + + + + + + + + +
Screenshot for the layout codeLayout code
+ +Here an example from the test target: [GroupDSLTests.swift (contains the same layout without the DSL)](./CompositionalLayoutDSLTests/LayoutTests/GroupDSLTests.swift) +```swift +let layout = CompositionalLayout { section, environment in + Section { + HGroup { + Item(width: .fractionalWidth(1 / 3)) + .contentInsets(trailing: 4) + VGroup(count: 2) { Item() } + .width(.fractionalWidth(1 / 3)) + .interItemSpacing(.fixed(8)) + .contentInsets(horizontal: 4) + VGroup(count: 3) { Item() } + .width(.fractionalWidth(1 / 3)) + .interItemSpacing(.fixed(8)) + .contentInsets(leading: 4) + } + .height(.absolute(100)) + .contentInsets(horizontal: 16) + } + .interGroupSpacing(8) +} +.interSectionSpacing(8) + +// Apply to a collection view +collectionView.setCollectionViewLayout(layout, animated: false) +// or +collectionView.collectionViewLayout = LayoutBuilder { layout } +``` + +
+ +Here is an example from the Example project: [GettingStartedCompositionalLayout.swift](./Example/CompositionalLayoutDSL_Example_iOS/App/ShowcaseViewController/CompositionalLayout/GettingStartedCompositionalLayout.swift) + +```swift +collectionView.collectionViewLayout = LayoutBuilder { + Section { + VGroup(count: 1) { Item() } + .height(.fractionalWidth(0.3)) + .width(.fractionalWidth(0.3)) + .interItemSpacing(.fixed(8)) + } + .interGroupSpacing(8) + .contentInsets(horizontal: 16, vertical: 8) + .orthogonalScrollingBehavior(.continuous) + .supplementariesFollowContentInsets(false) + .boundarySupplementaryItems { + BoundarySupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) + .height(.absolute(30)) + .alignment(.top) + .pinToVisibleBounds(true) + } +} +``` + +
+ +## Installation + +### Cocoapods + +To integrate `CompositionalLayoutDSL` into your Xcode project using CocoaPods, specify it in your Podfile: +``` +pod 'CompositionalLayoutDSL', '~> 0.1.0' +``` + +### Carthage + +To integrate `CompositionalLayoutDSL` into your Xcode project using Carthage, specify it in your `Cartfile`: + +``` +github "faberNovel/CompositionalLayoutDSL" ~> 0.1.0 +``` + +### Swift Package Manager + +`CompositionalLayoutDSL` can be installed as a Swift Package with Xcode 11 or higher. To install it, add a package using Xcode or a dependency to your Package.swift file: + +```swift +.package(url: "https://github.com/faberNovel/CompositionalLayoutDSL") +``` + +## Behind the scene + +Here some explanation of how this library work, it can be divided in 3 parts: the role of the core blocks, +how does the modifiers works, and how the conversion to the UIKit world is handled + +### Core structs + +This library contains all the core structs for creating a compositional layout, here the exhaustive list: +- `Configuration` +- `Section` +- `HGroup` +- `VGroup` +- `CustomGroup` +- `Item` +- `DecorationItem` +- `SupplementaryItem` +- `BoundarySupplementaryItem` + +Each of those building blocks conforms to their respective public protocol and handle the immutable properties +of their associated UIKit object. + +For example `SupplementaryItem` conforms to `LayoutSupplementaryItem` and handles the immutable +properties of `NSCollectionLayoutSupplementaryItem`, which are: +`layoutSize`, `elementKind`, `containerAnchor` and `itemAnchor`. + +Those immutable properties can only be changed on those core structs, and are not available globally +on `LayoutSupplementaryItem`. This is the same for all core structs. + +### Modifiers + +Mutable properties of the UIKit objects are handled by the extension of the `Layout...` protocols. +Here some example: `contentInset(_:)`, `edgeSpacing(_:)`, `zIndex(_:)`, `interItemSpacing(_:)`, `scrollDirection(_:)`. +Changing those mutable values are done with [modifiers](./Sources/CompositionalLayoutDSL/Internal/ModifiedLayout), +which are internal struct (e.g. [`ValueModifiedLayoutItem`](./Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutItem.swift)). +As those methods provided through extension of the `Layout...` protocol, their are available for custom +elements outside the library. + +Something to note is once you applied a modifier for mutable properties you no longer have an `Item`, +but you have a `LayoutItem`, so changing immutable values will not be possible afterward. + +### DSL to UIKit Conversion + +Finally once we have combined the core structs and the modifiers, the last step is the conversion of the `Layout...` to the UIKit world. +This is done with [builders](./Sources/CompositionalLayoutDSL/Internal/Builders), they all work in a similar way. +As an example here how `ItemBuilder` works: +- It tries to find a layoutItem conforming to the internal protocol `BuildableItem` by calling repeatedly `.layoutItem` . +- Then it calls `makeItem()` on the candidate and returns it + +**⚠️ Warning ⚠️** + +This means that only internal struct can be converted to a UIKit object, if you try to define a custom `LayoutItem` +and write `var layoutItem: LayoutItem { self }` like it is done internally, it will cause an infinite loop inside the ItemBuilder. + +User of the library **needs** to base their custom layout on core structs provided by this library. + + +## Credits + +CompositionalLayoutDSL is owned and maintained by [Fabernovel](https://www.fabernovel.com/). You can follow us on Twitter at [@Fabernovel](https://twitter.com/FabernovelTech). + +## License + +`CompositionalLayoutDSL` is available under the MIT license. See the LICENSE file for more info. diff --git a/Sources/CompositionalLayoutDSL/CompositionalLayoutDSL.swift b/Sources/CompositionalLayoutDSL/CompositionalLayoutDSL.swift deleted file mode 100644 index b485236..0000000 --- a/Sources/CompositionalLayoutDSL/CompositionalLayoutDSL.swift +++ /dev/null @@ -1,3 +0,0 @@ -struct CompositionalLayoutDSL { - var text = "Hello, World!" -} diff --git a/Sources/CompositionalLayoutDSL/Internal/Builders/BoundarySupplementaryItemBuilder.swift b/Sources/CompositionalLayoutDSL/Internal/Builders/BoundarySupplementaryItemBuilder.swift new file mode 100644 index 0000000..8b8c327 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/Builders/BoundarySupplementaryItemBuilder.swift @@ -0,0 +1,45 @@ +// +// BoundarySupplementaryItemBuilder.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 19/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +internal protocol BuildableBoundarySupplementaryItem: BuildableSupplementaryItem { + func makeBoundarySupplementaryItem() -> NSCollectionLayoutBoundarySupplementaryItem +} + +extension BuildableBoundarySupplementaryItem { + func makeSupplementaryItem() -> NSCollectionLayoutSupplementaryItem { + makeBoundarySupplementaryItem() + } +} + +internal enum BoundarySupplementaryItemBuilder { + static func make( + from layoutBoundarySupplementaryItem: LayoutBoundarySupplementaryItem + ) -> NSCollectionLayoutBoundarySupplementaryItem { + guard let buildable = getBuildableBoundarySupplementaryItem(from: layoutBoundarySupplementaryItem) else { + // swiftlint:disable:next line_length + fatalError("Unable to convert the given LayoutBoundarySupplementaryItem to NSCollectionLayoutBoundarySupplementaryItem") + } + return buildable.makeBoundarySupplementaryItem() + } + + private static func getBuildableBoundarySupplementaryItem( + from layoutBoundarySupplementaryItem: LayoutBoundarySupplementaryItem + ) -> BuildableBoundarySupplementaryItem? { + var currentBoundarySupplementaryItem = layoutBoundarySupplementaryItem + while !(currentBoundarySupplementaryItem is BuildableBoundarySupplementaryItem) { + currentBoundarySupplementaryItem = currentBoundarySupplementaryItem.layoutBoundarySupplementaryItem + } + return currentBoundarySupplementaryItem as? BuildableBoundarySupplementaryItem + } +} diff --git a/Sources/CompositionalLayoutDSL/Internal/Builders/ConfigurationBuilder.swift b/Sources/CompositionalLayoutDSL/Internal/Builders/ConfigurationBuilder.swift new file mode 100644 index 0000000..9138c80 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/Builders/ConfigurationBuilder.swift @@ -0,0 +1,46 @@ +// +// ConfigurationBuilder.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 19/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +internal protocol BuildableConfiguration { + func makeConfiguration() -> ConfigurationBuilder.TransformedType +} + +internal enum ConfigurationBuilder { + + #if os(macOS) + typealias TransformedType = NSCollectionViewCompositionalLayoutConfiguration + #else + typealias TransformedType = UICollectionViewCompositionalLayoutConfiguration + #endif + + static func make( + from layoutConfiguration: LayoutConfiguration + ) -> ConfigurationBuilder.TransformedType { + guard let buildableConfiguration = getBuildableConfiguration(from: layoutConfiguration) else { + // swiftlint:disable:next line_length + fatalError("Unable to convert the given LayoutConfiguration to UICollectionViewCompositionalLayoutConfiguration") + } + return buildableConfiguration.makeConfiguration() + } + + private static func getBuildableConfiguration( + from layoutConfiguration: LayoutConfiguration + ) -> BuildableConfiguration? { + var currentConfiguration = layoutConfiguration + while !(currentConfiguration is BuildableConfiguration) { + currentConfiguration = currentConfiguration.layoutConfiguration + } + return currentConfiguration as? BuildableConfiguration + } +} diff --git a/Sources/CompositionalLayoutDSL/Internal/Builders/DecorationItemBuilder.swift b/Sources/CompositionalLayoutDSL/Internal/Builders/DecorationItemBuilder.swift new file mode 100644 index 0000000..32a8661 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/Builders/DecorationItemBuilder.swift @@ -0,0 +1,44 @@ +// +// DecorationItemBuilder.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 19/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +internal protocol BuildableDecorationItem: BuildableItem { + func makeDecorationItem() -> NSCollectionLayoutDecorationItem +} + +extension BuildableDecorationItem { + func makeItem() -> NSCollectionLayoutItem { + makeDecorationItem() + } +} + +internal enum DecorationItemBuilder { + static func make( + from layoutDecorationItem: LayoutDecorationItem + ) -> NSCollectionLayoutDecorationItem { + guard let buildable = getBuildableDecorationItem(from: layoutDecorationItem) else { + fatalError("Unable to convert the given LayoutDecorationItem to NSCollectionLayoutDecorationItem") + } + return buildable.makeDecorationItem() + } + + private static func getBuildableDecorationItem( + from layoutDecorationItem: LayoutDecorationItem + ) -> BuildableDecorationItem? { + var currentDecorationItem = layoutDecorationItem + while !(currentDecorationItem is BuildableDecorationItem) { + currentDecorationItem = currentDecorationItem.layoutDecorationItem + } + return currentDecorationItem as? BuildableDecorationItem + } +} diff --git a/Sources/CompositionalLayoutDSL/Internal/Builders/GroupBuilder.swift b/Sources/CompositionalLayoutDSL/Internal/Builders/GroupBuilder.swift new file mode 100644 index 0000000..8cc3797 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/Builders/GroupBuilder.swift @@ -0,0 +1,40 @@ +// +// GroupBuilder.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 19/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +internal protocol BuildableGroup: BuildableItem { + func makeGroup() -> NSCollectionLayoutGroup +} + +extension BuildableGroup { + func makeItem() -> NSCollectionLayoutItem { + makeGroup() + } +} + +internal enum GroupBuilder { + static func make(from layoutGroup: LayoutGroup) -> NSCollectionLayoutGroup { + guard let buildableGroup = getBuildableGroup(from: layoutGroup) else { + fatalError("Unable to convert the given LayoutGroup to NSCollectionLayoutGroup") + } + return buildableGroup.makeGroup() + } + + private static func getBuildableGroup(from layoutGroup: LayoutGroup) -> BuildableGroup? { + var currentGroup = layoutGroup + while !(currentGroup is BuildableGroup) { + currentGroup = currentGroup.layoutGroup + } + return currentGroup as? BuildableGroup + } +} diff --git a/Sources/CompositionalLayoutDSL/Internal/Builders/ItemBuilder.swift b/Sources/CompositionalLayoutDSL/Internal/Builders/ItemBuilder.swift new file mode 100644 index 0000000..f2997eb --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/Builders/ItemBuilder.swift @@ -0,0 +1,34 @@ +// +// ItemBuilder.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 19/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +internal protocol BuildableItem { + func makeItem() -> NSCollectionLayoutItem +} + +internal enum ItemBuilder { + static func make(from layoutItem: LayoutItem) -> NSCollectionLayoutItem { + guard let buildableItem = getBuildableItem(from: layoutItem) else { + fatalError("Unable to convert the given LayoutItem to NSCollectionLayoutItem") + } + return buildableItem.makeItem() + } + + private static func getBuildableItem(from layoutItem: LayoutItem) -> BuildableItem? { + var currentItem = layoutItem + while !(currentItem is BuildableItem) { + currentItem = currentItem.layoutItem + } + return currentItem as? BuildableItem + } +} diff --git a/Sources/CompositionalLayoutDSL/Internal/Builders/SectionBuilder.swift b/Sources/CompositionalLayoutDSL/Internal/Builders/SectionBuilder.swift new file mode 100644 index 0000000..c7eaa49 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/Builders/SectionBuilder.swift @@ -0,0 +1,34 @@ +// +// SectionBuilder.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 19/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +internal protocol BuildableSection { + func makeSection() -> NSCollectionLayoutSection +} + +internal enum SectionBuilder { + static func make(from layoutSection: LayoutSection) -> NSCollectionLayoutSection { + guard let buildableSection = getBuildableSection(from: layoutSection) else { + fatalError("Unable to convert the given LayoutSection to NSCollectionLayoutSection") + } + return buildableSection.makeSection() + } + + private static func getBuildableSection(from layoutSection: LayoutSection) -> BuildableSection? { + var currentSection = layoutSection + while !(currentSection is BuildableSection) { + currentSection = currentSection.layoutSection + } + return currentSection as? BuildableSection + } +} diff --git a/Sources/CompositionalLayoutDSL/Internal/Builders/SupplementaryItemBuilder.swift b/Sources/CompositionalLayoutDSL/Internal/Builders/SupplementaryItemBuilder.swift new file mode 100644 index 0000000..e0bea53 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/Builders/SupplementaryItemBuilder.swift @@ -0,0 +1,44 @@ +// +// SupplementaryItemBuilder.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 19/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +internal protocol BuildableSupplementaryItem: BuildableItem { + func makeSupplementaryItem() -> NSCollectionLayoutSupplementaryItem +} + +extension BuildableSupplementaryItem { + func makeItem() -> NSCollectionLayoutItem { + makeSupplementaryItem() + } +} + +internal enum SupplementaryItemBuilder { + static func make( + from layoutSupplementaryItem: LayoutSupplementaryItem + ) -> NSCollectionLayoutSupplementaryItem { + guard let buildable = getBuildableSupplementaryItem(from: layoutSupplementaryItem) else { + fatalError("Unable to convert the given LayoutSupplementaryItem to NSCollectionLayoutSupplementaryItem") + } + return buildable.makeSupplementaryItem() + } + + private static func getBuildableSupplementaryItem( + from layoutSupplementaryItem: LayoutSupplementaryItem + ) -> BuildableSupplementaryItem? { + var currentSupplementaryItem = layoutSupplementaryItem + while !(currentSupplementaryItem is BuildableSupplementaryItem) { + currentSupplementaryItem = currentSupplementaryItem.layoutSupplementaryItem + } + return currentSupplementaryItem as? BuildableSupplementaryItem + } +} diff --git a/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutBoundarySupplementaryItem.swift b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutBoundarySupplementaryItem.swift new file mode 100644 index 0000000..b0e1df5 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutBoundarySupplementaryItem.swift @@ -0,0 +1,40 @@ +// +// ModifiedLayoutBoundarySupplementaryItem.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +struct ValueModifiedLayoutBoundarySupplementaryItem: LayoutBoundarySupplementaryItem, + BuildableBoundarySupplementaryItem { + let boundarySupplementaryItem: LayoutBoundarySupplementaryItem + let valueModifier: (inout NSCollectionLayoutBoundarySupplementaryItem) -> Void + + var layoutBoundarySupplementaryItem: LayoutBoundarySupplementaryItem { self } + + func makeBoundarySupplementaryItem() -> NSCollectionLayoutBoundarySupplementaryItem { + var collectionLayoutBoundarySupplementaryItem = BoundarySupplementaryItemBuilder + .make(from: boundarySupplementaryItem) + valueModifier(&collectionLayoutBoundarySupplementaryItem) + return collectionLayoutBoundarySupplementaryItem + } +} + +extension LayoutBoundarySupplementaryItem { + + func valueModifier( + _ value: T, + keyPath: WritableKeyPath + ) -> LayoutBoundarySupplementaryItem { + ValueModifiedLayoutBoundarySupplementaryItem(boundarySupplementaryItem: self) { + $0[keyPath: keyPath] = value + } + } +} diff --git a/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutConfiguration.swift b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutConfiguration.swift new file mode 100644 index 0000000..ea8dc61 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutConfiguration.swift @@ -0,0 +1,42 @@ +// +// ModifiedLayoutConfiguration.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 19/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +struct ValueModifiedLayoutConfiguration: LayoutConfiguration, BuildableConfiguration { + let configuration: LayoutConfiguration + let valueModifier: (inout ConfigurationBuilder.TransformedType) -> Void + + var layoutConfiguration: LayoutConfiguration { self } + + func makeConfiguration() -> ConfigurationBuilder.TransformedType { + var collectionLayoutConfiguration = ConfigurationBuilder.make(from: configuration) + valueModifier(&collectionLayoutConfiguration) + return collectionLayoutConfiguration + } +} + +extension LayoutConfiguration { + + func valueModifier( + _ value: T, + keyPath: WritableKeyPath + ) -> LayoutConfiguration { + ValueModifiedLayoutConfiguration(configuration: self) { $0[keyPath: keyPath] = value } + } + + func valueModifier( + modifier: @escaping (inout ConfigurationBuilder.TransformedType) -> Void + ) -> LayoutConfiguration { + ValueModifiedLayoutConfiguration(configuration: self, valueModifier: modifier) + } +} diff --git a/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutDecorationItem.swift b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutDecorationItem.swift new file mode 100644 index 0000000..7471ef2 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutDecorationItem.swift @@ -0,0 +1,35 @@ +// +// ModifiedLayoutDecorationItem.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 19/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +struct ValueModifiedLayoutDecorationItem: LayoutDecorationItem, BuildableDecorationItem { + let decorationItem: LayoutDecorationItem + let valueModifier: (inout NSCollectionLayoutDecorationItem) -> Void + + var layoutDecorationItem: LayoutDecorationItem { self } + + func makeDecorationItem() -> NSCollectionLayoutDecorationItem { + var collectionLayoutDecorationItem = DecorationItemBuilder.make(from: decorationItem) + valueModifier(&collectionLayoutDecorationItem) + return collectionLayoutDecorationItem + } +} + +extension LayoutDecorationItem { + func valueModifier( + _ value: T, + keyPath: WritableKeyPath + ) -> LayoutDecorationItem { + ValueModifiedLayoutDecorationItem(decorationItem: self) { $0[keyPath: keyPath] = value } + } +} diff --git a/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutGroup.swift b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutGroup.swift new file mode 100644 index 0000000..2d555cb --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutGroup.swift @@ -0,0 +1,39 @@ +// +// ModifiedLayoutGroup.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 19/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +struct ValueModifiedLayoutGroup: LayoutGroup, BuildableGroup { + let group: LayoutGroup + let valueModifier: (inout NSCollectionLayoutGroup) -> Void + + var layoutGroup: LayoutGroup { self } + + func makeGroup() -> NSCollectionLayoutGroup { + var collectionLayoutGroup = GroupBuilder.make(from: group) + valueModifier(&collectionLayoutGroup) + return collectionLayoutGroup + } +} + +extension LayoutGroup { + + func valueModifier(_ value: T, keyPath: WritableKeyPath) -> LayoutGroup { + ValueModifiedLayoutGroup(group: self) { $0[keyPath: keyPath] = value } + } + + func valueModifier( + modifier: @escaping (inout NSCollectionLayoutGroup) -> Void + ) -> LayoutGroup { + ValueModifiedLayoutGroup(group: self, valueModifier: modifier) + } +} diff --git a/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutItem.swift b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutItem.swift new file mode 100644 index 0000000..8309f0a --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutItem.swift @@ -0,0 +1,32 @@ +// +// ModifiedLayoutItem.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 19/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +struct ValueModifiedLayoutItem: LayoutItem, BuildableItem { + let item: LayoutItem + let valueModifier: (inout NSCollectionLayoutItem) -> Void + + var layoutItem: LayoutItem { self } + + func makeItem() -> NSCollectionLayoutItem { + var collectionLayoutItem = ItemBuilder.make(from: item) + valueModifier(&collectionLayoutItem) + return collectionLayoutItem + } +} + +extension LayoutItem { + func valueModifier(_ value: T, keyPath: WritableKeyPath) -> LayoutItem { + ValueModifiedLayoutItem(item: self) { $0[keyPath: keyPath] = value } + } +} diff --git a/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutSection.swift b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutSection.swift new file mode 100644 index 0000000..65e8c5b --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutSection.swift @@ -0,0 +1,39 @@ +// +// ModifiedLayoutSection.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 19/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +struct ValueModifiedLayoutSection: LayoutSection, BuildableSection { + let section: LayoutSection + let valueModifier: (inout NSCollectionLayoutSection) -> Void + + var layoutSection: LayoutSection { self } + + func makeSection() -> NSCollectionLayoutSection { + var collectionLayoutSection = SectionBuilder.make(from: section) + valueModifier(&collectionLayoutSection) + return collectionLayoutSection + } +} + +extension LayoutSection { + + func valueModifier(_ value: T, keyPath: WritableKeyPath) -> LayoutSection { + ValueModifiedLayoutSection(section: self) { $0[keyPath: keyPath] = value } + } + + func valueModifier( + modifier: @escaping (inout NSCollectionLayoutSection) -> Void + ) -> LayoutSection { + ValueModifiedLayoutSection(section: self, valueModifier: modifier) + } +} diff --git a/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutSupplementaryItem.swift b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutSupplementaryItem.swift new file mode 100644 index 0000000..06e82c0 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Internal/ModifiedLayout/ModifiedLayoutSupplementaryItem.swift @@ -0,0 +1,36 @@ +// +// ModifiedLayoutSupplementaryItem.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 19/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +struct ValueModifiedLayoutSupplementaryItem: LayoutSupplementaryItem, BuildableSupplementaryItem { + let supplementaryItem: LayoutSupplementaryItem + let valueModifier: (inout NSCollectionLayoutSupplementaryItem) -> Void + + var layoutSupplementaryItem: LayoutSupplementaryItem { self } + + func makeSupplementaryItem() -> NSCollectionLayoutSupplementaryItem { + var collectionLayoutSupplementaryItem = SupplementaryItemBuilder.make(from: supplementaryItem) + valueModifier(&collectionLayoutSupplementaryItem) + return collectionLayoutSupplementaryItem + } +} + +extension LayoutSupplementaryItem { + + func valueModifier( + _ value: T, + keyPath: WritableKeyPath + ) -> LayoutSupplementaryItem { + ValueModifiedLayoutSupplementaryItem(supplementaryItem: self) { $0[keyPath: keyPath] = value } + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/BoundarySupplementaryItem/BoundarySupplementaryItem.swift b/Sources/CompositionalLayoutDSL/Public/BoundarySupplementaryItem/BoundarySupplementaryItem.swift new file mode 100644 index 0000000..f4d4098 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/BoundarySupplementaryItem/BoundarySupplementaryItem.swift @@ -0,0 +1,95 @@ +// +// BoundarySupplementaryItem.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// An object used to add headers or footers to a collection view. +public struct BoundarySupplementaryItem: LayoutBoundarySupplementaryItem, ResizableItem { + + private var widthDimension: NSCollectionLayoutDimension + private var heightDimension: NSCollectionLayoutDimension + private var elementKind: String + + private var alignment: NSRectAlignment = .top + private var absoluteOffset: CGPoint = .zero + + // MARK: - Life cycle + + /// Creates a boundary supplementary item of the specified size, with a string to identify the + /// element kind. + public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), + height: NSCollectionLayoutDimension = .fractionalHeight(1), + elementKind: String) { + self.widthDimension = width + self.heightDimension = height + self.elementKind = elementKind + } + + /// Creates a boundary supplementary item of the specified size, with a string to identify the + /// element kind. + public init(size: NSCollectionLayoutSize, + elementKind: String) { + self.widthDimension = size.widthDimension + self.heightDimension = size.heightDimension + self.elementKind = elementKind + } + + // MARK: - BoundarySupplementaryItem + + /// The alignment of the boundary supplementary item relative to the section or layout it's attached to. + /// + /// The default value for this property is `NSRectAlignment.top` + public func alignment(_ alignment: NSRectAlignment) -> Self { + with(self) { $0.alignment = alignment } + } + + public func absoluteOffset(_ absoluteOffset: CGPoint) -> Self { + with(self) { $0.absoluteOffset = absoluteOffset } + } + + // MARK: - LayoutBoundarySupplementaryItem + + public var layoutBoundarySupplementaryItem: LayoutBoundarySupplementaryItem { + self + } + + // MARK: - ResizableItem + + /// Configure the width of the boundary supplementary item + /// + /// The default value is `.fractionalWidth(1.0)` + public func width(_ width: NSCollectionLayoutDimension) -> Self { + with(self) { $0.widthDimension = width } + } + + /// Configure the height of the boundary supplementary item + /// + /// The default value is `.fractionalHeight(1.0)` + public func height(_ height: NSCollectionLayoutDimension) -> Self { + with(self) { $0.heightDimension = height } + } +} + +extension BoundarySupplementaryItem: BuildableBoundarySupplementaryItem { + func makeBoundarySupplementaryItem() -> NSCollectionLayoutBoundarySupplementaryItem { + let boundarySupplementaryItem = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: NSCollectionLayoutSize( + widthDimension: widthDimension, + heightDimension: heightDimension + ), + elementKind: elementKind, + alignment: alignment, + absoluteOffset: absoluteOffset + ) + return boundarySupplementaryItem + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/BoundarySupplementaryItem/LayoutBoundarySupplementaryItem.swift b/Sources/CompositionalLayoutDSL/Public/BoundarySupplementaryItem/LayoutBoundarySupplementaryItem.swift new file mode 100644 index 0000000..cfd9c93 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/BoundarySupplementaryItem/LayoutBoundarySupplementaryItem.swift @@ -0,0 +1,165 @@ +// +// LayoutBoundarySupplementaryItem.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A type that represents a boundary supplementary item in a compositional layout and provides +/// modifiers to configure boundary supplementary items. +/// +/// You create custom boundary supplementary items by declaring types that conform to the +/// ``LayoutBoundarySupplementaryItem`` protocol. Implement the required +/// ``layoutBoundarySupplementaryItem`` computed property to provide the content and +/// configuration for your custom boundary supplementary item. +/// +/// struct MyBoundarySupplementaryItem: LayoutBoundarySupplementaryItem { +/// var layoutBoundarySupplementaryItem: LayoutBoundarySupplementaryItem { +/// BoundarySupplementaryItem(elementKind: "leadingKind") +/// .width(.absolute(40)) +/// .alignment(.leading) +/// } +/// } +/// +public protocol LayoutBoundarySupplementaryItem: LayoutSupplementaryItem { + var layoutBoundarySupplementaryItem: LayoutBoundarySupplementaryItem { get } +} + +public extension LayoutBoundarySupplementaryItem { + + // MARK: - LayoutSupplementaryItem + + var layoutSupplementaryItem: LayoutSupplementaryItem { self } +} + +extension LayoutBoundarySupplementaryItem { + + // MARK: - Boundary Supplementary Item mutable properties + + /// Configure whether a boundary supplementary item extends the content area of the section + /// or layout it's attached to. + /// + /// The default value of this property is true. + @warn_unqualified_access + public func extendsBoundary(_ extendsBoundary: Bool) -> LayoutBoundarySupplementaryItem { + valueModifier(extendsBoundary, keyPath: \.extendsBoundary) + } + + /// Configure whether a header or footer is pinned to the top or bottom visible boundary of + /// the section or layout it's attached to. + /// + /// The default value of this property is `false`, which means that the boundary supplementary + /// item (header or footer) remains in its original position during scrolling, and moves + /// offscreen as its section or layout scrolls. Set the value of this property to true to pin + /// the boundary supplementary item to the visible bounds of the section or layout it's + /// attached to. This way, the boundary supplementary item is shown while any portion of the + /// section or layout it's attached to is visible. + @warn_unqualified_access + public func pinToVisibleBounds(_ pinToVisibleBounds: Bool) -> LayoutBoundarySupplementaryItem { + valueModifier(pinToVisibleBounds, keyPath: \.pinToVisibleBounds) + } +} + +extension LayoutBoundarySupplementaryItem { + + // MARK: - Supplementary Item mutable properties + + /// Configure the vertical stacking order of the decoration item in relation to other items in the section. + /// + /// The default value of this property is 0, which means the decoration item appears below all + /// other items in the section. + @warn_unqualified_access + public func zIndex(zIndex: Int) -> LayoutBoundarySupplementaryItem { + valueModifier(zIndex, keyPath: \.zIndex) + } +} + +extension LayoutBoundarySupplementaryItem { + + // MARK: - Content Insets + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets(value: CGFloat) -> LayoutBoundarySupplementaryItem { + return self.contentInsets(top: value, leading: value, bottom: value, trailing: value) + } + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets(horizontal: CGFloat = 0, vertical: CGFloat = 0) -> LayoutBoundarySupplementaryItem { + return self.contentInsets(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) + } + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets( + top: CGFloat = 0, + leading: CGFloat = 0, + bottom: CGFloat = 0, + trailing: CGFloat = 0 + ) -> LayoutBoundarySupplementaryItem { + return self.contentInsets( + NSDirectionalEdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing) + ) + } + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets(_ insets: NSDirectionalEdgeInsets) -> LayoutBoundarySupplementaryItem { + valueModifier(insets, keyPath: \.contentInsets) + } +} + +extension LayoutBoundarySupplementaryItem { + + // MARK: - Edge Spacing + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing(value: NSCollectionLayoutSpacing?) -> LayoutBoundarySupplementaryItem { + return self.edgeSpacing(top: value, leading: value, bottom: value, trailing: value) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing( + horizontal: NSCollectionLayoutSpacing? = nil, + vertical: NSCollectionLayoutSpacing? = nil + ) -> LayoutBoundarySupplementaryItem { + return self.edgeSpacing(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing( + top: NSCollectionLayoutSpacing? = nil, + leading: NSCollectionLayoutSpacing? = nil, + bottom: NSCollectionLayoutSpacing? = nil, + trailing: NSCollectionLayoutSpacing? = nil + ) -> LayoutBoundarySupplementaryItem { + return self.edgeSpacing( + NSCollectionLayoutEdgeSpacing(leading: leading, top: top, trailing: trailing, bottom: bottom) + ) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing(_ edgeSpacing: NSCollectionLayoutEdgeSpacing) -> LayoutBoundarySupplementaryItem { + valueModifier(edgeSpacing, keyPath: \.edgeSpacing) + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/CompositionalLayout.swift b/Sources/CompositionalLayoutDSL/Public/CompositionalLayout.swift new file mode 100644 index 0000000..9b8478c --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/CompositionalLayout.swift @@ -0,0 +1,136 @@ +// +// CompositionalLayoutDSL.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 12/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// An object that completely represent a compositional layout. +/// +/// You can create a fully configured layout like showed in the example below +/// +/// let compositionalLayout = CompositionalLayout { (_, _) in +/// Section { +/// VGroup { Item() } +/// .width(.fractionalWidth(1/3)) +/// .interItemSpacing(.fixed(8)) +/// } +/// .interGroupSpacing(8) +/// .contentInsets(horizontal: 20) +/// } +/// .interSectionSpacing(16) +/// .boundarySupplementaryItems { +/// BoundarySupplementaryItem(elementKind: "globalHeader") +/// .alignment(.top) +/// } +/// +public struct CompositionalLayout { + + public typealias SectionProvider = (Int, NSCollectionLayoutEnvironment) -> LayoutSection? + + private let sectionBuilder: SectionProvider + private var configuration: LayoutConfiguration + + // MARK: - Life cycle + + public init(configuration: LayoutConfiguration = Configuration(), + sectionsBuilder: @escaping SectionProvider) { + self.sectionBuilder = sectionsBuilder + self.configuration = configuration + } + + // MARK: - CompositionalLayout + + #if os(macOS) + /// Configure the axis that the content in the collection view layout scrolls along. + /// + /// The default value of this property is `UICollectionView.ScrollDirection.vertical`. + public func scrollDirection( + _ scrollDirection: NSCollectionView.ScrollDirection + ) -> Self { + with(self) { $0.configuration = $0.configuration.scrollDirection(scrollDirection) } + } + #else + /// Configure the axis that the content in the collection view layout scrolls along. + /// + /// The default value of this property is `UICollectionView.ScrollDirection.vertical`. + public func scrollDirection( + _ scrollDirection: UICollectionView.ScrollDirection + ) -> Self { + with(self) { $0.configuration = $0.configuration.scrollDirection(scrollDirection) } + } + #endif + + /// Configure the amount of space between the sections in the layout. + /// + /// The default value of this property is `0.0`. + public func interSectionSpacing(_ interSectionSpacing: CGFloat) -> Self { + with(self) { $0.configuration = $0.configuration.interSectionSpacing(interSectionSpacing) } + } + + /// Add an array of the supplementary items that are associated with the boundary edges + /// of the entire layout, such as global headers and footers. + public func boundarySupplementaryItems( + @LayoutBoundarySupplementaryItemBuilder + _ boundarySupplementaryItems: () -> [LayoutBoundarySupplementaryItem] + ) -> Self { + with(self) { + $0.configuration = $0.configuration.boundarySupplementaryItems(boundarySupplementaryItems) + } + } + + #if !os(macOS) + /// Configure the boundary to reference when defining content insets. + /// + /// The default value of this property is ``UIContentInsetsReference.safeArea`` + @available(iOS 14.0, tvOS 14.0, *) + public func contentInsetsReference( + _ contentInsetsReference: UIContentInsetsReference + ) -> Self { + with(self) { + $0.configuration = $0.configuration.contentInsetsReference(contentInsetsReference) + } + } + #endif +} + +public extension CompositionalLayout { + + init(configuration: LayoutConfiguration = Configuration(), + repeatingSections sectionsBuilder: [SectionProvider]) { + self.init(configuration: configuration) { section, environment in + guard !sectionsBuilder.isEmpty else { return nil } + let sectionBuilder = sectionsBuilder[section % sectionsBuilder.count] + return sectionBuilder(section, environment) + } + } +} + +extension CompositionalLayout { + #if os(macOS) + func makeCollectionViewCompositionalLayout() -> NSCollectionViewCompositionalLayout { + return NSCollectionViewCompositionalLayout( + sectionProvider: { section, environment in + return sectionBuilder(section, environment).map(SectionBuilder.make(from:)) + }, + configuration: ConfigurationBuilder.make(from: configuration) + ) + } + #else + func makeCollectionViewCompositionalLayout() -> UICollectionViewCompositionalLayout { + return UICollectionViewCompositionalLayout( + sectionProvider: { section, environment in + return sectionBuilder(section, environment).map(SectionBuilder.make(from:)) + }, + configuration: ConfigurationBuilder.make(from: configuration) + ) + } + #endif +} diff --git a/Sources/CompositionalLayoutDSL/Public/CompositionalLayoutDSL.swift b/Sources/CompositionalLayoutDSL/Public/CompositionalLayoutDSL.swift new file mode 100644 index 0000000..90f801c --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/CompositionalLayoutDSL.swift @@ -0,0 +1,88 @@ +// +// CompositionalLayoutDSL.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 06/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +// swiftlint:disable identifier_name + +/// Converts a layout section into a `NSCollectionLayoutSection` +public func LayoutSectionBuilder( + layoutSection: () -> LayoutSection +) -> NSCollectionLayoutSection { + SectionBuilder.make(from: layoutSection()) +} + +#if os(macOS) +/// Converts a layout configuration and a layout section into an `NSCollectionViewCompositionalLayout` +public func LayoutBuilder( + configuration: LayoutConfiguration = Configuration(), + layoutSection: () -> LayoutSection +) -> NSCollectionViewCompositionalLayout { + return NSCollectionViewCompositionalLayout( + section: SectionBuilder.make(from: layoutSection()), + configuration: ConfigurationBuilder.make(from: configuration) + ) +} +#else +/// Converts a layout configuration and a layout section into a `UICollectionViewCompositionalLayout` +public func LayoutBuilder( + configuration: LayoutConfiguration = Configuration(), + layoutSection: () -> LayoutSection +) -> UICollectionViewCompositionalLayout { + return UICollectionViewCompositionalLayout( + section: SectionBuilder.make(from: layoutSection()), + configuration: ConfigurationBuilder.make(from: configuration) + ) +} +#endif + +#if os(macOS) +/// Converts a compositionalLayout into an `NSCollectionViewCompositionalLayout` +public func LayoutBuilder( + compositionalLayout: () -> CompositionalLayout +) -> NSCollectionViewCompositionalLayout { + compositionalLayout().makeCollectionViewCompositionalLayout() +} +#else +/// Converts a compositionalLayout into a `UICollectionViewCompositionalLayout` +public func LayoutBuilder( + compositionalLayout: () -> CompositionalLayout +) -> UICollectionViewCompositionalLayout { + compositionalLayout().makeCollectionViewCompositionalLayout() +} +#endif + +#if os(macOS) +extension NSCollectionView { + /// Configure a UICollectionView layout with a CompositionalLayout + public func setCollectionViewLayout( + _ layout: CompositionalLayout + ) { + self.collectionViewLayout = LayoutBuilder { layout } + } +} +#else +extension UICollectionView { + /// Configure a UICollectionView layout with a CompositionalLayout + public func setCollectionViewLayout( + _ layout: CompositionalLayout, + animated: Bool, + completion: ((Bool) -> Void)? = nil + ) { + self.setCollectionViewLayout( + LayoutBuilder { layout }, + animated: animated, + completion: completion + ) + } +} +#endif diff --git a/Sources/CompositionalLayoutDSL/Public/Configuration/Configuration.swift b/Sources/CompositionalLayoutDSL/Public/Configuration/Configuration.swift new file mode 100644 index 0000000..cbe4da0 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/Configuration/Configuration.swift @@ -0,0 +1,37 @@ +// +// CompositionalConfiguration.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 12/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// An object that defines scroll direction, section spacing, and headers or footers for the layout. +public struct Configuration: LayoutConfiguration { + + // MARK: - Life cycle + + public init() {} + + // MARK: - CompositionalLayoutConfiguration + + public var layoutConfiguration: LayoutConfiguration { + return self + } +} + +extension Configuration: BuildableConfiguration { + func makeConfiguration() -> ConfigurationBuilder.TransformedType { + #if os(macOS) + return NSCollectionViewCompositionalLayoutConfiguration() + #else + return UICollectionViewCompositionalLayoutConfiguration() + #endif + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/Configuration/LayoutConfiguration.swift b/Sources/CompositionalLayoutDSL/Public/Configuration/LayoutConfiguration.swift new file mode 100644 index 0000000..52c1707 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/Configuration/LayoutConfiguration.swift @@ -0,0 +1,95 @@ +// +// CompositionalLayoutConfiguration.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 12/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A type that represents a compositional layout configuration and provides +/// modifiers to change the configuration. +/// +/// You create custom configuration by declaring types that conform to the +/// ``LayoutConfiguration`` protocol. Implement the required ``layoutConfiguration`` +/// computed property to provide your customized settings. +/// +/// struct MyConfiguration: LayoutConfiguration { +/// var layoutConfiguration: LayoutConfiguration { +/// Configuration() +/// .scrollDirection(.horizontal) +/// .interSectionSpacing(16) +/// .contentInsetsReference(.readableContent) +/// } +/// } +/// +public protocol LayoutConfiguration { + var layoutConfiguration: LayoutConfiguration { get } +} + +extension LayoutConfiguration { + + // MARK: - Mutable properties + + #if os(macOS) + /// Configure the axis that the content in the collection view layout scrolls along. + /// + /// The default value of this property is `NSCollectionView.ScrollDirection.vertical`. + @warn_unqualified_access + public func scrollDirection( + _ scrollDirection: NSCollectionView.ScrollDirection + ) -> LayoutConfiguration { + valueModifier(scrollDirection, keyPath: \.scrollDirection) + } + #else + /// Configure the axis that the content in the collection view layout scrolls along. + /// + /// The default value of this property is `UICollectionView.ScrollDirection.vertical`. + @warn_unqualified_access + public func scrollDirection( + _ scrollDirection: UICollectionView.ScrollDirection + ) -> LayoutConfiguration { + valueModifier(scrollDirection, keyPath: \.scrollDirection) + } + #endif + + /// Configure the amount of space between the sections in the layout. + /// + /// The default value of this property is `0.0`. + @warn_unqualified_access + public func interSectionSpacing(_ interSectionSpacing: CGFloat) -> LayoutConfiguration { + valueModifier(interSectionSpacing, keyPath: \.interSectionSpacing) + } + + /// Add an array of the supplementary items that are associated with the boundary edges + /// of the entire layout, such as global headers and footers. + @warn_unqualified_access + public func boundarySupplementaryItems( + @LayoutBoundarySupplementaryItemBuilder + _ boundarySupplementaryItems: () -> [LayoutBoundarySupplementaryItem] + ) -> LayoutConfiguration { + let boundarySupplementaryItems = boundarySupplementaryItems() + .map(BoundarySupplementaryItemBuilder.make(from:)) + return valueModifier { + $0.boundarySupplementaryItems.append(contentsOf: boundarySupplementaryItems) + } + } + + #if !os(macOS) + /// Configure the boundary to reference when defining content insets. + /// + /// The default value of this property is ``UIContentInsetsReference.safeArea`` + @available(iOS 14.0, tvOS 14.0, *) + @warn_unqualified_access + public func contentInsetsReference( + _ contentInsetsReference: UIContentInsetsReference + ) -> LayoutConfiguration { + valueModifier(contentInsetsReference, keyPath: \.contentInsetsReference) + } + #endif +} diff --git a/Sources/CompositionalLayoutDSL/Public/DecorationItem/DecorationItem.swift b/Sources/CompositionalLayoutDSL/Public/DecorationItem/DecorationItem.swift new file mode 100644 index 0000000..49a0103 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/DecorationItem/DecorationItem.swift @@ -0,0 +1,38 @@ +// +// DecorationItem.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// An object used to add a background to a section of a collection view. +public struct DecorationItem: LayoutDecorationItem { + + private var elementKind: String + + // MARK: - Life cycle + + public init(elementKind: String) { + self.elementKind = elementKind + } + + // MARK: - LayoutDecorationItem + + public var layoutDecorationItem: LayoutDecorationItem { + return self + } +} + +extension DecorationItem: BuildableDecorationItem { + func makeDecorationItem() -> NSCollectionLayoutDecorationItem { + let decorationItem = NSCollectionLayoutDecorationItem.background(elementKind: elementKind) + return decorationItem + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/DecorationItem/LayoutDecorationItem.swift b/Sources/CompositionalLayoutDSL/Public/DecorationItem/LayoutDecorationItem.swift new file mode 100644 index 0000000..48804a0 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/DecorationItem/LayoutDecorationItem.swift @@ -0,0 +1,135 @@ +// +// LayoutDecorationItem.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A type that represents a decoration item in a compositional layout and provides +/// modifiers to configure decoration items. +/// +/// You create custom decoration items by declaring types that conform to the +/// ``LayoutDecorationItem`` protocol. Implement the required ``layoutDecorationItem`` +/// computed property to provide the content and configuration for your custom decoration item. +/// +/// struct MyDecorationItem: LayoutDecorationItem { +/// var layoutDecorationItem: LayoutDecorationItem { +/// DecorationItem(elementKind: "backgroundKind") +/// .contentInsets(value: 4) +/// } +/// } +/// +public protocol LayoutDecorationItem: LayoutItem { + var layoutDecorationItem: LayoutDecorationItem { get } +} + +public extension LayoutDecorationItem { + + // MARK: - LayoutItem + + var layoutItem: LayoutItem { self } +} + +extension LayoutDecorationItem { + + // MARK: - Decoration Item mutable properties + + /// Configure the vertical stacking order of the decoration item in relation to other items in the section. + /// + /// The default value of this property is 0, which means the decoration item appears below all + /// other items in the section. + @warn_unqualified_access + public func zIndex(zIndex: Int) -> LayoutDecorationItem { + valueModifier(zIndex, keyPath: \.zIndex) + } +} + +extension LayoutDecorationItem { + + // MARK: - Content Insets + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets(value: CGFloat) -> LayoutDecorationItem { + return self.contentInsets(top: value, leading: value, bottom: value, trailing: value) + } + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets(horizontal: CGFloat = 0, vertical: CGFloat = 0) -> LayoutDecorationItem { + return self.contentInsets(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) + } + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets( + top: CGFloat = 0, + leading: CGFloat = 0, + bottom: CGFloat = 0, + trailing: CGFloat = 0 + ) -> LayoutDecorationItem { + return self.contentInsets( + NSDirectionalEdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing) + ) + } + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets(_ insets: NSDirectionalEdgeInsets) -> LayoutDecorationItem { + valueModifier(insets, keyPath: \.contentInsets) + } +} + +extension LayoutDecorationItem { + + // MARK: - Edge Spacing + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing(value: NSCollectionLayoutSpacing?) -> LayoutDecorationItem { + return self.edgeSpacing(top: value, leading: value, bottom: value, trailing: value) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing( + horizontal: NSCollectionLayoutSpacing? = nil, + vertical: NSCollectionLayoutSpacing? = nil + ) -> LayoutDecorationItem { + return self.edgeSpacing(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing( + top: NSCollectionLayoutSpacing? = nil, + leading: NSCollectionLayoutSpacing? = nil, + bottom: NSCollectionLayoutSpacing? = nil, + trailing: NSCollectionLayoutSpacing? = nil + ) -> LayoutDecorationItem { + return self.edgeSpacing( + NSCollectionLayoutEdgeSpacing(leading: leading, top: top, trailing: trailing, bottom: bottom) + ) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing(_ edgeSpacing: NSCollectionLayoutEdgeSpacing) -> LayoutDecorationItem { + valueModifier(edgeSpacing, keyPath: \.edgeSpacing) + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/Group/CustomGroup.swift b/Sources/CompositionalLayoutDSL/Public/Group/CustomGroup.swift new file mode 100644 index 0000000..d583b1f --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/Group/CustomGroup.swift @@ -0,0 +1,80 @@ +// +// CustomGroup.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A customizable container for a set of items. +public struct CustomGroup: LayoutGroup, ResizableItem { + + private var widthDimension: NSCollectionLayoutDimension + private var heightDimension: NSCollectionLayoutDimension + private let itemProvider: NSCollectionLayoutGroupCustomItemProvider + + // MARK: - Life cycle + + /// Creates a group of the specified size, with an item provider that creates a custom + /// arrangement for those items. + public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), + height: NSCollectionLayoutDimension = .fractionalHeight(1), + itemProvider: @escaping NSCollectionLayoutGroupCustomItemProvider) { + self.widthDimension = width + self.heightDimension = height + self.itemProvider = itemProvider + } + + /// Creates a group of the specified size, with an item provider that creates a custom + /// arrangement for those items. + public init(size: NSCollectionLayoutSize, + itemProvider: @escaping NSCollectionLayoutGroupCustomItemProvider) { + self.widthDimension = size.widthDimension + self.heightDimension = size.heightDimension + self.itemProvider = itemProvider + } + + /// Creates a group with an item provider that creates a custom arrangement for those items. + public init(itemProvider: @escaping NSCollectionLayoutGroupCustomItemProvider) { + self.init(width: .fractionalWidth(1), height: .fractionalHeight(1), itemProvider: itemProvider) + } + + // MARK: - LayoutGroup + + public var layoutGroup: LayoutGroup { + return self + } + + // MARK: - ResizableItem + + /// Configure the width of the group + /// + /// The default value is `.fractionalWidth(1.0)` + public func width(_ width: NSCollectionLayoutDimension) -> Self { + with(self) { $0.widthDimension = width } + } + + /// Configure the height of the group + /// + /// The default value is `.fractionalHeight(1.0)` + public func height(_ height: NSCollectionLayoutDimension) -> Self { + with(self) { $0.heightDimension = height } + } +} + +extension CustomGroup: BuildableGroup { + func makeGroup() -> NSCollectionLayoutGroup { + let size = NSCollectionLayoutSize( + widthDimension: widthDimension, + heightDimension: heightDimension + ) + let group = NSCollectionLayoutGroup.custom(layoutSize: size, itemProvider: itemProvider) + return group + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/Group/HGroup.swift b/Sources/CompositionalLayoutDSL/Public/Group/HGroup.swift new file mode 100644 index 0000000..b7ff776 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/Group/HGroup.swift @@ -0,0 +1,117 @@ +// +// HGroup.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A container for a set of items that lays out the items horizontally. +public struct HGroup: LayoutGroup, ResizableItem { + + enum SubItems { + case list([LayoutItem]) + case repeated(LayoutItem, count: Int) + } + + private var widthDimension: NSCollectionLayoutDimension + private var heightDimension: NSCollectionLayoutDimension + private var subItems: SubItems + + // MARK: - Life cycle + + /// Creates a group of the specified size, containing an array of items arranged in a horizontal line. + public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), + height: NSCollectionLayoutDimension = .fractionalHeight(1), + @LayoutItemBuilder subItems: () -> [LayoutItem]) { + self.widthDimension = width + self.heightDimension = height + self.subItems = .list(subItems()) + } + + /// Creates a group of the specified size, containing an array of items arranged in a horizontal line. + public init(size: NSCollectionLayoutSize, + @LayoutItemBuilder subItems: () -> [LayoutItem]) { + self.widthDimension = size.widthDimension + self.heightDimension = size.heightDimension + self.subItems = .list(subItems()) + } + + /// Creates a group of the specified size, containing an array of equally sized items arranged + /// in a horizontal line up to the number specified by count. + public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), + height: NSCollectionLayoutDimension = .fractionalHeight(1), + count: Int, + subItem: () -> LayoutItem) { + self.widthDimension = width + self.heightDimension = height + self.subItems = .repeated(subItem(), count: count) + } + + /// Creates a group of the specified size, containing an array of equally sized items arranged + /// in a horizontal line up to the number specified by count. + public init(size: NSCollectionLayoutSize, + count: Int, + subItem: () -> LayoutItem) { + self.widthDimension = size.widthDimension + self.heightDimension = size.heightDimension + self.subItems = .repeated(subItem(), count: count) + } + + /// Creates a group containing an array of items arranged in a horizontal line. + public init(@LayoutItemBuilder subItems: () -> [LayoutItem]) { + self.init(width: .fractionalWidth(1), height: .fractionalHeight(1), subItems: subItems) + } + + // MARK: - LayoutGroup + + public var layoutGroup: LayoutGroup { + return self + } + + // MARK: - ResizableItem + + /// Configure the width of the group + /// + /// The default value is `.fractionalWidth(1.0)` + public func width(_ width: NSCollectionLayoutDimension) -> Self { + with(self) { $0.widthDimension = width } + } + + /// Configure the height of the group + /// + /// The default value is `.fractionalHeight(1.0)` + public func height(_ height: NSCollectionLayoutDimension) -> Self { + with(self) { $0.heightDimension = height } + } +} + +extension HGroup: BuildableGroup { + func makeGroup() -> NSCollectionLayoutGroup { + let size = NSCollectionLayoutSize( + widthDimension: widthDimension, + heightDimension: heightDimension + ) + let group: NSCollectionLayoutGroup + switch subItems { + case let .list(items): + group = NSCollectionLayoutGroup.horizontal( + layoutSize: size, + subitems: items.map(ItemBuilder.make(from:)) + ) + case let .repeated(item, count): + group = NSCollectionLayoutGroup.horizontal( + layoutSize: size, + subitem: ItemBuilder.make(from: item), + count: count + ) + } + return group + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/Group/LayoutGroup.swift b/Sources/CompositionalLayoutDSL/Public/Group/LayoutGroup.swift new file mode 100644 index 0000000..f9d97b3 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/Group/LayoutGroup.swift @@ -0,0 +1,144 @@ +// +// LayoutGroup.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A type that represents a group in a compositional layout and provides +/// modifiers to configure groups. +/// +/// You create custom groups by declaring types that conform to the +/// ``LayoutGroup`` protocol. Implement the required ``layoutGroup`` +/// computed property to provide the content and configuration for your custom group. +/// +/// struct MyGroup: LayoutGroup { +/// var layoutGroup: LayoutGroup { +/// HGroup(count: 4) { +/// Item() +/// } +/// .height(.absolute(300)) +/// .interItemSpacing(.fixed(8)) +/// } +/// } +/// +public protocol LayoutGroup: LayoutItem { + var layoutGroup: LayoutGroup { get } +} + +public extension LayoutGroup { + + // MARK: - LayoutItem + + var layoutItem: LayoutItem { layoutGroup } +} + +extension LayoutGroup { + + // MARK: - Group Mutable properties + + /// Add an array of the supplementary items that are anchored to the group. + @warn_unqualified_access + public func supplementaryItems( + @LayoutSupplementaryItemBuilder _ supplementaryItems: () -> [LayoutSupplementaryItem] + ) -> LayoutGroup { + let supplementaryItems = supplementaryItems().map(SupplementaryItemBuilder.make(from:)) + return valueModifier { $0.supplementaryItems.append(contentsOf: supplementaryItems) } + } + + /// Configure the amount of space between the items along the layout axis of the group. + @warn_unqualified_access + public func interItemSpacing(_ interItemSpacing: NSCollectionLayoutSpacing?) -> LayoutGroup { + valueModifier(interItemSpacing, keyPath: \.interItemSpacing) + } +} + +extension LayoutGroup { + + // MARK: - Content Insets + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets(value: CGFloat) -> LayoutGroup { + return self.contentInsets(top: value, leading: value, bottom: value, trailing: value) + } + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets(horizontal: CGFloat = 0, vertical: CGFloat = 0) -> LayoutGroup { + return self.contentInsets(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) + } + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets( + top: CGFloat = 0, + leading: CGFloat = 0, + bottom: CGFloat = 0, + trailing: CGFloat = 0 + ) -> LayoutGroup { + return self.contentInsets( + NSDirectionalEdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing) + ) + } + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets(_ insets: NSDirectionalEdgeInsets) -> LayoutGroup { + valueModifier(insets, keyPath: \.contentInsets) + } +} + +extension LayoutGroup { + + // MARK: - Edge Spacing + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing(value: NSCollectionLayoutSpacing?) -> LayoutGroup { + return self.edgeSpacing(top: value, leading: value, bottom: value, trailing: value) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing( + horizontal: NSCollectionLayoutSpacing? = nil, + vertical: NSCollectionLayoutSpacing? = nil + ) -> LayoutGroup { + return self.edgeSpacing(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing( + top: NSCollectionLayoutSpacing? = nil, + leading: NSCollectionLayoutSpacing? = nil, + bottom: NSCollectionLayoutSpacing? = nil, + trailing: NSCollectionLayoutSpacing? = nil + ) -> LayoutGroup { + return self.edgeSpacing( + NSCollectionLayoutEdgeSpacing(leading: leading, top: top, trailing: trailing, bottom: bottom) + ) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing(_ edgeSpacing: NSCollectionLayoutEdgeSpacing) -> LayoutGroup { + valueModifier(edgeSpacing, keyPath: \.edgeSpacing) + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/Group/VGroup.swift b/Sources/CompositionalLayoutDSL/Public/Group/VGroup.swift new file mode 100644 index 0000000..f39d804 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/Group/VGroup.swift @@ -0,0 +1,117 @@ +// +// VGroup.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A container for a set of items that lays out the items vertically. +public struct VGroup: LayoutGroup, ResizableItem { + + private enum SubItems { + case list([LayoutItem]) + case repeated(LayoutItem, count: Int) + } + + private var widthDimension: NSCollectionLayoutDimension + private var heightDimension: NSCollectionLayoutDimension + private var subItems: SubItems + + // MARK: - Life cycle + + /// Creates a group of the specified size, containing an array of items arranged in a vertical line. + public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), + height: NSCollectionLayoutDimension = .fractionalHeight(1), + @LayoutItemBuilder subItems: () -> [LayoutItem]) { + self.widthDimension = width + self.heightDimension = height + self.subItems = .list(subItems()) + } + + /// Creates a group of the specified size, containing an array of items arranged in a vertical line. + public init(size: NSCollectionLayoutSize, + @LayoutItemBuilder subItems: () -> [LayoutItem]) { + self.widthDimension = size.widthDimension + self.heightDimension = size.heightDimension + self.subItems = .list(subItems()) + } + + /// Creates a group of the specified size, containing an array of equally sized items arranged + /// in a horizontal line up to the number specified by count. + public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), + height: NSCollectionLayoutDimension = .fractionalHeight(1), + count: Int, + subItem: () -> LayoutItem) { + self.widthDimension = width + self.heightDimension = height + self.subItems = .repeated(subItem(), count: count) + } + + /// Creates a group of the specified size, containing an array of equally sized items arranged + /// in a horizontal line up to the number specified by count. + public init(size: NSCollectionLayoutSize, + count: Int, + subItem: () -> LayoutItem) { + self.widthDimension = size.widthDimension + self.heightDimension = size.heightDimension + self.subItems = .repeated(subItem(), count: count) + } + + /// Creates a group containing an array of items arranged in a vertical line. + public init(@LayoutItemBuilder subItems: () -> [LayoutItem]) { + self.init(width: .fractionalWidth(1), height: .fractionalHeight(1), subItems: subItems) + } + + // MARK: - LayoutGroup + + public var layoutGroup: LayoutGroup { + return self + } + + // MARK: - ResizableItem + + /// Configure the width of the group + /// + /// The default value is `.fractionalWidth(1.0)` + public func width(_ width: NSCollectionLayoutDimension) -> Self { + with(self) { $0.widthDimension = width } + } + + /// Configure the height of the group + /// + /// The default value is `.fractionalHeight(1.0)` + public func height(_ height: NSCollectionLayoutDimension) -> Self { + with(self) { $0.heightDimension = height } + } +} + +extension VGroup: BuildableGroup { + func makeGroup() -> NSCollectionLayoutGroup { + let size = NSCollectionLayoutSize( + widthDimension: widthDimension, + heightDimension: heightDimension + ) + let group: NSCollectionLayoutGroup + switch subItems { + case let .list(items): + group = NSCollectionLayoutGroup.vertical( + layoutSize: size, + subitems: items.map(ItemBuilder.make(from:)) + ) + case let .repeated(item, count): + group = NSCollectionLayoutGroup.vertical( + layoutSize: size, + subitem: ItemBuilder.make(from: item), + count: count + ) + } + return group + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/Item/Item.swift b/Sources/CompositionalLayoutDSL/Public/Item/Item.swift new file mode 100644 index 0000000..b3f92e3 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/Item/Item.swift @@ -0,0 +1,88 @@ +// +// Item.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// The most basic component of a collection view's layout. +public struct Item: LayoutItem, ResizableItem { + + private var widthDimension: NSCollectionLayoutDimension + private var heightDimension: NSCollectionLayoutDimension + private var supplementaryItems: [LayoutSupplementaryItem] + + // MARK: - Life cycle + + /// Creates an item of the specified size with an array of supplementary items to attach to the item. + public init(width: NSCollectionLayoutDimension, + height: NSCollectionLayoutDimension, + @LayoutSupplementaryItemBuilder supplementaryItems: () -> [LayoutSupplementaryItem]) { + self.widthDimension = width + self.heightDimension = height + self.supplementaryItems = supplementaryItems() + } + + /// Creates an item of the specified size + public init(width: NSCollectionLayoutDimension = .fractionalWidth(1), + height: NSCollectionLayoutDimension = .fractionalHeight(1)) { + self.widthDimension = width + self.heightDimension = height + self.supplementaryItems = [] + } + + /// Creates an item of the specified size + public init(size: NSCollectionLayoutSize) { + self.widthDimension = size.widthDimension + self.heightDimension = size.heightDimension + self.supplementaryItems = [] + } + + /// Creates an item with an array of supplementary items to attach to the item. + public init(@LayoutSupplementaryItemBuilder supplementaryItems: () -> [LayoutSupplementaryItem]) { + self.init(width: .fractionalWidth(1), height: .fractionalHeight(1), supplementaryItems: supplementaryItems) + } + + public init() { + self.init(width: .fractionalWidth(1), height: .fractionalHeight(1), supplementaryItems: {}) + } + + // MARK: - LayoutItem + + public var layoutItem: LayoutItem { + return self + } + + // MARK: - ResizableItem + + /// Configure the width of the item + /// + /// The default value is `.fractionalWidth(1.0)` + public func width(_ width: NSCollectionLayoutDimension) -> Self { + with(self) { $0.widthDimension = width } + } + + /// Configure the height of the item + /// + /// The default value is `.fractionalHeight(1.0)` + public func height(_ height: NSCollectionLayoutDimension) -> Self { + with(self) { $0.heightDimension = height } + } +} + +extension Item: BuildableItem { + func makeItem() -> NSCollectionLayoutItem { + let item = NSCollectionLayoutItem( + layoutSize: NSCollectionLayoutSize(widthDimension: widthDimension, heightDimension: heightDimension), + supplementaryItems: supplementaryItems.map(SupplementaryItemBuilder.make(from:)) + ) + return item + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/Item/LayoutItem.swift b/Sources/CompositionalLayoutDSL/Public/Item/LayoutItem.swift new file mode 100644 index 0000000..e05702b --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/Item/LayoutItem.swift @@ -0,0 +1,117 @@ +// +// LayoutItem.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A type that represents an item in a compositional layout and provides +/// modifiers to configure items. +/// +/// You create custom items by declaring types that conform to the ``LayoutItem`` +/// protocol. Implement the required ``layoutItem`` computed property to +/// provide the content and configuration for your custom item. +/// +/// struct MyItem: LayoutItem { +/// var layoutItem: LayoutItem { +/// Item { +/// SupplementaryItem(elementKind: "badge") +/// .containerAnchor( +/// edges: [.top, .trailing], +/// offset: .fractional(x: 0.5, y: -0.5) +/// ) +/// .height(.absolute(20)) +/// .width(.absolute(20)) +/// } +/// } +/// } +/// +public protocol LayoutItem { + var layoutItem: LayoutItem { get } +} + +extension LayoutItem { + + // MARK: - Content Insets + + /// Configure the amount of space between the content of the section and its boundaries. + @warn_unqualified_access + public func contentInsets(value: CGFloat) -> LayoutItem { + return self.contentInsets(top: value, leading: value, bottom: value, trailing: value) + } + + /// Configure the amount of space between the content of the section and its boundaries. + @warn_unqualified_access + public func contentInsets(horizontal: CGFloat = 0, vertical: CGFloat = 0) -> LayoutItem { + return self.contentInsets(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) + } + + /// Configure the amount of space between the content of the section and its boundaries. + @warn_unqualified_access + public func contentInsets( + top: CGFloat = 0, + leading: CGFloat = 0, + bottom: CGFloat = 0, + trailing: CGFloat = 0 + ) -> LayoutItem { + return self.contentInsets( + NSDirectionalEdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing) + ) + } + + /// Configure the amount of space between the content of the section and its boundaries. + @warn_unqualified_access + public func contentInsets(_ insets: NSDirectionalEdgeInsets) -> LayoutItem { + valueModifier(insets, keyPath: \.contentInsets) + } +} + +extension LayoutItem { + + // MARK: - Edge Spacing + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing(value: NSCollectionLayoutSpacing?) -> LayoutItem { + return self.edgeSpacing(top: value, leading: value, bottom: value, trailing: value) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing( + horizontal: NSCollectionLayoutSpacing? = nil, + vertical: NSCollectionLayoutSpacing? = nil + ) -> LayoutItem { + return self.edgeSpacing(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing( + top: NSCollectionLayoutSpacing? = nil, + leading: NSCollectionLayoutSpacing? = nil, + bottom: NSCollectionLayoutSpacing? = nil, + trailing: NSCollectionLayoutSpacing? = nil + ) -> LayoutItem { + return self.edgeSpacing( + NSCollectionLayoutEdgeSpacing(leading: leading, top: top, trailing: trailing, bottom: bottom) + ) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing(_ edgeSpacing: NSCollectionLayoutEdgeSpacing) -> LayoutItem { + valueModifier(edgeSpacing, keyPath: \.edgeSpacing) + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/ResizableItem.swift b/Sources/CompositionalLayoutDSL/Public/ResizableItem.swift new file mode 100644 index 0000000..f9f0d07 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/ResizableItem.swift @@ -0,0 +1,25 @@ +// +// ResizableItem.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +public protocol ResizableItem { + func size(_ size: NSCollectionLayoutSize) -> Self + func width(_ width: NSCollectionLayoutDimension) -> Self + func height(_ height: NSCollectionLayoutDimension) -> Self +} + +public extension ResizableItem { + func size(_ size: NSCollectionLayoutSize) -> Self { + self.width(size.widthDimension).height(size.heightDimension) + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/ResultBuilders.swift b/Sources/CompositionalLayoutDSL/Public/ResultBuilders.swift new file mode 100644 index 0000000..3d220bd --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/ResultBuilders.swift @@ -0,0 +1,12 @@ +// +// ResultBuilders.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +public typealias LayoutItemBuilder = ListResultBuilder +public typealias LayoutBoundarySupplementaryItemBuilder = ListResultBuilder +public typealias LayoutSupplementaryItemBuilder = ListResultBuilder +public typealias LayoutDecorationItemBuilder = ListResultBuilder diff --git a/Sources/CompositionalLayoutDSL/Public/Section/LayoutSection.swift b/Sources/CompositionalLayoutDSL/Public/Section/LayoutSection.swift new file mode 100644 index 0000000..ddb76f8 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/Section/LayoutSection.swift @@ -0,0 +1,166 @@ +// +// LayoutSection.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A type that represents a section in a compositional layout and provides +/// modifiers to configure sections. +/// +/// You create custom sections by declaring types that conform to the ``LayoutSection`` +/// protocol. Implement the required ``layoutSection`` computed property to +/// provide the content and configuration for your custom section. +/// +/// struct MySection: LayoutSection { +/// var layoutSection: LayoutSection { +/// Section { +/// HGroup(count: 2) { Item() } +/// } +/// .boundarySupplementaryItems { +/// BoundarySupplementaryItem(elementKind: "kind") +/// } +/// .contentInsets(horizontal: 20, vertical: 8) +/// } +/// } +/// +public protocol LayoutSection { + var layoutSection: LayoutSection { get } +} + +extension LayoutSection { + + /// Configure the amount of space between the groups in the section. + @warn_unqualified_access + public func interGroupSpacing(_ spacing: CGFloat) -> LayoutSection { + valueModifier(spacing, keyPath: \.interGroupSpacing) + } + + #if !os(macOS) + /// Configure the boundary to reference when defining content insets. + /// + /// This represents the reference point to use when defining contentInsets. + /// + /// The default value of this property is `UIContentInsetsReference.automatic`, + /// which means the section follows the layout configuration’s `contentInsetsReference`. + @available(iOS 14.0, tvOS 14.0, *) + @warn_unqualified_access + public func contentInsetsReference(_ reference: UIContentInsetsReference) -> LayoutSection { + valueModifier(reference, keyPath: \.contentInsetsReference) + } + #endif + + #if os(macOS) + /// Configure the section's scrolling behavior in relation to the main layout axis. + /// + /// The default value of this property is `UICollectionLayoutSectionOrthogonalScrollingBehavior.none`, + /// which means the section lays out its content along the main axis of its layout, defined by + /// the layout configuration's `scrollDirection` property. Set a different value for this + /// property to get the section to lay out its content orthogonally to the main layout axis. + @warn_unqualified_access + public func orthogonalScrollingBehavior( + _ orthogonalScrollingBehavior: NSCollectionLayoutSectionOrthogonalScrollingBehavior + ) -> LayoutSection { + valueModifier(orthogonalScrollingBehavior, keyPath: \.orthogonalScrollingBehavior) + } + #else + /// Configure the section's scrolling behavior in relation to the main layout axis. + /// + /// The default value of this property is `UICollectionLayoutSectionOrthogonalScrollingBehavior.none`, + /// which means the section lays out its content along the main axis of its layout, defined by + /// the layout configuration's `scrollDirection` property. Set a different value for this + /// property to get the section to lay out its content orthogonally to the main layout axis. + @warn_unqualified_access + public func orthogonalScrollingBehavior( + _ orthogonalScrollingBehavior: UICollectionLayoutSectionOrthogonalScrollingBehavior + ) -> LayoutSection { + valueModifier(orthogonalScrollingBehavior, keyPath: \.orthogonalScrollingBehavior) + } + #endif + + /// Add an array of the supplementary items that are associated with the boundary edges of + /// the section, such as headers and footers. + @warn_unqualified_access + public func boundarySupplementaryItems( + @LayoutBoundarySupplementaryItemBuilder + _ boundarySupplementaryItems: () -> [LayoutBoundarySupplementaryItem] + ) -> LayoutSection { + let boundarySupplementaryItems = boundarySupplementaryItems() + .map(BoundarySupplementaryItemBuilder.make(from:)) + return valueModifier { + $0.boundarySupplementaryItems.append(contentsOf: boundarySupplementaryItems) + } + } + + /// Configure if the section's supplementary items follow the specified content insets for the section. + /// + /// The default value of this property is true. + @warn_unqualified_access + public func supplementariesFollowContentInsets( + _ supplementariesFollowContentInsets: Bool + ) -> LayoutSection { + valueModifier(supplementariesFollowContentInsets, keyPath: \.supplementariesFollowContentInsets) + } + + /// Install a closure called before each layout cycle to allow modification of the items in + /// the section immediately before they are displayed. + @warn_unqualified_access + public func visibleItemsInvalidationHandler( + _ visibleItemsInvalidationHandler: NSCollectionLayoutSectionVisibleItemsInvalidationHandler? + ) -> LayoutSection { + valueModifier(visibleItemsInvalidationHandler, keyPath: \.visibleItemsInvalidationHandler) + } + + /// Add an array of the decoration items that are anchored to the section, such as + /// background decoration views. + @warn_unqualified_access + public func decorationItems( + @LayoutDecorationItemBuilder _ decorationItems: () -> [LayoutDecorationItem] + ) -> LayoutSection { + let decorationItems = decorationItems().map(DecorationItemBuilder.make(from:)) + return valueModifier { $0.decorationItems.append(contentsOf: decorationItems) } + } +} + +extension LayoutSection { + + // MARK: - Content Insets + + /// Configure the amount of space between the content of the section and its boundaries. + @warn_unqualified_access + public func contentInsets(value: CGFloat) -> LayoutSection { + return self.contentInsets(top: value, leading: value, bottom: value, trailing: value) + } + + /// Configure the amount of space between the content of the section and its boundaries. + @warn_unqualified_access + public func contentInsets(horizontal: CGFloat = 0, vertical: CGFloat = 0) -> LayoutSection { + return self.contentInsets(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) + } + + /// Configure the amount of space between the content of the section and its boundaries. + @warn_unqualified_access + public func contentInsets( + top: CGFloat = 0, + leading: CGFloat = 0, + bottom: CGFloat = 0, + trailing: CGFloat = 0 + ) -> LayoutSection { + return self.contentInsets( + NSDirectionalEdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing) + ) + } + + /// Configure the amount of space between the content of the section and its boundaries. + @warn_unqualified_access + public func contentInsets(_ insets: NSDirectionalEdgeInsets) -> LayoutSection { + valueModifier(insets, keyPath: \.contentInsets) + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/Section/ListSection.swift b/Sources/CompositionalLayoutDSL/Public/Section/ListSection.swift new file mode 100644 index 0000000..dd10917 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/Section/ListSection.swift @@ -0,0 +1,90 @@ +// +// ListSection.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 20/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if !os(macOS) +import UIKit + +@available(iOS 14.0, tvOS 14, *) +public struct ListSection: LayoutSection { + + private var configuration: UICollectionLayoutListConfiguration + private let layoutEnvironment: NSCollectionLayoutEnvironment + + // MARK: - Life cycle + + /// Creates a list section with the specified list configuration and layout environment. + public init( + configuration: UICollectionLayoutListConfiguration, + layoutEnvironment: NSCollectionLayoutEnvironment + ) { + self.configuration = configuration + self.layoutEnvironment = layoutEnvironment + } + + /// Creates a list section with the specified list appearance and layout environment. + public init( + appearance: UICollectionLayoutListConfiguration.Appearance, + layoutEnvironment: NSCollectionLayoutEnvironment + ) { + self.configuration = UICollectionLayoutListConfiguration(appearance: appearance) + self.layoutEnvironment = layoutEnvironment + } + + // MARK: - ListSection + + /// A Boolean value that determines whether the list shows separators between cells. + @available(tvOS, unavailable) + public func showsSeparators(_ showsSeparators: Bool) -> Self { + with(self) { $0.configuration.showsSeparators = showsSeparators } + } + + /// The background color of the list. + /// + /// The default value is nil, which means that the configuration uses the system background + /// color for the specified appearance. + public func backgroundColor(_ backgroundColor: UIColor?) -> Self { + with(self) { $0.configuration.backgroundColor = backgroundColor } + } + + @available(tvOS, unavailable) + public func trailingSwipeActionsConfigurationProvider( + // swiftlint:disable:next line_length + _ trailingSwipeActionsConfigurationProvider: UICollectionLayoutListConfiguration.SwipeActionsConfigurationProvider? + ) -> Self { + // swiftlint:disable:next line_length + with(self) { $0.configuration.trailingSwipeActionsConfigurationProvider = trailingSwipeActionsConfigurationProvider } + } + + /// The type of header to use for the list. + /// + /// The default value is `UICollectionLayoutListConfiguration.HeaderMode.none`. + public func headerMode(_ headerMode: UICollectionLayoutListConfiguration.HeaderMode) -> Self { + with(self) { $0.configuration.headerMode = headerMode } + } + + /// The type of footer to use for the list. + /// + /// The default value is `UICollectionLayoutListConfiguration.FooterMode.none`. + public func footerMode(_ footerMode: UICollectionLayoutListConfiguration.FooterMode) -> Self { + with(self) { $0.configuration.footerMode = footerMode } + } + + // MARK: - LayoutSection + + public var layoutSection: LayoutSection { + return self + } +} + +@available(iOS 14.0, tvOS 14, *) +extension ListSection: BuildableSection { + func makeSection() -> NSCollectionLayoutSection { + return .list(using: configuration, layoutEnvironment: layoutEnvironment) + } +} +#endif diff --git a/Sources/CompositionalLayoutDSL/Public/Section/RawSection.swift b/Sources/CompositionalLayoutDSL/Public/Section/RawSection.swift new file mode 100644 index 0000000..056fdf1 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/Section/RawSection.swift @@ -0,0 +1,38 @@ +// +// RawSection.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 27/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A container to allow usage of `NSCollectionLayoutSection` with this library +public struct RawSection: LayoutSection { + + private let rawLayoutSection: NSCollectionLayoutSection + + // MARK: - Life cycle + + public init(rawLayoutSection: NSCollectionLayoutSection) { + self.rawLayoutSection = rawLayoutSection + } + + // MARK: - LayoutSection + + public var layoutSection: LayoutSection { + return self + } +} + +extension RawSection: BuildableSection { + + func makeSection() -> NSCollectionLayoutSection { + return rawLayoutSection + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/Section/Section.swift b/Sources/CompositionalLayoutDSL/Public/Section/Section.swift new file mode 100644 index 0000000..25c7d9d --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/Section/Section.swift @@ -0,0 +1,38 @@ +// +// Section.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A container that combines a set of groups into distinct visual groupings. +public struct Section: LayoutSection { + + private let group: LayoutGroup + + // MARK: - Life cycle + + public init(group: () -> LayoutGroup) { + self.group = group() + } + + // MARK: - LayoutSection + + public var layoutSection: LayoutSection { + return self + } +} + +extension Section: BuildableSection { + + func makeSection() -> NSCollectionLayoutSection { + return NSCollectionLayoutSection(group: GroupBuilder.make(from: group)) + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/SupplementaryItem/LayoutSupplementaryItem.swift b/Sources/CompositionalLayoutDSL/Public/SupplementaryItem/LayoutSupplementaryItem.swift new file mode 100644 index 0000000..f912c1a --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/SupplementaryItem/LayoutSupplementaryItem.swift @@ -0,0 +1,136 @@ +// +// LayoutSupplementaryItem.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A type that represents a supplementary item in a compositional layout and provides +/// modifiers to configure supplementary items. +/// +/// You create custom supplementary items by declaring types that conform to the +/// ``LayoutSupplementaryItem`` protocol. Implement the required ``layoutSupplementaryItem`` +/// computed property to provide the content and configuration for your custom supplementary item. +/// +/// struct MySupplementaryItem: LayoutSupplementaryItem { +/// var layoutSupplementaryItem: LayoutSupplementaryItem { +/// SupplementaryItem(elementKind: UICollectionView.elementKindSectionHeader) +/// .height(.absolute(40)) +/// .containerAnchor(edges: .top) +/// .zIndex(zIndex: 10) +/// } +/// } +/// +public protocol LayoutSupplementaryItem: LayoutItem { + var layoutSupplementaryItem: LayoutSupplementaryItem { get } +} + +public extension LayoutSupplementaryItem { + // MARK: - LayoutItem + + var layoutItem: LayoutItem { self } +} + +extension LayoutSupplementaryItem { + + // MARK: - Supplementary Item mutable properties + + /// Configure the vertical stacking order of the decoration item in relation to other items in the section. + /// + /// The default value of this property is 0, which means the decoration item appears below all + /// other items in the section. + @warn_unqualified_access + public func zIndex(zIndex: Int) -> LayoutSupplementaryItem { + valueModifier(zIndex, keyPath: \.zIndex) + } +} + +extension LayoutSupplementaryItem { + + // MARK: - Content Insets + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets(value: CGFloat) -> LayoutSupplementaryItem { + return self.contentInsets(top: value, leading: value, bottom: value, trailing: value) + } + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets(horizontal: CGFloat = 0, vertical: CGFloat = 0) -> LayoutSupplementaryItem { + return self.contentInsets(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) + } + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets( + top: CGFloat = 0, + leading: CGFloat = 0, + bottom: CGFloat = 0, + trailing: CGFloat = 0 + ) -> LayoutSupplementaryItem { + return self.contentInsets( + NSDirectionalEdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing) + ) + } + + /// Configure the amount of space added around the content of the item to adjust its final + /// size after its position is computed. + @warn_unqualified_access + public func contentInsets(_ insets: NSDirectionalEdgeInsets) -> LayoutSupplementaryItem { + valueModifier(insets, keyPath: \.contentInsets) + } +} + +extension LayoutSupplementaryItem { + + // MARK: - Edge Spacing + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing(value: NSCollectionLayoutSpacing?) -> LayoutSupplementaryItem { + return self.edgeSpacing(top: value, leading: value, bottom: value, trailing: value) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing( + horizontal: NSCollectionLayoutSpacing? = nil, + vertical: NSCollectionLayoutSpacing? = nil + ) -> LayoutSupplementaryItem { + return self.edgeSpacing(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing( + top: NSCollectionLayoutSpacing? = nil, + leading: NSCollectionLayoutSpacing? = nil, + bottom: NSCollectionLayoutSpacing? = nil, + trailing: NSCollectionLayoutSpacing? = nil + ) -> LayoutSupplementaryItem { + return self.edgeSpacing( + NSCollectionLayoutEdgeSpacing(leading: leading, top: top, trailing: trailing, bottom: bottom) + ) + } + + /// Configure the amount of space added around the boundaries of the item between other items + /// and this item's container. + @warn_unqualified_access + public func edgeSpacing(_ edgeSpacing: NSCollectionLayoutEdgeSpacing) -> LayoutSupplementaryItem { + valueModifier(edgeSpacing, keyPath: \.edgeSpacing) + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/SupplementaryItem/SupplementaryItem.swift b/Sources/CompositionalLayoutDSL/Public/SupplementaryItem/SupplementaryItem.swift new file mode 100644 index 0000000..2e736cb --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/SupplementaryItem/SupplementaryItem.swift @@ -0,0 +1,156 @@ +// +// SupplementaryItem.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 07/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// An object used to add an extra visual decoration, such as a badge or a frame, to an +/// item in a collection view. +public struct SupplementaryItem: LayoutSupplementaryItem, ResizableItem { + + public enum AnchorOffset { + case absolute(CGPoint) + case fractional(CGPoint) + } + + private var widthDimension: NSCollectionLayoutDimension + private var heightDimension: NSCollectionLayoutDimension + private var elementKind: String + + private var containerAnchor = NSCollectionLayoutAnchor(edges: [.top, .leading]) + private var itemAnchor: NSCollectionLayoutAnchor? + + // MARK: - Life cycle + + /// Creates a supplementary item of the specified size, with a string to identify the + /// element kind and an anchor relative to a container. + public init(widthDimension: NSCollectionLayoutDimension = .fractionalWidth(1), + heightDimension: NSCollectionLayoutDimension = .fractionalHeight(1), + elementKind: String) { + self.widthDimension = widthDimension + self.heightDimension = heightDimension + self.elementKind = elementKind + } + + /// Creates a supplementary item of the specified size, with a string to identify the + /// element kind and an anchor relative to a container. + public init(size: NSCollectionLayoutSize, + elementKind: String) { + self.widthDimension = size.widthDimension + self.heightDimension = size.heightDimension + self.elementKind = elementKind + } + + // MARK: - SupplementaryItem + + /// The anchor between the supplementary item and the container it's attached to. + /// + /// The defaults container anchor is attached to top leading + public func containerAnchor(_ containerAnchor: NSCollectionLayoutAnchor) -> Self { + with(self) { $0.containerAnchor = containerAnchor } + } + + /// The anchor between the supplementary item and the container it's attached to. + /// + /// The defaults container anchor is attached to top leading + public func containerAnchor(edges: NSDirectionalRectEdge) -> Self { + containerAnchor(NSCollectionLayoutAnchor(edges: edges)) + } + + /// The anchor between the supplementary item and the container it's attached to. + /// + /// The defaults container anchor is attached to top leading + public func containerAnchor(edges: NSDirectionalRectEdge, offset: AnchorOffset) -> Self { + switch offset { + case let .absolute(point): + return containerAnchor(NSCollectionLayoutAnchor(edges: edges, absoluteOffset: point)) + case let .fractional(point): + return containerAnchor(NSCollectionLayoutAnchor(edges: edges, fractionalOffset: point)) + } + } + + /// The anchor between the supplementary item and the item it's attached to. + public func itemAnchor(_ itemAnchor: NSCollectionLayoutAnchor?) -> Self { + with(self) { $0.itemAnchor = itemAnchor } + } + + /// The anchor between the supplementary item and the item it's attached to. + public func itemAnchor(edges: NSDirectionalRectEdge) -> Self { + itemAnchor(NSCollectionLayoutAnchor(edges: edges)) + } + + /// The anchor between the supplementary item and the item it's attached to. + public func itemAnchor(edges: NSDirectionalRectEdge, offset: AnchorOffset) -> Self { + switch offset { + case let .absolute(point): + return itemAnchor(NSCollectionLayoutAnchor(edges: edges, absoluteOffset: point)) + case let .fractional(point): + return itemAnchor(NSCollectionLayoutAnchor(edges: edges, fractionalOffset: point)) + } + } + + // MARK: - LayoutSupplementaryItem + + public var layoutSupplementaryItem: LayoutSupplementaryItem { + return self + } + + // MARK: - ResizableItem + + /// Configure the width of the supplementary item + /// + /// The default value is `.fractionalWidth(1.0)` + public func width(_ width: NSCollectionLayoutDimension) -> Self { + with(self) { $0.widthDimension = width } + } + + /// Configure the height of the supplementary item + /// + /// The default value is `.fractionalHeight(1.0)` + public func height(_ height: NSCollectionLayoutDimension) -> Self { + with(self) { $0.heightDimension = height } + } +} + +public extension SupplementaryItem.AnchorOffset { + static func absolute(x: CGFloat, y: CGFloat) -> SupplementaryItem.AnchorOffset { + return .absolute(CGPoint(x: x, y: y)) + } + + static func fractional(x: CGFloat, y: CGFloat) -> SupplementaryItem.AnchorOffset { + return .fractional(CGPoint(x: x, y: y)) + } +} + +extension SupplementaryItem: BuildableSupplementaryItem { + func makeSupplementaryItem() -> NSCollectionLayoutSupplementaryItem { + let size = NSCollectionLayoutSize( + widthDimension: widthDimension, + heightDimension: heightDimension + ) + let supplementaryItem: NSCollectionLayoutSupplementaryItem + if let itemAnchor = itemAnchor { + supplementaryItem = NSCollectionLayoutSupplementaryItem( + layoutSize: size, + elementKind: elementKind, + containerAnchor: containerAnchor, + itemAnchor: itemAnchor + ) + } else { + supplementaryItem = NSCollectionLayoutSupplementaryItem( + layoutSize: size, + elementKind: elementKind, + containerAnchor: containerAnchor + ) + } + return supplementaryItem + } +} diff --git a/Sources/CompositionalLayoutDSL/Public/Utils.swift b/Sources/CompositionalLayoutDSL/Public/Utils.swift new file mode 100644 index 0000000..04d9bf3 --- /dev/null +++ b/Sources/CompositionalLayoutDSL/Public/Utils.swift @@ -0,0 +1,86 @@ +// +// Utils.swift +// CompositionalLayoutDSL +// +// Created by Alexandre Podlewski on 06/04/2021. +// Copyright © 2021 Fabernovel. All rights reserved. +// + +import Foundation + +func with(_ object: T, modifier: (inout T) -> Void) -> T { + var copy = object + modifier(©) + return copy +} + +/// A custom parameter attribute that constructs list of `Element` from closures. +/// +/// You typically use ``ListResultBuilder`` (or a typealias of it) as a parameter attribute for +/// elements producing closure parameters, allowing those closures to provide multiple elements +/// For example, the following `HGroup` initialiser accepts a closure that produces +/// one or more items via the view builder. +/// +/// struct HGroup { +/// init(@ListResultBuilder subItems: () -> [LayoutItem]) { /* ... */ } +/// } +/// +/// Clients of this function can use multiple-statement closures to provide +/// several elements, as shown in the following example: +/// +/// HGroup { +/// Item().width(.fractionalWidth(0.5)) +/// if condition { +/// VGroup(count: 3) { Item() } +/// .width(.fractionalWidth(0.5)) +/// } else { +/// Item().width(.fractionalWidth(0.5)) +/// } +/// } +/// +@_functionBuilder +public enum ListResultBuilder { + + public static func buildBlock(_ components: [Element]...) -> [Element] { + return components.flatMap { $0 } + } + + public static func buildExpression(_ expression: Element) -> [Element] { + return [expression] + } + + /// Provides support for “if” statements in multi-statement closures, + /// producing an optional view that is visible only when the condition + /// evaluates to `true`. + // swiftlint:disable:next discouraged_optional_collection + public static func buildOptional(_ component: [Element]?) -> [Element] { + return component ?? [] + } + + /// Provides support for "if" and "switch" statements in multi-statement closures, + /// producing conditional content for the "then" branch. + public static func buildEither(first component: [Element]) -> [Element] { + return component + } + + /// Provides support for "if-else" and "switch" statements in multi-statement closures, + /// producing conditional content for the "else" branch. + public static func buildEither(second component: [Element]) -> [Element] { + return component + } + + public static func buildArray(_ components: [[Element]]) -> [Element] { + return components.flatMap { $0 } + } +} + +@available(iOS 14.0, tvOS 14.0, *) +extension ListResultBuilder { + + /// Provides support for "if" statements with `#available()` clauses in + /// multi-statement closures, producing conditional content for the "then" + /// branch, i.e. the conditionally-available branch. + public static func buildLimitedAvailability(_ component: [Element]) -> [Element] { + return component + } +} diff --git a/Tests/CompositionalLayoutDSLTests/CompositionalLayoutDSLTests.swift b/Tests/CompositionalLayoutDSLTests/CompositionalLayoutDSLTests.swift deleted file mode 100644 index a350cd6..0000000 --- a/Tests/CompositionalLayoutDSLTests/CompositionalLayoutDSLTests.swift +++ /dev/null @@ -1,11 +0,0 @@ -import XCTest -@testable import CompositionalLayoutDSL - -final class CompositionalLayoutDSLTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(CompositionalLayoutDSL().text, "Hello, World!") - } -} diff --git a/fastlane/.env.default b/fastlane/.env.default new file mode 100644 index 0000000..bfc9f5c --- /dev/null +++ b/fastlane/.env.default @@ -0,0 +1,3 @@ +PODSPEC = "CompositionalLayoutDSL.podspec" +CHANGELOG = "CHANGELOG.md" +REPO = "faberNovel/CompositionalLayoutDSL" \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..b70e099 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,151 @@ +# CI + +desc "Run all unit tests" +lane :tests do + scan( + workspace: "CompositionalLayoutDSL.xcworkspace", + scheme: "CompositionalLayoutDSLApp", + derived_data_path: "tests_derived_data", + clean: true, + devices: ["iPhone 12"] + ) +end + +desc "Run CI check for a commit" +lane :ci_check do + tests + danger( + github_api_token: ENV["GITHUB_API_TOKEN"], + verbose: true, + fail_on_errors: true + ) + pod_lib_lint( + use_bundle_exec: true, + allow_warnings: true + ) +end + +# Peepare release + +desc "Create release branch" +lane :create_release_branch do |options| + target_version = options[:version] + raise "The version is missing. Use `fastlane create_release_pr version:{version_number}`.`" if target_version.nil? + release_branch = "release/v" + target_version + sh("git", "checkout", "-b", release_branch) + push_to_git_remote( + local_branch: release_branch, + remote_branch: release_branch, + set_upstream: true + ) +end + +desc "Prepare release of a new version" +lane :prepare_release do |options| + ensure_git_branch(branch: 'release/*') + ensure_git_status_clean + + bypass_confirmations = options[:bypass_confirmations] + target_version = target_version_from_branch + + next unless bypass_confirmations || UI.confirm("Is your CHANGELOG up to date?") + bump_version(target_version) + update_changelog(target_version) + + ensure_git_status_clean + + if bypass_confirmations || UI.confirm("Push?") + push_to_git_remote + UI.success "Release preparation pushed" + end +end + +desc "Create release PR" +lane :create_release_pr do + ensure_git_branch(branch: 'release/*') + ensure_git_status_clean + + ["main", "develop"].each do |base| + create_pull_request( + api_bearer: ENV["GITHUB_TOKEN"], + repo: ENV["REPO"], + title: "Release #{target_version_from_branch}", + base: base + ) + end +end + +# Release + +desc "Publish release" +lane :publish_release do + ensure_git_branch(branch: 'main') + + target_version = version_get_podspec(path: ENV["PODSPEC"]) + changelog = read_changelog( + changelog_path: ENV["CHANGELOG"], + section_identifier: "[#{target_version}]" + ) + + carthage(command: "build", no_skip_current: true) + carthage( + frameworks: ["CompositionalLayoutDSL"], + output: "CompositionalLayoutDSL.framework.zip", + command: "archive" + ) + + set_github_release( + repository_name: ENV["REPO"], + api_bearer: ENV["GITHUB_TOKEN"], + name: "v#{target_version}", + tag_name: "v#{target_version}", + description: changelog, + commitish: "main", + upload_assets: ["CompositionalLayoutDSL.framework.zip"] + ) + + pod_push(allow_warnings: true) +end + +##################################################### +# Private +##################################################### + +def update_changelog(target_version) + changelog_path = ENV["CHANGELOG"] + stamp_changelog( + changelog_path: changelog_path, + section_identifier: "#{target_version}" + ) + + git_add(path: changelog_path) + git_commit( + path: changelog_path, + message: "Update CHANGELOG" + ) +end + +def bump_version(target_version) + podspec_path = ENV["PODSPEC"] + version_bump_podspec( + path: podspec_path, + version_number: target_version + ) + + pod_install # update the Podfile.lock with the new version + + path = [podspec_path, "Podfile.lock"] + git_add(path: path) + git_commit( + path: path, + message: "Bump to #{target_version}" + ) +end + +def target_version_from_branch + git_branch.gsub(/release\/[A-Za-z]*/, "") +end + +def pod_install + sh "bundle exec pod install" +end \ No newline at end of file diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile new file mode 100644 index 0000000..8f896e8 --- /dev/null +++ b/fastlane/Pluginfile @@ -0,0 +1,5 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +gem 'fastlane-plugin-changelog' diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 0000000..19ab489 --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,53 @@ +fastlane documentation +================ +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +``` +xcode-select --install +``` + +Install _fastlane_ using +``` +[sudo] gem install fastlane -NV +``` +or alternatively using `brew install fastlane` + +# Available Actions +### tests +``` +fastlane tests +``` +Run all unit tests +### ci_check +``` +fastlane ci_check +``` +Run CI check for a commit +### create_release_branch +``` +fastlane create_release_branch +``` +Create release branch +### prepare_release +``` +fastlane prepare_release +``` +Prepare release of a new version +### create_release_pr +``` +fastlane create_release_pr +``` +Create release PR +### publish_release +``` +fastlane publish_release +``` +Publish release + +---- + +This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. +More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). +The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/images/GettingStartedExample.jpg b/images/GettingStartedExample.jpg new file mode 100644 index 0000000..8fe82eb Binary files /dev/null and b/images/GettingStartedExample.jpg differ