From 2cc4e1404d9b5b245a607d0306128df911e65cac Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 6 Feb 2025 15:32:02 +0400 Subject: [PATCH 1/3] 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 | 74 ++++++++++ .github/workflows/danger.yml | 31 ++++ .swiftformat | 64 +++++++++ .swiftlint.yml | 135 ++++++++++++++++++ CHANGELOG.md | 2 + CODE_OF_CONDUCT.md | 74 ++++++++++ CONTRIBUTING.md | 61 ++++++++ Dangerfile | 1 + Gemfile | 3 + Makefile | 19 +++ Mintfile | 2 + Package.swift | 24 ++++ README.md | 56 +++++++- SEQURITY.md | 7 + Sources/snacker/Classes/snacker.swift | 6 + Tests/snackerTests/snackerTests.swift | 8 ++ hooks/pre-commit | 38 +++++ 22 files changed, 711 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 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 SEQURITY.md create mode 100644 Sources/snacker/Classes/snacker.swift create mode 100644 Tests/snackerTests/snackerTests.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..5b2f57d --- /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 snacker, 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..57c6f88 --- /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: + - ns-vasilev + reviewers: + - ns-vasilev + + + - package-ecosystem: swift + directory: / + open-pull-requests-limit: 10 + schedule: + interval: daily + time: '07:00' + timezone: Europe/Berlin + + assignees: + - ns-vasilev + reviewers: + - ns-vasilev + \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0f22007 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: "snacker" + +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=18.1,name=iPhone 16 Pro" + name: "iOS 18.1" + xcode: "Xcode_16.1" + runsOn: macOS-14 + - destination: "OS=17.5,name=iPhone 15 Pro" + name: "iOS 17.5" + xcode: "Xcode_15.4" + runsOn: macOS-14 + - 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 "snacker" -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 + + discover-typos: + name: Discover Typos + runs-on: macOS-13 + env: + DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer + steps: + - uses: actions/checkout@v4 + - 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..f91ea37 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,64 @@ +# Stream rules + +--swiftversion 5.3 + +# Use 'swiftformat --options' to list all of the possible options + +--header "\nsnacker\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..89efd09 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,135 @@ +excluded: + - Tests + - Package.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/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..e6661d0 --- /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/snacker/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/snacker/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. \ 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..856d64b --- /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 \ No newline at end of file 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..156036a --- /dev/null +++ b/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "snacker", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "snacker", + targets: ["snacker"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "snacker"), + .testTarget( + name: "snackerTests", + dependencies: ["snacker"] + ), + ] +) diff --git a/README.md b/README.md index 2ba7f3a..2b6bf0e 100644 --- a/README.md +++ b/README.md @@ -1 +1,55 @@ -# snacker \ No newline at end of file +

snacker

+ +

+License +Swift Compatibility +Platform Compatibility +CI + +## Description +`snacker` 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 `snacker` does support its use on supported platforms. + +Once you have your Swift package set up, adding `snacker` 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/snacker.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 +snacker 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/snacker/Classes/snacker.swift b/Sources/snacker/Classes/snacker.swift new file mode 100644 index 0000000..c0af11e --- /dev/null +++ b/Sources/snacker/Classes/snacker.swift @@ -0,0 +1,6 @@ +// +// snacker +// Copyright © 2025 Space Code. All rights reserved. +// + +final class snacker {} diff --git a/Tests/snackerTests/snackerTests.swift b/Tests/snackerTests/snackerTests.swift new file mode 100644 index 0000000..eeb82fd --- /dev/null +++ b/Tests/snackerTests/snackerTests.swift @@ -0,0 +1,8 @@ +// +// snacker +// Copyright © 2025 Space Code. All rights reserved. +// + +import XCTest + +final class snackerTests: 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 9d3172384445e5550b6b69416d5a073176a57eb0 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 24 Mar 2025 21:01:31 +0400 Subject: [PATCH 2/3] Implement `Snacker` (#1) --- .github/workflows/ci.yml | 14 +- .../contents.xcworkspacedata | 7 + Package.swift | 18 +-- README.md | 24 ++- .../Classes/Core/Snacker/Snacker.swift | 110 +++++++++++++ .../Classes/Model/SnackbarAction.swift | 13 ++ .../Classes/Model/SnackbarAlignment.swift | 10 ++ .../snacker/Classes/Model/SnackbarData.swift | 22 +++ .../Classes/Model/SnackbarInsets.swift | 18 +++ .../Presentation/SnackbarController.swift | 145 ++++++++++++++++++ Sources/snacker/Classes/snacker.swift | 6 - .../Resources/Media.xcassets/Contents.json | 6 + .../Media.xcassets/Snack/Contents.json | 9 ++ 13 files changed, 368 insertions(+), 34 deletions(-) create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 Sources/snacker/Classes/Core/Snacker/Snacker.swift create mode 100644 Sources/snacker/Classes/Model/SnackbarAction.swift create mode 100644 Sources/snacker/Classes/Model/SnackbarAlignment.swift create mode 100644 Sources/snacker/Classes/Model/SnackbarData.swift create mode 100644 Sources/snacker/Classes/Model/SnackbarInsets.swift create mode 100644 Sources/snacker/Classes/Presentation/SnackbarController.swift delete mode 100644 Sources/snacker/Classes/snacker.swift create mode 100644 Sources/snacker/Resources/Media.xcassets/Contents.json create mode 100644 Sources/snacker/Resources/Media.xcassets/Snack/Contents.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f22007..69db477 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,22 +38,10 @@ jobs: name: "iOS 18.1" xcode: "Xcode_16.1" runsOn: macOS-14 - - destination: "OS=17.5,name=iPhone 15 Pro" - name: "iOS 17.5" - xcode: "Xcode_15.4" - runsOn: macOS-14 - - 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 "snacker" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES -resultBundlePath "test_output/${{ matrix.name }}.xcresult" || exit 1 + run: xcodebuild test -scheme "Snacker" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES -resultBundlePath "test_output/${{ matrix.name }}.xcresult" || exit 1 - uses: actions/upload-artifact@v4 with: name: ${{ matrix.name }} 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/Package.swift b/Package.swift index 156036a..7c5b8d0 100644 --- a/Package.swift +++ b/Package.swift @@ -4,21 +4,13 @@ import PackageDescription let package = Package( - name: "snacker", + name: "Snacker", + platforms: [.iOS(.v17)], products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. - .library( - name: "snacker", - targets: ["snacker"]), + .library(name: "Snacker", targets: ["Snacker"]), ], targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - .target( - name: "snacker"), - .testTarget( - name: "snackerTests", - dependencies: ["snacker"] - ), + .target(name: "Snacker"), + .testTarget(name: "SnackerTests", dependencies: ["Snacker"]), ] ) diff --git a/README.md b/README.md index 2b6bf0e..4934766 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ CI ## Description -`snacker` description. +`snacker` is a lightweight Swift library for displaying snackbars in iOS applications. - [Usage](#usage) - [Requirements](#requirements) @@ -19,8 +19,28 @@ ## Usage +```swift +import Snackner + +Snacker.shared.action( + .snack( + view: view, + data: SnackbarData( + snackbarAlignment: .top(spacing: 20), + insets: .zero, + animationDuration: 0.25 + ) + ), + container: window +) +``` + ## Requirements +- iOS 17.0+ +- Xcode 16.0 +- Swift 6.0 + ## Installation ### Swift Package Manager @@ -52,4 +72,4 @@ Please feel free to help out with this project! If you see something that could Nikita Vasilev, nv3212@gmail.com ## License -snacker is available under the MIT license. See the LICENSE file for more info. \ No newline at end of file +snacker is available under the MIT license. See the LICENSE file for more info. diff --git a/Sources/snacker/Classes/Core/Snacker/Snacker.swift b/Sources/snacker/Classes/Core/Snacker/Snacker.swift new file mode 100644 index 0000000..153192f --- /dev/null +++ b/Sources/snacker/Classes/Core/Snacker/Snacker.swift @@ -0,0 +1,110 @@ +// +// snacker +// Copyright © 2025 Space Code. All rights reserved. +// + +import UIKit + +// MARK: - ISnacker + +@MainActor +public protocol ISnacker { + func action(_ action: SnackbarAction, container: UIView) +} + +// MARK: - Snacker + +@MainActor +public final class Snacker { + // MARK: Properties + + public static let shared: ISnacker = Snacker() + + private var cachedViews: [Int: [SnackbarController]] = [:] + + // MARK: Initialization + + private init() {} + + // MARK: Private + + private func save(controller: SnackbarController, id: Int) { + if var views = cachedViews[id] { + views.append(controller) + cachedViews[id] = views + } else { + cachedViews[id] = [controller] + } + } + + private func remove(controller: SnackbarController, id: Int) { + controller.hide(animated: true) { [weak self] in + guard let index = self?.cachedViews[id]?.firstIndex(of: controller) else { + return + } + self?.cachedViews[id]?.remove(at: index) + } + } + + private func makeController( + for view: UIView, + inContainerView containerView: UIView, + data: SnackbarData + ) -> SnackbarController { + let controller = SnackbarController( + contentView: view, + containerView: containerView, + alignment: data.snackbarAlignment, + insets: data.insets + ) + return controller + } +} + +// MARK: ISnacker + +extension Snacker: ISnacker { + // swiftlint:disable:next function_body_length + public func action(_ action: SnackbarAction, container: UIView) { + let hash = container.hash + + switch action { + case let .snack(view, data): + let controller = makeController( + for: view, + inContainerView: container, + data: data + ) + save(controller: controller, id: hash) + + DispatchQueue.main.async { controller.show(animated: true) } + + DispatchQueue.main.asyncAfter(deadline: .now() + data.animationDuration) { [weak self] in + self?.remove(controller: controller, id: hash) + } + case let .snackSingle(view, data) where (cachedViews[hash] ?? []).isEmpty: + let controller = makeController( + for: view, + inContainerView: container, + data: data + ) + save(controller: controller, id: hash) + + DispatchQueue.main.async { controller.show(animated: true) } + + DispatchQueue.main.asyncAfter(deadline: .now() + data.animationDuration) { [weak self] in + self?.remove(controller: controller, id: hash) + } + case .removeLast: + if let controller = cachedViews[hash]?.last { + remove(controller: controller, id: hash) + } + case .removeAll: + (cachedViews[hash] ?? []).forEach { + remove(controller: $0, id: hash) + } + default: + break + } + } +} diff --git a/Sources/snacker/Classes/Model/SnackbarAction.swift b/Sources/snacker/Classes/Model/SnackbarAction.swift new file mode 100644 index 0000000..4004cc1 --- /dev/null +++ b/Sources/snacker/Classes/Model/SnackbarAction.swift @@ -0,0 +1,13 @@ +// +// snacker +// Copyright © 2025 Space Code. All rights reserved. +// + +import UIKit + +public enum SnackbarAction { + case snack(view: UIView, data: SnackbarData) + case snackSingle(view: UIView, data: SnackbarData) + case removeLast + case removeAll +} diff --git a/Sources/snacker/Classes/Model/SnackbarAlignment.swift b/Sources/snacker/Classes/Model/SnackbarAlignment.swift new file mode 100644 index 0000000..29de4cd --- /dev/null +++ b/Sources/snacker/Classes/Model/SnackbarAlignment.swift @@ -0,0 +1,10 @@ +// +// snacker +// Copyright © 2025 Space Code. All rights reserved. +// + +import Foundation +public enum SnackbarAlignment: Sendable { + case top(spacing: CGFloat) + case bottom(spacing: CGFloat) +} diff --git a/Sources/snacker/Classes/Model/SnackbarData.swift b/Sources/snacker/Classes/Model/SnackbarData.swift new file mode 100644 index 0000000..5e4e54c --- /dev/null +++ b/Sources/snacker/Classes/Model/SnackbarData.swift @@ -0,0 +1,22 @@ +// +// snacker +// Copyright © 2025 Space Code. All rights reserved. +// + +import Foundation + +public struct SnackbarData: Sendable { + let snackbarAlignment: SnackbarAlignment + let insets: SnackbarInsets + let animationDuration: TimeInterval + + public init( + snackbarAlignment: SnackbarAlignment = .bottom(spacing: 20.0), + insets: SnackbarInsets = .zero, + animationDuration: TimeInterval = 0.5 + ) { + self.snackbarAlignment = snackbarAlignment + self.insets = insets + self.animationDuration = animationDuration + } +} diff --git a/Sources/snacker/Classes/Model/SnackbarInsets.swift b/Sources/snacker/Classes/Model/SnackbarInsets.swift new file mode 100644 index 0000000..d0cf0e4 --- /dev/null +++ b/Sources/snacker/Classes/Model/SnackbarInsets.swift @@ -0,0 +1,18 @@ +// +// snacker +// Copyright © 2025 Space Code. All rights reserved. +// + +import Foundation + +public struct SnackbarInsets: Sendable { + public static let zero = SnackbarInsets(leading: .zero, trailing: .zero) + + let leading: CGFloat + let trailing: CGFloat + + public init(leading: CGFloat, trailing: CGFloat) { + self.leading = leading + self.trailing = trailing + } +} diff --git a/Sources/snacker/Classes/Presentation/SnackbarController.swift b/Sources/snacker/Classes/Presentation/SnackbarController.swift new file mode 100644 index 0000000..9382aff --- /dev/null +++ b/Sources/snacker/Classes/Presentation/SnackbarController.swift @@ -0,0 +1,145 @@ +// +// snacker +// Copyright © 2025 Space Code. All rights reserved. +// + +import UIKit + +// MARK: - SnackbarController + +@MainActor +final class SnackbarController: NSObject { + // MARK: Properties + + private let contentView: UIView + private weak var containerView: UIView! + private let alignment: SnackbarAlignment + private let insets: SnackbarInsets + + private var disabledConstraints: [NSLayoutConstraint] = [] + private var activeConstraints: [NSLayoutConstraint] = [] + + // MARK: Initialization + + init( + contentView: UIView, + containerView: UIView, + alignment: SnackbarAlignment, + insets: SnackbarInsets + ) { + self.contentView = contentView + self.containerView = containerView + self.alignment = alignment + self.insets = insets + } + + // MARK: Private + + private func staticConstraints() -> [NSLayoutConstraint] { + [ + contentView.leadingAnchor.constraint(greaterThanOrEqualTo: containerView.leadingAnchor, constant: insets.leading), + containerView.trailingAnchor.constraint(greaterThanOrEqualTo: contentView.trailingAnchor, constant: insets.trailing), + contentView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + ] + } + + private func activeConstraints(contentView: UIView, container: UIView) -> [NSLayoutConstraint] { + switch alignment { + case let .top(spacing): + [ + contentView.topAnchor.constraint( + equalTo: container.safeAreaLayoutGuide.topAnchor, + constant: spacing + ), + ] + case let .bottom(spacing): + [ + contentView.bottomAnchor.constraint( + equalTo: container.safeAreaLayoutGuide.bottomAnchor, + constant: -1 * spacing + ), + ] + } + } + + private func disabledConstraints(contentView: UIView, container: UIView) -> [NSLayoutConstraint] { + switch alignment { + case .top: + [ + contentView.bottomAnchor.constraint(equalTo: container.topAnchor), + ] + case .bottom: + [ + contentView.topAnchor.constraint(equalTo: container.bottomAnchor), + ] + } + } +} + +extension SnackbarController { + func show(animated: Bool, completion: (() -> Void)? = nil) { + contentView.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(contentView) + + let baseConstraints = staticConstraints() + + disabledConstraints = disabledConstraints(contentView: contentView, container: containerView) + activeConstraints = activeConstraints(contentView: contentView, container: containerView) + + NSLayoutConstraint.activate(baseConstraints) + + if animated { + NSLayoutConstraint.activate(disabledConstraints) + containerView.layoutIfNeeded() + + UIView.animate( + withDuration: 0.5, + delay: .zero, + options: .curveEaseInOut, + animations: { [weak self] in + guard let self else { + return + } + NSLayoutConstraint.deactivate(self.disabledConstraints) + NSLayoutConstraint.activate(self.activeConstraints) + self.containerView.layoutIfNeeded() + }, + completion: { _ in + completion?() + } + ) + } else { + NSLayoutConstraint.activate(activeConstraints) + completion?() + } + } + + func hide(animated: Bool, completion: (() -> Void)? = nil) { + NSLayoutConstraint.deactivate(activeConstraints) + containerView.layoutIfNeeded() + + if animated { + UIView.animate( + withDuration: 0.5, + delay: .zero, + options: .curveEaseInOut, + animations: { [weak self] in + guard let self else { + return + } + NSLayoutConstraint.activate(self.disabledConstraints) + self.containerView.layoutIfNeeded() + }, + completion: { [weak self] _ in + self?.contentView.removeFromSuperview() + self?.containerView = nil + completion?() + } + ) + } else { + contentView.removeFromSuperview() + containerView = nil + completion?() + } + } +} diff --git a/Sources/snacker/Classes/snacker.swift b/Sources/snacker/Classes/snacker.swift deleted file mode 100644 index c0af11e..0000000 --- a/Sources/snacker/Classes/snacker.swift +++ /dev/null @@ -1,6 +0,0 @@ -// -// snacker -// Copyright © 2025 Space Code. All rights reserved. -// - -final class snacker {} diff --git a/Sources/snacker/Resources/Media.xcassets/Contents.json b/Sources/snacker/Resources/Media.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Sources/snacker/Resources/Media.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/snacker/Resources/Media.xcassets/Snack/Contents.json b/Sources/snacker/Resources/Media.xcassets/Snack/Contents.json new file mode 100644 index 0000000..6e96565 --- /dev/null +++ b/Sources/snacker/Resources/Media.xcassets/Snack/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} From 949011cfe1b0ac86b705fef42758b18c77b0a3db Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 24 Mar 2025 21:14:13 +0400 Subject: [PATCH 3/3] Update `CHANGELOG.md` --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e9885a..cac70f5 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/snacker/releases/tag/1.0.0) +Released on 2025-03-24. + +#### Added +- Initial release of snacker. + - Added by [Nikita Vasilev](https://github.com/ns-vasiev). \ No newline at end of file