├── .codebeatsettings ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feedback.md │ └── submit-a-request.md ├── pull_request_template.md ├── release-drafter.yml └── workflows │ ├── CD.yml │ ├── main.yml │ ├── needs-attention.yml │ ├── pod_lib_lint.yml │ ├── pod_trunk.yml │ ├── release.yml │ ├── release_notes.yml │ ├── stale.yml │ └── validations.yml ├── .gitignore ├── .swift-version ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Assets ├── all_skeletonables.jpg ├── all_skeletonables_result.png ├── container_no_skeletonable.jpg ├── container_skeletonable.jpg ├── container_skeletonable_result.png ├── debug_description.png ├── debug_mode.png ├── demoApp2.png ├── flatcolors.png ├── gradient.png ├── gradient_animated.gif ├── header.jpg ├── header2.jpg ├── hierarchy_output.png ├── multiline_corner.png ├── multiline_customize.png ├── multiline_insets.png ├── multiline_lastline.png ├── multiline_lineHeight.png ├── multiline_lineSpacing.png ├── multilines2.png ├── no_skeletonable.jpg ├── no_skeletonables_result.png ├── skeleton_transition_fade.gif ├── skeleton_transition_nofade.gif ├── sliding_bottomRight_to_topLeft.gif ├── sliding_bottom_to_top.gif ├── sliding_left_to_right.gif ├── sliding_right_to_left.gif ├── sliding_topLeft_to_bottomRight.gif ├── sliding_top_to_bottom.gif ├── solid.png ├── solid_animated.gif ├── solid_animated2.gif ├── storyboard.png ├── tableview_no_skeletonable.jpg ├── tableview_no_skeletonable_result.png ├── tableview_scheme.png ├── tableview_skeletonable.jpg ├── tableview_skeletonable_result.png └── thumb_getting_started.png ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dangerfile.swift ├── Examples ├── CollectionView │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── avatar.imageset │ │ │ ├── Contents.json │ │ │ └── avatar.png │ │ └── picture.imageset │ │ │ ├── Contents.json │ │ │ └── picture.png │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── CollectionViewCell.swift │ ├── Main.storyboard │ ├── SkeletonViewExampleCollectionview-Info.plist │ └── ViewController.swift ├── iOS Example │ ├── Sources │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ └── avatar.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── avatar.png │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Cell.swift │ │ ├── Constants.swift │ │ ├── HeaderFooterSection.swift │ │ ├── Info.plist │ │ ├── UITextViewByCodeViewController.swift │ │ └── ViewController.swift │ └── iOS Example.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── tvOS Example │ ├── Sources │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── App Icon & Top Shelf Image.brandassets │ │ │ ├── App Icon - App Store.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ ├── App Icon.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Top Shelf Image Wide.imageset │ │ │ │ └── Contents.json │ │ │ └── Top Shelf Image.imageset │ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── ViewController.swift │ └── tvOS Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift ├── README.md ├── SkeletonVIew.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── SkeletonView.podspec ├── SkeletonView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ ├── IDETemplateMacros.plist │ └── xcschemes │ ├── SkeletonView iOS.xcscheme │ └── SkeletonView tvOS.xcscheme ├── SkeletonViewCore ├── Sources │ ├── API │ │ ├── AnimationBuilder │ │ │ └── SkeletonAnimationBuilder.swift │ │ ├── Appearance │ │ │ └── SkeletonAppearance.swift │ │ ├── Collections │ │ │ ├── CollectionViews │ │ │ │ └── SkeletonCollectionViewProtocols.swift │ │ │ └── TableViews │ │ │ │ └── SkeletonTableViewProtocols.swift │ │ ├── Deprecated.swift │ │ ├── FoundationExtensions │ │ │ └── Notification+SkeletonFlow.swift │ │ ├── Models │ │ │ ├── GradientDirection.swift │ │ │ ├── SkeletonGradient.swift │ │ │ ├── SkeletonTextLineHeight.swift │ │ │ ├── SkeletonTextNumberOfLines.swift │ │ │ ├── SkeletonTransitionStyle.swift │ │ │ └── SkeletonType.swift │ │ ├── SkeletonExtended.swift │ │ ├── SkeletonView.swift │ │ └── UIKitExtensions │ │ │ ├── CALayer+Animations.swift │ │ │ ├── UICollectionView+Extensions.swift │ │ │ ├── UILabel+IBInspectable.swift │ │ │ ├── UILabel+SKExtensions.swift │ │ │ ├── UITextView+IBInspectable.swift │ │ │ ├── UITextView+SKExtensions.swift │ │ │ ├── UIView+IBInspectable.swift │ │ │ └── UIView+SKExtensions.swift │ ├── Internal │ │ ├── Collections │ │ │ ├── CollectionSkeleton.swift │ │ │ ├── SkeletonCollectionDataSource.swift │ │ │ ├── SkeletonCollectionDelegate.swift │ │ │ └── SkeletonReusableCell.swift │ │ ├── Debug │ │ │ └── SkeletonDebug.swift │ │ ├── FoundationExtensions │ │ │ ├── DispatchQueue+Extensions.swift │ │ │ ├── Int+Extensions.swift │ │ │ ├── Notification+Extensions.swift │ │ │ └── ProcessInfo+Extensions.swift │ │ ├── Helpers │ │ │ ├── AssociationPolicy.swift │ │ │ ├── Recursive.swift │ │ │ └── Swizzling.swift │ │ ├── Models │ │ │ ├── RecoverableViewState.swift │ │ │ └── SkeletonLayer.swift │ │ ├── SkeletonConfigs │ │ │ ├── SkeletonConfig.swift │ │ │ └── SkeletonMultilinesLayerConfig.swift │ │ ├── SkeletonExtensions │ │ │ ├── GradientDirection+Animations.swift │ │ │ ├── PrepareViewForSkeleton.swift │ │ │ ├── Recoverable.swift │ │ │ ├── SkeletonTextNode.swift │ │ │ └── SubviewsSkeletonables.swift │ │ ├── SkeletonFlowHandler.swift │ │ ├── SkeletonLayerBuilders │ │ │ ├── SkeletonLayerBuilder.swift │ │ │ └── SkeletonMultilineLayerBuilder.swift │ │ ├── SkeletonTree │ │ │ └── SkeletonTreeNode.swift │ │ └── UIKitExtensions │ │ │ ├── CALayer+Extensions.swift │ │ │ ├── SkeletonTreeNode+Extensions.swift │ │ │ ├── UICollectionView+CollectionSkeleton.swift │ │ │ ├── UIColor+Skeleton.swift │ │ │ ├── UILabel+Extensions.swift │ │ │ ├── UITableView+CollectionSkeleton.swift │ │ │ ├── UITableView+Extensions.swift │ │ │ ├── UIView+AppLifecycleNotifications.swift │ │ │ ├── UIView+AssociatedObjects.swift │ │ │ ├── UIView+CollectionSkeleton.swift │ │ │ ├── UIView+Extensions.swift │ │ │ ├── UIView+SkeletonView.swift │ │ │ ├── UIView+Swizzling.swift │ │ │ └── UIView+Transitions.swift │ └── Supporting Files │ │ ├── Info.plist │ │ └── PrivacyInfo.xcprivacy └── Tests │ ├── Debug │ └── SkeletonDebugTests.swift │ └── Supporting Files │ └── Info.plist ├── Translations ├── README_de.md ├── README_es.md ├── README_fr.md ├── README_ko.md ├── README_pt-br.md └── README_zh.md └── fastlane ├── Fastfile └── README.md /.codebeatsettings: -------------------------------------------------------------------------------- 1 | { 2 | "SWIFT": { 3 | "TOO_MANY_FUNCTIONS": [50, 100, 150, 200], 4 | "TOTAL_LOC": [200, 400, 500, 600] 5 | } 6 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh linguist-language=Swift 2 | *.podspec linguist-language=Swift 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [juanpe] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Report a bug or unexpected behavior while using SkeletonView 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Description 11 | 12 | Describe your issue here. 13 | 14 | ### What type of issue is this? (place an `x` in one of the `[ ]`) 15 | - [ ] bug 16 | - [ ] enhancement (feature request) 17 | - [ ] question 18 | - [ ] documentation related 19 | - [ ] discussion 20 | 21 | ### Requirements (place an `x` in each of the `[ ]`) 22 | * [ ] I've read and understood the [Contributing guidelines](https://github.com/Juanpe/SkeletonView/blob/main/CONTRIBUTING.md) and have done my best effort to follow them. 23 | * [ ] I've read and agree to the [Code of Conduct](https://github.com/Juanpe/SkeletonView/blob/main/CODE_OF_CONDUCT.md). 24 | * [ ] I've searched for any related issues and avoided creating a duplicate issue. 25 | 26 | --- 27 | 28 | ### Bug Report 29 | 30 | Filling out the following details about bugs will help us solve your issue sooner. 31 | 32 | ### SkeletonView Environment: 33 | 34 | **SkeletonView version:** 35 | **Xcode version:** 36 | **Swift version:** 37 | 38 | #### Steps to reproduce: 39 | 40 | *Please replace this with the steps to reproduce the behavior.* 41 | 42 | 1. 43 | 2. 44 | 3. 45 | 46 | #### Expected result: 47 | 48 | *Please replace this with what you expected to happen.* 49 | 50 | #### Actual result: 51 | 52 | *Please replace this with of what happened instead.* 53 | 54 | #### Attachments: 55 | 56 | Logs, screenshots, sample project, funny gif, etc. 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feedback.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4E3 Feedback" 3 | about: Give us general feedback about the SkeletonView 4 | title: '' 5 | labels: feedback 6 | assignees: '' 7 | 8 | --- 9 | 10 | # SkeletonView Feedback 11 | 12 | You can use this template to give us structured feedback or just wipe it and leave us a note. Thank you! 13 | 14 | ## What have you loved? 15 | 16 | _eg "the nice colors"_ 17 | 18 | ## What was confusing or gave you pause? 19 | 20 | _eg "it did something unexpected"_ 21 | 22 | ## Are there features you'd like to see added? 23 | 24 | _eg "SkeletonView should be compatible with SwiftUI"_ 25 | 26 | ## Anything else? 27 | 28 | _eg "have a nice day"_ 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/submit-a-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "⭐ Submit a request" 3 | about: Surface a feature or problem that you think should be solved 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Describe the feature or problem you’d like to solve 11 | 12 | A clear and concise description of what the feature or problem is. 13 | 14 | ### Proposed solution 15 | 16 | How will it benefit SkeletonView and its users? 17 | 18 | ### Additional context 19 | 20 | Add any other context like screenshots or mockups are helpful, if applicable. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | Describe the goal of this PR. Mention any related Issue numbers. 4 | 5 | ### Requirements (place an `x` in each of the `[ ]`) 6 | * [ ] I've read and understood the [Contributing guidelines](https://github.com/Juanpe/SkeletonView/blob/main/CONTRIBUTING.md) and have done my best effort to follow them. 7 | * [ ] I've read and agree to the [Code of Conduct](https://github.com/Juanpe/SkeletonView/blob/main/CODE_OF_CONDUCT.md). 8 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: '📦 $RESOLVED_VERSION' 2 | tag-template: '$RESOLVED_VERSION' 3 | category-template: '#### $TITLE' 4 | change-template: '- **#$NUMBER**: $TITLE - @$AUTHOR' 5 | template: | 6 | $CHANGES 7 | categories: 8 | - title: '🚨 Breaking' 9 | label: 'breaking' 10 | - title: '🔬Improvements' 11 | label: '💡 enhancement' 12 | - title: '🙌 New' 13 | label: 'feature' 14 | - title: '🩹 Bug fixes' 15 | label: '🐞 bug' 16 | - title: '⚙️ Maintenance' 17 | label: '⚙️ maintenance' 18 | - title: '📚 Documentation' 19 | label: '📚 docs' 20 | - title: '💾 Dependency Updates' 21 | label: 'dependencies' 22 | 23 | version-resolver: 24 | major: 25 | labels: 26 | - 'breaking' 27 | minor: 28 | labels: 29 | - '💡 enhancement' 30 | - 'feature' 31 | patch: 32 | labels: 33 | - '🐞 bug' 34 | - '⚙️ maintenance' 35 | - '📚 docs' 36 | - 'dependencies' 37 | 38 | exclude-labels: 39 | - 'skip-changelog' 40 | -------------------------------------------------------------------------------- /.github/workflows/CD.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | pull_request_target: 5 | branches: [main] 6 | types: [closed] 7 | 8 | jobs: 9 | release_version: 10 | if: github.event.pull_request.milestone == null && github.event.pull_request.merged == true 11 | runs-on: macOS-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Publish release 16 | id: publish_release 17 | uses: release-drafter/release-drafter@v5 18 | with: 19 | publish: true 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | - name: Update podspec 24 | run: fastlane bump_version next_version:${{ steps.publish_release.outputs.tag_name }} 25 | 26 | - name: Commit changes 27 | uses: stefanzweifel/git-auto-commit-action@v4 28 | with: 29 | branch: 'main' 30 | commit_message: 'Bump version ${{ steps.publish_release.outputs.tag_name }}' 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Deploy to Cocoapods 35 | continue-on-error: true 36 | env: 37 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 38 | run: | 39 | set -eo pipefail 40 | pod lib lint --allow-warnings 41 | pod trunk push --allow-warnings 42 | 43 | - name: Tweet the release 44 | uses: ethomson/send-tweet-action@v1 45 | with: 46 | consumer-key: ${{ secrets.TWITTER_CONSUMER_API_KEY }} 47 | consumer-secret: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} 48 | access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }} 49 | access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} 50 | status: | 51 | 🎉 New release ${{ steps.publish_release.outputs.tag_name }} is out 🚀 52 | 53 | Check out all the changes here: 54 | ${{ steps.publish_release.outputs.html_url }} 55 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-latest 11 | strategy: 12 | matrix: 13 | build-config: 14 | - { scheme: 'SkeletonView iOS', destination: 'platform=iOS Simulator,name=iPhone 8', sdk: 'iphonesimulator' } 15 | - { scheme: 'SkeletonView tvOS', destination: 'platform=tvOS Simulator,name=Apple TV', sdk: 'appletvsimulator' } 16 | - { scheme: 'iOS Example', destination: 'platform=iOS Simulator,name=iPhone 8', sdk: 'iphonesimulator' } 17 | - { scheme: 'tvOS Example', destination: 'platform=tvOS Simulator,name=Apple TV', sdk: 'appletvsimulator' } 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Build 21 | run: xcodebuild clean build -workspace 'SkeletonView.xcworkspace' -scheme '${{ matrix.build-config['scheme'] }}' -sdk '${{ matrix.build-config['sdk'] }}' -destination '${{ matrix.build-config['destination'] }}' 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/needs-attention.yml: -------------------------------------------------------------------------------- 1 | name: Issue Needs Attention 2 | # This workflow is triggered on issue comments. 3 | on: 4 | issue_comment: 5 | types: created 6 | 7 | jobs: 8 | applyNeedsAttentionLabel: 9 | name: Apply Needs Attention Label 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Apply Needs Attention Label 14 | uses: hramos/needs-attention@v1 15 | with: 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | response-required-label: 'awaiting user info' 18 | needs-attention-label: 'needs triage' 19 | -------------------------------------------------------------------------------- /.github/workflows/pod_lib_lint.yml: -------------------------------------------------------------------------------- 1 | name: Pod lint 2 | on: [workflow_dispatch] 3 | 4 | jobs: 5 | pod_lib_lint: 6 | runs-on: macOS-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | 10 | - env: 11 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 12 | run: | 13 | set -eo pipefail 14 | pod lib lint --allow-warnings 15 | -------------------------------------------------------------------------------- /.github/workflows/pod_trunk.yml: -------------------------------------------------------------------------------- 1 | name: Pod trunk 2 | on: [workflow_dispatch] 3 | 4 | jobs: 5 | release_version: 6 | runs-on: macOS-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | 10 | - name: Deploy to Cocoapods 11 | env: 12 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 13 | run: | 14 | set -eo pipefail 15 | pod lib lint --allow-warnings 16 | pod trunk push --allow-warnings 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: [workflow_dispatch] 3 | 4 | jobs: 5 | release_version: 6 | runs-on: macOS-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | 10 | - name: Publish release 11 | id: publish_release 12 | uses: release-drafter/release-drafter@v5 13 | with: 14 | publish: true 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | - name: Update podspec 19 | run: fastlane bump_version next_version:${{ steps.publish_release.outputs.tag_name }} 20 | 21 | - name: Commit changes 22 | uses: stefanzweifel/git-auto-commit-action@v4 23 | with: 24 | branch: 'main' 25 | commit_message: 'Bump version ${{ steps.publish_release.outputs.tag_name }}' 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - name: Deploy to Cocoapods 30 | env: 31 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 32 | run: | 33 | set -eo pipefail 34 | pod lib lint --allow-warnings 35 | pod trunk push --allow-warnings 36 | 37 | - name: Tweet the release 38 | uses: ethomson/send-tweet-action@v1 39 | with: 40 | consumer-key: ${{ secrets.TWITTER_CONSUMER_API_KEY }} 41 | consumer-secret: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} 42 | access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }} 43 | access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} 44 | status: | 45 | 🎉 New release ${{ steps.publish_release.outputs.tag_name }} is out 🚀 46 | Check out all the changes here: 47 | ${{ steps.publish_release.outputs.html_url }} 48 | -------------------------------------------------------------------------------- /.github/workflows/release_notes.yml: -------------------------------------------------------------------------------- 1 | name: Release Notes 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_notes: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@master 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 5 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v4 11 | with: 12 | close-issue-message: 'Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please feel free to create a new issue with up-to-date information.' 13 | stale-issue-message: '🤖 This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions 🙂' 14 | days-before-stale: 5 15 | days-before-close: 3 16 | enable-statistics: true 17 | operations-per-run: 60 18 | only-labels: 'awaiting user input' -------------------------------------------------------------------------------- /.github/workflows/validations.yml: -------------------------------------------------------------------------------- 1 | name: Validations 2 | 3 | on: 4 | pull_request_target: 5 | branches: [main] 6 | types: [opened, reoneped, edited, synchronized] 7 | 8 | # workflow_dispatch: 9 | # inputs: 10 | # commit hash: 11 | # description: "Commit hash" 12 | # required: true 13 | # default: "" 14 | 15 | jobs: 16 | lint: 17 | runs-on: macos-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Run SwiftLint 21 | run: swiftlint lint --reporter github-actions-logging 22 | 23 | danger: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Danger 28 | uses: docker://frmeloni/danger-swift-with-swiftlint:1.3.1 29 | with: 30 | args: --failOnErrors --verbose 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | .DS_Store 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots 68 | fastlane/test_output 69 | 70 | # JetBrains 71 | 72 | .idea 73 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - SkeletonViewCore/Sources 3 | disabled_rules: 4 | - trailing_whitespace 5 | - line_length 6 | - type_body_length 7 | - identifier_name 8 | - multiple_closures_with_trailing_closure 9 | - class_delegate_protocol 10 | - force_unwrapping 11 | - force_try 12 | - force_cast 13 | - function_parameter_count 14 | - discouraged_optional_collection 15 | - shorthand_operator 16 | - reduce_boolean 17 | - weak_delegate 18 | - nesting 19 | - closure_end_indentation 20 | - function_default_parameter_at_end 21 | - unowned_variable_capture 22 | - legacy_constructor 23 | - redundant_type_annotation 24 | - vertical_whitespace_opening_braces 25 | opt_in_rules: 26 | - multiline_arguments 27 | - multiline_parameters 28 | - closure_spacing 29 | - closure_body_length 30 | - collection_alignment 31 | - contains_over_filter_is_empty 32 | - contains_over_filter_count 33 | - contains_over_first_not_nil 34 | - contains_over_range_nil_comparison 35 | - convenience_type 36 | - discouraged_object_literal 37 | - discouraged_optional_boolean 38 | - empty_count 39 | - empty_string 40 | - fallthrough 41 | - file_name_no_space 42 | - first_where 43 | - flatmap_over_map_reduce 44 | - implicitly_unwrapped_optional 45 | - joined_default_parameter 46 | - last_where 47 | - literal_expression_end_indentation 48 | - multiline_function_chains 49 | - operator_usage_whitespace 50 | - private_action 51 | - private_outlet 52 | - redundant_optional_initialization 53 | - redundant_set_access_control 54 | - sorted_first_last 55 | - switch_case_on_newline 56 | - unneeded_parentheses_in_closure_argument 57 | - unused_declaration 58 | - unused_import 59 | - discouraged_optional_collection 60 | - enum_case_associated_values_count 61 | - legacy_multiple 62 | - legacy_random 63 | indentation: 2 64 | type_name: 65 | min_length: 2 66 | max_length: 67 | warning: 50 68 | error: 60 69 | file_length: 70 | - 2500 71 | - 3000 72 | large_tuple: 73 | - 5 74 | - 6 -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Assets/all_skeletonables.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/all_skeletonables.jpg -------------------------------------------------------------------------------- /Assets/all_skeletonables_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/all_skeletonables_result.png -------------------------------------------------------------------------------- /Assets/container_no_skeletonable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/container_no_skeletonable.jpg -------------------------------------------------------------------------------- /Assets/container_skeletonable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/container_skeletonable.jpg -------------------------------------------------------------------------------- /Assets/container_skeletonable_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/container_skeletonable_result.png -------------------------------------------------------------------------------- /Assets/debug_description.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/debug_description.png -------------------------------------------------------------------------------- /Assets/debug_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/debug_mode.png -------------------------------------------------------------------------------- /Assets/demoApp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/demoApp2.png -------------------------------------------------------------------------------- /Assets/flatcolors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/flatcolors.png -------------------------------------------------------------------------------- /Assets/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/gradient.png -------------------------------------------------------------------------------- /Assets/gradient_animated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/gradient_animated.gif -------------------------------------------------------------------------------- /Assets/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/header.jpg -------------------------------------------------------------------------------- /Assets/header2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/header2.jpg -------------------------------------------------------------------------------- /Assets/hierarchy_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/hierarchy_output.png -------------------------------------------------------------------------------- /Assets/multiline_corner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/multiline_corner.png -------------------------------------------------------------------------------- /Assets/multiline_customize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/multiline_customize.png -------------------------------------------------------------------------------- /Assets/multiline_insets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/multiline_insets.png -------------------------------------------------------------------------------- /Assets/multiline_lastline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/multiline_lastline.png -------------------------------------------------------------------------------- /Assets/multiline_lineHeight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/multiline_lineHeight.png -------------------------------------------------------------------------------- /Assets/multiline_lineSpacing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/multiline_lineSpacing.png -------------------------------------------------------------------------------- /Assets/multilines2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/multilines2.png -------------------------------------------------------------------------------- /Assets/no_skeletonable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/no_skeletonable.jpg -------------------------------------------------------------------------------- /Assets/no_skeletonables_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/no_skeletonables_result.png -------------------------------------------------------------------------------- /Assets/skeleton_transition_fade.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/skeleton_transition_fade.gif -------------------------------------------------------------------------------- /Assets/skeleton_transition_nofade.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/skeleton_transition_nofade.gif -------------------------------------------------------------------------------- /Assets/sliding_bottomRight_to_topLeft.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/sliding_bottomRight_to_topLeft.gif -------------------------------------------------------------------------------- /Assets/sliding_bottom_to_top.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/sliding_bottom_to_top.gif -------------------------------------------------------------------------------- /Assets/sliding_left_to_right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/sliding_left_to_right.gif -------------------------------------------------------------------------------- /Assets/sliding_right_to_left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/sliding_right_to_left.gif -------------------------------------------------------------------------------- /Assets/sliding_topLeft_to_bottomRight.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/sliding_topLeft_to_bottomRight.gif -------------------------------------------------------------------------------- /Assets/sliding_top_to_bottom.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/sliding_top_to_bottom.gif -------------------------------------------------------------------------------- /Assets/solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/solid.png -------------------------------------------------------------------------------- /Assets/solid_animated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/solid_animated.gif -------------------------------------------------------------------------------- /Assets/solid_animated2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/solid_animated2.gif -------------------------------------------------------------------------------- /Assets/storyboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/storyboard.png -------------------------------------------------------------------------------- /Assets/tableview_no_skeletonable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/tableview_no_skeletonable.jpg -------------------------------------------------------------------------------- /Assets/tableview_no_skeletonable_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/tableview_no_skeletonable_result.png -------------------------------------------------------------------------------- /Assets/tableview_scheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/tableview_scheme.png -------------------------------------------------------------------------------- /Assets/tableview_skeletonable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/tableview_skeletonable.jpg -------------------------------------------------------------------------------- /Assets/tableview_skeletonable_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/tableview_skeletonable_result.png -------------------------------------------------------------------------------- /Assets/thumb_getting_started.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Assets/thumb_getting_started.png -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The Code of Conduct governs how we behave in public or in private 4 | whenever the project will be judged by our actions. 5 | We expect it to be honored by everyone who represents the project 6 | officially or informally, 7 | claims affiliation with the project, 8 | or participates directly. 9 | 10 | We strive to: 11 | 12 | * **Be open**: We invite anybody to participate in any aspect of our projects. 13 | Our community is open, and any responsibility can be carried 14 | by any contributor who demonstrates the required capacity and competence. 15 | * **Be empathetic**: We work together to resolve conflict, 16 | assume good intentions, 17 | and do our best to act in an empathic fashion. 18 | By understanding that humanity drops a few packets in online interactions, 19 | and adjusting accordingly, 20 | we can create a comfortable environment for everyone to share their ideas. 21 | * **Be collaborative**: We prefer to work transparently 22 | and to involve interested parties early on in the process. 23 | Wherever possible, we work closely with others in the open source community 24 | to coordinate our efforts. 25 | * **Be decisive**: We expect participants in the project to resolve disagreements constructively. 26 | When they cannot, we escalate the matter to structures 27 | with designated leaders to arbitrate and provide clarity and direction. 28 | * **Be responsible**: We hold ourselves accountable for our actions. 29 | When we make mistakes, we take responsibility for them. 30 | When we need help, we reach out to others. 31 | When it comes time to move on from a project, 32 | we take the proper steps to ensure that others can pick up where we left off. 33 | 34 | This code is not exhaustive or complete. 35 | It serves to distill our common understanding of a 36 | collaborative, shared environment and goals. 37 | We expect it to be followed in spirit as much as in the letter. 38 | 39 | --- 40 | 41 | The **SkeletonView** Code of Conduct is adapted from the [Contributor Covenant][homepage], 42 | version 2.0, available at 43 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 44 | 45 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 46 | enforcement ladder](https://github.com/mozilla/diversity). 47 | 48 | [homepage]: https://www.contributor-covenant.org 49 | 50 | For answers to common questions about this code of conduct, see the FAQ at 51 | https://www.contributor-covenant.org/faq. Translations are available at 52 | https://www.contributor-covenant.org/translations. 53 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributors Guide 2 | 3 | Interested in contributing? Awesome! Before you do though, please read our 4 | [Code of Conduct](https://github.com/Juanpe/SkeletonView/blob/main/CODE_OF_CONDUCT.md). We take it very seriously, and expect that you will as 5 | well. 6 | 7 | There are many ways you can contribute! :heart: 8 | 9 | ### Bug Reports and Fixes :bug: 10 | - If you find a bug, please search for it in the [Issues](https://github.com/Juanpe/SkeletonView/issues), and if it isn't already tracked, 11 | [create a new issue](https://github.com/slackhq/PanModal/issues/new). Fill out the "Bug Report" section of the issue template. Even if an Issue is closed, feel free to comment and add details, it will still 12 | be reviewed. 13 | - Issues that have already been identified as a bug (note: able to reproduce) will be labelled `🐞 Bug`. 14 | - If you'd like to submit a fix for a bug, [send a Pull Request](#creating_a_pull_request) and mention the Issue number. 15 | 16 | ### New Features :bulb: 17 | - If you'd like to add new functionality to this project, describe the problem you want to solve in a [new Issue](https://github.com/Juanpe/SkeletonView/issues/new). 18 | - Issues that have been identified as a feature request will be labelled `💡 Enhancement`. 19 | - If you'd like to implement the new feature, please wait for feedback from the project 20 | maintainers before spending too much time writing the code. In some cases, `💡 Enhancement`s may 21 | not align well with the project objectives at the time. 22 | 23 | ### Miscellaneous :sparkles: 24 | - If you have an alternative implementation of something that may have advantages over the way its currently 25 | done, or you have any other change, we would be happy to hear about it! 26 | - If its a trivial change, go ahead and [send a Pull Request](#creating_a_pull_request) with the changes you have in mind. 27 | - If not, [open an Issue](https://github.com/Juanpe/SkeletonView/issues/new) to discuss the idea first. 28 | 29 | If you're new to our project and looking for some way to make your first contribution, look for 30 | Issues labelled `good first issue`. 31 | 32 | ## Requirements 33 | 34 | For your contribution to be accepted: 35 | 36 | - [x] The changes must be approved by code review. 37 | - [x] Commits should be atomic and messages must be descriptive. Related issues should be mentioned by Issue number. 38 | 39 | If the contribution doesn't meet the above criteria, you may fail our automated checks or a maintainer will discuss it with you. You can continue to improve a Pull Request by adding commits to the branch from which the PR was created. 40 | 41 | ## Creating a Pull Request 42 | 43 | 1. :fork_and_knife: Fork the repository on GitHub. 44 | 2. :runner: Clone/fetch your fork to your local development machine. 45 | 3. :herb: Create a new branch and check it out. 46 | 4. :crystal_ball: Make your changes and commit them locally. 47 | 5. :arrow_heading_up: Push your new branch to your fork. (e.g. `git push username fix-issue-300`). 48 | 6. :inbox_tray: Open a Pull Request on github.com from your new branch on your fork to `main` in this 49 | repository. 50 | 51 | ## Developer's Certificate of Origin 1.1 52 | 53 | By making a contribution to this project, I certify that: 54 | 55 | - (a) The contribution was created in whole or in part by me and I 56 | have the right to submit it under the open source license 57 | indicated in the file; or 58 | 59 | - (b) The contribution is based upon previous work that, to the best 60 | of my knowledge, is covered under an appropriate open source 61 | license and I have the right under that license to submit that 62 | work with modifications, whether created in whole or in part 63 | by me, under the same open source license (unless I am 64 | permitted to submit under a different license), as indicated 65 | in the file; or 66 | 67 | - (c) The contribution was provided directly to me by some other 68 | person who certified (a), (b) or (c) and I have not modified 69 | it. 70 | 71 | - (d) I understand and agree that this project and the contribution 72 | are public and that a record of the contribution (including all 73 | personal information I submit with it, including my sign-off) is 74 | maintained indefinitely and may be redistributed consistent with 75 | this project or the open source license(s) involved. 76 | 77 | *Wording of statement copied from [elinux.org](http://elinux.org/Developer_Certificate_Of_Origin)* 78 | -------------------------------------------------------------------------------- /Dangerfile.swift: -------------------------------------------------------------------------------- 1 | import Danger 2 | 3 | let danger = Danger() 4 | let github = danger.github 5 | 6 | // Make it more obvious that a PR is a work in progress and shouldn't be merged yet 7 | if danger.github.pullRequest.title.contains("WIP") { 8 | warn("PR is classed as Work in Progress") 9 | } 10 | 11 | // Warn, asking to update all README files if only English README are updated 12 | let enReameModified = danger.git.modifiedFiles.contains { $0.contains("README.md") } 13 | let zhReameModified = danger.git.modifiedFiles.contains { $0.contains("README_zh.md") } 14 | let koReameModified = danger.git.modifiedFiles.contains { $0.contains("README_ko.md") } 15 | let ptBrReameModified = danger.git.modifiedFiles.contains { $0.contains("README_pt-br.md") } 16 | let otherLanguagesReadmeHaveBeenModified = zhReameModified && koReameModified && ptBrReameModified 17 | 18 | if (enReameModified && !otherLanguagesReadmeHaveBeenModified) { 19 | warn("Consider **also** updating the README for other languages.") 20 | } 21 | 22 | // Warn when there is a big PR 23 | if (danger.github.pullRequest.additions ?? 0) > 500 { 24 | warn("Big PR, try to keep changes smaller if you can") 25 | } 26 | 27 | // Added (or removed) library files need to be added (or removed) from the 28 | // Xcode project to avoid breaking things. 29 | let addedSwiftLibraryFiles = danger.git.createdFiles.contains { $0.fileType == .swift && $0.hasPrefix("Sources") } 30 | let deletedSwiftLibraryFiles = danger.git.deletedFiles.contains { $0.fileType == .swift && $0.hasPrefix("Sources") } 31 | let modifiedXcodeProject = danger.git.modifiedFiles.contains { $0.contains(".xcodeproj") } 32 | if (addedSwiftLibraryFiles || deletedSwiftLibraryFiles) && !modifiedXcodeProject { 33 | fail("Added or removed files require the Xcode project to be updated.") 34 | } 35 | -------------------------------------------------------------------------------- /Examples/CollectionView/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | 5 | @UIApplicationMain 6 | class AppDelegate: UIResponder, UIApplicationDelegate { 7 | 8 | var window: UIWindow? 9 | 10 | 11 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 12 | // Override point for customization after application launch. 13 | return true 14 | } 15 | 16 | func applicationWillResignActive(_ application: UIApplication) { 17 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 18 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 19 | } 20 | 21 | func applicationDidEnterBackground(_ application: UIApplication) { 22 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 23 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 24 | } 25 | 26 | func applicationWillEnterForeground(_ application: UIApplication) { 27 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 28 | } 29 | 30 | func applicationDidBecomeActive(_ application: UIApplication) { 31 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 32 | } 33 | 34 | func applicationWillTerminate(_ application: UIApplication) { 35 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 36 | } 37 | 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Examples/CollectionView/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Examples/CollectionView/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Examples/CollectionView/Assets.xcassets/avatar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "avatar.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/CollectionView/Assets.xcassets/avatar.imageset/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Examples/CollectionView/Assets.xcassets/avatar.imageset/avatar.png -------------------------------------------------------------------------------- /Examples/CollectionView/Assets.xcassets/picture.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "picture.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/CollectionView/Assets.xcassets/picture.imageset/picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Examples/CollectionView/Assets.xcassets/picture.imageset/picture.png -------------------------------------------------------------------------------- /Examples/CollectionView/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Examples/CollectionView/CollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | import SkeletonView 5 | 6 | class CollectionViewCell: UICollectionViewCell { 7 | 8 | var label: UILabel! 9 | var imageView: UIImageView! 10 | 11 | override init(frame: CGRect) { 12 | super.init(frame: frame) 13 | 14 | isSkeletonable = true 15 | createLabel() 16 | createImageView() 17 | 18 | } 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | 24 | private func createImageView() { 25 | imageView = UIImageView(image: UIImage(named: "picture")) 26 | imageView.isSkeletonable = true 27 | imageView.translatesAutoresizingMaskIntoConstraints = false 28 | imageView.contentMode = .scaleAspectFit 29 | addSubview(imageView) 30 | NSLayoutConstraint.activate([ 31 | imageView.centerXAnchor.constraint(equalTo: centerXAnchor), 32 | imageView.topAnchor.constraint(equalTo: topAnchor), 33 | imageView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.75), 34 | imageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.75) 35 | ]) 36 | 37 | 38 | } 39 | 40 | private func createLabel() { 41 | label = UILabel() 42 | label.isSkeletonable = true 43 | label.text = "Lorem ipsum" 44 | label.textAlignment = .center 45 | label.translatesAutoresizingMaskIntoConstraints = false 46 | addSubview(label) 47 | NSLayoutConstraint.activate([ 48 | label.centerXAnchor.constraint(equalTo: centerXAnchor), 49 | label.bottomAnchor.constraint(equalTo: bottomAnchor), 50 | label.heightAnchor.constraint(equalToConstant: 40), 51 | label.widthAnchor.constraint(equalToConstant: frame.width) 52 | ]) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Examples/CollectionView/SkeletonViewExampleCollectionview-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Examples/iOS Example/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SkeletonViewExample 4 | // 5 | // Created by Juanpe Catalán on 02/11/2017. 6 | // Copyright © 2017 SkeletonView. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Examples/iOS Example/Sources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "universal", 6 | "reference" : "systemBlueColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "color-space" : "srgb", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "1.000", 22 | "green" : "1.000", 23 | "red" : "1.000" 24 | } 25 | }, 26 | "idiom" : "universal" 27 | } 28 | ], 29 | "info" : { 30 | "author" : "xcode", 31 | "version" : 1 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Examples/iOS Example/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Examples/iOS Example/Sources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/iOS Example/Sources/Assets.xcassets/avatar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "avatar.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/iOS Example/Sources/Assets.xcassets/avatar.imageset/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juanpe/SkeletonView/30c92f0992888e7b249e788405ac31e2103f5c69/Examples/iOS Example/Sources/Assets.xcassets/avatar.imageset/avatar.png -------------------------------------------------------------------------------- /Examples/iOS Example/Sources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Examples/iOS Example/Sources/Cell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cell.swift 3 | // SkeletonViewExample 4 | // 5 | // Created by Juanpe Catalán on 03/11/2017. 6 | // Copyright © 2017 SkeletonView. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class Cell: UITableViewCell { 12 | 13 | @IBOutlet weak var avatar: UIImageView! 14 | @IBOutlet weak var label1: UILabel! 15 | @IBOutlet weak var textField: UITextField! 16 | 17 | override func awakeFromNib() { 18 | super.awakeFromNib() 19 | setUpInputAccessoryView() 20 | } 21 | 22 | func setUpInputAccessoryView() { 23 | let bar = UIToolbar() 24 | let reset = UIBarButtonItem(title: "InputAccessoryView", style: .plain, target: self, action: #selector(resetTapped)) 25 | bar.items = [reset] 26 | bar.sizeToFit() 27 | textField.inputAccessoryView = bar 28 | } 29 | 30 | @objc func resetTapped() { 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Examples/iOS Example/Sources/Constants.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | 5 | let colors = [(UIColor.skeletonDefault,"skeletonDefault"),(UIColor.turquoise,"turquoise"), (UIColor.emerald,"emerald"), (UIColor.peterRiver,"peterRiver"), (UIColor.amethyst,"amethyst"),(UIColor.wetAsphalt,"wetAsphalt"), (UIColor.nephritis,"nephritis"), (UIColor.belizeHole,"belizeHole"), (UIColor.wisteria,"wisteria"), (UIColor.midnightBlue,"midnightBlue"), (UIColor.sunFlower,"sunFlower"), (UIColor.carrot,"carrot"), (UIColor.alizarin,"alizarin"),(UIColor.clouds,"clouds"), (UIColor.concrete,"concrete"), (UIColor.flatOrange,"flatOrange"), (UIColor.pumpkin,"pumpkin"), (UIColor.pomegranate,"pomegranate"), (UIColor.silver,"silver"), (UIColor.asbestos,"asbestos")] 6 | -------------------------------------------------------------------------------- /Examples/iOS Example/Sources/HeaderFooterSection.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | 5 | class HeaderFooterSection: UITableViewHeaderFooterView { 6 | 7 | lazy var titleLabel: UILabel = { 8 | let label = UILabel() 9 | 10 | label.text = " " 11 | label.isSkeletonable = true 12 | label.linesCornerRadius = 10 13 | 14 | return label 15 | }() 16 | 17 | override init(reuseIdentifier: String?) { 18 | super.init(reuseIdentifier: reuseIdentifier) 19 | 20 | isSkeletonable = true 21 | 22 | contentView.addSubview(titleLabel) 23 | 24 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 25 | 26 | NSLayoutConstraint.activate([ 27 | titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), 28 | titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), 29 | titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), 30 | titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10) 31 | ]) 32 | 33 | backgroundView = UIView() 34 | if #available(iOS 13.0, *) { 35 | backgroundView?.backgroundColor = .systemBackground 36 | } else { 37 | backgroundView?.backgroundColor = .white 38 | } 39 | } 40 | 41 | required init?(coder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Examples/iOS Example/Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSupportsIndirectInputEvents 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Examples/iOS Example/Sources/UITextViewByCodeViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | import SkeletonView 5 | 6 | class UITextViewByCodeViewController: UIViewController { 7 | lazy var textView: UITextView = { 8 | let tv = UITextView() 9 | 10 | tv.text = " " 11 | tv.linesCornerRadius = 10 12 | tv.isSkeletonable = true 13 | tv.translatesAutoresizingMaskIntoConstraints = false 14 | 15 | return tv 16 | }() 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | setupUI() 22 | setupElementsConstraints() 23 | showSkeletonForElements() 24 | } 25 | 26 | override func viewWillAppear(_ animated: Bool) { 27 | super.viewWillAppear(animated) 28 | } 29 | 30 | func setupUI() { 31 | view.addSubview(textView) 32 | } 33 | 34 | func setupElementsConstraints() { 35 | textView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 10).isActive = true 36 | textView.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor, constant: 10).isActive = true 37 | textView.rightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true 38 | textView.heightAnchor.constraint(equalToConstant: 100).isActive = true 39 | } 40 | 41 | func showSkeletonForElements() { 42 | textView.showSkeleton() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // tvOS Example 4 | // 5 | // Created by Juanpe Catalán on 18/8/21. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | } 29 | 30 | func applicationWillEnterForeground(_ application: UIApplication) { 31 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 32 | } 33 | 34 | func applicationDidBecomeActive(_ application: UIApplication) { 35 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 36 | } 37 | 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | arm64 30 | 31 | UIUserInterfaceStyle 32 | Automatic 33 | 34 | 35 | -------------------------------------------------------------------------------- /Examples/tvOS Example/Sources/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // tvOS Example 4 | // 5 | // Created by Juanpe Catalán on 18/8/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | // Do any additional setup after loading the view. 15 | } 16 | 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Examples/tvOS Example/tvOS Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/tvOS Example/tvOS Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | gem 'cocoapods', '~> 1.7.0.beta.2' 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Juanpe Catalán 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SkeletonView", 7 | platforms: [ 8 | .iOS(.v9), 9 | .tvOS(.v9) 10 | ], 11 | products: [ 12 | .library( 13 | name: "SkeletonView", 14 | targets: ["SkeletonView"] 15 | ) 16 | ], 17 | targets: [ 18 | .target( 19 | name: "SkeletonView", 20 | path: "SkeletonViewCore/Sources", 21 | resources: [.copy("Supporting Files/PrivacyInfo.xcprivacy")] 22 | ), 23 | .testTarget( 24 | name: "SkeletonViewTests", 25 | dependencies: ["SkeletonView"], 26 | path: "SkeletonViewCore/Tests" 27 | ) 28 | ], 29 | swiftLanguageVersions: [.v5] 30 | ) 31 | -------------------------------------------------------------------------------- /SkeletonVIew.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /SkeletonVIew.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SkeletonView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "SkeletonView" 3 | s.version = "1.31.0" 4 | s.summary = "An elegant way to show users that something is happening and also prepare them to which contents he is waiting" 5 | s.description = <<-DESC 6 | Today almost all apps have async processes, as API requests, long runing processes, etc. And while the processes are working, usually developers place a loading view to show users that something is going on. 7 | SkeletonView has been conceived to address this need, an elegant way to show users that something is happening and also prepare them to which contents he is waiting. 8 | DESC 9 | s.homepage = "https://github.com/Juanpe/SkeletonView" 10 | s.license = { :type => "MIT", :file => "LICENSE" } 11 | s.author = { "Juanpe Catalán" => "juanpecm@gmail.com" } 12 | s.social_media_url = "https://x.com/JuanpeCatalan" 13 | s.ios.deployment_target = "9.0" 14 | s.tvos.deployment_target = "9.0" 15 | s.swift_version = "5.0" 16 | s.source = { :git => "https://github.com/Juanpe/SkeletonView.git", :tag => s.version.to_s } 17 | s.source_files = "SkeletonViewCore/Sources/**/*.{swift,h}" 18 | end 19 | -------------------------------------------------------------------------------- /SkeletonView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /SkeletonView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SkeletonView.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /SkeletonView.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // Copyright SkeletonView. All Rights Reserved. 8 | // 9 | // Licensed under the MIT License (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // https://opensource.org/licenses/MIT 14 | // 15 | // ___FILENAME___ 16 | // 17 | // Created by ___FULLUSERNAME___ on ___DATE___. 18 | 19 | 20 | -------------------------------------------------------------------------------- /SkeletonView.xcodeproj/xcshareddata/xcschemes/SkeletonView iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 63 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /SkeletonView.xcodeproj/xcshareddata/xcschemes/SkeletonView tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/AnimationBuilder/SkeletonAnimationBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonAnimationBuilder.swift 3 | // SkeletonView-iOS 4 | // 5 | // Created by Juanpe Catalán on 17/11/2017. 6 | // Copyright © 2017 SkeletonView. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public typealias SkeletonLayerAnimation = (CALayer) -> CAAnimation 12 | 13 | public class SkeletonAnimationBuilder { 14 | 15 | public init() { } 16 | 17 | public func makeSlidingAnimation(withDirection direction: GradientDirection, duration: CFTimeInterval = 1.5, autoreverses: Bool = false) -> SkeletonLayerAnimation { 18 | { _ in 19 | let startPointAnim = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.startPoint)) 20 | startPointAnim.fromValue = direction.startPoint.from 21 | startPointAnim.toValue = direction.startPoint.to 22 | 23 | let endPointAnim = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.endPoint)) 24 | endPointAnim.fromValue = direction.endPoint.from 25 | endPointAnim.toValue = direction.endPoint.to 26 | 27 | let animGroup = CAAnimationGroup() 28 | animGroup.animations = [startPointAnim, endPointAnim] 29 | animGroup.duration = duration 30 | animGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) 31 | animGroup.repeatCount = .infinity 32 | animGroup.autoreverses = autoreverses 33 | animGroup.isRemovedOnCompletion = false 34 | 35 | return animGroup 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/Appearance/SkeletonAppearance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // SkeletonAppearance.swift 11 | // 12 | 13 | import UIKit 14 | 15 | public enum SkeletonAppearance { 16 | public static var `default` = SkeletonViewAppearance.shared 17 | } 18 | 19 | // codebeat:disable[TOO_MANY_IVARS] 20 | public class SkeletonViewAppearance { 21 | 22 | static var shared = SkeletonViewAppearance() 23 | 24 | public var tintColor: UIColor = .skeletonDefault 25 | 26 | public var gradient = SkeletonGradient(baseColor: .skeletonDefault) 27 | 28 | public var multilineHeight: CGFloat = 15 29 | 30 | public lazy var textLineHeight: SkeletonTextLineHeight = .fixed(SkeletonAppearance.default.multilineHeight) 31 | 32 | public var multilineSpacing: CGFloat = 10 33 | 34 | public var multilineLastLineFillPercent: Int = 70 35 | 36 | public var multilineCornerRadius: Int = 0 37 | 38 | public var renderSingleLineAsView: Bool = false 39 | 40 | public var skeletonCornerRadius: Float = 0 41 | 42 | } 43 | // codebeat:enable[TOO_MANY_IVARS] 44 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/Collections/CollectionViews/SkeletonCollectionViewProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonCollectionViewProtocols.swift 3 | // SkeletonView-iOS 4 | // 5 | // Created by Juanpe Catalán on 06/11/2017. 6 | // Copyright © 2017 SkeletonView. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol SkeletonCollectionViewDataSource: UICollectionViewDataSource { 12 | func numSections(in collectionSkeletonView: UICollectionView) -> Int 13 | func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int 14 | func collectionSkeletonView(_ skeletonView: UICollectionView, cellIdentifierForItemAt indexPath: IndexPath) -> ReusableCellIdentifier 15 | func collectionSkeletonView(_ skeletonView: UICollectionView, supplementaryViewIdentifierOfKind: String, at indexPath: IndexPath) -> ReusableCellIdentifier? 16 | func collectionSkeletonView(_ skeletonView: UICollectionView, skeletonCellForItemAt indexPath: IndexPath) -> UICollectionViewCell? 17 | func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath) 18 | func collectionSkeletonView(_ skeletonView: UICollectionView, prepareViewForSkeleton view: UICollectionReusableView, at indexPath: IndexPath) 19 | } 20 | 21 | public extension SkeletonCollectionViewDataSource { 22 | func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 23 | UICollectionView.automaticNumberOfSkeletonItems 24 | } 25 | 26 | func collectionSkeletonView(_ skeletonView: UICollectionView, supplementaryViewIdentifierOfKind: String, at indexPath: IndexPath) -> ReusableCellIdentifier? { 27 | nil 28 | } 29 | 30 | func numSections(in collectionSkeletonView: UICollectionView) -> Int { 31 | 1 32 | } 33 | 34 | func collectionSkeletonView(_ skeletonView: UICollectionView, skeletonCellForItemAt indexPath: IndexPath) -> UICollectionViewCell? { 35 | nil 36 | } 37 | 38 | func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath) { } 39 | 40 | func collectionSkeletonView(_ skeletonView: UICollectionView, prepareViewForSkeleton view: UICollectionReusableView, at indexPath: IndexPath) { } 41 | } 42 | 43 | public protocol SkeletonCollectionViewDelegate: UICollectionViewDelegate { } 44 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/Collections/TableViews/SkeletonTableViewProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonTableViewProtocols.swift 3 | // SkeletonView-iOS 4 | // 5 | // Created by Juanpe Catalán on 06/11/2017. 6 | // Copyright © 2017 SkeletonView. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UITableView { 12 | public static let automaticNumberOfSkeletonRows = -1 13 | } 14 | 15 | public typealias ReusableHeaderFooterIdentifier = String 16 | 17 | public protocol SkeletonTableViewDataSource: UITableViewDataSource { 18 | func numSections(in collectionSkeletonView: UITableView) -> Int 19 | func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int 20 | func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier 21 | func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell? 22 | func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath) 23 | } 24 | 25 | public extension SkeletonTableViewDataSource { 26 | func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int { 27 | return UITableView.automaticNumberOfSkeletonRows 28 | } 29 | 30 | func numSections(in collectionSkeletonView: UITableView) -> Int { return 1 } 31 | 32 | /// Keeping the misspelled version around until it can be deprecated 33 | /// Right now, it just calls the new correctly spelled method and returns its result 34 | @available(*, deprecated, renamed: "collectionSkeletonView(_:cellIdentifierForRowAt:)") 35 | func collectionSkeletonView(_ skeletonView: UITableView, cellIdenfierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier { 36 | return collectionSkeletonView(skeletonView, cellIdentifierForRowAt: indexPath) 37 | } 38 | 39 | func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell? { 40 | nil 41 | } 42 | 43 | func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath) { } 44 | } 45 | 46 | public protocol SkeletonTableViewDelegate: UITableViewDelegate { 47 | func collectionSkeletonView(_ skeletonView: UITableView, identifierForHeaderInSection section: Int) -> ReusableHeaderFooterIdentifier? 48 | func collectionSkeletonView(_ skeletonView: UITableView, identifierForFooterInSection section: Int) -> ReusableHeaderFooterIdentifier? 49 | } 50 | 51 | public extension SkeletonTableViewDelegate { 52 | func collectionSkeletonView(_ skeletonView: UITableView, identifierForHeaderInSection section: Int) -> ReusableHeaderFooterIdentifier? { 53 | return nil 54 | } 55 | 56 | func collectionSkeletonView(_ skeletonView: UITableView, identifierForFooterInSection section: Int) -> ReusableHeaderFooterIdentifier? { 57 | return nil 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/Deprecated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // Deprecated.swift 11 | // 12 | // Created by Juanpe Catalán on 18/8/21. 13 | 14 | import UIKit 15 | 16 | public extension Notification.Name { 17 | 18 | @available(*, deprecated, renamed: "skeletonWillAppear") 19 | static let willBeginShowingSkeletons = Notification.Name.skeletonWillAppearNotification 20 | 21 | @available(*, deprecated, renamed: "skeletonDidAppear") 22 | static let didShowSkeletons = Notification.Name.skeletonDidAppearNotification 23 | 24 | @available(*, deprecated, renamed: "skeletonWillUpdate") 25 | static let willBeginUpdatingSkeletons = Notification.Name.skeletonWillUpdateNotification 26 | 27 | @available(*, deprecated, renamed: "skeletonDidUpdate") 28 | static let didUpdateSkeletons = Notification.Name.skeletonDidUpdateNotification 29 | 30 | @available(*, deprecated, renamed: "skeletonWillDisappear") 31 | static let willBeginHidingSkeletons = Notification.Name.skeletonWillDisappearNotification 32 | 33 | @available(*, deprecated, renamed: "skeletonDidDisappear") 34 | static let didHideSkeletons = Notification.Name.skeletonDidDisappearNotification 35 | 36 | } 37 | 38 | public extension UIView { 39 | 40 | @available(*, deprecated, renamed: "sk.treeNodesDescription") 41 | var skeletonDescription: String { 42 | sk.skeletonTreeDescription 43 | } 44 | 45 | @available(*, deprecated, renamed: "sk.isSkeletonActive") 46 | var isSkeletonActive: Bool { 47 | sk.isSkeletonActive 48 | } 49 | 50 | } 51 | 52 | public extension UILabel { 53 | 54 | @IBInspectable 55 | @available(*, deprecated, renamed: "skeletonTextLineHeight") 56 | var useFontLineHeight: Bool { 57 | get { 58 | textLineHeight == .relativeToFont 59 | } 60 | set { 61 | textLineHeight = newValue ? .relativeToFont : .fixed(SkeletonAppearance.default.multilineHeight) 62 | } 63 | } 64 | 65 | } 66 | 67 | public extension UITextView { 68 | 69 | @IBInspectable 70 | @available(*, deprecated, renamed: "skeletonTextLineHeight") 71 | var useFontLineHeight: Bool { 72 | get { 73 | textLineHeight == .relativeToFont 74 | } 75 | set { 76 | textLineHeight = newValue ? .relativeToFont : .fixed(SkeletonAppearance.default.multilineHeight) 77 | } 78 | } 79 | 80 | } 81 | 82 | public extension SkeletonViewAppearance { 83 | 84 | @available(*, deprecated, renamed: "textLineHeight") 85 | var useFontLineHeight: Bool { 86 | get { 87 | textLineHeight == .relativeToFont 88 | } 89 | set { 90 | textLineHeight = newValue ? .relativeToFont : .fixed(SkeletonAppearance.default.multilineHeight) 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/FoundationExtensions/Notification+SkeletonFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // Notification+SkeletonFlow.swift 11 | // 12 | // Created by Juanpe Catalán on 18/8/21. 13 | 14 | import Foundation 15 | 16 | public extension Notification.Name { 17 | 18 | static let skeletonWillAppearNotification = Notification.Name("skeletonWillAppear") 19 | static let skeletonDidAppearNotification = Notification.Name("skeletonDidAppear") 20 | static let skeletonWillUpdateNotification = Notification.Name("skeletonWillUpdate") 21 | static let skeletonDidUpdateNotification = Notification.Name("skeletonDidUpdate") 22 | static let skeletonWillDisappearNotification = Notification.Name("skeletonWillDisappear") 23 | static let skeletonDidDisappearNotification = Notification.Name("skeletonDidDisappear") 24 | 25 | } 26 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/Models/GradientDirection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // GradientDirection.swift 11 | // 12 | // Created by Juanpe Catalán on 19/8/21. 13 | 14 | import UIKit 15 | 16 | public enum GradientDirection { 17 | 18 | case leftRight 19 | case rightLeft 20 | case topBottom 21 | case bottomTop 22 | case topLeftBottomRight 23 | case bottomRightTopLeft 24 | 25 | public func slidingAnimation(duration: CFTimeInterval = 1.5, autoreverses: Bool = false) -> SkeletonLayerAnimation { 26 | return SkeletonAnimationBuilder().makeSlidingAnimation(withDirection: self, duration: duration, autoreverses: autoreverses) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/Models/SkeletonGradient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // SkeletonGradient.swift 11 | // 12 | // Created by Juanpe Catalán on 05/11/2017. 13 | 14 | import UIKit 15 | 16 | public struct SkeletonGradient { 17 | 18 | private let gradientColors: [UIColor] 19 | 20 | public var colors: [UIColor] { 21 | return gradientColors 22 | } 23 | 24 | public init(baseColor: UIColor, secondaryColor: UIColor? = nil) { 25 | if let secondary = secondaryColor { 26 | self.gradientColors = [baseColor, secondary, baseColor] 27 | } else { 28 | self.gradientColors = baseColor.makeGradient() 29 | } 30 | } 31 | 32 | public init(colors: [UIColor]) { 33 | self.gradientColors = colors 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/Models/SkeletonTextLineHeight.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // SkeletonTextLineHeight.swift 11 | // 12 | // Created by Juanpe Catalán on 22/11/21. 13 | 14 | import UIKit 15 | 16 | public enum SkeletonTextLineHeight: Equatable { 17 | 18 | /// Calculates the line height based on the font line height. 19 | case relativeToFont 20 | 21 | /// Calculates the line height based on the height constraints. 22 | /// 23 | /// If no constraints exist, the height will be set to the `multilineHeight` 24 | /// value defined in the `SkeletonAppearance`. 25 | case relativeToConstraints 26 | 27 | /// Returns the specific height specified as the associated value. 28 | case fixed(CGFloat) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/Models/SkeletonTextNumberOfLines.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // SkeletonTextNumberOfLines.swift 11 | // 12 | // Created by Juanpe Catalán on 10/1/22. 13 | 14 | import UIKit 15 | 16 | public enum SkeletonTextNumberOfLines: Equatable, ExpressibleByIntegerLiteral { 17 | 18 | /// Returns `numberOfLines` value. 19 | case inherited 20 | 21 | /// Returns the specific number of lines specified as the associated value. 22 | case custom(Int) 23 | 24 | } 25 | 26 | public extension SkeletonTextNumberOfLines { 27 | 28 | init(integerLiteral value: Int) { 29 | self = .custom(value) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/Models/SkeletonTransitionStyle.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | 5 | public enum SkeletonTransitionStyle: Equatable { 6 | case none 7 | case crossDissolve(TimeInterval) 8 | } 9 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/Models/SkeletonType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // SkeletonType.swift 11 | // 12 | // Created by Juanpe Catalán on 19/8/21. 13 | 14 | import UIKit 15 | 16 | public enum SkeletonType { 17 | 18 | case solid 19 | case gradient 20 | 21 | var layer: CALayer { 22 | switch self { 23 | case .solid: 24 | return CALayer() 25 | case .gradient: 26 | return CAGradientLayer() 27 | } 28 | } 29 | 30 | func defaultLayerAnimation(isRTL: Bool) -> SkeletonLayerAnimation { 31 | switch self { 32 | case .solid: 33 | return { $0.pulse } 34 | case .gradient: 35 | return { SkeletonAnimationBuilder().makeSlidingAnimation(withDirection: isRTL ? .rightLeft : .leftRight) }() 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/SkeletonExtended.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // SkeletonExtended.swift 11 | // 12 | // Created by Juanpe Catalán on 23/8/21. 13 | 14 | import Foundation 15 | 16 | /// Type that acts as a generic extension point for all `SkeletonViewExtended` types. 17 | public struct SkeletonViewExtension { 18 | /// Stores the type or meta-type of any extended type. 19 | public private(set) var type: ExtendedType 20 | 21 | /// Create an instance from the provided value. 22 | /// 23 | /// - Parameter type: Instance being extended. 24 | public init(_ type: ExtendedType) { 25 | self.type = type 26 | } 27 | } 28 | 29 | /// Protocol describing the `sk` extension points for SkeletonView extended types. 30 | public protocol SkeletonViewExtended { 31 | /// Type being extended. 32 | associatedtype ExtendedType 33 | 34 | /// Instance SkeletonView extension point. 35 | var sk: SkeletonViewExtension { get set } 36 | } 37 | 38 | extension SkeletonViewExtended { 39 | /// Instance SkeletonView extension point. 40 | public var sk: SkeletonViewExtension { 41 | get { SkeletonViewExtension(self) } 42 | // swiftlint:disable:next unused_setter_value 43 | set {} 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/UIKitExtensions/CALayer+Animations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // CALayer+Animations.swift 11 | // 12 | // Created by Juanpe Catalán on 18/8/21. 13 | 14 | import UIKit 15 | 16 | public extension CALayer { 17 | 18 | var pulse: CAAnimation { 19 | let pulseAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.backgroundColor)) 20 | pulseAnimation.fromValue = backgroundColor 21 | 22 | // swiftlint:disable:next force_unwrapping 23 | pulseAnimation.toValue = UIColor(cgColor: backgroundColor!).complementaryColor.cgColor 24 | pulseAnimation.duration = 1 25 | pulseAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 26 | pulseAnimation.autoreverses = true 27 | pulseAnimation.repeatCount = .infinity 28 | pulseAnimation.isRemovedOnCompletion = false 29 | return pulseAnimation 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/UIKitExtensions/UICollectionView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // UICollectionView+Extensions.swift 11 | // 12 | // Created by Juanpe Catalán on 19/8/21. 13 | 14 | import UIKit 15 | 16 | public extension UICollectionView { 17 | 18 | static let automaticNumberOfSkeletonItems = -1 19 | 20 | func prepareSkeleton(completion: @escaping (Bool) -> Void) { 21 | guard let originalDataSource = self.dataSource as? SkeletonCollectionViewDataSource, 22 | !(originalDataSource is SkeletonCollectionDataSource) 23 | else { return } 24 | 25 | let dataSource = SkeletonCollectionDataSource(collectionViewDataSource: originalDataSource, rowHeight: 0.0) 26 | self.skeletonDataSource = dataSource 27 | performBatchUpdates({ 28 | self.reloadData() 29 | }) { done in 30 | completion(done) 31 | 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/UIKitExtensions/UILabel+IBInspectable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // UILabel+IBInspectable.swift 11 | // 12 | // Created by Juanpe Catalán on 19/8/21. 13 | 14 | import UIKit 15 | 16 | public extension UILabel { 17 | 18 | @IBInspectable 19 | var lastLineFillPercent: Int { 20 | get { return lastLineFillingPercent } 21 | set { lastLineFillingPercent = min(newValue, 100) } 22 | } 23 | 24 | @IBInspectable 25 | var linesCornerRadius: Int { 26 | get { return multilineCornerRadius } 27 | set { multilineCornerRadius = newValue } 28 | } 29 | 30 | @IBInspectable 31 | var skeletonLineSpacing: CGFloat { 32 | get { return multilineSpacing } 33 | set { multilineSpacing = newValue } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/UIKitExtensions/UILabel+SKExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // UILabel+SKExtensions.swift 11 | // 12 | // Created by Juanpe Catalán on 23/8/21. 13 | 14 | import UIKit 15 | 16 | public extension UILabel { 17 | 18 | /// Defines the skeleton paddings. 19 | var skeletonPaddingInsets: UIEdgeInsets { 20 | get { 21 | paddingInsets 22 | } 23 | set { 24 | paddingInsets = newValue 25 | } 26 | } 27 | 28 | /// Defines the logic for calculating the height of the skeleton lines. 29 | /// Default: `SkeletonAppearance.default.textLineHeight` 30 | var skeletonTextLineHeight: SkeletonTextLineHeight { 31 | get { 32 | textLineHeight 33 | } 34 | set { 35 | textLineHeight = newValue 36 | } 37 | } 38 | 39 | /// Defines the logic for calculating the number of lines of the skeleton. 40 | /// Default: `inherited` 41 | var skeletonTextNumberOfLines: SkeletonTextNumberOfLines { 42 | get { 43 | skeletonNumberOfLines 44 | } 45 | set { 46 | skeletonNumberOfLines = newValue 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/UIKitExtensions/UITextView+IBInspectable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // UITextView+IBInspectable.swift 11 | // 12 | // Created by Juanpe Catalán on 19/8/21. 13 | 14 | import UIKit 15 | 16 | public extension UITextView { 17 | 18 | @IBInspectable 19 | var lastLineFillPercent: Int { 20 | get { return lastLineFillingPercent } 21 | set { lastLineFillingPercent = min(newValue, 100) } 22 | } 23 | 24 | @IBInspectable 25 | var linesCornerRadius: Int { 26 | get { return multilineCornerRadius } 27 | set { multilineCornerRadius = newValue } 28 | } 29 | 30 | @IBInspectable 31 | var skeletonLineSpacing: CGFloat { 32 | get { return multilineSpacing } 33 | set { multilineSpacing = newValue } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/UIKitExtensions/UITextView+SKExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // UITextView+SKExtensions.swift 11 | // 12 | // Created by Juanpe Catalán on 19/8/21. 13 | 14 | import UIKit 15 | 16 | public extension UITextView { 17 | 18 | /// Defines the skeleton paddings. 19 | var skeletonPaddingInsets: UIEdgeInsets { 20 | get { 21 | paddingInsets 22 | } 23 | set { 24 | paddingInsets = newValue 25 | } 26 | } 27 | 28 | /// Defines the logic for calculating the height of the skeleton lines. 29 | /// Default: `SkeletonAppearance.default.textLineHeight` 30 | var skeletonTextLineHeight: SkeletonTextLineHeight { 31 | get { 32 | textLineHeight 33 | } 34 | set { 35 | textLineHeight = newValue 36 | } 37 | } 38 | 39 | /// Defines the logic for calculating the number of lines of the skeleton. 40 | /// Default: `inherited` 41 | var skeletonTextNumberOfLines: SkeletonTextNumberOfLines { 42 | get { 43 | skeletonNumberOfLines 44 | } 45 | set { 46 | skeletonNumberOfLines = newValue 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/UIKitExtensions/UIView+IBInspectable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // UIView+IBInspectable.swift 11 | // 12 | // Created by Juanpe Catalán on 18/8/21. 13 | 14 | import UIKit 15 | 16 | public extension UIView { 17 | 18 | @IBInspectable 19 | var isSkeletonable: Bool { 20 | get { _skeletonable } 21 | set { _skeletonable = newValue } 22 | } 23 | 24 | @IBInspectable 25 | var isHiddenWhenSkeletonIsActive: Bool { 26 | get { _hiddenWhenSkeletonIsActive } 27 | set { _hiddenWhenSkeletonIsActive = newValue } 28 | } 29 | 30 | @IBInspectable 31 | var isUserInteractionDisabledWhenSkeletonIsActive: Bool { 32 | get { _disabledWhenSkeletonIsActive } 33 | set { _disabledWhenSkeletonIsActive = newValue } 34 | } 35 | 36 | @IBInspectable 37 | var skeletonCornerRadius: Float { 38 | get { _skeletonableCornerRadius } 39 | set { _skeletonableCornerRadius = newValue } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/API/UIKitExtensions/UIView+SKExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // UIView+SKExtensions.swift 11 | // 12 | // Created by Juanpe Catalán on 18/8/21. 13 | 14 | import UIKit 15 | 16 | public extension SkeletonViewExtension where ExtendedType: UIView { 17 | 18 | /// Returns a string that describes the hierarchy of the skeleton, indicating 19 | /// whether the receiver is skeletonable and all skeletonable children. 20 | var skeletonTreeDescription: String { 21 | guard let theJSONData = try? JSONSerialization.data(withJSONObject: treeNode.dictionaryRepresentation, options: [.prettyPrinted]) else { 22 | skeletonLog("Skeleton tree generation has failed!") 23 | return "" 24 | } 25 | 26 | return String(data: theJSONData, encoding: .utf8)! 27 | } 28 | 29 | var isSkeletonActive: Bool { 30 | type._status == .on || type.subviewsSkeletonables.contains(where: { $0.sk.isSkeletonActive }) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/Collections/CollectionSkeleton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionSkeleton.swift 3 | // SkeletonView-iOS 4 | // 5 | // Created by Juanpe Catalán on 02/11/2017. 6 | // Copyright © 2017 SkeletonView. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum CollectionAssociatedKeys { 12 | static var dummyDataSource = "dummyDataSource" 13 | static var dummyDelegate = "dummyDelegate" 14 | } 15 | 16 | protocol CollectionSkeleton { 17 | 18 | var skeletonDataSource: SkeletonCollectionDataSource? { get set } 19 | var skeletonDelegate: SkeletonCollectionDelegate? { get set } 20 | var estimatedNumberOfRows: Int { get } 21 | 22 | func addDummyDataSource() 23 | func updateDummyDataSource() 24 | func removeDummyDataSource(reloadAfter: Bool) 25 | func disableUserInteraction() 26 | func enableUserInteraction() 27 | 28 | } 29 | 30 | extension CollectionSkeleton where Self: UIScrollView { 31 | 32 | var estimatedNumberOfRows: Int { return 0 } 33 | func addDummyDataSource() {} 34 | func removeDummyDataSource(reloadAfter: Bool) {} 35 | 36 | func disableUserInteraction() { 37 | if isUserInteractionDisabledWhenSkeletonIsActive { 38 | isUserInteractionEnabled = false 39 | isScrollEnabled = false 40 | } 41 | } 42 | 43 | func enableUserInteraction() { 44 | if isUserInteractionDisabledWhenSkeletonIsActive { 45 | isUserInteractionEnabled = true 46 | isScrollEnabled = true 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/Collections/SkeletonCollectionDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonCollectionDataSource.swift 3 | // SkeletonView-iOS 4 | // 5 | // Created by Juanpe Catalán on 02/11/2017. 6 | // Copyright © 2017 SkeletonView. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public typealias ReusableCellIdentifier = String 12 | 13 | class SkeletonCollectionDataSource: NSObject { 14 | weak var originalTableViewDataSource: SkeletonTableViewDataSource? 15 | weak var originalCollectionViewDataSource: SkeletonCollectionViewDataSource? 16 | var rowHeight: CGFloat = 0.0 17 | var originalRowHeight: CGFloat = 0.0 18 | 19 | convenience init(tableViewDataSource: SkeletonTableViewDataSource? = nil, collectionViewDataSource: SkeletonCollectionViewDataSource? = nil, rowHeight: CGFloat = 0.0, originalRowHeight: CGFloat = 0.0) { 20 | self.init() 21 | self.originalTableViewDataSource = tableViewDataSource 22 | self.originalCollectionViewDataSource = collectionViewDataSource 23 | self.rowHeight = rowHeight 24 | self.originalRowHeight = originalRowHeight 25 | } 26 | } 27 | 28 | // MARK: - UITableViewDataSource 29 | extension SkeletonCollectionDataSource: UITableViewDataSource { 30 | func numberOfSections(in tableView: UITableView) -> Int { 31 | originalTableViewDataSource?.numSections(in: tableView) ?? 0 32 | } 33 | 34 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 35 | guard let originalTableViewDataSource = originalTableViewDataSource else { 36 | return 0 37 | } 38 | 39 | let numberOfRows = originalTableViewDataSource.collectionSkeletonView(tableView, numberOfRowsInSection: section) 40 | 41 | if numberOfRows == UITableView.automaticNumberOfSkeletonRows { 42 | return tableView.estimatedNumberOfRows 43 | } else { 44 | return numberOfRows 45 | } 46 | } 47 | 48 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 49 | guard let cell = originalTableViewDataSource?.collectionSkeletonView(tableView, skeletonCellForRowAt: indexPath) else { 50 | let cellIdentifier = originalTableViewDataSource?.collectionSkeletonView(tableView, cellIdentifierForRowAt: indexPath) ?? "" 51 | let fakeCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) 52 | 53 | originalTableViewDataSource?.collectionSkeletonView(tableView, prepareCellForSkeleton: fakeCell, at: indexPath) 54 | skeletonizeViewIfContainerSkeletonIsActive(container: tableView, view: fakeCell) 55 | 56 | return fakeCell 57 | } 58 | 59 | originalTableViewDataSource?.collectionSkeletonView(tableView, prepareCellForSkeleton: cell, at: indexPath) 60 | skeletonizeViewIfContainerSkeletonIsActive(container: tableView, view: cell) 61 | return cell 62 | } 63 | } 64 | 65 | // MARK: - UICollectionViewDataSource 66 | extension SkeletonCollectionDataSource: UICollectionViewDataSource { 67 | func numberOfSections(in collectionView: UICollectionView) -> Int { 68 | originalCollectionViewDataSource?.numSections(in: collectionView) ?? 0 69 | } 70 | 71 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 72 | guard let originalCollectionViewDataSource = originalCollectionViewDataSource else { 73 | return 0 74 | } 75 | 76 | let numberOfItems = originalCollectionViewDataSource.collectionSkeletonView(collectionView, numberOfItemsInSection: section) 77 | 78 | if numberOfItems == UICollectionView.automaticNumberOfSkeletonItems { 79 | return collectionView.estimatedNumberOfRows 80 | } else { 81 | return numberOfItems 82 | } 83 | } 84 | 85 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 86 | guard let cell = originalCollectionViewDataSource?.collectionSkeletonView(collectionView, skeletonCellForItemAt: indexPath) else { 87 | let cellIdentifier = originalCollectionViewDataSource?.collectionSkeletonView(collectionView, cellIdentifierForItemAt: indexPath) ?? "" 88 | let fakeCell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) 89 | 90 | originalCollectionViewDataSource?.collectionSkeletonView(collectionView, prepareCellForSkeleton: fakeCell, at: indexPath) 91 | skeletonizeViewIfContainerSkeletonIsActive(container: collectionView, view: fakeCell) 92 | 93 | return fakeCell 94 | } 95 | 96 | originalCollectionViewDataSource?.collectionSkeletonView(collectionView, prepareCellForSkeleton: cell, at: indexPath) 97 | skeletonizeViewIfContainerSkeletonIsActive(container: collectionView, view: cell) 98 | return cell 99 | } 100 | 101 | func collectionView(_ collectionView: UICollectionView, 102 | viewForSupplementaryElementOfKind kind: String, 103 | at indexPath: IndexPath) -> UICollectionReusableView { 104 | if let viewIdentifier = originalCollectionViewDataSource?.collectionSkeletonView(collectionView, supplementaryViewIdentifierOfKind: kind, at: indexPath) { 105 | let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: viewIdentifier, for: indexPath) 106 | 107 | originalCollectionViewDataSource?.collectionSkeletonView(collectionView, prepareViewForSkeleton: view, at: indexPath) 108 | skeletonizeViewIfContainerSkeletonIsActive(container: collectionView, view: view) 109 | return view 110 | } 111 | 112 | return originalCollectionViewDataSource?.collectionView?(collectionView, viewForSupplementaryElementOfKind: kind, at: indexPath) ?? UICollectionReusableView() 113 | } 114 | 115 | } 116 | 117 | extension SkeletonCollectionDataSource { 118 | private func skeletonizeViewIfContainerSkeletonIsActive(container: UIView, view: UIView) { 119 | guard container.sk.isSkeletonActive, 120 | let skeletonConfig = container._currentSkeletonConfig else { 121 | return 122 | } 123 | 124 | view.showSkeleton( 125 | skeletonConfig: skeletonConfig, 126 | notifyDelegate: false 127 | ) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/Collections/SkeletonCollectionDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonCollectionDelegate.swift 3 | // SkeletonView-iOS 4 | // 5 | // Created by Juanpe Catalán on 30/03/2018. 6 | // Copyright © 2018 SkeletonView. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SkeletonCollectionDelegate: NSObject { 12 | 13 | weak var originalTableViewDelegate: SkeletonTableViewDelegate? 14 | weak var originalCollectionViewDelegate: SkeletonCollectionViewDelegate? 15 | 16 | init( 17 | tableViewDelegate: SkeletonTableViewDelegate? = nil, 18 | collectionViewDelegate: SkeletonCollectionViewDelegate? = nil 19 | ) { 20 | self.originalTableViewDelegate = tableViewDelegate 21 | self.originalCollectionViewDelegate = collectionViewDelegate 22 | } 23 | 24 | } 25 | 26 | // MARK: - UITableViewDelegate 27 | extension SkeletonCollectionDelegate: UITableViewDelegate { 28 | 29 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 30 | headerOrFooterView(tableView, for: originalTableViewDelegate?.collectionSkeletonView(tableView, identifierForHeaderInSection: section)) 31 | } 32 | 33 | func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { 34 | headerOrFooterView(tableView, for: originalTableViewDelegate?.collectionSkeletonView(tableView, identifierForFooterInSection: section)) 35 | } 36 | 37 | func tableView(_ tableView: UITableView, didEndDisplayingHeaderView view: UIView, forSection section: Int) { 38 | view.hideSkeleton() 39 | originalTableViewDelegate?.tableView?(tableView, didEndDisplayingHeaderView: view, forSection: section) 40 | } 41 | 42 | func tableView(_ tableView: UITableView, didEndDisplayingFooterView view: UIView, forSection section: Int) { 43 | view.hideSkeleton() 44 | originalTableViewDelegate?.tableView?(tableView, didEndDisplayingFooterView: view, forSection: section) 45 | } 46 | 47 | func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { 48 | cell.hideSkeleton() 49 | originalTableViewDelegate?.tableView?(tableView, didEndDisplaying: cell, forRowAt: indexPath) 50 | } 51 | 52 | } 53 | 54 | // MARK: - UICollectionViewDelegate 55 | extension SkeletonCollectionDelegate: UICollectionViewDelegate { } 56 | 57 | private extension SkeletonCollectionDelegate { 58 | 59 | func skeletonizeViewIfContainerSkeletonIsActive(container: UIView, view: UIView) { 60 | guard container.sk.isSkeletonActive, 61 | let skeletonConfig = container._currentSkeletonConfig 62 | else { 63 | return 64 | } 65 | 66 | view.showSkeleton( 67 | skeletonConfig: skeletonConfig, 68 | notifyDelegate: false 69 | ) 70 | } 71 | 72 | func headerOrFooterView(_ tableView: UITableView, for viewIdentifier: String? ) -> UIView? { 73 | guard let viewIdentifier = viewIdentifier, 74 | let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: viewIdentifier) 75 | else { 76 | return nil 77 | } 78 | 79 | skeletonizeViewIfContainerSkeletonIsActive( 80 | container: tableView, 81 | view: header 82 | ) 83 | 84 | return header 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/Collections/SkeletonReusableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonReusableCell.swift 3 | // SkeletonView-iOS 4 | // 5 | // Created by Juanpe Catalán on 30/03/2018. 6 | // Copyright © 2018 SkeletonView. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol SkeletonReusableCell { } 12 | 13 | extension UITableViewCell: SkeletonReusableCell { } 14 | 15 | extension UICollectionViewCell: SkeletonReusableCell { } 16 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/Debug/SkeletonDebug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // SkeletonDebug.swift 11 | // 12 | // Created by Juanpe Catalán on 18/8/21. 13 | 14 | import Foundation 15 | import UIKit 16 | 17 | enum SkeletonEnvironmentKey: String { 18 | case debugMode = "SKELETON_DEBUG" 19 | } 20 | 21 | extension Dictionary { 22 | subscript (_ key: SkeletonEnvironmentKey) -> Value? { 23 | // swiftlint:disable:next force_cast 24 | return self[key.rawValue as! Key] 25 | } 26 | } 27 | 28 | func skeletonLog(_ message: String) { 29 | #if DEBUG 30 | if ProcessInfo.processInfo.environment[.debugMode] != nil { 31 | print(message) 32 | } 33 | #endif 34 | } 35 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/FoundationExtensions/DispatchQueue+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // DispatchQueue+Extensions.swift 11 | // 12 | // Created by Juanpe Catalán on 19/8/21. 13 | 14 | import Foundation 15 | 16 | extension DispatchQueue { 17 | 18 | private static var _onceTracker = [String]() 19 | 20 | class func once(token: String, block: () -> Void) { 21 | objc_sync_enter(self) 22 | defer { objc_sync_exit(self) } 23 | guard !_onceTracker.contains(token) else { return } 24 | 25 | _onceTracker.append(token) 26 | block() 27 | } 28 | 29 | class func removeOnce(token: String, block: () -> Void) { 30 | objc_sync_enter(self) 31 | defer { objc_sync_exit(self) } 32 | guard let index = _onceTracker.firstIndex(of: token) else { return } 33 | _onceTracker.remove(at: index) 34 | block() 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/FoundationExtensions/Int+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // Int+Extensions.swift 11 | // 12 | 13 | import Foundation 14 | 15 | extension Int { 16 | 17 | var whitespace: String { 18 | whitespaces 19 | } 20 | 21 | var whitespaces: String { 22 | String(repeating: " ", count: self) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/FoundationExtensions/Notification+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // Notification+Extensions.swift 11 | // 12 | // Created by Juanpe Catalán on 18/8/21. 13 | 14 | import UIKit 15 | 16 | extension Notification.Name { 17 | 18 | static let applicationDidBecomeActiveNotification = UIApplication.didBecomeActiveNotification 19 | static let applicationWillTerminateNotification = UIApplication.willTerminateNotification 20 | static let applicationDidEnterForegroundNotification = UIApplication.didEnterBackgroundNotification 21 | 22 | } 23 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/FoundationExtensions/ProcessInfo+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // ProcessInfo+Extensions.swift 11 | // 12 | // Created by Juanpe Catalán on 18/8/21. 13 | 14 | import Foundation 15 | 16 | extension ProcessInfo { 17 | 18 | enum Constants { 19 | static let testConfigurationFilePathKey = "XCTestConfigurationFilePath" 20 | } 21 | 22 | static var isRunningXCTest: Bool { 23 | return processInfo.environment[Constants.testConfigurationFilePathKey] != nil 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/Helpers/AssociationPolicy.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 SkeletonView. All rights reserved. 2 | 3 | import Foundation 4 | 5 | // Partially copy/pasted from https://github.com/jameslintaylor/AssociatedObjects/blob/master/AssociatedObjects/AssociatedObjects.swift 6 | enum AssociationPolicy: UInt { 7 | // raw values map to objc_AssociationPolicy's raw values 8 | case assign = 0 9 | case copy = 771 10 | case copyNonatomic = 3 11 | case retain = 769 12 | case retainNonatomic = 1 13 | 14 | var objc: objc_AssociationPolicy { 15 | // swiftlint:disable:next force_unwrapping 16 | return objc_AssociationPolicy(rawValue: rawValue)! 17 | } 18 | } 19 | 20 | protocol AssociatedObjects: AnyObject { } 21 | 22 | extension AssociatedObjects { 23 | /// wrapper around `objc_getAssociatedObject` 24 | func ao_get(pkey: UnsafeRawPointer) -> Any? { 25 | return objc_getAssociatedObject(self, pkey) 26 | } 27 | 28 | /// wrapper around `objc_setAssociatedObject` 29 | func ao_setOptional(_ value: Any?, pkey: UnsafeRawPointer, policy: AssociationPolicy = .retainNonatomic) { 30 | guard let value = value else { return } 31 | objc_setAssociatedObject(self, pkey, value, policy.objc) 32 | } 33 | 34 | /// wrapper around `objc_setAssociatedObject` 35 | func ao_set(_ value: Any, pkey: UnsafeRawPointer, policy: AssociationPolicy = .retainNonatomic) { 36 | objc_setAssociatedObject(self, pkey, value, policy.objc) 37 | } 38 | 39 | /// wrapper around 'objc_removeAssociatedObjects' 40 | func ao_removeAll() { 41 | objc_removeAssociatedObjects(self) 42 | } 43 | } 44 | 45 | extension NSObject: AssociatedObjects { } 46 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/Helpers/Recursive.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | 5 | typealias VoidBlock = () -> Void 6 | typealias RecursiveBlock = (T) -> Void 7 | 8 | protocol IterableElement {} 9 | extension UIView: IterableElement {} 10 | extension CALayer: IterableElement {} 11 | 12 | // MARK: Recursive 13 | protocol Recursive { 14 | associatedtype Element: IterableElement 15 | func recursiveSearch(leafBlock: VoidBlock, recursiveBlock: RecursiveBlock) 16 | } 17 | 18 | extension Array: Recursive where Element: IterableElement { 19 | func recursiveSearch(leafBlock: VoidBlock, recursiveBlock: RecursiveBlock) { 20 | guard !isEmpty else { 21 | leafBlock() 22 | return 23 | } 24 | forEach { recursiveBlock($0) } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/Helpers/Swizzling.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 SkeletonView. All rights reserved. 2 | 3 | import Foundation 4 | 5 | func swizzle(selector originalSelector: Selector, with swizzledSelector: Selector, inClass: AnyClass, usingClass: AnyClass) { 6 | guard let originalMethod = class_getInstanceMethod(inClass, originalSelector), 7 | let swizzledMethod = class_getInstanceMethod(usingClass, swizzledSelector) 8 | else { return } 9 | 10 | if class_addMethod(inClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)) { 11 | class_replaceMethod(inClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) 12 | } else { 13 | method_exchangeImplementations(originalMethod, swizzledMethod) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/Models/RecoverableViewState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecoverableViewState.swift 3 | // SkeletonView 4 | // 5 | // Created by Juanpe Catalán on 13/05/2018. 6 | // Copyright © 2018 SkeletonView. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct RecoverableViewState { 12 | 13 | var backgroundColor: UIColor? 14 | var cornerRadius: CGFloat 15 | var clipToBounds: Bool 16 | var isUserInteractionsEnabled: Bool 17 | 18 | init(view: UIView) { 19 | self.backgroundColor = view.backgroundColor 20 | self.clipToBounds = view.layer.masksToBounds 21 | self.cornerRadius = view.layer.cornerRadius 22 | self.isUserInteractionsEnabled = view.isUserInteractionEnabled 23 | } 24 | 25 | } 26 | 27 | struct RecoverableLabelState { 28 | var attributedText: NSAttributedString? // we mess with `textColor`, which impacts attributed string if defined 29 | var text: String? // we mess with `text` if the label is within a `UIStackView` 30 | var textColor: UIColor? 31 | 32 | init(view: UILabel) { 33 | if let attributedText = view.attributedText { 34 | self.attributedText = attributedText 35 | } else { 36 | self.text = view.text 37 | } 38 | self.textColor = view.textColor 39 | } 40 | } 41 | 42 | struct RecoverableTextViewState { 43 | var attributedText: NSAttributedString? // we mess with `textColor`, which impacts attributed string if defined 44 | var textColor: UIColor? 45 | 46 | init(view: UITextView) { 47 | self.attributedText = view.attributedText 48 | self.textColor = view.textColor 49 | } 50 | } 51 | 52 | struct RecoverableTextFieldState { 53 | var attributedText: NSAttributedString? // we mess with `textColor`, which impacts attributed string if defined 54 | var textColor: UIColor? 55 | var placeholder: String? 56 | 57 | init(view: UITextField) { 58 | self.attributedText = view.attributedText 59 | self.textColor = view.textColor 60 | self.placeholder = view.placeholder 61 | } 62 | } 63 | 64 | struct RecoverableImageViewState { 65 | var image: UIImage? 66 | 67 | init(view: UIImageView) { 68 | self.image = view.image 69 | } 70 | } 71 | 72 | struct RecoverableButtonViewState { 73 | var title: String? 74 | 75 | init(view: UIButton) { 76 | self.title = view.titleLabel?.text 77 | } 78 | } 79 | 80 | struct RecoverableTableViewHeaderFooterViewState { 81 | var backgroundViewColor: UIColor? 82 | 83 | init(view: UITableViewHeaderFooterView) { 84 | self.backgroundViewColor = view.backgroundView?.backgroundColor 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/Models/SkeletonLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonLayer.swift 3 | // SkeletonView-iOS 4 | // 5 | // Created by Juanpe Catalán on 02/11/2017. 6 | // Copyright © 2017 SkeletonView. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct SkeletonLayer { 12 | 13 | private var maskLayer: CALayer 14 | private weak var holder: UIView? 15 | 16 | var type: SkeletonType { 17 | return maskLayer is CAGradientLayer ? .gradient : .solid 18 | } 19 | 20 | var contentLayer: CALayer { 21 | return maskLayer 22 | } 23 | 24 | init(type: SkeletonType, colors: [UIColor], skeletonHolder holder: UIView) { 25 | self.holder = holder 26 | self.maskLayer = type.layer 27 | self.maskLayer.anchorPoint = .zero 28 | self.maskLayer.bounds = holder.definedMaxBounds 29 | self.maskLayer.cornerRadius = CGFloat(holder.skeletonCornerRadius) 30 | addTextLinesIfNeeded() 31 | self.maskLayer.tint(withColors: colors, traitCollection: holder.traitCollection) 32 | } 33 | 34 | func update(usingColors colors: [UIColor]) { 35 | layoutIfNeeded() 36 | maskLayer.tint(withColors: colors, traitCollection: holder?.traitCollection) 37 | } 38 | 39 | func layoutIfNeeded() { 40 | if let bounds = holder?.definedMaxBounds { 41 | maskLayer.bounds = bounds 42 | } 43 | updateLinesIfNeeded() 44 | } 45 | 46 | func removeLayer(transition: SkeletonTransitionStyle, completion: (() -> Void)? = nil) { 47 | switch transition { 48 | case .none: 49 | maskLayer.removeFromSuperlayer() 50 | completion?() 51 | case .crossDissolve(let duration): 52 | maskLayer.setOpacity(from: 1, to: 0, duration: duration) { 53 | self.maskLayer.removeFromSuperlayer() 54 | completion?() 55 | } 56 | } 57 | } 58 | 59 | /// If there is more than one line, or custom preferences have been set for a single line, draw custom layers 60 | func addTextLinesIfNeeded() { 61 | guard let textView = holderAsTextView else { return } 62 | let config = SkeletonMultilinesLayerConfig(lines: textView.estimatedNumberOfLines, 63 | lineHeight: textView.estimatedLineHeight, 64 | type: type, 65 | lastLineFillPercent: textView.lastLineFillingPercent, 66 | multilineCornerRadius: textView.multilineCornerRadius, 67 | multilineSpacing: textView.multilineSpacing, 68 | paddingInsets: textView.paddingInsets, 69 | alignment: textView.textAlignment, 70 | isRTL: holder?.isRTL ?? false, 71 | shouldCenterVertically: textView.shouldCenterTextVertically) 72 | 73 | maskLayer.addMultilinesLayers(for: config) 74 | } 75 | 76 | func updateLinesIfNeeded() { 77 | guard let textView = holderAsTextView else { return } 78 | let config = SkeletonMultilinesLayerConfig(lines: textView.estimatedNumberOfLines, 79 | lineHeight: textView.estimatedLineHeight, 80 | type: type, 81 | lastLineFillPercent: textView.lastLineFillingPercent, 82 | multilineCornerRadius: textView.multilineCornerRadius, 83 | multilineSpacing: textView.multilineSpacing, 84 | paddingInsets: textView.paddingInsets, 85 | alignment: textView.textAlignment, 86 | isRTL: holder?.isRTL ?? false, 87 | shouldCenterVertically: textView.shouldCenterTextVertically) 88 | 89 | maskLayer.updateMultilinesLayers(for: config) 90 | } 91 | 92 | var holderAsTextView: SkeletonTextNode? { 93 | guard let textView = holder as? SkeletonTextNode, 94 | (textView.estimatedNumberOfLines == -1 || textView.estimatedNumberOfLines == 0 || textView.estimatedNumberOfLines > 1 || textView.estimatedNumberOfLines == 1 && !SkeletonAppearance.default.renderSingleLineAsView) else { 95 | return nil 96 | } 97 | return textView 98 | } 99 | 100 | } 101 | 102 | extension SkeletonLayer { 103 | 104 | func start(_ anim: SkeletonLayerAnimation? = nil, completion: (() -> Void)? = nil) { 105 | let animation = anim ?? type.defaultLayerAnimation(isRTL: holder?.isRTL ?? false) 106 | contentLayer.playAnimation(animation, key: "skeletonAnimation", completion: completion) 107 | } 108 | 109 | func stopAnimation() { 110 | contentLayer.stopAnimation(forKey: "skeletonAnimation") 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/SkeletonConfigs/SkeletonConfig.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | 5 | /// Used to store all config needed to activate the skeleton layer. 6 | struct SkeletonConfig { 7 | /// Type of skeleton layer 8 | let type: SkeletonType 9 | 10 | /// Colors used in skeleton layer 11 | let colors: [UIColor] 12 | 13 | /// If type is gradient, which gradient direction 14 | let gradientDirection: GradientDirection? 15 | 16 | /// Specify if skeleton is animated or not 17 | let animated: Bool 18 | 19 | /// Used to execute a custom animation 20 | let animation: SkeletonLayerAnimation? 21 | 22 | /// Transition style 23 | var transition: SkeletonTransitionStyle 24 | 25 | init(type: SkeletonType, 26 | colors: [UIColor], 27 | gradientDirection: GradientDirection? = nil, 28 | animated: Bool = false, 29 | animation: SkeletonLayerAnimation? = nil, 30 | transition: SkeletonTransitionStyle = .crossDissolve(0.25)) { 31 | self.type = type 32 | self.colors = colors 33 | self.gradientDirection = gradientDirection 34 | self.animated = animated 35 | self.animation = animation 36 | self.transition = transition 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/SkeletonConfigs/SkeletonMultilinesLayerConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // SkeletonMultilinesLayerConfig.swift 11 | // 12 | // Created by Juanpe Catalán on 18/8/21. 13 | 14 | import UIKit 15 | 16 | struct SkeletonMultilinesLayerConfig { 17 | 18 | var lines: Int 19 | var lineHeight: CGFloat 20 | var type: SkeletonType 21 | var lastLineFillPercent: Int 22 | var multilineCornerRadius: Int 23 | var multilineSpacing: CGFloat 24 | var paddingInsets: UIEdgeInsets 25 | var alignment: NSTextAlignment 26 | var isRTL: Bool 27 | var shouldCenterVertically: Bool 28 | 29 | /// Returns padding insets taking into account if the RTL is activated 30 | var calculatedPaddingInsets: UIEdgeInsets { 31 | UIEdgeInsets(top: paddingInsets.top, 32 | left: isRTL ? paddingInsets.right : paddingInsets.left, 33 | bottom: paddingInsets.bottom, 34 | right: isRTL ? paddingInsets.left : paddingInsets.right) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/SkeletonExtensions/GradientDirection+Animations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // GradientDirection+Animations.swift 11 | // 12 | // Created by Juanpe Catalán on 19/8/21. 13 | 14 | import UIKit 15 | 16 | typealias GradientAnimationPoint = (from: CGPoint, to: CGPoint) 17 | 18 | extension GradientDirection { 19 | 20 | // codebeat:disable[ABC] 21 | var startPoint: GradientAnimationPoint { 22 | switch self { 23 | case .leftRight: 24 | return (from: CGPoint(x: -1, y: 0.5), to: CGPoint(x: 1, y: 0.5)) 25 | case .rightLeft: 26 | return (from: CGPoint(x: 1, y: 0.5), to: CGPoint(x: -1, y: 0.5)) 27 | case .topBottom: 28 | return (from: CGPoint(x: 0.5, y: -1), to: CGPoint(x: 0.5, y: 1)) 29 | case .bottomTop: 30 | return (from: CGPoint(x: 0.5, y: 1), to: CGPoint(x: 0.5, y: -1)) 31 | case .topLeftBottomRight: 32 | return (from: CGPoint(x: -1, y: -1), to: CGPoint(x: 1, y: 1)) 33 | case .bottomRightTopLeft: 34 | return (from: CGPoint(x: 1, y: 1), to: CGPoint(x: -1, y: -1)) 35 | } 36 | } 37 | 38 | var endPoint: GradientAnimationPoint { 39 | switch self { 40 | case .leftRight: 41 | return (from: CGPoint(x: 0, y: 0.5), to: CGPoint(x: 2, y: 0.5)) 42 | case .rightLeft: 43 | return ( from: CGPoint(x: 2, y: 0.5), to: CGPoint(x: 0, y: 0.5)) 44 | case .topBottom: 45 | return ( from: CGPoint(x: 0.5, y: 0), to: CGPoint(x: 0.5, y: 2)) 46 | case .bottomTop: 47 | return ( from: CGPoint(x: 0.5, y: 2), to: CGPoint(x: 0.5, y: 0)) 48 | case .topLeftBottomRight: 49 | return ( from: CGPoint(x: 0, y: 0), to: CGPoint(x: 2, y: 2)) 50 | case .bottomRightTopLeft: 51 | return ( from: CGPoint(x: 2, y: 2), to: CGPoint(x: 0, y: 0)) 52 | } 53 | } 54 | // codebeat:enable[ABC] 55 | 56 | } 57 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/SkeletonExtensions/PrepareViewForSkeleton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // PrepareViewForSkeleton.swift 11 | // 12 | // Created by Juanpe Catalán on 04/11/2017. 13 | 14 | import UIKit 15 | 16 | extension UIView { 17 | 18 | @objc func prepareViewForSkeleton() { 19 | if isUserInteractionDisabledWhenSkeletonIsActive { 20 | isUserInteractionEnabled = false 21 | } 22 | 23 | startTransition { [weak self] in 24 | self?.backgroundColor = .clear 25 | } 26 | } 27 | 28 | } 29 | 30 | extension UILabel { 31 | 32 | override func prepareViewForSkeleton() { 33 | backgroundColor = .clear 34 | 35 | if isUserInteractionDisabledWhenSkeletonIsActive { 36 | isUserInteractionEnabled = false 37 | } 38 | 39 | resignFirstResponder() 40 | startTransition { [weak self] in 41 | self?.updateHeightConstraintsIfNeeded() 42 | self?.textColor = .clear 43 | } 44 | } 45 | } 46 | 47 | extension UITextView { 48 | 49 | override func prepareViewForSkeleton() { 50 | backgroundColor = .clear 51 | 52 | if isUserInteractionDisabledWhenSkeletonIsActive { 53 | isUserInteractionEnabled = false 54 | } 55 | 56 | resignFirstResponder() 57 | startTransition { [weak self] in 58 | self?.textColor = .clear 59 | } 60 | } 61 | 62 | } 63 | 64 | extension UITextField { 65 | 66 | override func prepareViewForSkeleton() { 67 | backgroundColor = .clear 68 | resignFirstResponder() 69 | 70 | startTransition { [weak self] in 71 | self?.textColor = .clear 72 | self?.placeholder = nil 73 | } 74 | } 75 | 76 | } 77 | 78 | extension UIImageView { 79 | 80 | override func prepareViewForSkeleton() { 81 | backgroundColor = .clear 82 | 83 | if isUserInteractionDisabledWhenSkeletonIsActive { 84 | isUserInteractionEnabled = false 85 | } 86 | 87 | startTransition { [weak self] in 88 | self?.image = nil 89 | } 90 | } 91 | 92 | } 93 | 94 | extension UIButton { 95 | 96 | override func prepareViewForSkeleton() { 97 | backgroundColor = .clear 98 | 99 | if isUserInteractionDisabledWhenSkeletonIsActive { 100 | isUserInteractionEnabled = false 101 | } 102 | 103 | startTransition { [weak self] in 104 | self?.setTitle(nil, for: .normal) 105 | } 106 | } 107 | 108 | } 109 | 110 | extension UITableViewHeaderFooterView { 111 | 112 | override func prepareViewForSkeleton() { 113 | backgroundView?.backgroundColor = .clear 114 | 115 | if isUserInteractionDisabledWhenSkeletonIsActive { 116 | isUserInteractionEnabled = false 117 | } 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/SkeletonExtensions/SubviewsSkeletonables.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | 5 | extension UIView { 6 | 7 | @objc var subviewsSkeletonables: [UIView] { 8 | subviewsToSkeleton.filter { $0.isSkeletonable } 9 | } 10 | 11 | @objc var subviewsToSkeleton: [UIView] { 12 | subviews 13 | } 14 | 15 | } 16 | 17 | extension UITableView { 18 | 19 | override var subviewsToSkeleton: [UIView] { 20 | // on `UIViewController'S onViewDidLoad`, the window is still nil. 21 | // Some developer trying to call `view.showAnimatedSkeleton()` 22 | // when the request or data is loading which sometimes happens before the ViewDidAppear 23 | guard window != nil else { return [] } 24 | 25 | var result = [UIView]() 26 | 27 | for subview in subviews { 28 | if String(describing: type(of: subview)) == "UITableViewWrapperView" { 29 | result.append(contentsOf: subview.subviews) 30 | } else { 31 | result.append(subview) 32 | } 33 | } 34 | 35 | return result 36 | } 37 | 38 | } 39 | 40 | extension UITableViewCell { 41 | override var subviewsToSkeleton: [UIView] { 42 | contentView.subviews 43 | } 44 | } 45 | 46 | extension UITableViewHeaderFooterView { 47 | override var subviewsToSkeleton: [UIView] { 48 | contentView.subviews 49 | } 50 | } 51 | 52 | extension UICollectionView { 53 | override var subviewsToSkeleton: [UIView] { 54 | subviews 55 | } 56 | } 57 | 58 | extension UICollectionViewCell { 59 | override var subviewsToSkeleton: [UIView] { 60 | contentView.subviews 61 | } 62 | } 63 | 64 | extension UIStackView { 65 | override var subviewsToSkeleton: [UIView] { 66 | arrangedSubviews 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/SkeletonFlowHandler.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | 5 | protocol SkeletonFlowDelegate: AnyObject { 6 | func willBeginShowingSkeletons(rootView: UIView) 7 | func didShowSkeletons(rootView: UIView) 8 | func willBeginUpdatingSkeletons(rootView: UIView) 9 | func didUpdateSkeletons(rootView: UIView) 10 | func willBeginLayingSkeletonsIfNeeded(rootView: UIView) 11 | func didLayoutSkeletonsIfNeeded(rootView: UIView) 12 | func willBeginHidingSkeletons(rootView: UIView) 13 | func didHideSkeletons(rootView: UIView) 14 | } 15 | 16 | class SkeletonFlowHandler: SkeletonFlowDelegate { 17 | func willBeginShowingSkeletons(rootView: UIView) { 18 | NotificationCenter.default.post(name: .skeletonWillAppearNotification, object: rootView, userInfo: nil) 19 | rootView.startObservingAppLifecycleNotifications() 20 | } 21 | 22 | func didShowSkeletons(rootView: UIView) { 23 | skeletonLog(rootView.sk.skeletonTreeDescription) 24 | NotificationCenter.default.post(name: .skeletonDidAppearNotification, object: rootView, userInfo: nil) 25 | } 26 | 27 | func willBeginUpdatingSkeletons(rootView: UIView) { 28 | NotificationCenter.default.post(name: .skeletonWillUpdateNotification, object: rootView, userInfo: nil) 29 | } 30 | 31 | func didUpdateSkeletons(rootView: UIView) { 32 | NotificationCenter.default.post(name: .skeletonDidUpdateNotification, object: rootView, userInfo: nil) 33 | } 34 | 35 | func willBeginLayingSkeletonsIfNeeded(rootView: UIView) { 36 | } 37 | 38 | func didLayoutSkeletonsIfNeeded(rootView: UIView) { 39 | } 40 | 41 | func willBeginHidingSkeletons(rootView: UIView) { 42 | NotificationCenter.default.post(name: .skeletonWillDisappearNotification, object: rootView, userInfo: nil) 43 | rootView.stopObservingAppLifecycleNotications() 44 | } 45 | 46 | func didHideSkeletons(rootView: UIView) { 47 | rootView._flowDelegate = nil 48 | NotificationCenter.default.post(name: .skeletonDidDisappearNotification, object: rootView, userInfo: nil) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/SkeletonLayerBuilders/SkeletonLayerBuilder.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | 5 | /// Object that facilitates the creation of skeleton layers, 6 | /// based on the builder pattern 7 | class SkeletonLayerBuilder { 8 | 9 | var skeletonType: SkeletonType? 10 | var colors: [UIColor] = [] 11 | var holder: UIView? 12 | 13 | @discardableResult 14 | func setSkeletonType(_ type: SkeletonType) -> SkeletonLayerBuilder { 15 | self.skeletonType = type 16 | return self 17 | } 18 | 19 | @discardableResult 20 | func addColor(_ color: UIColor) -> SkeletonLayerBuilder { 21 | addColors([color]) 22 | } 23 | 24 | @discardableResult 25 | func addColors(_ colors: [UIColor]) -> SkeletonLayerBuilder { 26 | self.colors.append(contentsOf: colors) 27 | return self 28 | } 29 | 30 | @discardableResult 31 | func setHolder(_ holder: UIView) -> SkeletonLayerBuilder { 32 | self.holder = holder 33 | return self 34 | } 35 | 36 | @discardableResult 37 | func build() -> SkeletonLayer? { 38 | guard let type = skeletonType, 39 | let holder = holder 40 | else { return nil } 41 | 42 | return SkeletonLayer(type: type, 43 | colors: colors, 44 | skeletonHolder: holder) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/SkeletonLayerBuilders/SkeletonMultilineLayerBuilder.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | 5 | /// Object that facilitates the creation of skeleton layers for multiline 6 | /// elements, based on the builder pattern 7 | class SkeletonMultilineLayerBuilder { 8 | 9 | var skeletonType: SkeletonType? 10 | var index: Int? 11 | var height: CGFloat? 12 | var width: CGFloat? 13 | var cornerRadius: Int? 14 | var multilineSpacing: CGFloat = SkeletonAppearance.default.multilineSpacing 15 | var paddingInsets: UIEdgeInsets = .zero 16 | var alignment: NSTextAlignment = .natural 17 | var isRTL: Bool = false 18 | 19 | @discardableResult 20 | func setSkeletonType(_ type: SkeletonType) -> SkeletonMultilineLayerBuilder { 21 | self.skeletonType = type 22 | return self 23 | } 24 | 25 | @discardableResult 26 | func setIndex(_ index: Int) -> SkeletonMultilineLayerBuilder { 27 | self.index = index 28 | return self 29 | } 30 | 31 | @discardableResult 32 | func setHeight(_ height: CGFloat) -> SkeletonMultilineLayerBuilder { 33 | self.height = height 34 | return self 35 | } 36 | 37 | @discardableResult 38 | func setWidth(_ width: CGFloat) -> SkeletonMultilineLayerBuilder { 39 | self.width = width 40 | return self 41 | } 42 | 43 | @discardableResult 44 | func setCornerRadius(_ radius: Int) -> SkeletonMultilineLayerBuilder { 45 | self.cornerRadius = radius 46 | return self 47 | } 48 | 49 | @discardableResult 50 | func setMultilineSpacing(_ spacing: CGFloat) -> SkeletonMultilineLayerBuilder { 51 | self.multilineSpacing = spacing 52 | return self 53 | } 54 | 55 | @discardableResult 56 | func setPadding(_ insets: UIEdgeInsets) -> SkeletonMultilineLayerBuilder { 57 | self.paddingInsets = insets 58 | return self 59 | } 60 | 61 | @discardableResult 62 | func setAlignment(_ alignment: NSTextAlignment) -> SkeletonMultilineLayerBuilder { 63 | self.alignment = alignment 64 | return self 65 | } 66 | 67 | @discardableResult 68 | func setIsRTL(_ isRTL: Bool) -> SkeletonMultilineLayerBuilder { 69 | self.isRTL = isRTL 70 | return self 71 | } 72 | 73 | func build() -> CALayer? { 74 | guard let type = skeletonType, 75 | let index = index, 76 | let width = width, 77 | let height = height, 78 | let radius = cornerRadius 79 | else { return nil } 80 | 81 | let layer = type.layer 82 | layer.anchorPoint = .zero 83 | layer.name = CALayer.Constants.skeletonSubLayersName 84 | layer.updateLayerFrame(for: index, 85 | totalLines: layer.skeletonSublayers.count, 86 | size: CGSize(width: width, height: height), 87 | multilineSpacing: multilineSpacing, 88 | paddingInsets: paddingInsets, 89 | alignment: alignment, 90 | isRTL: isRTL) 91 | 92 | layer.cornerRadius = CGFloat(radius) 93 | layer.masksToBounds = true 94 | 95 | return layer 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/SkeletonTree/SkeletonTreeNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // SkeletonTreeNode.swift 11 | // 12 | // Created by Juanpe Catalán on 23/8/21. 13 | 14 | import UIKit 15 | 16 | public struct SkeletonTreeNode { 17 | /// Base object to extend. 18 | let base: Base 19 | 20 | /// Creates extensions with base object. 21 | /// 22 | /// - parameter base: Base object. 23 | init(_ base: Base) { 24 | self.base = base 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/UIKitExtensions/SkeletonTreeNode+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // SkeletonTreeNode+Extensions.swift 11 | // 12 | // Created by Juanpe Catalán on 23/8/21. 13 | 14 | import UIKit 15 | 16 | extension UIView: SkeletonViewExtended { } 17 | 18 | extension SkeletonTreeNode where Base: UIView { 19 | 20 | var children: [SkeletonTreeNode] { 21 | base.subviewsSkeletonables.map { $0.sk.treeNode } 22 | } 23 | 24 | var parent: SkeletonTreeNode? { 25 | base.superview?.sk.treeNode 26 | } 27 | 28 | } 29 | 30 | // MARK: Debug 31 | 32 | extension SkeletonTreeNode where Base: UIView { 33 | 34 | var dictionaryRepresentation: [String: Any] { 35 | let skeletonableChildren = children 36 | 37 | var nodeInfo: [String: Any] = [ 38 | "type": "\(type(of: base))", 39 | "reference": "\(Unmanaged.passUnretained(base).toOpaque())", 40 | "isSkeletonable": base.isSkeletonable 41 | ] 42 | 43 | if !skeletonableChildren.isEmpty { 44 | nodeInfo["children"] = skeletonableChildren.map { $0.dictionaryRepresentation } 45 | } 46 | 47 | return nodeInfo 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/UIKitExtensions/UICollectionView+CollectionSkeleton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+CollectionSkeleton.swift 3 | // SkeletonView-iOS 4 | // 5 | // Created by Juanpe Catalán on 02/02/2018. 6 | // Copyright © 2018 SkeletonView. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UICollectionView: CollectionSkeleton { 12 | 13 | var estimatedNumberOfRows: Int { 14 | guard let flowlayout = collectionViewLayout as? UICollectionViewFlowLayout else { return 0 } 15 | switch flowlayout.scrollDirection { 16 | case .vertical: 17 | return Int(ceil(frame.height / flowlayout.itemSize.height)) 18 | case .horizontal: 19 | return Int(ceil(frame.width / flowlayout.itemSize.width)) 20 | default: 21 | return 0 22 | } 23 | } 24 | 25 | var skeletonDataSource: SkeletonCollectionDataSource? { 26 | get { return ao_get(pkey: &CollectionAssociatedKeys.dummyDataSource) as? SkeletonCollectionDataSource } 27 | set { 28 | ao_setOptional(newValue, pkey: &CollectionAssociatedKeys.dummyDataSource) 29 | self.dataSource = newValue 30 | } 31 | } 32 | 33 | var skeletonDelegate: SkeletonCollectionDelegate? { 34 | get { return ao_get(pkey: &CollectionAssociatedKeys.dummyDelegate) as? SkeletonCollectionDelegate } 35 | set { 36 | ao_setOptional(newValue, pkey: &CollectionAssociatedKeys.dummyDelegate) 37 | self.delegate = newValue 38 | } 39 | } 40 | 41 | func addDummyDataSource() { 42 | guard let originalDataSource = self.dataSource as? SkeletonCollectionViewDataSource, 43 | !(originalDataSource is SkeletonCollectionDataSource) 44 | else { return } 45 | 46 | let dataSource = SkeletonCollectionDataSource(collectionViewDataSource: originalDataSource) 47 | self.skeletonDataSource = dataSource 48 | reloadData() 49 | } 50 | 51 | func updateDummyDataSource() { 52 | if (dataSource as? SkeletonCollectionDataSource) != nil { 53 | reloadData() 54 | } else { 55 | addDummyDataSource() 56 | } 57 | } 58 | 59 | func removeDummyDataSource(reloadAfter: Bool) { 60 | guard let dataSource = self.dataSource as? SkeletonCollectionDataSource else { return } 61 | self.skeletonDataSource = nil 62 | self.dataSource = dataSource.originalCollectionViewDataSource 63 | if reloadAfter { self.reloadData() } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/UIKitExtensions/UIColor+Skeleton.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | 5 | // codebeat:disable[TOO_MANY_IVARS] 6 | public extension UIColor { 7 | 8 | static var greenSea = UIColor(0x16a085) 9 | static var turquoise = UIColor(0x1abc9c) 10 | static var emerald = UIColor(0x2ecc71) 11 | static var peterRiver = UIColor(0x3498db) 12 | static var amethyst = UIColor(0x9b59b6) 13 | static var wetAsphalt = UIColor(0x34495e) 14 | static var nephritis = UIColor(0x27ae60) 15 | static var belizeHole = UIColor(0x2980b9) 16 | static var wisteria = UIColor(0x8e44ad) 17 | static var midnightBlue = UIColor(0x2c3e50) 18 | static var sunFlower = UIColor(0xf1c40f) 19 | static var carrot = UIColor(0xe67e22) 20 | static var alizarin = UIColor(0xe74c3c) 21 | static var clouds = UIColor(0xecf0f1) 22 | static var darkClouds = UIColor(0x1c2325) 23 | static var concrete = UIColor(0x95a5a6) 24 | static var flatOrange = UIColor(0xf39c12) 25 | static var pumpkin = UIColor(0xd35400) 26 | static var pomegranate = UIColor(0xc0392b) 27 | static var silver = UIColor(0xbdc3c7) 28 | static var asbestos = UIColor(0x7f8c8d) 29 | 30 | static var skeletonDefault: UIColor { 31 | if #available(iOS 13, tvOS 13, *) { 32 | return UIColor { traitCollection in 33 | switch traitCollection.userInterfaceStyle { 34 | case .dark: 35 | return .darkClouds 36 | default: 37 | return .clouds 38 | } 39 | } 40 | } else { 41 | return .clouds 42 | } 43 | } 44 | 45 | var complementaryColor: UIColor { 46 | if #available(iOS 13, tvOS 13, *) { 47 | return UIColor { _ in 48 | self.isLight ? self.darker : self.lighter 49 | } 50 | } else { 51 | return isLight ? darker : lighter 52 | } 53 | } 54 | 55 | var lighter: UIColor { 56 | adjust(by: 1.35) 57 | } 58 | 59 | var darker: UIColor { 60 | adjust(by: 0.94) 61 | } 62 | 63 | } 64 | 65 | extension UIColor { 66 | 67 | convenience init(_ hex: UInt) { 68 | self.init( 69 | red: CGFloat((hex & 0xFF0000) >> 16) / 255.0, 70 | green: CGFloat((hex & 0x00FF00) >> 8) / 255.0, 71 | blue: CGFloat(hex & 0x0000FF) / 255.0, 72 | alpha: CGFloat(1.0) 73 | ) 74 | } 75 | 76 | var isLight: Bool { 77 | guard let components = cgColor.components, 78 | components.count >= 3 else { return false } 79 | let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 80 | return !(brightness < 0.5) 81 | } 82 | 83 | func adjust(by percent: CGFloat) -> UIColor { 84 | var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 85 | getHue(&h, saturation: &s, brightness: &b, alpha: &a) 86 | return UIColor(hue: h, saturation: s, brightness: b * percent, alpha: a) 87 | } 88 | 89 | func makeGradient() -> [UIColor] { 90 | [self, self.complementaryColor, self] 91 | } 92 | 93 | } 94 | // codebeat:enable[TOO_MANY_IVARS] 95 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/UIKitExtensions/UILabel+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // UILabel+Extensions.swift 11 | // 12 | // Created by Juanpe Catalán on 19/8/21. 13 | 14 | import UIKit 15 | 16 | extension UILabel { 17 | 18 | var desiredHeightBasedOnNumberOfLines: CGFloat { 19 | let spaceNeededForEachLine = estimatedLineHeight * CGFloat(estimatedNumberOfLines) 20 | let spaceNeededForSpaces = skeletonLineSpacing * CGFloat(estimatedNumberOfLines - 1) 21 | let padding = paddingInsets.top + paddingInsets.bottom 22 | 23 | return spaceNeededForEachLine + spaceNeededForSpaces + padding 24 | } 25 | 26 | func updateHeightConstraintsIfNeeded() { 27 | guard estimatedNumberOfLines > 1 || estimatedNumberOfLines == 0 else { return } 28 | 29 | // Workaround to simulate content when the label is contained in a `UIStackView`. 30 | if isSuperviewAStackView, bounds.height == 0, (text?.isEmpty ?? true) { 31 | // This is a placeholder text to simulate content because it's contained in a stack view in order to prevent that the content size will be zero. 32 | text = " " 33 | } 34 | 35 | let desiredHeight = desiredHeightBasedOnNumberOfLines 36 | if desiredHeight > definedMaxHeight { 37 | backupHeightConstraints = heightConstraints 38 | NSLayoutConstraint.deactivate(heightConstraints) 39 | setHeight(equalToConstant: desiredHeight) 40 | } 41 | } 42 | 43 | func restoreBackupHeightConstraintsIfNeeded() { 44 | guard !backupHeightConstraints.isEmpty else { return } 45 | NSLayoutConstraint.activate(backupHeightConstraints) 46 | backupHeightConstraints.removeAll() 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/UIKitExtensions/UITableView+CollectionSkeleton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+CollectionSkeleton.swift 3 | // SkeletonView-iOS 4 | // 5 | // Created by Juanpe Catalán on 02/02/2018. 6 | // Copyright © 2018 SkeletonView. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UITableView: CollectionSkeleton { 12 | 13 | var estimatedNumberOfRows: Int { 14 | return Int(ceil(frame.height / rowHeight)) 15 | } 16 | 17 | var skeletonDataSource: SkeletonCollectionDataSource? { 18 | get { return ao_get(pkey: &CollectionAssociatedKeys.dummyDataSource) as? SkeletonCollectionDataSource } 19 | set { 20 | ao_setOptional(newValue, pkey: &CollectionAssociatedKeys.dummyDataSource) 21 | self.dataSource = newValue 22 | } 23 | } 24 | 25 | var skeletonDelegate: SkeletonCollectionDelegate? { 26 | get { return ao_get(pkey: &CollectionAssociatedKeys.dummyDelegate) as? SkeletonCollectionDelegate } 27 | set { 28 | ao_setOptional(newValue, pkey: &CollectionAssociatedKeys.dummyDelegate) 29 | self.delegate = newValue 30 | } 31 | } 32 | 33 | func addDummyDataSource() { 34 | guard let originalDataSource = self.dataSource as? SkeletonTableViewDataSource, 35 | !(originalDataSource is SkeletonCollectionDataSource) 36 | else { return } 37 | let calculatedRowHeight = calculateRowHeight() 38 | let dataSource = SkeletonCollectionDataSource(tableViewDataSource: originalDataSource, 39 | rowHeight: rowHeight, 40 | originalRowHeight: self.rowHeight) 41 | rowHeight = calculatedRowHeight 42 | self.skeletonDataSource = dataSource 43 | 44 | if let originalDelegate = self.delegate as? SkeletonTableViewDelegate, 45 | !(originalDelegate is SkeletonCollectionDelegate) { 46 | let delegate = SkeletonCollectionDelegate(tableViewDelegate: originalDelegate) 47 | self.skeletonDelegate = delegate 48 | } 49 | 50 | reloadData() 51 | } 52 | 53 | func updateDummyDataSource() { 54 | if (dataSource as? SkeletonCollectionDataSource) != nil { 55 | reloadData() 56 | } else { 57 | addDummyDataSource() 58 | } 59 | } 60 | 61 | func removeDummyDataSource(reloadAfter: Bool) { 62 | guard let dataSource = self.dataSource as? SkeletonCollectionDataSource else { return } 63 | restoreRowHeight() 64 | self.skeletonDataSource = nil 65 | self.dataSource = dataSource.originalTableViewDataSource 66 | 67 | if let delegate = self.delegate as? SkeletonCollectionDelegate { 68 | self.skeletonDelegate = nil 69 | self.delegate = delegate.originalTableViewDelegate 70 | } 71 | 72 | if reloadAfter { self.reloadData() } 73 | } 74 | 75 | } 76 | 77 | private extension UITableView { 78 | 79 | func restoreRowHeight() { 80 | guard let dataSource = self.dataSource as? SkeletonCollectionDataSource else { return } 81 | rowHeight = dataSource.originalRowHeight 82 | } 83 | 84 | func calculateRowHeight() -> CGFloat { 85 | guard rowHeight == UITableView.automaticDimension else { return rowHeight } 86 | return estimatedRowHeight 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/UIKitExtensions/UITableView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // UITableView+Extensions.swift 11 | // 12 | // Created by Juanpe Catalán on 18/8/21. 13 | 14 | import UIKit 15 | 16 | extension UITableView { 17 | 18 | var indexesOfVisibleSections: [Int] { 19 | (0.. { 19 | SkeletonTreeNode(self.type) 20 | } 21 | 22 | } 23 | 24 | extension UIView { 25 | 26 | /// Flags 27 | 28 | var isSuperviewAStackView: Bool { 29 | superview is UIStackView 30 | } 31 | 32 | var isRTL: Bool { 33 | if #available(iOS 10.0, *), #available(tvOS 10.0, *) { 34 | return effectiveUserInterfaceLayoutDirection == .rightToLeft 35 | } else { 36 | return false 37 | } 38 | } 39 | 40 | /// Math 41 | 42 | var definedMaxBounds: CGRect { 43 | if let parentStackView = (superview as? UIStackView) { 44 | var origin: CGPoint = .zero 45 | switch parentStackView.alignment { 46 | case .trailing: 47 | origin.x = definedMaxWidth 48 | default: 49 | break 50 | } 51 | return CGRect(origin: origin, size: definedMaxSize) 52 | } 53 | return CGRect(origin: .zero, size: definedMaxSize) 54 | } 55 | 56 | var definedMaxSize: CGSize { 57 | CGSize(width: definedMaxWidth, height: definedMaxHeight) 58 | } 59 | 60 | var definedMaxWidth: CGFloat { 61 | let constraintsMaxWidth = widthConstraints 62 | .map { $0.constant } 63 | .max() ?? 0 64 | 65 | return max(frame.size.width, constraintsMaxWidth) 66 | } 67 | 68 | var definedMaxHeight: CGFloat { 69 | let constraintsMaxHeight = heightConstraints 70 | .map { $0.constant } 71 | .max() ?? 0 72 | 73 | return max(frame.size.height, constraintsMaxHeight) 74 | } 75 | 76 | /// Autolayout 77 | 78 | var widthConstraints: [NSLayoutConstraint] { 79 | nonContentSizeLayoutConstraints.filter { $0.firstAttribute == NSLayoutConstraint.Attribute.width } 80 | } 81 | 82 | var heightConstraints: [NSLayoutConstraint] { 83 | nonContentSizeLayoutConstraints.filter { $0.firstAttribute == NSLayoutConstraint.Attribute.height } 84 | } 85 | 86 | var skeletonHeightConstraints: [NSLayoutConstraint] { 87 | nonContentSizeLayoutConstraints.filter { 88 | $0.firstAttribute == NSLayoutConstraint.Attribute.height 89 | && $0.identifier?.contains("SkeletonView.Constraint.Height") ?? false 90 | } 91 | } 92 | 93 | @discardableResult 94 | func setHeight(equalToConstant constant: CGFloat) -> NSLayoutConstraint { 95 | let heightConstraint = heightAnchor.constraint(equalToConstant: constant) 96 | heightConstraint.identifier = "SkeletonView.Constraint.Height.\(constant)" 97 | NSLayoutConstraint.activate([heightConstraint]) 98 | return heightConstraint 99 | } 100 | 101 | var nonContentSizeLayoutConstraints: [NSLayoutConstraint] { 102 | constraints.filter({ "\(type(of: $0))" != "NSContentSizeLayoutConstraint" }) 103 | } 104 | 105 | /// Animations 106 | 107 | func startSkeletonLayerAnimationBlock(_ anim: SkeletonLayerAnimation? = nil) -> VoidBlock { 108 | { 109 | self._isSkeletonAnimated = true 110 | guard let layer = self._skeletonLayer else { return } 111 | layer.start(anim) { [weak self] in 112 | self?._isSkeletonAnimated = false 113 | } 114 | } 115 | } 116 | 117 | var stopSkeletonLayerAnimationBlock: VoidBlock { 118 | { 119 | self._isSkeletonAnimated = false 120 | guard let layer = self._skeletonLayer else { return } 121 | layer.stopAnimation() 122 | } 123 | } 124 | 125 | /// Skeleton Layer 126 | 127 | func addSkeletonLayer(skeletonConfig config: SkeletonConfig) { 128 | guard let skeletonLayer = SkeletonLayerBuilder() 129 | .setSkeletonType(config.type) 130 | .addColors(config.colors) 131 | .setHolder(self) 132 | .build() 133 | else { return } 134 | 135 | self._skeletonLayer = skeletonLayer 136 | layer.insertSkeletonLayer( 137 | skeletonLayer, 138 | atIndex: UInt32.max, 139 | transition: config.transition 140 | ) { [weak self] in 141 | guard let self = self else { return } 142 | 143 | // Workaround to fix the problem when inserting a sublayer and 144 | // the content offset is modified by the system. 145 | (self as? UITextView)?.setContentOffset(.zero, animated: false) 146 | 147 | if config.animated { 148 | self.startSkeletonAnimation(config.animation) 149 | } 150 | } 151 | _status = .on 152 | } 153 | 154 | func updateSkeletonLayer(skeletonConfig config: SkeletonConfig) { 155 | guard let skeletonLayer = _skeletonLayer else { return } 156 | skeletonLayer.update(usingColors: config.colors) 157 | if config.animated { 158 | startSkeletonAnimation(config.animation) 159 | } else { 160 | skeletonLayer.stopAnimation() 161 | } 162 | } 163 | 164 | func layoutSkeletonLayerIfNeeded() { 165 | guard let skeletonLayer = _skeletonLayer else { return } 166 | skeletonLayer.layoutIfNeeded() 167 | } 168 | 169 | func removeSkeletonLayer() { 170 | guard sk.isSkeletonActive, 171 | let skeletonLayer = _skeletonLayer, 172 | let transitionStyle = _currentSkeletonConfig?.transition else { return } 173 | skeletonLayer.stopAnimation() 174 | _status = .off 175 | skeletonLayer.removeLayer(transition: transitionStyle) { 176 | self._skeletonLayer = nil 177 | self._currentSkeletonConfig = nil 178 | } 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/UIKitExtensions/UIView+SkeletonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // UIView+SkeletonView.swift 11 | // 12 | // Created by Juanpe Catalán on 19/8/21. 13 | 14 | import UIKit 15 | 16 | extension UIView { 17 | 18 | func showSkeleton( 19 | skeletonConfig config: SkeletonConfig, 20 | notifyDelegate: Bool = true 21 | ) { 22 | _isSkeletonAnimated = config.animated 23 | 24 | if notifyDelegate { 25 | _flowDelegate = SkeletonFlowHandler() 26 | _flowDelegate?.willBeginShowingSkeletons(rootView: self) 27 | } 28 | 29 | recursiveShowSkeleton(skeletonConfig: config, root: self) 30 | } 31 | 32 | func updateSkeleton( 33 | skeletonConfig config: SkeletonConfig, 34 | notifyDelegate: Bool = true 35 | ) { 36 | _isSkeletonAnimated = config.animated 37 | 38 | if notifyDelegate { 39 | _flowDelegate?.willBeginUpdatingSkeletons(rootView: self) 40 | } 41 | 42 | recursiveUpdateSkeleton(skeletonConfig: config, root: self) 43 | } 44 | 45 | func recursiveLayoutSkeletonIfNeeded(root: UIView? = nil) { 46 | subviewsSkeletonables.recursiveSearch(leafBlock: { 47 | guard isSkeletonable, sk.isSkeletonActive else { return } 48 | layoutSkeletonLayerIfNeeded() 49 | if let config = _currentSkeletonConfig, config.animated, !_isSkeletonAnimated { 50 | startSkeletonAnimation(config.animation) 51 | } 52 | }) { subview in 53 | subview.recursiveLayoutSkeletonIfNeeded() 54 | } 55 | 56 | if let root = root { 57 | _flowDelegate?.didLayoutSkeletonsIfNeeded(rootView: root) 58 | } 59 | } 60 | 61 | func recursiveHideSkeleton(reloadDataAfter reload: Bool, transition: SkeletonTransitionStyle, root: UIView? = nil) { 62 | guard sk.isSkeletonActive else { return } 63 | if isHiddenWhenSkeletonIsActive { 64 | isHidden = false 65 | } 66 | _currentSkeletonConfig?.transition = transition 67 | unSwizzleLayoutSubviews() 68 | unSwizzleTraitCollectionDidChange() 69 | removeDummyDataSourceIfNeeded(reloadAfter: reload) 70 | subviewsSkeletonables.recursiveSearch(leafBlock: { 71 | recoverViewState(forced: false) 72 | removeSkeletonLayer() 73 | }) { subview in 74 | subview.recursiveHideSkeleton(reloadDataAfter: reload, transition: transition) 75 | } 76 | 77 | if let root = root { 78 | _flowDelegate?.didHideSkeletons(rootView: root) 79 | } 80 | } 81 | 82 | } 83 | 84 | private extension UIView { 85 | 86 | func showSkeletonIfNotActive(skeletonConfig config: SkeletonConfig) { 87 | guard !sk.isSkeletonActive else { return } 88 | saveViewState() 89 | 90 | prepareViewForSkeleton() 91 | addSkeletonLayer(skeletonConfig: config) 92 | } 93 | 94 | func recursiveShowSkeleton(skeletonConfig config: SkeletonConfig, root: UIView? = nil) { 95 | if isHiddenWhenSkeletonIsActive { 96 | isHidden = true 97 | } 98 | guard isSkeletonable && !sk.isSkeletonActive else { return } 99 | _currentSkeletonConfig = config 100 | swizzleLayoutSubviews() 101 | swizzleTraitCollectionDidChange() 102 | addDummyDataSourceIfNeeded() 103 | subviewsSkeletonables.recursiveSearch(leafBlock: { 104 | showSkeletonIfNotActive(skeletonConfig: config) 105 | }) { subview in 106 | subview.recursiveShowSkeleton(skeletonConfig: config) 107 | } 108 | 109 | if let root = root { 110 | _flowDelegate?.didShowSkeletons(rootView: root) 111 | } 112 | } 113 | 114 | func recursiveUpdateSkeleton(skeletonConfig config: SkeletonConfig, root: UIView? = nil) { 115 | guard sk.isSkeletonActive else { return } 116 | _currentSkeletonConfig = config 117 | updateDummyDataSourceIfNeeded() 118 | subviewsSkeletonables.recursiveSearch(leafBlock: { 119 | if let skeletonLayer = _skeletonLayer, 120 | skeletonLayer.type != config.type { 121 | removeSkeletonLayer() 122 | addSkeletonLayer(skeletonConfig: config) 123 | } else { 124 | updateSkeletonLayer(skeletonConfig: config) 125 | } 126 | }) { subview in 127 | subview.recursiveUpdateSkeleton(skeletonConfig: config) 128 | } 129 | 130 | if let root = root { 131 | _flowDelegate?.didUpdateSkeletons(rootView: root) 132 | } 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/UIKitExtensions/UIView+Swizzling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // UIView+Swizzling.swift 11 | // 12 | // Created by Juanpe Catalán on 19/8/21. 13 | 14 | import UIKit 15 | 16 | extension UIView { 17 | 18 | @objc func skeletonLayoutSubviews() { 19 | guard Thread.isMainThread else { return } 20 | skeletonLayoutSubviews() 21 | guard sk.isSkeletonActive else { return } 22 | layoutSkeletonIfNeeded() 23 | } 24 | 25 | @objc func skeletonTraitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 26 | skeletonTraitCollectionDidChange(previousTraitCollection) 27 | guard isSkeletonable, sk.isSkeletonActive, let config = _currentSkeletonConfig else { return } 28 | updateSkeleton(skeletonConfig: config) 29 | } 30 | 31 | func swizzleLayoutSubviews() { 32 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { 33 | DispatchQueue.once(token: "UIView.SkeletonView.swizzleLayoutSubviews") { 34 | swizzle(selector: #selector(UIView.layoutSubviews), 35 | with: #selector(UIView.skeletonLayoutSubviews), 36 | inClass: UIView.self, 37 | usingClass: UIView.self) 38 | self.layoutSkeletonIfNeeded() 39 | } 40 | } 41 | } 42 | 43 | func unSwizzleLayoutSubviews() { 44 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { 45 | DispatchQueue.removeOnce(token: "UIView.SkeletonView.swizzleLayoutSubviews") { 46 | swizzle(selector: #selector(UIView.skeletonLayoutSubviews), 47 | with: #selector(UIView.layoutSubviews), 48 | inClass: UIView.self, 49 | usingClass: UIView.self) 50 | } 51 | } 52 | } 53 | 54 | func swizzleTraitCollectionDidChange() { 55 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { 56 | DispatchQueue.once(token: "UIView.SkeletonView.swizzleTraitCollectionDidChange") { 57 | swizzle(selector: #selector(UIView.traitCollectionDidChange(_:)), 58 | with: #selector(UIView.skeletonTraitCollectionDidChange(_:)), 59 | inClass: UIView.self, 60 | usingClass: UIView.self) 61 | } 62 | } 63 | } 64 | 65 | func unSwizzleTraitCollectionDidChange() { 66 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { 67 | DispatchQueue.removeOnce(token: "UIView.SkeletonView.swizzleTraitCollectionDidChange") { 68 | swizzle(selector: #selector(UIView.skeletonTraitCollectionDidChange(_:)), 69 | with: #selector(UIView.traitCollectionDidChange(_:)), 70 | inClass: UIView.self, 71 | usingClass: UIView.self) 72 | } 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Internal/UIKitExtensions/UIView+Transitions.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 SkeletonView. All rights reserved. 2 | 3 | import UIKit 4 | 5 | extension UIView { 6 | 7 | func startTransition(transitionBlock: @escaping () -> Void) { 8 | guard let transitionStyle = _currentSkeletonConfig?.transition, 9 | transitionStyle != .none else { 10 | transitionBlock() 11 | return 12 | } 13 | 14 | if case let .crossDissolve(duration) = transitionStyle { 15 | UIView.transition(with: self, 16 | duration: duration, 17 | options: .transitionCrossDissolve, 18 | animations: transitionBlock, 19 | completion: nil) 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SkeletonViewCore/Sources/Supporting Files/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyTrackingDomains 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | NSPrivacyAccessedAPIType 15 | NSPrivacyAccessedAPICategoryUserDefaults 16 | NSPrivacyAccessedAPITypeReasons 17 | 18 | CA92.1 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /SkeletonViewCore/Tests/Debug/SkeletonDebugTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright SkeletonView. All Rights Reserved. 3 | // 4 | // Licensed under the MIT License (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // SkeletonDebugTests.swift 11 | // 12 | // Created by Juanpe Catalán on 18/8/21. 13 | 14 | import XCTest 15 | @testable import SkeletonView 16 | 17 | class SkeletonDebugTests: XCTestCase { 18 | 19 | func testSkeletonDescriptionWithViewNotSkeletonableNotReturnsSkullEmojiAndChildren() { 20 | /// given 21 | let view = UIView() 22 | let expectedDictionary: [String : Any] = [ 23 | "isSkeletonable" : false, 24 | "type" : "UIView", 25 | "reference" : "\(Unmanaged.passUnretained(view).toOpaque())" 26 | ] 27 | 28 | /// when 29 | let obtainedDictionary = view.sk.treeNode.dictionaryRepresentation 30 | 31 | /// then 32 | XCTAssertEqual(expectedDictionary.keys, obtainedDictionary.keys) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /SkeletonViewCore/Tests/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:ios) 2 | podspec_name = "SkeletonView.podspec" 3 | 4 | lane :bump_version do |options| 5 | version_bump_podspec(path: @podspec_name, version_number: options[:next_version]) 6 | end 7 | 8 | lane :release_current do 9 | version = version_get_podspec(path: @podspec_name) 10 | if git_tag_exists(tag: version) 11 | UI.user_error!("The tag #{version} already exists on the repo. To release a new version of the library bump the version on #{@podspec_name}") 12 | end 13 | pod_lib_lint 14 | add_git_tag(tag: "#{version}") 15 | push_git_tags 16 | pod_push 17 | end 18 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ### bump_version 17 | 18 | ```sh 19 | [bundle exec] fastlane bump_version 20 | ``` 21 | 22 | 23 | 24 | ### release_current 25 | 26 | ```sh 27 | [bundle exec] fastlane release_current 28 | ``` 29 | 30 | 31 | 32 | ---- 33 | 34 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 35 | 36 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 37 | 38 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 39 | --------------------------------------------------------------------------------