From 96806cd1305b337e7f4d5dc992a859fde6de00f6 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Sun, 30 Jun 2024 14:36:21 +0400 Subject: [PATCH 1/7] Initial commit --- .github/ISSUE_TEMPLATE/bug_report.md | 41 ++++++ .github/ISSUE_TEMPLATE/feature_request.md | 11 ++ .github/PULL_REQUEST_TEMPLATE/bug_template.md | 9 ++ .../PULL_REQUEST_TEMPLATE/feature_template.md | 12 ++ .github/dependabot.yml | 34 +++++ .github/workflows/ci.yml | 126 ++++++++++++++++ .github/workflows/danger.yml | 31 ++++ .swiftformat | 64 ++++++++ .swiftlint.yml | 138 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + CHANGELOG.md | 2 + CODE_OF_CONDUCT.md | 74 ++++++++++ CONTRIBUTING.md | 61 ++++++++ Dangerfile | 1 + Gemfile | 3 + Makefile | 19 +++ Mintfile | 2 + Package.swift | 16 ++ Package@swift-5.7.swift | 16 ++ Package@swift-5.8.swift | 16 ++ Package@swift-5.9.swift | 16 ++ README.md | 59 +++++++- SEQURITY.md | 7 + .../Classes/OverlayContainer.swift | 6 + .../OverlayContainerTests.swift | 8 + hooks/pre-commit | 38 +++++ 26 files changed, 816 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/bug_template.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/feature_template.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/danger.yml create mode 100644 .swiftformat create mode 100644 .swiftlint.yml create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Dangerfile create mode 100644 Gemfile create mode 100644 Makefile create mode 100644 Mintfile create mode 100644 Package.swift create mode 100644 Package@swift-5.7.swift create mode 100644 Package@swift-5.8.swift create mode 100644 Package@swift-5.9.swift create mode 100644 SEQURITY.md create mode 100644 Sources/OverlayContainer/Classes/OverlayContainer.swift create mode 100644 Tests/OverlayContainerTests/OverlayContainerTests.swift create mode 100755 hooks/pre-commit diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8dc7e75 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: "🐛 Bug Report" +about: Report a reproducible bug or regression. +title: 'Bug: ' +labels: 'bug' + +--- + + + +Application version: + +## Steps To Reproduce + +1. +2. + + + +Link to code example: + + + +## The current behavior + + +## The expected behavior \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6168df4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,11 @@ +--- +name: 🛠 Feature request +about: If you have a feature request for the overlay-container, file it here. +labels: 'type: enhancement' +--- + +**Feature description** +Clearly and concisely describe the feature. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_template.md b/.github/PULL_REQUEST_TEMPLATE/bug_template.md new file mode 100644 index 0000000..7d6a149 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/bug_template.md @@ -0,0 +1,9 @@ +## Bug description +Clearly and concisely describe the problem. + +## Solution description +Describe your code changes in detail for reviewers. Explain the technical solution you have provided and how it fixes the issue case. + +## Covered unit test cases +- [x] yes +- [x] no \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/feature_template.md b/.github/PULL_REQUEST_TEMPLATE/feature_template.md new file mode 100644 index 0000000..ab3978b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/feature_template.md @@ -0,0 +1,12 @@ +## Feature description +Clearly and concisely describe the feature. + +## Solution description +Describe your code changes in detail for reviewers. + +## Areas affected and ensured +List out the areas affected by your code changes. + +## Covered unit test cases +- [x] yes +- [x] no \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c3e658c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,34 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + open-pull-requests-limit: 10 + schedule: + interval: daily + time: '07:00' + timezone: Europe/Berlin + + assignees: + - nik3212 + reviewers: + - nik3212 + + + - package-ecosystem: swift + directory: / + open-pull-requests-limit: 10 + schedule: + interval: daily + time: '07:00' + timezone: Europe/Berlin + + assignees: + - nik3212 + reviewers: + - nik3212 + \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fd45e5e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,126 @@ +name: "overlay-container" + +on: + push: + branches: + - main + - dev + pull_request: + paths: + - '.swiftlint.yml' + - ".github/workflows/**" + - "Package.swift" + - "Source/**" + - "Tests/**" + +jobs: + SwiftLint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: GitHub Action for SwiftLint + uses: norio-nomura/action-swiftlint@3.2.1 + with: + args: --strict + env: + + DIFF_BASE: ${{ github.base_ref }} + + + + + + iOS: + + name: ${{ matrix.name }} + runs-on: ${{ matrix.runsOn }} + + env: + + DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" + + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - destination: "OS=17.0.1,name=iPhone 14 Pro" + name: "iOS 17.0.1" + xcode: "Xcode_15.0" + runsOn: macos-13 + - destination: "OS=16.4,name=iPhone 14 Pro" + name: "iOS 16.4" + xcode: "Xcode_14.3.1" + runsOn: macos-13 + steps: + - uses: actions/checkout@v3 + + - name: ${{ matrix.name }} + run: xcodebuild test -scheme "{{ cookiecutter.name }}" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES -resultBundlePath "test_output/${{ matrix.name }}.xcresult" || exit 1 + + - uses: actions/upload-artifact@v4 + with: + + name: ${{ matrix.name }} + path: test_output + + + + + + + + spm: + + name: ${{ matrix.name }} + runs-on: ${{ matrix.runsOn }} + + env: + + DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" + + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - name: "Xcode 15" + xcode: "Xcode_15.0" + runsOn: macos-13 + - name: "Xcode 14" + xcode: "Xcode_14.3.1" + runsOn: macos-13 + steps: + - uses: actions/checkout@v3 + + - name: ${{ matrix.name }} + run: swift build -c release --target "{{ cookiecutter.name }}" + + merge-test-reports: + needs: [iOS, macOS, watchOS, tvOS] + runs-on: macos-13 + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: test_output + - run: xcrun xcresulttool merge test_output/**/*.xcresult --output-path test_output/final/final.xcresult + - name: Upload Merged Artifact + uses: actions/upload-artifact@v4 + with: + name: MergedResult + path: test_output/final + + discover-typos: + name: Discover Typos + runs-on: macOS-12 + env: + DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer + steps: + - uses: actions/checkout@v2 + - name: Discover typos + run: | + export PATH="$PATH:/Library/Frameworks/Python.framework/Versions/3.11/bin" + python3 -m pip install --upgrade pip + python3 -m pip install codespell + codespell --ignore-words-list="hart,inout,msdos,sur" --skip="./.build/*,./.git/*" diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml new file mode 100644 index 0000000..3f63d38 --- /dev/null +++ b/.github/workflows/danger.yml @@ -0,0 +1,31 @@ +name: Danger + +on: + pull_request: + types: [synchronize, opened, reopened, labeled, unlabeled, edited] + +env: + LC_CTYPE: en_US.UTF-8 + LANG: en_US.UTF-8 + +jobs: + run-danger: + runs-on: ubuntu-latest + steps: + - name: ruby setup + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.1.4 + bundler-cache: true + - name: Checkout code + uses: actions/checkout@v2 + - name: Setup gems + run: | + gem install bundler + bundle install --clean --path vendor/bundle + - name: danger + env: + + DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} + + run: bundle exec danger --verbose \ No newline at end of file diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..ba65f63 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,64 @@ +# Stream rules + +--swiftversion 5.3 + +# Use 'swiftformat --options' to list all of the possible options + +--header "\noverlay-container\nCopyright © {created.year} Space Code. All rights reserved.\n//" + +--enable blankLinesBetweenScopes +--enable blankLinesAtStartOfScope +--enable blankLinesAtEndOfScope +--enable blankLinesAroundMark +--enable anyObjectProtocol +--enable consecutiveBlankLines +--enable consecutiveSpaces +--enable duplicateImports +--enable elseOnSameLine +--enable emptyBraces +--enable initCoderUnavailable +--enable leadingDelimiters +--enable numberFormatting +--enable preferKeyPath +--enable redundantBreak +--enable redundantExtensionACL +--enable redundantFileprivate +--enable redundantGet +--enable redundantInit +--enable redundantLet +--enable redundantLetError +--enable redundantNilInit +--enable redundantObjc +--enable redundantParens +--enable redundantPattern +--enable redundantRawValues +--enable redundantReturn +--enable redundantSelf +--enable redundantVoidReturnType +--enable semicolons +--enable sortImports +--enable sortSwitchCases +--enable spaceAroundBraces +--enable spaceAroundBrackets +--enable spaceAroundComments +--enable spaceAroundGenerics +--enable spaceAroundOperators +--enable spaceInsideBraces +--enable spaceInsideBrackets +--enable spaceInsideComments +--enable spaceInsideGenerics +--enable spaceInsideParens +--enable strongOutlets +--enable strongifiedSelf +--enable todos +--enable trailingClosures +--enable unusedArguments +--enable void +--enable markTypes +--enable isEmpty + +# format options + +--wraparguments before-first +--wrapcollections before-first +--maxwidth 140 \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..969b766 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,138 @@ +excluded: + - Tests + - Package.swift + - Package@swift-5.7.swift + - Package@swift-5.8.swift + - Package@swift-5.9.swift + - .build + +# Rules + +disabled_rules: + - trailing_comma + - todo + - opening_brace + +opt_in_rules: # some rules are only opt-in + - anyobject_protocol + - array_init + - 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 + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - explicit_init + - fallthrough + - fatal_error_message + - file_name + - file_types_order + - first_where + - flatmap_over_map_reduce + - force_unwrapping + - ibinspectable_in_extension + - identical_operands + - implicit_return + - inert_defer + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_random + - literal_expression_end_indentation + - lower_acl_than_parent + - multiline_arguments + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - multiline_parameters_brackets + - no_space_in_method_call + - operator_usage_whitespace + - optional_enum_case_matching + - orphaned_doc_comment + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - prefixed_toplevel_constant + - private_action + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - reduce_into + - redundant_nil_coalescing + - redundant_objc_attribute + - redundant_type_annotation + - required_enum_case + - single_test_class + - sorted_first_last + - sorted_imports + - static_operator + - strict_fileprivate + - switch_case_on_newline + - toggle_bool + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition + +force_cast: warning +force_try: warning + +identifier_name: + excluded: + - id + - URL + +analyzer_rules: + - unused_import + - unused_declaration + +line_length: + warning: 130 + error: 200 + +type_body_length: + warning: 300 + error: 400 + +file_length: + warning: 500 + error: 1200 + +function_body_length: + warning: 30 + error: 50 + +large_tuple: + error: 3 + +nesting: + type_level: + warning: 2 + statement_level: + warning: 10 + + +type_name: + max_length: + warning: 40 + error: 50 \ No newline at end of file diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2e9885a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +# Change Log +All notable changes to this project will be documented in this file. \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..56c1661 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting one of the project maintainers https://github.com/orgs/space-code/people. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f96b11f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing Guidelines + +This document contains information and guidelines about contributing to this project. +Please read it before you start participating. + +**Topics** + +* [Reporting Issues](#reporting-issues) +* [Submitting Pull Requests](#submitting-pull-requests) +* [Developers Certificate of Origin](#developers-certificate-of-origin) +* [Code of Conduct](#code-of-conduct) + +## Reporting Issues + +A great way to contribute to the project is to send a detailed issue when you encounter a problem. We always appreciate a well-written, thorough bug report. + +Check that the project issues database doesn't already include that problem or suggestion before submitting an issue. If you find a match, feel free to vote for the issue by adding a reaction. Doing this helps prioritize the most common problems and requests. + +When reporting issues, please fill out our issue template. The information the template asks for will help us review and fix your issue faster. + +## Submitting Pull Requests + +You can contribute by fixing bugs or adding new features. For larger code changes, we recommend first discussing your ideas on our [GitHub Discussions](https://github.com/space-code/overlay-container/discussions). When submitting a pull request, please add relevant tests and ensure your changes don't break any existing tests. + +## Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +- (a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +- (b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +- (c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +- (d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. + +## Code of Conduct + +The Code of Conduct governs how we behave in public or in private +whenever the project will be judged by our actions. +We expect it to be honored by everyone who contributes to this project. + +See [CODE_OF_CONDUCT.md](https://github.com/space-code/overlay-container/blob/master/CODE_OF_CONDUCT.md) for details. + +--- + +*Some of the ideas and wording for the statements above were based on work by the [Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md) and [Linux](https://elinux.org/Developer_Certificate_Of_Origin) communities. We commend them for their efforts to facilitate collaboration in their projects.* \ No newline at end of file diff --git a/Dangerfile b/Dangerfile new file mode 100644 index 0000000..b266982 --- /dev/null +++ b/Dangerfile @@ -0,0 +1 @@ +danger.import_dangerfile(github: 'space-code/dangerfile') \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..20dff64 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem 'danger' \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..de1897f --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +all: bootstrap + +bootstrap: hook + mint bootstrap + +hook: + ln -sf ../../hooks/pre-commit .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit + +mint: + mint bootstrap + +lint: + mint run swiftlint + +fmt: + mint run swiftformat Sources Tests + +.PHONY: all bootstrap hook mint lint fmt diff --git a/Mintfile b/Mintfile new file mode 100644 index 0000000..e2cdefa --- /dev/null +++ b/Mintfile @@ -0,0 +1,2 @@ +nicklockwood/SwiftFormat@0.52.7 +realm/SwiftLint@0.53.0 \ No newline at end of file diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..a513efa --- /dev/null +++ b/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "overlay-container", + platforms: [.iOS(.v12)], + products: [ + .library(name: "OverlayContainer", targets: ["OverlayContainer"]), + ], + targets: [ + .target(name: "OverlayContainer"), + .testTarget(name: "OverlayContainerTests", dependencies: ["OverlayContainer"]), + ] +) diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift new file mode 100644 index 0000000..3062d29 --- /dev/null +++ b/Package@swift-5.7.swift @@ -0,0 +1,16 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "overlay-container", + platforms: [.iOS(.v12)], + products: [ + .library(name: "OverlayContainer", targets: ["OverlayContainer"]), + ], + targets: [ + .target(name: "OverlayContainer"), + .testTarget(name: "OverlayContainerTests", dependencies: ["OverlayContainer"]), + ] +) diff --git a/Package@swift-5.8.swift b/Package@swift-5.8.swift new file mode 100644 index 0000000..48c09c9 --- /dev/null +++ b/Package@swift-5.8.swift @@ -0,0 +1,16 @@ +// swift-tools-version: 5.8 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "overlay-container", + platforms: [.iOS(.v12)], + products: [ + .library(name: "OverlayContainer", targets: ["OverlayContainer"]), + ], + targets: [ + .target(name: "OverlayContainer"), + .testTarget(name: "OverlayContainerTests", dependencies: ["OverlayContainer"]), + ] +) diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift new file mode 100644 index 0000000..b8b4ffb --- /dev/null +++ b/Package@swift-5.9.swift @@ -0,0 +1,16 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "overlay-container", + platforms: [.iOS(.v12)], + products: [ + .library(name: "OverlayContainer", targets: ["OverlayContainer"]), + ], + targets: [ + .target(name: "OverlayContainer"), + .testTarget(name: "OverlayContainerTests", dependencies: ["OverlayContainer"]), + ] +) diff --git a/README.md b/README.md index a78c898..76e0486 100644 --- a/README.md +++ b/README.md @@ -1 +1,58 @@ -# overlay-container \ No newline at end of file +

overlay-container

+ +

+License +Swift Compatibility +Platform Compatibility +CI +GitHub release; latest by date + +

+ +## Description +`overlay-container` description. + +- [Usage](#usage) +- [Requirements](#requirements) +- [Installation](#installation) +- [Communication](#communication) +- [Contributing](#contributing) +- [Author](#author) +- [License](#license) + +## Usage + +## Requirements + +## Installation +### Swift Package Manager + +The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but `overlay-container` does support its use on supported platforms. + +Once you have your Swift package set up, adding `overlay-container` as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. + +```swift +dependencies: [ + .package(url: "https://github.com/space-code/overlay-container.git", .upToNextMajor(from: "1.0.0")) +] +``` + +## Communication +- If you **found a bug**, open an issue. +- If you **have a feature request**, open an issue. +- If you **want to contribute**, submit a pull request. + +## Contributing +Bootstrapping development environment + +``` +make bootstrap +``` + +Please feel free to help out with this project! If you see something that could be made better or want a new feature, open up an issue or send a Pull Request! + +## Author +Nikita Vasilev, nv3212@gmail.com + +## License +overlay-container is available under the MIT license. See the LICENSE file for more info. \ No newline at end of file diff --git a/SEQURITY.md b/SEQURITY.md new file mode 100644 index 0000000..20dffca --- /dev/null +++ b/SEQURITY.md @@ -0,0 +1,7 @@ +# Reporting Security Vulnerabilities + +This software is built with security and data privacy in mind to ensure your data is safe. We are grateful for security researchers and users reporting a vulnerability to us, first. To ensure that your request is handled in a timely manner and non-disclosure of vulnerabilities can be assured, please follow the below guideline. + +**Please do not report security vulnerabilities directly on GitHub. GitHub Issues can be publicly seen and therefore would result in a direct disclosure.** + +* Please address questions about data privacy, security concepts, and other media requests to the nv3212@gmail.com mailbox. \ No newline at end of file diff --git a/Sources/OverlayContainer/Classes/OverlayContainer.swift b/Sources/OverlayContainer/Classes/OverlayContainer.swift new file mode 100644 index 0000000..1e8bb90 --- /dev/null +++ b/Sources/OverlayContainer/Classes/OverlayContainer.swift @@ -0,0 +1,6 @@ +// +// overlay-container +// Copyright © 2024 Space Code. All rights reserved. +// + +final class OverlayContainer {} diff --git a/Tests/OverlayContainerTests/OverlayContainerTests.swift b/Tests/OverlayContainerTests/OverlayContainerTests.swift new file mode 100644 index 0000000..0b39cc1 --- /dev/null +++ b/Tests/OverlayContainerTests/OverlayContainerTests.swift @@ -0,0 +1,8 @@ +// +// overlay-container +// Copyright © 2024 Space Code. All rights reserved. +// + +import XCTest + +final class OverlayContainerTests: XCTestCase {} diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..956fdcb --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,38 @@ +#!/bin/bash +git diff --diff-filter=d --staged --name-only | grep -e '\.swift$' | while read line; do + if [[ $line == *"/Generated"* ]]; then + echo "IGNORING GENERATED FILE: " "$line"; + else + mint run swiftformat swiftformat "${line}"; + git add "$line"; + fi +done + +LINT=$(which mint) +if [[ -e "${LINT}" ]]; then + # Export files in SCRIPT_INPUT_FILE_$count to lint against later + count=0 + while IFS= read -r file_path; do + export SCRIPT_INPUT_FILE_$count="$file_path" + count=$((count + 1)) + done < <(git diff --name-only --cached --diff-filter=d | grep ".swift$") + export SCRIPT_INPUT_FILE_COUNT=$count + + if [ "$count" -eq 0 ]; then + echo "No files to lint!" + exit 0 + fi + + echo "Found $count lintable files! Linting now.." + mint run swiftlint --use-script-input-files --strict --config .swiftlint.yml + RESULT=$? # swiftline exit value is number of errors + + if [ $RESULT -eq 0 ]; then + echo "🎉 Well done. No violation." + fi + exit $RESULT +else + echo "⚠️ WARNING: SwiftLint not found" + echo "⚠️ You might want to edit .git/hooks/pre-commit to locate your swiftlint" + exit 0 +fi \ No newline at end of file From e84e0e972ae5f30f95b27d9f81a27e473298ce3d Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Sun, 30 Jun 2024 20:00:01 +0400 Subject: [PATCH 2/7] Implement bottom sheet view --- .swiftlint.yml | 3 +- .../Classes/Core/Model/Configuration.swift | 46 ++++ .../Classes/OverlayContainer.swift | 158 +++++++++++- .../BottomSheetAnimatedTransition.swift | 99 ++++++++ .../BottomSheetAnimatedTransitionDriver.swift | 83 ++++++ .../BottomSheetPresentationController.swift | 237 ++++++++++++++++++ .../Classes/Presentation/GrabberView.swift | 92 +++++++ 7 files changed, 715 insertions(+), 3 deletions(-) create mode 100644 Sources/OverlayContainer/Classes/Core/Model/Configuration.swift create mode 100644 Sources/OverlayContainer/Classes/Presentation/BottomSheetAnimatedTransition.swift create mode 100644 Sources/OverlayContainer/Classes/Presentation/BottomSheetAnimatedTransitionDriver.swift create mode 100644 Sources/OverlayContainer/Classes/Presentation/BottomSheetPresentationController.swift create mode 100644 Sources/OverlayContainer/Classes/Presentation/GrabberView.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 969b766..88a1ebe 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -21,7 +21,6 @@ opt_in_rules: # some rules are only opt-in - 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 @@ -135,4 +134,4 @@ nesting: type_name: max_length: warning: 40 - error: 50 \ No newline at end of file + error: 50 diff --git a/Sources/OverlayContainer/Classes/Core/Model/Configuration.swift b/Sources/OverlayContainer/Classes/Core/Model/Configuration.swift new file mode 100644 index 0000000..59577f1 --- /dev/null +++ b/Sources/OverlayContainer/Classes/Core/Model/Configuration.swift @@ -0,0 +1,46 @@ +// +// overlay-container +// Copyright © 2024 Space Code. All rights reserved. +// + +import UIKit + +/// A struct to configure the appearance and behavior of a UI component. +public struct Configuration { + // MARK: Properties + + /// The dimming color for the component. + public let dimmingColor: UIColor + /// The background color of the component. + public let backgroundColor: UIColor + /// The duration of the animation for the component. + public let animationDuration: TimeInterval + /// The corner radius of the component. + public let cornerRadius: CGFloat + /// The color of the grabber pin view. + public let grabberColor: UIColor + + // MARK: Initialization + + /// Initializes a new `Configuration` instance with specified properties. + /// + /// - Parameters: + /// - dimmingColor: The dimming color for the component. + /// - backgroundColor: The background color of the component. + /// - animationDuration: The duration of the animation for the component. + /// - cornerRadius: The corner radius of the component. + /// - grabberColor: The color of the grabber pin view. + public init( + dimmingColor: UIColor = .black.withAlphaComponent(0.4), + backgroundColor: UIColor = .white, + animationDuration: TimeInterval = 0.35, + cornerRadius: CGFloat = 16.0, + grabberColor: UIColor = .gray + ) { + self.dimmingColor = dimmingColor + self.backgroundColor = backgroundColor + self.animationDuration = animationDuration + self.cornerRadius = cornerRadius + self.grabberColor = grabberColor + } +} diff --git a/Sources/OverlayContainer/Classes/OverlayContainer.swift b/Sources/OverlayContainer/Classes/OverlayContainer.swift index 1e8bb90..5446d03 100644 --- a/Sources/OverlayContainer/Classes/OverlayContainer.swift +++ b/Sources/OverlayContainer/Classes/OverlayContainer.swift @@ -3,4 +3,160 @@ // Copyright © 2024 Space Code. All rights reserved. // -final class OverlayContainer {} +import UIKit + +// MARK: - OverlayContainer + +public final class OverlayContainer: UIViewController { + // MARK: Properties + + /// Controller responsible for managing the presentation of the bottom sheet. + private var bottomSheetController: BottomSheetPresentationController? + /// Driver responsible for handling the animated transitions of the bottom sheet. + private var transitionDriver: BottomSheetAnimatedTransitionDriver? + /// The view controller that contains the content to be presented in the bottom sheet. + private let contentContainer: UIViewController + /// The view that holds the content to be displayed in the bottom sheet. + private let contentView = UIView() + /// The view that provides a visual handle for dragging the bottom sheet. + private let grabberView = GrabberView() + /// Configuration settings for the appearance and behavior of the bottom sheet. + private let configuration: Configuration + + // MARK: Initialization + + /// Create a new `OverlayContainer` instance. + /// + /// - Parameters: + /// - contentContainer: The view controller that contains the content to be presented in the bottom sheet. + /// - configuration: Configuration settings for the appearance and behavior of the bottom sheet. + public init(contentContainer: UIViewController, configuration: Configuration = .init()) { + self.contentContainer = contentContainer + self.configuration = configuration + + super.init(nibName: nil, bundle: nil) + + modalPresentationStyle = .custom + transitioningDelegate = self + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Life Cycle + + override public func viewDidLoad() { + super.viewDidLoad() + + setupLayout() + + transitionDriver = BottomSheetAnimatedTransitionDriver(controller: self) + + view.layoutIfNeeded() + } + + override public func systemLayoutFittingSizeDidChange(forChildContentContainer container: any UIContentContainer) { + super.systemLayoutFittingSizeDidChange(forChildContentContainer: container) + presentationController?.systemLayoutFittingSizeDidChange(forChildContentContainer: container) + } + + override public func preferredContentSizeDidChange(forChildContentContainer container: any UIContentContainer) { + super.preferredContentSizeDidChange(forChildContentContainer: container) + + let grabberHeight = grabberView.intrinsicContentSize.height + var preferredContentSize = container.preferredContentSize + + preferredContentSize.height += grabberHeight + } + + // MARK: Public + + public func present(viewController: UIViewController) { + viewController.present(self, animated: true, completion: nil) + } + + // MARK: Private + + private func setupLayout() { + addChild(contentContainer) + + view.addSubview(grabberView) + view.addSubview(contentView) + + contentContainer.view.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(contentContainer.view) + + grabberView.backgroundColor = configuration.backgroundColor + grabberView.cornerRadius = configuration.cornerRadius + grabberView.grabberColor = configuration.grabberColor + grabberView.translatesAutoresizingMaskIntoConstraints = false + + contentView.backgroundColor = configuration.backgroundColor + contentView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate( + [ + grabberView.topAnchor.constraint(equalTo: view.topAnchor), + grabberView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + view.trailingAnchor.constraint(equalTo: grabberView.trailingAnchor), + + contentView.topAnchor.constraint(equalTo: grabberView.bottomAnchor), + contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + + contentContainer.view.topAnchor.constraint(equalTo: contentView.topAnchor), + contentContainer.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: contentContainer.view.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: contentContainer.view.bottomAnchor), + ] + ) + + contentContainer.didMove(toParent: self) + } +} + +// MARK: UIViewControllerTransitioningDelegate + +extension OverlayContainer: UIViewControllerTransitioningDelegate { + public func presentationController( + forPresented presented: UIViewController, + presenting: UIViewController?, + source _: UIViewController + ) -> UIPresentationController? { + bottomSheetController = BottomSheetPresentationController( + configuration: configuration, + presentedViewController: presented, + presentingViewController: presenting + ) + return bottomSheetController + } + + public func animationController( + forDismissed _: UIViewController + ) -> (any UIViewControllerAnimatedTransitioning)? { + BottomSheetAnimatedTransition( + transitionType: .dismiss, + animationDuration: configuration.animationDuration + ) + } + + public func animationController( + forPresented _: UIViewController, + presenting _: UIViewController, + source _: UIViewController + ) -> (any UIViewControllerAnimatedTransitioning)? { + BottomSheetAnimatedTransition( + transitionType: .present, + animationDuration: configuration.animationDuration + ) + } + + public func interactionControllerForDismissal( + using _: any UIViewControllerAnimatedTransitioning + ) -> (any UIViewControllerInteractiveTransitioning)? { + transitionDriver?.wantsInteractiveStart == true ? transitionDriver : nil + } +} diff --git a/Sources/OverlayContainer/Classes/Presentation/BottomSheetAnimatedTransition.swift b/Sources/OverlayContainer/Classes/Presentation/BottomSheetAnimatedTransition.swift new file mode 100644 index 0000000..b1190fe --- /dev/null +++ b/Sources/OverlayContainer/Classes/Presentation/BottomSheetAnimatedTransition.swift @@ -0,0 +1,99 @@ +// +// overlay-container +// Copyright © 2024 Space Code. All rights reserved. +// + +import UIKit + +// MARK: - BottomSheetAnimatedTransition + +final class BottomSheetAnimatedTransition: NSObject { + // MARK: Types + + /// Enum to define different types of transitions for view controllers + enum TransitionType { + /// Represents the transition type when presenting a view controller + case present + /// Represents the transition type when dismissing a view controller + case dismiss + } + + // MARK: Properties + + /// The animator responsible for performing the view transition animations. + private var animator: UIViewPropertyAnimator? + /// The type of transition to be performed (present or dismiss). + private let transitionType: TransitionType + /// The duration of the animation for the transition. + private let animationDuration: TimeInterval + + // MARK: Initialization + + /// Initializes a new instance of the transition animator. + /// + /// - Parameters: + /// - transitionType: The type of transition to be performed (present or dismiss). + /// - animationDuration: The type of transition to be performed (present or dismiss). + init(transitionType: TransitionType, animationDuration: TimeInterval) { + self.transitionType = transitionType + self.animationDuration = animationDuration + } +} + +// MARK: UIViewControllerAnimatedTransitioning + +extension BottomSheetAnimatedTransition: UIViewControllerAnimatedTransitioning { + func transitionDuration(using _: (any UIViewControllerContextTransitioning)?) -> TimeInterval { + animationDuration + } + + func animateTransition(using transitionContext: any UIViewControllerContextTransitioning) { + interruptibleAnimator(using: transitionContext).startAnimation() + } + + func interruptibleAnimator( + using transitionContext: any UIViewControllerContextTransitioning + ) -> any UIViewImplicitlyAnimating { + if let animator { return animator } + + if transitionType == .present { + if let controller = transitionContext.viewController(forKey: .to) { + let frame = transitionContext.finalFrame(for: controller) + controller.view.frame = frame + + transitionContext.containerView.addSubview(controller.view) + transitionContext.containerView.layoutIfNeeded() + + controller.view.transform = CGAffineTransform(translationX: .zero, y: frame.height) + } + } + + let animator = UIViewPropertyAnimator( + duration: transitionDuration(using: transitionContext), + controlPoint1: CGPoint(x: 0.2, y: 1), + controlPoint2: CGPoint(x: 0.42, y: 1) + ) { + let key: UITransitionContextViewKey = self.transitionType == .present ? .to : .from + guard let view = transitionContext.view(forKey: key) else { return } + + let isPresent = self.transitionType == .present + view.transform = isPresent ? .identity : CGAffineTransform(translationX: .zero, y: view.frame.height) + } + + animator.addCompletion { _ in + if transitionContext.transitionWasCancelled, self.transitionType == .present { + transitionContext.view(forKey: .to)?.removeFromSuperview() + } + + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + } + + self.animator = animator + + return animator + } + + func animationEnded(_: Bool) { + animator = nil + } +} diff --git a/Sources/OverlayContainer/Classes/Presentation/BottomSheetAnimatedTransitionDriver.swift b/Sources/OverlayContainer/Classes/Presentation/BottomSheetAnimatedTransitionDriver.swift new file mode 100644 index 0000000..4579976 --- /dev/null +++ b/Sources/OverlayContainer/Classes/Presentation/BottomSheetAnimatedTransitionDriver.swift @@ -0,0 +1,83 @@ +// +// overlay-container +// Copyright © 2024 Space Code. All rights reserved. +// + +import UIKit + +// MARK: - BottomSheetAnimatedTransitionDriver + +final class BottomSheetAnimatedTransitionDriver: UIPercentDrivenInteractiveTransition { + // MARK: Private + + private weak var controller: OverlayContainer? + + private lazy var panGesture: UIPanGestureRecognizer = { + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) + panGesture.delegate = self + return panGesture + }() + + // MARK: Initialization + + init(controller: OverlayContainer) { + self.controller = controller + super.init() + wantsInteractiveStart = false + setupGesture() + } + + // MARK: Private + + private func setupGesture() { + controller?.view.addGestureRecognizer(panGesture) + } + + // MARK: Actions + + @objc + private func handlePan(_ sender: UIPanGestureRecognizer) { + guard let view = sender.view else { return } + + let translation = sender.translation(in: view) + var progress = translation.y / view.frame.height + progress = min(max(progress, 0.0), 1.0) + + switch sender.state { + case .began: + wantsInteractiveStart = true + controller?.dismiss(animated: true) + case .changed: + update(progress) + case .ended: + wantsInteractiveStart = false + + let velocity = sender.velocity(in: view).y + + if progress > 0.5 || velocity > 1000 { + finish() + } else { + cancel() + } + case .cancelled, .failed: + wantsInteractiveStart = false + cancel() + default: + break + } + } +} + +// MARK: UIGestureRecognizerDelegate + +extension BottomSheetAnimatedTransitionDriver: UIGestureRecognizerDelegate { + func gestureRecognizerShouldBegin(_: UIGestureRecognizer) -> Bool { + let velocity = panGesture.velocity(in: nil) + + if velocity.y > 0, abs(velocity.y) > abs(velocity.x) { + return true + } else { + return false + } + } +} diff --git a/Sources/OverlayContainer/Classes/Presentation/BottomSheetPresentationController.swift b/Sources/OverlayContainer/Classes/Presentation/BottomSheetPresentationController.swift new file mode 100644 index 0000000..6f52ced --- /dev/null +++ b/Sources/OverlayContainer/Classes/Presentation/BottomSheetPresentationController.swift @@ -0,0 +1,237 @@ +// +// overlay-container +// Copyright © 2024 Space Code. All rights reserved. +// + +import UIKit + +// MARK: - BottomSheetPresentationController + +/// A controller that manages the presentation of a bottom sheet. +final class BottomSheetPresentationController: UIPresentationController { + // MARK: Types + + /// Represents the different states of the bottom sheet presentation lifecycle. + private enum State { + /// The bottom sheet is dismissed and not visible. + case dismissed + /// The bottom sheet is in the process of being presented. + case presenting + /// The bottom sheet is fully presented and visible. + case presented + /// The bottom sheet is in the process of being dismissed. + case dismissing + } + + // MARK: Properties + + /// The current state of the bottom sheet's presentation lifecycle. + private var state: State = .dismissed + /// The current frame of the bottom sheet. + private var currentFrame: CGRect = .zero + /// A view that dims the background to highlight the bottom sheet. + private var dimmingView: UIView? + + // Dependencies + + /// The configuration settings for the appearance and behavior of the bottom sheet. + private let configuration: Configuration + + // MARK: Initialization + + /// Initializes a new instance of `BottomSheetPresentationController` with the specified configuration and view controllers. + /// + /// - Parameters: + /// - configuration: The configuration settings for the appearance and behavior of the bottom sheet. + /// - presentedViewController: The view controller being presented as a bottom sheet. + /// - presentingViewController: The view controller that is presenting the bottom sheet. + init( + configuration: Configuration, + presentedViewController: UIViewController, + presentingViewController: UIViewController? + ) { + self.configuration = configuration + super.init(presentedViewController: presentedViewController, presenting: presentingViewController) + } + + // MARK: UIPresentationController + + override var shouldPresentInFullscreen: Bool { + false + } + + override func presentationTransitionWillBegin() { + state = .presenting + setupDimmingView() + + performAlongsideTransition { + self.dimmingView?.alpha = 1 + } + } + + override func presentationTransitionDidEnd(_ completed: Bool) { + if completed { + state = .presented + } else { + state = .dismissed + + presentedView?.removeFromSuperview() + dimmingView?.removeFromSuperview() + } + } + + override func dismissalTransitionWillBegin() { + state = .dismissing + + performAlongsideTransition { + self.dimmingView?.alpha = 0 + } + } + + override func dismissalTransitionDidEnd(_ completed: Bool) { + if completed { + state = .dismissed + removeDimmingView() + } else { + state = .presented + } + } + + override var frameOfPresentedViewInContainerView: CGRect { + guard let containerView else { return .zero } + + let frame = super.frameOfPresentedViewInContainerView + let windowInsets = presentedView?.window?.safeAreaInsets ?? .zero + + var preferredContentSize = presentedViewController.preferredContentSize + + if preferredContentSize == .zero { + preferredContentSize = presentedViewController.view.systemLayoutSizeFitting( + CGSize(width: frame.width, height: UIView.layoutFittingCompressedSize.height), + withHorizontalFittingPriority: .required, + verticalFittingPriority: .fittingSizeLevel + ) + } + + let preferredHeight = preferredContentSize.height + let maxHeight = containerView.bounds.height - windowInsets.top + + let height = min(preferredHeight, maxHeight) + + return CGRect( + x: .zero, + y: containerView.bounds.height - height, + width: containerView.bounds.width, + height: height + ) + } + + override func containerViewWillLayoutSubviews() { + super.containerViewWillLayoutSubviews() + + dimmingView?.frame = containerView?.frame ?? .zero + presentedView?.frame = frameOfPresentedViewInContainerView + } + + override func preferredContentSizeDidChange(forChildContentContainer container: any UIContentContainer) { + super.preferredContentSizeDidChange(forChildContentContainer: container) + invalidateOverlayMetrics() + } + + override func systemLayoutFittingSizeDidChange(forChildContentContainer container: any UIContentContainer) { + super.systemLayoutFittingSizeDidChange(forChildContentContainer: container) + invalidateOverlayMetrics() + } + + override func willTransition( + to newCollection: UITraitCollection, + with coordinator: any UIViewControllerTransitionCoordinator + ) { + super.willTransition(to: newCollection, with: coordinator) + coordinator.animate { _ in + self.presentedView?.frame = self.frameOfPresentedViewInContainerView + } + } + + // MARK: Private + + private func setupDimmingView() { + guard let containerView else { return } + + let dimmingView = UIView() + dimmingView.translatesAutoresizingMaskIntoConstraints = false + dimmingView.backgroundColor = configuration.dimmingColor + dimmingView.alpha = .zero + + containerView.addSubview(dimmingView) + + NSLayoutConstraint.activate( + [ + dimmingView.topAnchor.constraint(equalTo: containerView.topAnchor), + dimmingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + containerView.trailingAnchor.constraint(equalTo: dimmingView.trailingAnchor), + containerView.bottomAnchor.constraint(equalTo: dimmingView.bottomAnchor), + ] + ) + + let tapGesture = UITapGestureRecognizer() + dimmingView.addGestureRecognizer(tapGesture) + + tapGesture.addTarget(self, action: #selector(handleTapGesture)) + + self.dimmingView = dimmingView + } + + private func removeDimmingView() { + dimmingView?.removeFromSuperview() + dimmingView = nil + } + + private func dismissIfPossible() { + let canBeDismissed = state == .presented + + if canBeDismissed { + presentedViewController.dismiss(animated: true) + } + } + + private func performAlongsideTransition(_ animation: @escaping () -> Void) { + guard let coordinator = presentedViewController.transitionCoordinator else { + animation() + return + } + + coordinator.animate(alongsideTransition: { _ in + animation() + }) + } + + private func invalidateOverlayMetrics() { + guard let containerView else { return } + + let frame = containerView.frame + + if frame != currentFrame { + containerView.setNeedsLayout() + + currentFrame = frame + + UIView.animate(withDuration: .animationDuration) { + containerView.layoutIfNeeded() + } + } + } + + // MARK: Actions + + @objc + private func handleTapGesture() { + dismissIfPossible() + } +} + +// MARK: - Constants + +private extension TimeInterval { + static let animationDuration = 0.25 +} diff --git a/Sources/OverlayContainer/Classes/Presentation/GrabberView.swift b/Sources/OverlayContainer/Classes/Presentation/GrabberView.swift new file mode 100644 index 0000000..394694f --- /dev/null +++ b/Sources/OverlayContainer/Classes/Presentation/GrabberView.swift @@ -0,0 +1,92 @@ +// +// overlay-container +// Copyright © 2024 Space Code. All rights reserved. +// + +import UIKit + +// MARK: - GrabberView + +final class GrabberView: UIView { + // MARK: Properties + + private let grabberLayer = CALayer() + + var cornerRadius: CGFloat = 16.0 { + didSet { + layer.cornerRadius = cornerRadius + setNeedsLayout() + } + } + + var grabberColor: UIColor = .systemGray { + didSet { + grabberLayer.backgroundColor = grabberColor.cgColor + setNeedsLayout() + } + } + + // MARK: Initialization + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + override var alignmentRectInsets: UIEdgeInsets { + UIEdgeInsets(top: .zero, left: .zero, bottom: max(cornerRadius * 2 - CGSize.size.height, 0), right: .zero) + } + + override var intrinsicContentSize: CGSize { + .size + } + + // MARK: Life Cycle + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if #available(iOS 13.0, *) { + if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + grabberLayer.backgroundColor = grabberColor.cgColor + } + } + } + + override func layoutSublayers(of layer: CALayer) { + super.layoutSublayers(of: layer) + + let origin = CGPoint(x: (layer.bounds.width - CGSize.grabberSize.width) / 2, y: 8) + grabberLayer.frame = CGRect(origin: origin, size: .grabberSize) + } + + // MARK: Private + + private func configure() { + layer.cornerRadius = cornerRadius + layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + if #available(iOS 13.0, *) { + layer.cornerCurve = .continuous + grabberLayer.cornerCurve = .continuous + } + + grabberLayer.backgroundColor = grabberColor.cgColor + grabberLayer.cornerRadius = 2 + layer.addSublayer(grabberLayer) + } +} + +// MARK: - Constants + +private extension CGSize { + static let size = CGSize(width: 48, height: 20) + static let grabberSize = CGSize(width: 40, height: 4) +} From f2df3ab03ac716ed057ed717349beb97823b75ec Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 24 Mar 2025 20:39:08 +0400 Subject: [PATCH 3/7] Implement `OverlayContainer` --- .github/workflows/ci.yml | 29 +---------- .swiftlint.yml | 1 + Package.swift | 2 +- Package@swift-5.10.swift | 16 ++++++ README.md | 24 ++++++++- .../Classes/Core/Model/Configuration.swift | 35 +++++++++++-- .../Classes/OverlayContainer.swift | 52 +++++++++++++------ 7 files changed, 108 insertions(+), 51 deletions(-) create mode 100644 Package@swift-5.10.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd45e5e..a239d83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,22 +23,12 @@ jobs: with: args: --strict env: - DIFF_BASE: ${{ github.base_ref }} - - - - - iOS: - name: ${{ matrix.name }} runs-on: ${{ matrix.runsOn }} - env: - DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" - timeout-minutes: 20 strategy: fail-fast: false @@ -54,31 +44,17 @@ jobs: runsOn: macos-13 steps: - uses: actions/checkout@v3 - - name: ${{ matrix.name }} run: xcodebuild test -scheme "{{ cookiecutter.name }}" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES -resultBundlePath "test_output/${{ matrix.name }}.xcresult" || exit 1 - - uses: actions/upload-artifact@v4 with: - name: ${{ matrix.name }} path: test_output - - - - - - - spm: - name: ${{ matrix.name }} runs-on: ${{ matrix.runsOn }} - env: - DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" - timeout-minutes: 20 strategy: fail-fast: false @@ -92,10 +68,8 @@ jobs: runsOn: macos-13 steps: - uses: actions/checkout@v3 - - name: ${{ matrix.name }} run: swift build -c release --target "{{ cookiecutter.name }}" - merge-test-reports: needs: [iOS, macOS, watchOS, tvOS] runs-on: macos-13 @@ -110,10 +84,9 @@ jobs: with: name: MergedResult path: test_output/final - discover-typos: name: Discover Typos - runs-on: macOS-12 + runs-on: macOS-13 env: DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer steps: diff --git a/.swiftlint.yml b/.swiftlint.yml index 88a1ebe..8e2fe2b 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -4,6 +4,7 @@ excluded: - Package@swift-5.7.swift - Package@swift-5.8.swift - Package@swift-5.9.swift + - Package@swift-5.10.swift - .build # Rules diff --git a/Package.swift b/Package.swift index a513efa..12da4e1 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.10 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Package@swift-5.10.swift b/Package@swift-5.10.swift new file mode 100644 index 0000000..a513efa --- /dev/null +++ b/Package@swift-5.10.swift @@ -0,0 +1,16 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "overlay-container", + platforms: [.iOS(.v12)], + products: [ + .library(name: "OverlayContainer", targets: ["OverlayContainer"]), + ], + targets: [ + .target(name: "OverlayContainer"), + .testTarget(name: "OverlayContainerTests", dependencies: ["OverlayContainer"]), + ] +) diff --git a/README.md b/README.md index 76e0486..8e6b188 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@

## Description -`overlay-container` description. +`overlay-container` is a lightweight Swift library for managing overlays and bottom sheets in iOS applications. It provides a flexible and customizable way to present draggable, resizable, and interactive overlays, making it easy to implement bottom sheets, modals, and other layered UI components. - [Usage](#usage) - [Requirements](#requirements) @@ -22,8 +22,28 @@ ## Usage +```swift +import OverlayContainer + +func presentSheet(_ viewController: UIViewController) { + let sheetViewController = OverlayContainer( + contentContainer: viewController, + configuration: .init( + cornerRadius: 16, + insets: .zero, + grabberType: .hidden + ) + ) + present(sheetViewController, animated: true) +} +``` + ## Requirements +- iOS 12.0+ +- Xcode 16.0 +- Swift 5.7 + ## Installation ### Swift Package Manager @@ -55,4 +75,4 @@ Please feel free to help out with this project! If you see something that could Nikita Vasilev, nv3212@gmail.com ## License -overlay-container is available under the MIT license. See the LICENSE file for more info. \ No newline at end of file +overlay-container is available under the MIT license. See the LICENSE file for more info. diff --git a/Sources/OverlayContainer/Classes/Core/Model/Configuration.swift b/Sources/OverlayContainer/Classes/Core/Model/Configuration.swift index 59577f1..efcfe9a 100644 --- a/Sources/OverlayContainer/Classes/Core/Model/Configuration.swift +++ b/Sources/OverlayContainer/Classes/Core/Model/Configuration.swift @@ -7,6 +7,20 @@ import UIKit /// A struct to configure the appearance and behavior of a UI component. public struct Configuration { + public struct GrabberConfiguration { + /// The color of the grabber pin view. + public let grabberColor: UIColor + + public init(grabberColor: UIColor = .gray) { + self.grabberColor = grabberColor + } + } + + public enum GrabberType { + case hidden + case plain(GrabberConfiguration) + } + // MARK: Properties /// The dimming color for the component. @@ -17,8 +31,12 @@ public struct Configuration { public let animationDuration: TimeInterval /// The corner radius of the component. public let cornerRadius: CGFloat - /// The color of the grabber pin view. - public let grabberColor: UIColor + + public let maskedCorners: CACornerMask + /// The sheet insets. + public let insets: UIEdgeInsets + + public let grabberType: GrabberType // MARK: Initialization @@ -35,12 +53,21 @@ public struct Configuration { backgroundColor: UIColor = .white, animationDuration: TimeInterval = 0.35, cornerRadius: CGFloat = 16.0, - grabberColor: UIColor = .gray + maskedCorners: CACornerMask = [ + .layerMaxXMaxYCorner, + .layerMinXMinYCorner, + .layerMinXMaxYCorner, + .layerMaxXMinYCorner, + ], + insets: UIEdgeInsets = .zero, + grabberType: GrabberType = .plain(GrabberConfiguration()) ) { self.dimmingColor = dimmingColor self.backgroundColor = backgroundColor self.animationDuration = animationDuration self.cornerRadius = cornerRadius - self.grabberColor = grabberColor + self.maskedCorners = maskedCorners + self.insets = insets + self.grabberType = grabberType } } diff --git a/Sources/OverlayContainer/Classes/OverlayContainer.swift b/Sources/OverlayContainer/Classes/OverlayContainer.swift index 5446d03..38bc090 100644 --- a/Sources/OverlayContainer/Classes/OverlayContainer.swift +++ b/Sources/OverlayContainer/Classes/OverlayContainer.swift @@ -23,6 +23,13 @@ public final class OverlayContainer: UIViewController { /// Configuration settings for the appearance and behavior of the bottom sheet. private let configuration: Configuration + private var isGrabberHidden: Bool { + if case .hidden = configuration.grabberType { + return true + } + return false + } + // MARK: Initialization /// Create a new `OverlayContainer` instance. @@ -50,7 +57,7 @@ public final class OverlayContainer: UIViewController { override public func viewDidLoad() { super.viewDidLoad() - setupLayout() + setupUI() transitionDriver = BottomSheetAnimatedTransitionDriver(controller: self) @@ -79,33 +86,46 @@ public final class OverlayContainer: UIViewController { // MARK: Private - private func setupLayout() { + // swiftlint:disable:next function_body_length + private func setupUI() { addChild(contentContainer) - view.addSubview(grabberView) + if case let .plain(data) = configuration.grabberType { + view.addSubview(grabberView) + grabberView.backgroundColor = configuration.backgroundColor + grabberView.grabberColor = data.grabberColor + grabberView.translatesAutoresizingMaskIntoConstraints = false + } + view.addSubview(contentView) + contentView.layer.cornerRadius = configuration.cornerRadius + contentView.layer.maskedCorners = configuration.maskedCorners + contentView.layer.masksToBounds = true + contentView.clipsToBounds = true + contentContainer.view.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(contentContainer.view) - grabberView.backgroundColor = configuration.backgroundColor - grabberView.cornerRadius = configuration.cornerRadius - grabberView.grabberColor = configuration.grabberColor - grabberView.translatesAutoresizingMaskIntoConstraints = false - contentView.backgroundColor = configuration.backgroundColor contentView.translatesAutoresizingMaskIntoConstraints = false + if !isGrabberHidden { + NSLayoutConstraint.activate( + [ + grabberView.topAnchor.constraint(equalTo: view.topAnchor, constant: configuration.insets.top), + grabberView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: configuration.insets.left), + view.trailingAnchor.constraint(equalTo: grabberView.trailingAnchor, constant: configuration.insets.right), + ] + ) + } + NSLayoutConstraint.activate( [ - grabberView.topAnchor.constraint(equalTo: view.topAnchor), - grabberView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - view.trailingAnchor.constraint(equalTo: grabberView.trailingAnchor), - - contentView.topAnchor.constraint(equalTo: grabberView.bottomAnchor), - contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + contentView.topAnchor.constraint(equalTo: isGrabberHidden ? view.topAnchor : grabberView.bottomAnchor), + contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: configuration.insets.left), + view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: configuration.insets.right), + view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: configuration.insets.bottom), contentContainer.view.topAnchor.constraint(equalTo: contentView.topAnchor), contentContainer.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), From 0024aafd5e4d2abdace110a61fefdbae27f161ed Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 24 Mar 2025 20:42:43 +0400 Subject: [PATCH 4/7] Update `ci.yml` --- .github/workflows/ci.yml | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a239d83..e65611f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: ${{ matrix.name }} - run: xcodebuild test -scheme "{{ cookiecutter.name }}" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES -resultBundlePath "test_output/${{ matrix.name }}.xcresult" || exit 1 + run: xcodebuild test -scheme "overlay-container" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES -resultBundlePath "test_output/${{ matrix.name }}.xcresult" || exit 1 - uses: actions/upload-artifact@v4 with: name: ${{ matrix.name }} @@ -69,21 +69,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: ${{ matrix.name }} - run: swift build -c release --target "{{ cookiecutter.name }}" - merge-test-reports: - needs: [iOS, macOS, watchOS, tvOS] - runs-on: macos-13 - steps: - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - path: test_output - - run: xcrun xcresulttool merge test_output/**/*.xcresult --output-path test_output/final/final.xcresult - - name: Upload Merged Artifact - uses: actions/upload-artifact@v4 - with: - name: MergedResult - path: test_output/final + run: swift build -c release --target "OverlayContainer" discover-typos: name: Discover Typos runs-on: macOS-13 From 913407c4c975d301c5982672112c7be3a2192081 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 24 Mar 2025 20:59:35 +0400 Subject: [PATCH 5/7] Implement `ci.yml` --- .github/workflows/ci.yml | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e65611f..24f7bbe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,26 +50,6 @@ jobs: with: name: ${{ matrix.name }} path: test_output - spm: - name: ${{ matrix.name }} - runs-on: ${{ matrix.runsOn }} - env: - DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - include: - - name: "Xcode 15" - xcode: "Xcode_15.0" - runsOn: macos-13 - - name: "Xcode 14" - xcode: "Xcode_14.3.1" - runsOn: macos-13 - steps: - - uses: actions/checkout@v3 - - name: ${{ matrix.name }} - run: swift build -c release --target "OverlayContainer" discover-typos: name: Discover Typos runs-on: macOS-13 From bfce76f44db367842d8d3b653335e2a3a6b4e772 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 24 Mar 2025 20:59:46 +0400 Subject: [PATCH 6/7] Update `CONTRIBUTING.md` --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f96b11f..5007a7f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,4 +58,4 @@ See [CODE_OF_CONDUCT.md](https://github.com/space-code/overlay-container/blob/ma --- -*Some of the ideas and wording for the statements above were based on work by the [Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md) and [Linux](https://elinux.org/Developer_Certificate_Of_Origin) communities. We commend them for their efforts to facilitate collaboration in their projects.* \ No newline at end of file +*Some of the ideas and wording for the statements above were based on work by the [Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md) and [Linux](https://elinux.org/Developer_Certificate_Of_Origin) communities. \ No newline at end of file From 8e9804bb92428e397de50fafa7d8b0b327bff137 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 24 Mar 2025 21:11:36 +0400 Subject: [PATCH 7/7] Update `CHAGELOG.md` --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e9885a..360f376 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,9 @@ # Change Log -All notable changes to this project will be documented in this file. \ No newline at end of file +All notable changes to this project will be documented in this file. + +## [1.0.0](https://github.com/space-code/overlay-container/releases/tag/1.0.0) +Released on 2025-03-24. + +#### Added +- Initial release of overlay-container. + - Added by [Nikita Vasilev](https://github.com/ns-vasiev). \ No newline at end of file