Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 795fd7d0f1 | |||
| 6cc6f5aa80 | |||
| 3ecc3c3b39 | |||
| 511535921f | |||
| a6d1ae0b95 | |||
| 220fc4016d | |||
| ee94dd8aec | |||
| be2aa4f4ab | |||
| 55f16d9d51 | |||
| 9fccaf4fbd | |||
| 58959a5f9b | |||
| 5838f7881b | |||
| 41173471f6 | |||
| 12e5688b31 | |||
| 816b2965ff | |||
| e12e4a0fd1 | |||
| 6f78f5c378 | |||
| c8fdd6998d | |||
| 134463e529 | |||
| ee59239c59 | |||
| f1e61aa9c0 | |||
| 135778aa1a | |||
| e8d5eb61d8 | |||
| c2a029ed51 | |||
| a1c8276980 | |||
| e9ac3a5ab3 | |||
| fb83a62f7b | |||
| 12521c1d87 | |||
| c266035888 | |||
| 74b5172ea5 | |||
| 318e629d04 | |||
| 62193db76f | |||
| 19e7866d3d | |||
| 36668f450b | |||
| 1bdb3b9c72 | |||
| 6c4e9091a7 | |||
| 61a6efbc7b | |||
| 4143a6065c | |||
| f3baeedc14 | |||
| 29f7d8fd6a | |||
| 83e1600a73 | |||
| 450cecbd9c | |||
| 1676f8720a | |||
| 32ab6d3852 | |||
| 7ce5e0623a | |||
| f0c7d2d6a0 | |||
| 26476fd3ed | |||
| f0c2cf4ab4 | |||
| cadf8bb136 | |||
| 91c8f953ca | |||
| 0a91aebf2a | |||
| 7d8a057b64 | |||
| 06f7d240ff | |||
| c4c725eea3 | |||
| cc9dbdb499 | |||
| c6dfcf9c1a | |||
| dd7e5243bd | |||
| 7e002ddce0 | |||
| 6dcfb325dc | |||
| 7b5739295a | |||
| 2e1808ea50 | |||
| cec3aeb781 | |||
| b16400294d | |||
| abb8ea55f6 | |||
| c833da20f8 | |||
| 292fadf5b0 | |||
| 2ce041901c | |||
| 961cc77661 | |||
| 11b623fb13 | |||
| 4899802b01 | |||
| 0dfe1ca43b | |||
| 499d01c232 | |||
| f8f368ab8a | |||
| 023a57a763 | |||
| d426a0916e | |||
| 611c248c51 | |||
| 6f20c34d56 | |||
| be25b572f8 | |||
| 3c67812453 | |||
| 0431eb2a7d | |||
| fa0b95d923 | |||
| 523cd1c657 | |||
| 29e65c70d3 | |||
| 4b7360a011 | |||
| 2753c54216 | |||
| c7b3bc67be | |||
| 0c8bbf1fed | |||
| 2e99bc98bf | |||
| d5d52d1416 | |||
| 91c7735d0c | |||
| 18c5c1deb8 | |||
| 1ecfa31743 | |||
| 82b70ea1ee | |||
| d8ee06f1c5 | |||
| 13ce7426dd | |||
| 7d5039536c | |||
| b7a7731784 | |||
| 07f92d098a | |||
| f45405c9d5 | |||
| 768354ca35 | |||
| d5819763a1 | |||
| a73fc993aa | |||
| 0d027b5927 | |||
| bc482f58b3 | |||
| 78e941e01c | |||
| 7282008606 | |||
| 52d657faa0 | |||
| cba096becf | |||
| c095f49028 | |||
| 89e292c11b | |||
| 5089295771 | |||
| 39971af22d | |||
| 81d182259d | |||
| 1d82545a3d | |||
| 9086d7e874 | |||
| 666ebdf472 | |||
| 5a5d7b66e7 | |||
| 78f2bbe9f1 | |||
| c7133a6711 | |||
| aee5cdfd8f | |||
| 638c535767 | |||
| 782dd45b33 | |||
| 073baa6483 | |||
| a26dbf7c89 | |||
| 7ebde2b030 | |||
| 4009ed2b1e | |||
| a246c16cfe | |||
| ef8e4578d7 |
@@ -1,3 +1,12 @@
|
||||
---
|
||||
name: "\U0001F41B Bug report"
|
||||
about: Report a bug or unexpected behavior while using SkeletonView
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Describe your issue here.
|
||||
@@ -10,8 +19,8 @@ Describe your issue here.
|
||||
- [ ] discussion
|
||||
|
||||
### Requirements (place an `x` in each of the `[ ]`)
|
||||
* [ ] I've read and understood the [Contributing guidelines](https://github.com/Juanpe/SkeletonView/blob/develop/CONTRIBUTING.md) and have done my best effort to follow them.
|
||||
* [ ] I've read and agree to the [Code of Conduct](https://github.com/Juanpe/SkeletonView/blob/develop/CODE_OF_CONDUCT.md).
|
||||
* [ ] 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.
|
||||
* [ ] I've read and agree to the [Code of Conduct](https://github.com/Juanpe/SkeletonView/blob/main/CODE_OF_CONDUCT.md).
|
||||
* [ ] I've searched for any related issues and avoided creating a duplicate issue.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: "\U0001F4E3 Feedback"
|
||||
about: Give us general feedback about the SkeletonView
|
||||
title: ''
|
||||
labels: feedback
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# SkeletonView Feedback
|
||||
|
||||
You can use this template to give us structured feedback or just wipe it and leave us a note. Thank you!
|
||||
|
||||
## What have you loved?
|
||||
|
||||
_eg "the nice colors"_
|
||||
|
||||
## What was confusing or gave you pause?
|
||||
|
||||
_eg "it did something unexpected"_
|
||||
|
||||
## Are there features you'd like to see added?
|
||||
|
||||
_eg "SkeletonView should be compatible with SwiftUI"_
|
||||
|
||||
## Anything else?
|
||||
|
||||
_eg "have a nice day"_
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "⭐ Submit a request"
|
||||
about: Surface a feature or problem that you think should be solved
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Describe the feature or problem you’d like to solve
|
||||
|
||||
A clear and concise description of what the feature or problem is.
|
||||
|
||||
### Proposed solution
|
||||
|
||||
How will it benefit SkeletonView and its users?
|
||||
|
||||
### Additional context
|
||||
|
||||
Add any other context like screenshots or mockups are helpful, if applicable.
|
||||
@@ -3,5 +3,5 @@
|
||||
Describe the goal of this PR. Mention any related Issue numbers.
|
||||
|
||||
### Requirements (place an `x` in each of the `[ ]`)
|
||||
* [ ] I've read and understood the [Contributing guidelines](https://github.com/Juanpe/SkeletonView/blob/develop/CONTRIBUTING.md) and have done my best effort to follow them.
|
||||
* [ ] I've read and agree to the [Code of Conduct](https://github.com/Juanpe/SkeletonView/blob/develop/CODE_OF_CONDUCT.md).
|
||||
* [ ] 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.
|
||||
* [ ] I've read and agree to the [Code of Conduct](https://github.com/Juanpe/SkeletonView/blob/main/CODE_OF_CONDUCT.md).
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
name-template: '📦 $RESOLVED_VERSION'
|
||||
tag-template: '$RESOLVED_VERSION'
|
||||
category-template: '#### $TITLE'
|
||||
change-template: '- **#$NUMBER**: $TITLE - @$AUTHOR'
|
||||
template: |
|
||||
$CHANGES
|
||||
categories:
|
||||
- title: '🚨 Breaking'
|
||||
label: 'breaking'
|
||||
- title: '🔬Improvements'
|
||||
label: '💡 enhancement'
|
||||
- title: '🙌 New'
|
||||
label: 'feature'
|
||||
- title: '🩹 Bug fixes'
|
||||
label: '🐞 bug'
|
||||
- title: '⚙️ Maintenance'
|
||||
label: '⚙️ maintenance'
|
||||
- title: '📚 Documentation'
|
||||
label: '📚 docs'
|
||||
- title: '💾 Dependency Updates'
|
||||
label: 'dependencies'
|
||||
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- 'breaking'
|
||||
minor:
|
||||
labels:
|
||||
- '💡 enhancement'
|
||||
- 'feature'
|
||||
patch:
|
||||
labels:
|
||||
- '🐞 bug'
|
||||
- '⚙️ maintenance'
|
||||
- '📚 docs'
|
||||
- 'dependencies'
|
||||
|
||||
exclude-labels:
|
||||
- 'skip-changelog'
|
||||
+2
-2
@@ -4,7 +4,7 @@ onlyLabels:
|
||||
- awaiting user input
|
||||
staleLabel: given up
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
🤖 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.
|
||||
for your contributions 🙂
|
||||
closeComment: false
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
name: CD
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
release_version:
|
||||
if: github.event.pull_request.milestone == null && github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup fastlane
|
||||
run: brew install fastlane
|
||||
|
||||
- name: Publish release
|
||||
id: publish_release
|
||||
uses: release-drafter/release-drafter@v5
|
||||
with:
|
||||
publish: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update podspec
|
||||
run: fastlane bump_version next_version:${{ steps.publish_release.outputs.tag_name }}
|
||||
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
branch: 'main'
|
||||
commit_message: 'Bump version ${{ steps.publish_release.outputs.tag_name }}'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Cocoapods
|
||||
env:
|
||||
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
|
||||
run: |
|
||||
set -eo pipefail
|
||||
pod lib lint --allow-warnings
|
||||
pod trunk push --allow-warnings
|
||||
|
||||
- name: Communicate on PR released
|
||||
uses: unsplash/comment-on-pr@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
msg: |
|
||||
Congratulations! 🎉 This was released as part of [SkeletonView ${{ steps.publish_release.outputs.tag_name }}](${{ steps.publish_release.outputs.html_url }}) 🚀
|
||||
|
||||
- name: Tweet the release
|
||||
uses: ethomson/send-tweet-action@v1
|
||||
with:
|
||||
consumer-key: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
|
||||
consumer-secret: ${{ secrets.TWITTER_CONSUMER_API_SECRET }}
|
||||
access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }}
|
||||
access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
|
||||
status: |
|
||||
🎉 New release ${{ steps.publish_release.outputs.tag_name }} is out 🚀
|
||||
|
||||
Check out all the changes here:
|
||||
${{ steps.publish_release.outputs.html_url }}
|
||||
@@ -1,20 +0,0 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: xcodebuild
|
||||
run: |
|
||||
xcodebuild -project SkeletonView.xcodeproj -target SkeletonView-iOS -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 7,OS=12'
|
||||
xcodebuild -project SkeletonView.xcodeproj -target SkeletonView-tvOS -sdk appletvsimulator -destination 'platform=tvOS Simulator,name=Apple TV,OS=12'
|
||||
xcodebuild -project SkeletonView.xcodeproj -target SkeletonViewExample -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 7,OS=12'
|
||||
@@ -1,15 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
danger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Execute danger
|
||||
uses: danger/swift@3.5.0
|
||||
with:
|
||||
args: --failOnErrors --no-publish-check
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,19 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
build-config:
|
||||
- { target: 'SkeletonView-iOS', destination: 'platform=iOS Simulator,name=iPhone 8', sdk: 'iphonesimulator' }
|
||||
- { target: 'SkeletonView-tvOS', destination: 'platform=tvOS Simulator,name=Apple TV', sdk: 'appletvsimulator' }
|
||||
- { target: 'SkeletonViewExample', destination: 'platform=iOS Simulator,name=iPhone 8', sdk: 'iphonesimulator' }
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: xcodebuild clean build -target '${{ matrix.build-config['target'] }}' -sdk '${{ matrix.build-config['sdk'] }}' -destination '${{ matrix.build-config['destination'] }}'
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Pod lint
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
pod_lib_lint:
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- env:
|
||||
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
|
||||
run: |
|
||||
set -eo pipefail
|
||||
pod lib lint --allow-warnings
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Release
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
release_version:
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup fastlane
|
||||
run: brew install fastlane
|
||||
|
||||
- name: Publish release
|
||||
id: publish_release
|
||||
uses: release-drafter/release-drafter@v5
|
||||
with:
|
||||
publish: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update podspec
|
||||
run: fastlane bump_version next_version:${{ steps.publish_release.outputs.tag_name }}
|
||||
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
branch: 'main'
|
||||
commit_message: 'Bump version ${{ steps.publish_release.outputs.tag_name }}'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Cocoapods
|
||||
env:
|
||||
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
|
||||
run: |
|
||||
set -eo pipefail
|
||||
pod lib lint --allow-warnings
|
||||
pod trunk push --allow-warnings
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Release Notes
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
update_release_notes:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,17 +0,0 @@
|
||||
name: SwiftLint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
paths:
|
||||
- '.github/workflows/swiftlint-macos.yml'
|
||||
- '.swiftlint.yml'
|
||||
- '**/*.swift'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run SwiftLint
|
||||
run: swiftlint lint --reporter github-actions-logging
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Validations
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [main]
|
||||
types: [opened, reoneped, edited, synchronized]
|
||||
|
||||
# workflow_dispatch:
|
||||
# inputs:
|
||||
# commit hash:
|
||||
# description: "Commit hash"
|
||||
# required: true
|
||||
# default: ""
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run SwiftLint
|
||||
run: swiftlint lint --reporter github-actions-logging
|
||||
|
||||
danger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Danger
|
||||
uses: docker://frmeloni/danger-swift-with-swiftlint:1.3.1
|
||||
with:
|
||||
args: --failOnErrors --verbose
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
+7
-2
@@ -5,9 +5,12 @@ All notable changes to this project will be documented in this file
|
||||
|
||||
#### 🙌 New
|
||||
|
||||
#### 🔬Improvements
|
||||
|
||||
#### 🔬 Improvements
|
||||
* [**369**](https://github.com/Juanpe/SkeletonView/pull/369): remove useless corner radius constraint - [@Juanpe](https://github.com/Juanpe)
|
||||
* [**357**](https://github.com/Juanpe/SkeletonView/pull/357): Removed duplicate code in SkeletonCollectionDelegate. - [@keshavamurthy1](https://github.com/keshavamurthy1)
|
||||
|
||||
#### 🩹 Bug fixes
|
||||
* [**359**](https://github.com/Juanpe/SkeletonView/pull/359): SkeletonView respecting Font's height, rather than the `UIView` actual height. - [@keshavamurthy1](https://github.com/keshavamurthy1)
|
||||
|
||||
|
||||
## 📦 [1.11.0](https://github.com/Juanpe/SkeletonView/releases/tag/1.11.0)
|
||||
@@ -15,6 +18,8 @@ All notable changes to this project will be documented in this file
|
||||
#### 🙌 New
|
||||
* [**339**](https://github.com/Juanpe/SkeletonView/pull/339): Add `hiddenWhenSkeletonIsActive` property - [@mohn93](https://github.com/mohn93)
|
||||
* [**341**](https://github.com/Juanpe/SkeletonView/pull/341): Support autoreverses in gradient animations - [@Juanpe](https://github.com/Juanpe)
|
||||
|
||||
#### 🔬Improvements
|
||||
* [**344**](https://github.com/Juanpe/SkeletonView/pull/344): Resize labels based on number of lines - [@Juanpe](https://github.com/Juanpe)
|
||||
|
||||
#### 🩹 Bug fixes
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
# Contributors Guide
|
||||
|
||||
Interested in contributing? Awesome! Before you do though, please read our
|
||||
[Code of Conduct](https://github.com/Juanpe/SkeletonView/blob/develop/CODE_OF_CONDUCT.md). We take it very seriously, and expect that you will as
|
||||
[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
|
||||
well.
|
||||
|
||||
There are many ways you can contribute! :heart:
|
||||
@@ -45,7 +45,7 @@ If the contribution doesn't meet the above criteria, you may fail our automated
|
||||
3. :herb: Create a new branch and check it out.
|
||||
4. :crystal_ball: Make your changes and commit them locally.
|
||||
5. :arrow_heading_up: Push your new branch to your fork. (e.g. `git push username fix-issue-300`).
|
||||
6. :inbox_tray: Open a Pull Request on github.com from your new branch on your fork to `develop` in this
|
||||
6. :inbox_tray: Open a Pull Request on github.com from your new branch on your fork to `main` in this
|
||||
repository.
|
||||
|
||||
## Developer's Certificate of Origin 1.1
|
||||
|
||||
@@ -3,16 +3,6 @@ import Danger
|
||||
let danger = Danger()
|
||||
let github = danger.github
|
||||
|
||||
// Changelog entries are required for changes to library files.
|
||||
let allSourceFiles = danger.git.modifiedFiles + danger.git.createdFiles
|
||||
let noChangelogEntry = !allSourceFiles.contains("CHANGELOG.md")
|
||||
let sourceChanges = allSourceFiles.contains { $0.hasPrefix("Sources") }
|
||||
let isNotTrivial = !danger.github.pullRequest.title.contains("#trivial")
|
||||
|
||||
if isNotTrivial && noChangelogEntry && sourceChanges {
|
||||
fail("Any changes to library code should be reflected in the Changelog.")
|
||||
}
|
||||
|
||||
// Make it more obvious that a PR is a work in progress and shouldn't be merged yet
|
||||
if danger.github.pullRequest.title.contains("WIP") {
|
||||
warn("PR is classed as Work in Progress")
|
||||
|
||||
@@ -174,6 +174,12 @@ extension ViewController: SkeletonCollectionViewDataSource {
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return 10
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, skeletonCellForItemAt indexPath: IndexPath) -> UICollectionViewCell? {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as? CollectionViewCell
|
||||
cell?.isSkeletonable = indexPath.row != 0
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDataSource
|
||||
|
||||
@@ -184,6 +190,10 @@ extension ViewController: SkeletonCollectionViewDataSource {
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
|
||||
return cell
|
||||
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath) {
|
||||
let cell = cell as? CollectionViewCell
|
||||
cell?.isSkeletonable = indexPath.row != 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Va7-1y-Tel">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Va7-1y-Tel">
|
||||
<device id="retina5_9" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
@@ -50,12 +50,9 @@
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSkeletonable" value="YES"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CJW-A4-Fb8">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="3" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CJW-A4-Fb8">
|
||||
<rect key="frame" x="166" y="27" width="166" height="12"/>
|
||||
<color key="backgroundColor" red="0.92156862750000001" green="0.16862745100000001" blue="0.54901960780000003" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="lessThanOrEqual" constant="80" id="XAA-g8-NVH"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="10"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -128,14 +125,25 @@
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="placeholder" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="dha-bH-Ipf">
|
||||
<rect key="frame" x="119" y="57" width="235" height="34"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSkeletonable" value="YES"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="dha-bH-Ipf" firstAttribute="top" secondItem="VhU-1t-AaI" secondAttribute="bottom" constant="8" symbolic="YES" id="1Ek-1L-ZVs"/>
|
||||
<constraint firstItem="oiE-tt-nc2" firstAttribute="leading" secondItem="7IN-F3-Mr6" secondAttribute="leadingMargin" id="1be-ak-AH1"/>
|
||||
<constraint firstAttribute="bottom" secondItem="oiE-tt-nc2" secondAttribute="bottom" constant="20" id="CKt-oA-eBI"/>
|
||||
<constraint firstItem="oiE-tt-nc2" firstAttribute="top" secondItem="7IN-F3-Mr6" secondAttribute="topMargin" constant="7" id="EKn-ST-LDX"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="VhU-1t-AaI" secondAttribute="trailing" constant="5" id="I7C-Bq-mfK"/>
|
||||
<constraint firstItem="VhU-1t-AaI" firstAttribute="leading" secondItem="oiE-tt-nc2" secondAttribute="trailing" constant="21" id="Ojr-Kz-1k6"/>
|
||||
<constraint firstItem="VhU-1t-AaI" firstAttribute="top" secondItem="7IN-F3-Mr6" secondAttribute="topMargin" constant="18" id="ZW6-JY-S4c"/>
|
||||
<constraint firstItem="dha-bH-Ipf" firstAttribute="trailing" secondItem="VhU-1t-AaI" secondAttribute="trailing" id="baX-Nw-8sB"/>
|
||||
<constraint firstItem="dha-bH-Ipf" firstAttribute="leading" secondItem="VhU-1t-AaI" secondAttribute="leading" id="kzA-mV-IDt"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSkeletonable" value="YES"/>
|
||||
@@ -147,6 +155,7 @@
|
||||
<connections>
|
||||
<outlet property="avatar" destination="oiE-tt-nc2" id="Dkh-R5-Qhu"/>
|
||||
<outlet property="label1" destination="VhU-1t-AaI" id="kUW-HV-KrD"/>
|
||||
<outlet property="textField" destination="dha-bH-Ipf" id="OHI-6P-tuU"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
|
||||
@@ -9,11 +9,25 @@
|
||||
import UIKit
|
||||
|
||||
class Cell: UITableViewCell {
|
||||
|
||||
|
||||
@IBOutlet weak var avatar: UIImageView!
|
||||
@IBOutlet weak var label1: UILabel!
|
||||
|
||||
@IBOutlet weak var textField: UITextField!
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
setUpInputAccessoryView()
|
||||
}
|
||||
|
||||
func setUpInputAccessoryView() {
|
||||
let bar = UIToolbar()
|
||||
let reset = UIBarButtonItem(title: "InputAccessoryView", style: .plain, target: self, action: #selector(resetTapped))
|
||||
bar.items = [reset]
|
||||
bar.sizeToFit()
|
||||
textField.inputAccessoryView = bar
|
||||
}
|
||||
|
||||
@objc func resetTapped() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +183,17 @@ extension ViewController: SkeletonTableViewDataSource {
|
||||
cell.label1.text = "cell -> \(indexPath.row)"
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell? {
|
||||
let cell = skeletonView.dequeueReusableCell(withIdentifier: "CellIdentifier", for: indexPath) as? Cell
|
||||
cell?.textField.isHidden = indexPath.row == 0
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath) {
|
||||
let cell = cell as? Cell
|
||||
cell?.textField.isHidden = indexPath.row == 0
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewController: SkeletonTableViewDelegate {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||

|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Juanpe/SkeletonView/workflows/build">
|
||||
<img src="https://github.com/Juanpe/SkeletonView/workflows/build/badge.svg">
|
||||
<a href="https://github.com/Juanpe/SkeletonView/actions?query=workflow%3ACI">
|
||||
<img src="https://github.com/Juanpe/SkeletonView/workflows/CI/badge.svg">
|
||||
</a>
|
||||
<a href="https://codebeat.co/projects/github-com-juanpe-skeletonview-master"><img alt="codebeat badge" src="https://codebeat.co/badges/f854fdfd-31e5-4689-ba04-075d83653e60" /></a>
|
||||
<img src="http://img.shields.io/badge/dependency%20manager-swiftpm%2Bcocoapods%2Bcarthage-green" />
|
||||
<img src="https://img.shields.io/badge/platforms-ios%2Btvos-green" />
|
||||
<a href="https://badge.bow-swift.io/recipe?name=SkeletonView&description=An%20elegant%20way%20to%20show%20users%20that%20something%20is%20happening%20and%20also%20prepare%20them%20to%20which%20contents%20he%20is%20waiting&url=https://github.com/juanpe/skeletonview&owner=Juanpe&avatar=https://avatars0.githubusercontent.com/u/1409041?v=4&tag=1.8.7"><img src="https://raw.githubusercontent.com/bow-swift/bow-art/master/badges/nef-playgrounds-badge.svg" alt="SkeletonView Playground" style="height:20px"></a>
|
||||
<a href="https://codebeat.co/projects/github-com-juanpe-skeletonview-main"><img alt="codebeat badge" src="https://codebeat.co/badges/1f37bbab-a1c8-4a4a-94d7-f21740d461e9" /></a>
|
||||
<a href="https://cocoapods.org/pods/SkeletonView"><img src="https://img.shields.io/cocoapods/v/SkeletonView.svg?style=flat"></a>
|
||||
<a href="https://github.com/Carthage/Carthage/"><img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat"></a>
|
||||
<a href="https://swift.org/package-manager/"><img src="https://img.shields.io/badge/SPM-supported-Green.svg?style=flat"></a>
|
||||
<img src="https://img.shields.io/badge/platforms-iOS_tvOS-green" />
|
||||
<a href="https://badge.bow-swift.io/recipe?name=SkeletonView&description=An%20elegant%20way%20to%20show%20users%20that%20something%20is%20happening%20and%20also%20prepare%20them%20to%20which%20contents%20he%20is%20waiting&url=https://github.com/juanpe/skeletonview&owner=Juanpe&avatar=https://avatars0.githubusercontent.com/u/1409041?v=4&tag=1.20.0"><img src="https://raw.githubusercontent.com/bow-swift/bow-art/master/badges/nef-playgrounds-badge.svg" alt="SkeletonView Playground" style="height:20px"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -19,7 +21,7 @@
|
||||
• <a href="#️-contributing">Contributing</a>
|
||||
</p>
|
||||
|
||||
**🌎 README is available in other languages: [🇪🇸](https://github.com/Juanpe/SkeletonView/blob/develop/README_es.md) . [🇨🇳](https://github.com/Juanpe/SkeletonView/blob/master/README_zh.md) . [🇧🇷](https://github.com/Juanpe/SkeletonView/blob/master/README_pt-br.md) . [🇰🇷](https://github.com/Juanpe/SkeletonView/blob/master/README_ko.md) . [🇫🇷](https://github.com/Juanpe/SkeletonView/blob/master/README_fr.md)**
|
||||
**🌎 README is available in other languages: [🇪🇸](https://github.com/Juanpe/SkeletonView/blob/main/README_es.md) . [🇨🇳](https://github.com/Juanpe/SkeletonView/blob/master/README_zh.md) . [🇧🇷](https://github.com/Juanpe/SkeletonView/blob/master/README_pt-br.md) . [🇰🇷](https://github.com/Juanpe/SkeletonView/blob/master/README_ko.md) . [🇫🇷](https://github.com/Juanpe/SkeletonView/blob/master/README_fr.md)**
|
||||
|
||||
Today almost all apps have async processes, such as API requests, long running processes, etc. While the processes are working, usually developers place a loading view to show users that something is going on.
|
||||
|
||||
@@ -28,7 +30,7 @@ Today almost all apps have async processes, such as API requests, long running p
|
||||
Enjoy it! 🙂
|
||||
|
||||
|
||||
##
|
||||
##
|
||||
- [🌟 Features](#-features)
|
||||
- [🎬 Guides](#-guides)
|
||||
- [📲 Installation](#-installation)
|
||||
@@ -37,10 +39,15 @@ Enjoy it! 🙂
|
||||
- [🔠 Texts](#-texts)
|
||||
- [🦋 Appearance](#-appearance)
|
||||
- [🎨 Custom colors](#-custom-colors)
|
||||
- [ 🏃♀️ Animations](#%EF%B8%8F-animations)
|
||||
- [Image captured from website https://flatuicolors.com](#image-captured-from-website-httpsflatuicolorscom)
|
||||
- [🏃♀️ Animations](#️-animations)
|
||||
- [🏄 Transitions](#-transitions)
|
||||
- [✨ Miscellaneous](#-miscellaneous)
|
||||
- [❤️ Contributing](#️-contributing)
|
||||
- [📢 Mentions](#-mentions)
|
||||
- [🏆 Sponsors](#-sponsors)
|
||||
- [👨🏻💻 Author](#-author)
|
||||
- [👮🏻 License](#-license)
|
||||
|
||||
|
||||
|
||||
@@ -165,19 +172,16 @@ If you want to show the skeleton in a ```UITableView```, you need to conform to
|
||||
|
||||
``` swift
|
||||
public protocol SkeletonTableViewDataSource: UITableViewDataSource {
|
||||
func numSections(in collectionSkeletonView: UITableView) -> Int
|
||||
func numSections(in collectionSkeletonView: UITableView) -> Int // Default: 1
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell? // Default: nil
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath)
|
||||
}
|
||||
```
|
||||
As you can see, this protocol inherits from ```UITableViewDataSource```, so you can replace this protocol with the skeleton protocol.
|
||||
|
||||
This protocol has a default implementation:
|
||||
|
||||
``` swift
|
||||
func numSections(in collectionSkeletonView: UITableView) -> Int
|
||||
// Default: 1
|
||||
```
|
||||
This protocol has a default implementation for some methods. For example, the number of rows for each section is calculated in runtime:
|
||||
|
||||
``` swift
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int
|
||||
@@ -185,18 +189,35 @@ func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection s
|
||||
// It calculates how many cells need to populate whole tableview
|
||||
```
|
||||
|
||||
There is only one method you need to implement to let Skeleton know the cell identifier. This method doesn't have default implementation:
|
||||
``` swift
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier
|
||||
```
|
||||
> 📣 **IMPORTANT!**
|
||||
>
|
||||
> If you return `UITableView.automaticNumberOfSkeletonRows` in the above method, it acts like the default behavior (i.e. it calculates how many cells needed to populate the whole tableview).
|
||||
|
||||
**Example**
|
||||
There is only one method you need to implement to let Skeleton know the cell identifier. This method doesn't have default implementation:
|
||||
``` swift
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier {
|
||||
return "CellIdentifier"
|
||||
}
|
||||
```
|
||||
|
||||
By default, the library dequeues the cells from each indexPath, but you can also do this if you want to make some changes before the skeleton appears:
|
||||
``` swift
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell? {
|
||||
let cell = skeletonView.dequeueReusableCell(withIdentifier: "CellIdentifier", for: indexPath) as? Cell
|
||||
cell?.textField.isHidden = indexPath.row == 0
|
||||
return cell
|
||||
}
|
||||
```
|
||||
|
||||
If you prefer to leave the deque part to the library you can configure the cell using this method:
|
||||
``` swift
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath) {
|
||||
let cell = cell as? Cell
|
||||
cell?.textField.isHidden = indexPath.row == 0
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Besides, you can skeletonize both the headers and footers. You need to conform to `SkeletonTableViewDelegate` protocol.
|
||||
|
||||
```swift
|
||||
@@ -224,10 +245,12 @@ For `UICollectionView`, you need to conform to `SkeletonCollectionViewDataSource
|
||||
|
||||
``` swift
|
||||
public protocol SkeletonCollectionViewDataSource: UICollectionViewDataSource {
|
||||
func numSections(in collectionSkeletonView: UICollectionView) -> Int // default: 1
|
||||
func numSections(in collectionSkeletonView: UICollectionView) -> Int // default: 1
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, cellIdentifierForItemAt indexPath: IndexPath) -> ReusableCellIdentifier
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, supplementaryViewIdentifierOfKind: String, at indexPath: IndexPath) -> ReusableCellIdentifier? // default: nil
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, skeletonCellForItemAt indexPath: IndexPath) -> UICollectionViewCell? // default: nil
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -487,6 +510,33 @@ Sometimes you wanna hide some view when the animation starts, so there is a quic
|
||||
view.isHiddenWhenSkeletonIsActive = true // This works only when isSkeletonable = true
|
||||
```
|
||||
|
||||
**Don't modify user interaction when the skeleton is active**
|
||||
|
||||
|
||||
By default, the user interaction is disabled for skeletonized items, but if you don't want to modify the user interaction indicator when skeleton is active, you can use the `isUserInteractionDisabledWhenSkeletonIsActive` property:
|
||||
|
||||
```swift
|
||||
view.isUserInteractionDisabledWhenSkeletonIsActive = false // The view will be active when the skeleton will be active.
|
||||
```
|
||||
|
||||
**Delayed show skeleton**
|
||||
|
||||
You can delay the presentation of the skeleton if the views update quickly.
|
||||
|
||||
```swift
|
||||
func showSkeleton(usingColor: UIColor,
|
||||
animated: Bool,
|
||||
delay: TimeInterval,
|
||||
transition: SkeletonTransitionStyle)
|
||||
```
|
||||
|
||||
```swift
|
||||
func showGradientSkeleton(usingGradient: SkeletonGradient,
|
||||
animated: Bool,
|
||||
delay: TimeInterval,
|
||||
transition: SkeletonTransitionStyle)
|
||||
```
|
||||
|
||||
**Debug**
|
||||
|
||||
To facilitate the debug tasks when something is not working fine. **`SkeletonView`** has some new tools.
|
||||
@@ -516,18 +566,18 @@ Then, when the skeleton appears, you can see the view hierarchy in the Xcode con
|
||||
|
||||
* iOS 9.0+
|
||||
* tvOS 9.0+
|
||||
* Swift 5
|
||||
|
||||
* Swift 5.3
|
||||
|
||||
## ❤️ Contributing
|
||||
This is an open source project, so feel free to contribute. How?
|
||||
|
||||
- Open an [issue](https://github.com/Juanpe/SkeletonView/issues/new).
|
||||
- Send feedback via [email](mailto://juanpecatalan.com).
|
||||
- Propose your own fixes, suggestions and open a pull request with the changes.
|
||||
|
||||
See [all contributors](https://github.com/Juanpe/SkeletonView/graphs/contributors)
|
||||
|
||||
For more information, please read the [contributing guidelines](https://github.com/Juanpe/SkeletonView/blob/develop/CONTRIBUTING.md).
|
||||
For more information, please read the [contributing guidelines](https://github.com/Juanpe/SkeletonView/blob/main/CONTRIBUTING.md).
|
||||
|
||||
|
||||
## 📢 Mentions
|
||||
@@ -546,7 +596,12 @@ For more information, please read the [contributing guidelines](https://github.c
|
||||
- [Swift News #36](https://www.youtube.com/watch?v=mAGpsQiy6so)
|
||||
- [Best iOS articles, new tools & more](https://medium.com/flawless-app-stories/best-ios-articles-new-tools-more-fcbe673e10d)
|
||||
|
||||
## 🏆 Sponsors
|
||||
|
||||
Open-source projects cannot live long without your help. If you find **SkeletonView** is useful, please consider supporting this
|
||||
project by becoming a sponsor.
|
||||
|
||||
Become a sponsor through [GitHub Sponsors](https://github.com/sponsors/Juanpe) :heart:
|
||||
|
||||
## 👨🏻💻 Author
|
||||
|
||||
|
||||
+8
-5
@@ -1,8 +1,8 @@
|
||||

|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Juanpe/SkeletonView/workflows/build">
|
||||
<img src="https://github.com/Juanpe/SkeletonView/workflows/build/badge.svg">
|
||||
<a href="https://github.com/Juanpe/SkeletonView/actions?query=workflow%3ACI">
|
||||
<img src="https://github.com/Juanpe/SkeletonView/workflows/CI/badge.svg">
|
||||
</a>
|
||||
<a href="https://codebeat.co/projects/github-com-juanpe-skeletonview-master"><img alt="codebeat badge" src="https://codebeat.co/badges/f854fdfd-31e5-4689-ba04-075d83653e60" /></a>
|
||||
<img src="http://img.shields.io/badge/dependency%20manager-swiftpm%2Bcocoapods%2Bcarthage-green" />
|
||||
@@ -27,18 +27,21 @@ Hoy en día, La mayoría de las apps tiene procesos asíncronos, como peticiones
|
||||
Enjoy it! 🙂
|
||||
|
||||
##
|
||||
- [](#)
|
||||
- [🌟 Destacado](#-destacado)
|
||||
- [🎬 Videotutoriales](#-videotutoriales)
|
||||
- [📲 Instalación](#-instalación)
|
||||
- [🐒 ¿Cómo funciona?](#-cómo-funciona)
|
||||
- [](#-1)
|
||||
- [🌿 Colecciones](#-colecciones)
|
||||
- [🔠 Textos](#-textos)
|
||||
- [🦋 Apariencia](#-apariencia)
|
||||
- [🎨 Colores](#-colors)
|
||||
- [🎨 Colores](#-colores)
|
||||
- [Imagen extraída de la web https://flatuicolors.com](#imagen-extraída-de-la-web-httpsflatuicolorscom)
|
||||
- [🏃♀️ Animaciones](#️-animaciones)
|
||||
- [🏄 Transiciones](#-transiciones)
|
||||
- [✨ Miscelánea](#-miscelánea)
|
||||
- [❤️ Contribuir](#️-contribuir)
|
||||
- [❤️ Contributing](#️-contributing)
|
||||
- [📢 Menciones](#-menciones)
|
||||
- [👨🏻💻 Autor](#-autor)
|
||||
- [👮🏻 Licencia](#-licencia)
|
||||
@@ -474,7 +477,7 @@ Esto es un proyecto open source, siéntete libre de contribuir. ¿Cómo?
|
||||
|
||||
Échale un vistazo a [los que ya han contribuído](https://github.com/Juanpe/SkeletonView/graphs/contributors)
|
||||
|
||||
Para más información, por favor, lee la [guía de contribución](https://github.com/Juanpe/SkeletonView/blob/develop/CONTRIBUTING.md).
|
||||
Para más información, por favor, lee la [guía de contribución](https://github.com/Juanpe/SkeletonView/blob/main/CONTRIBUTING.md).
|
||||
|
||||
|
||||
## 📢 Menciones
|
||||
|
||||
+32
-24
@@ -1,8 +1,8 @@
|
||||

|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Juanpe/SkeletonView/workflows/build">
|
||||
<img src="https://github.com/Juanpe/SkeletonView/workflows/build/badge.svg">
|
||||
<a href="https://github.com/Juanpe/SkeletonView/actions?query=workflow%3ACI">
|
||||
<img src="https://github.com/Juanpe/SkeletonView/workflows/CI/badge.svg">
|
||||
</a>
|
||||
<a href="https://codebeat.co/projects/github-com-juanpe-skeletonview-master"><img alt="codebeat badge" src="https://codebeat.co/badges/f854fdfd-31e5-4689-ba04-075d83653e60" /></a>
|
||||
<img src="https://img.shields.io/badge/Swift-5-orange.svg" />
|
||||
@@ -37,28 +37,36 @@ Aujourd'hui, presque toutes les applications ont des processus asynchrones, tels
|
||||
|
||||
Profitez-en! 🙂
|
||||
|
||||
* [Caractéristiques](#-caractéristiques)
|
||||
* [Guides](#-guides)
|
||||
* [Installation](#-installation)
|
||||
* [Cocoapods](#utilisation-de-cocoapods)
|
||||
* [Carthage](#utilisation-de-carthage)
|
||||
* [SPM](#utilisation-du-gestionnaire-de-paquets-Swift)
|
||||
* [Comment utiliser](#-mode-d'emploi)
|
||||
* [Collections](#-collections)
|
||||
* [Texte Multiligne](#-texte-multiligne)
|
||||
* [Couleurs personnalisées](#-couleurs-personnalisées)
|
||||
* [Présentation](#-présentation)
|
||||
* [Animations personnalisées](#-animations-personnalisées)
|
||||
* [Transitions](#-transitions)
|
||||
* [Hiérarchie](#-hiérarchie)
|
||||
* [Débugger](#-débugger)
|
||||
* [Documentation](#-documentation)
|
||||
* [Versions OS & SDK supportées](#-versions-os-et-sdk-supportées)
|
||||
* [Prochaines étapes](#-prochaines-étapes)
|
||||
* [Contribuer](#-contribuer)
|
||||
* [Mentions](#-mentions)
|
||||
* [Auteur](#-auteur)
|
||||
* [Licence](#-licence)
|
||||
- [🌟 Caractéristiques](#-caractéristiques)
|
||||
- [🎬 Guides](#-guides)
|
||||
- [📲 Installation](#-installation)
|
||||
- [Utilisation de CocoaPods](#utilisation-de-cocoapods)
|
||||
- [Utilisation de Carthage](#utilisation-de-carthage)
|
||||
- [Utilisation du gestionnaire de paquets Swift](#utilisation-du-gestionnaire-de-paquets-swift)
|
||||
- [🐒 Mode d'emploi](#-mode-demploi)
|
||||
- [Extra](#extra)
|
||||
- [Mise en page des vues squelettes](#mise-en-page-des-vues-squelettes)
|
||||
- [Mise à jour de la configuration du squelette](#mise-à-jour-de-la-configuration-du-squelette)
|
||||
- [🌿 Collections](#-collections)
|
||||
- [UITableView](#uitableview)
|
||||
- [UICollectionView](#uicollectionview)
|
||||
- [📰 Texte multiligne](#-texte-multiligne)
|
||||
- [🎛 Personnaliser](#-personnaliser)
|
||||
- [🎨 Couleurs personnalisées](#-couleurs-personnalisées)
|
||||
- [Image tirée du site web https://flatuicolors.com](#image-tirée-du-site-web-httpsflatuicolorscom)
|
||||
- [🦋 Présentation](#-présentation)
|
||||
- [🤓 Animations personnalisées](#-animations-personnalisées)
|
||||
- [🏄 Transitions](#-transitions)
|
||||
- [👨👧👦 Hiérarchie](#-hiérarchie)
|
||||
- [🔬 Débugger](#-débugger)
|
||||
- [📚 Documentation](#-documentation)
|
||||
- [📋 Versions OS et SDK supportées](#-versions-os-et-sdk-supportées)
|
||||
- [📬 Prochaines étapes](#-prochaines-étapes)
|
||||
- [❤️ Contribuer](#️-contribuer)
|
||||
- [Projet généré avec SwiftPlate](#projet-généré-avec-swiftplate)
|
||||
- [📢 Mentions](#-mentions)
|
||||
- [👨🏻💻 Auteur](#-auteur)
|
||||
- [👮🏻 Licence](#-licence)
|
||||
|
||||
## 🌟 Caractéristiques
|
||||
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||

|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Juanpe/SkeletonView/workflows/build">
|
||||
<img src="https://github.com/Juanpe/SkeletonView/workflows/build/badge.svg">
|
||||
<a href="https://github.com/Juanpe/SkeletonView/actions?query=workflow%3ACI">
|
||||
<img src="https://github.com/Juanpe/SkeletonView/workflows/CI/badge.svg">
|
||||
</a>
|
||||
<a href="https://codebeat.co/projects/github-com-juanpe-skeletonview-master"><img alt="codebeat badge" src="https://codebeat.co/badges/f854fdfd-31e5-4689-ba04-075d83653e60" /></a>
|
||||
<img src="https://img.shields.io/badge/Swift-5-orange.svg" />
|
||||
|
||||
+26
-21
@@ -1,8 +1,8 @@
|
||||

|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Juanpe/SkeletonView/workflows/build">
|
||||
<img src="https://github.com/Juanpe/SkeletonView/workflows/build/badge.svg">
|
||||
<a href="https://github.com/Juanpe/SkeletonView/actions?query=workflow%3ACI">
|
||||
<img src="https://github.com/Juanpe/SkeletonView/workflows/CI/badge.svg">
|
||||
</a>
|
||||
<a href="https://codebeat.co/projects/github-com-juanpe-skeletonview-master"><img alt="codebeat badge" src="https://codebeat.co/badges/f854fdfd-31e5-4689-ba04-075d83653e60" /></a>
|
||||
<img src="https://img.shields.io/badge/Swift-5-orange.svg" />
|
||||
@@ -36,25 +36,30 @@ Hoje, quase todos os apps têm processos assíncronos, como requisições de API
|
||||
|
||||
Aproveite! 🙂
|
||||
|
||||
* [Features](#-features)
|
||||
* [Requerimentos](#-supported-os--sdk-versions)
|
||||
* [Projeto de exemplo](#-example)
|
||||
* [Instalação](#-installation)
|
||||
* [Cocoapods](#using-cocoapods)
|
||||
* [Carthage](#using-carthage)
|
||||
* [Como usar](#-how-to-use)
|
||||
* [Coleções](#-collections)
|
||||
* [Texto em várias linhas](#-multiline-text)
|
||||
* [Cores customizadas](#-custom-colors)
|
||||
* [Aparência](#-appearance)
|
||||
* [Animaçōes customizadas](#-custom-animations)
|
||||
* [Hierarquia](#-hierarchy)
|
||||
* [Documentação](#-documentation)
|
||||
* [Próximos passos](#-next-steps)
|
||||
* [Contribuindo](#-contributing)
|
||||
* [Menções](#-mentions)
|
||||
* [Autor](#-author)
|
||||
* [Licença](#-license)
|
||||
- [🌟 Features](#-features)
|
||||
- [📋 Versões do SDK e OS suportados](#-versões-do-sdk-e-os-suportados)
|
||||
- [🔮 Exemplo](#-exemplo)
|
||||
- [📲 Instalação](#-instalação)
|
||||
- [Usando CocoaPods](#usando-cocoapods)
|
||||
- [Usando Carthage](#usando-carthage)
|
||||
- [🐒 Como usar](#-como-usar)
|
||||
- [🌿 Coleções](#-coleções)
|
||||
- [UITableView](#uitableview)
|
||||
- [UICollectionView](#uicollectionview)
|
||||
- [📰 Texto de várias linhas](#-texto-de-várias-linhas)
|
||||
- [🎛 Customização](#-customização)
|
||||
- [🎨 Cores customizadas](#-cores-customizadas)
|
||||
- [Imagem capturada do site https://flatuicolors.com](#imagem-capturada-do-site-httpsflatuicolorscom)
|
||||
- [🦋 Aparência](#-aparência)
|
||||
- [🤓 Animações customizadas](#-animações-customizadas)
|
||||
- [👨👧👦 Hierarquia](#-hierarquia)
|
||||
- [📚 Documentação](#-documentação)
|
||||
- [📬 Próximos passos](#-próximos-passos)
|
||||
- [❤️ Contribuindo](#️-contribuindo)
|
||||
- [Projeto gerado com SwiftPlate](#projeto-gerado-com-swiftplate)
|
||||
- [📢 Menções](#-menções)
|
||||
- [👨🏻💻 Autor](#-autor)
|
||||
- [👮🏻 Licença](#-licença)
|
||||
|
||||
|
||||
## 🌟 Features
|
||||
|
||||
+25
-20
@@ -1,8 +1,8 @@
|
||||

|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Juanpe/SkeletonView/workflows/build">
|
||||
<img src="https://github.com/Juanpe/SkeletonView/workflows/build/badge.svg">
|
||||
<a href="https://github.com/Juanpe/SkeletonView/actions?query=workflow%3ACI">
|
||||
<img src="https://github.com/Juanpe/SkeletonView/workflows/CI/badge.svg">
|
||||
</a>
|
||||
<a href="https://codebeat.co/projects/github-com-juanpe-skeletonview-master"><img alt="codebeat badge" src="https://codebeat.co/badges/f854fdfd-31e5-4689-ba04-075d83653e60" /></a>
|
||||
<img src="https://img.shields.io/badge/Swift-5-orange.svg" />
|
||||
@@ -36,24 +36,29 @@
|
||||
|
||||
好好享受! 🙂
|
||||
|
||||
* [特征](#-特征)
|
||||
* [版本要求](#-版本要求)
|
||||
* [示例项目](#-示例)
|
||||
* [安装](#-安装)
|
||||
* [Cocoapods](#使用-cocoapods)
|
||||
* [Carthage](#使用-carthage)
|
||||
* [如何使用](#-如何使用)
|
||||
* [集合](#-集合)
|
||||
* [多行文字](#-多行文字)
|
||||
* [自定义颜色](#-自定义颜色)
|
||||
* [自定义动画](#-自定义动画)
|
||||
* [等级制度](#-等级制度)
|
||||
* [文档](#-文档)
|
||||
* [下一步](#-下一步)
|
||||
* [特约](#-特约)
|
||||
* [提及](#-提及)
|
||||
* [作者](#-作者)
|
||||
* [许可证](#-许可证)
|
||||
- [🌟 特征](#-特征)
|
||||
- [📋 版本要求](#-版本要求)
|
||||
- [🔮 示例](#-示例)
|
||||
- [📲 安装](#-安装)
|
||||
- [使用 CocoaPods](#使用-cocoapods)
|
||||
- [使用 Carthage](#使用-carthage)
|
||||
- [🐒 如何使用](#-如何使用)
|
||||
- [🌿 集合](#-集合)
|
||||
- [UITableView](#uitableview)
|
||||
- [UICollectionView](#uicollectionview)
|
||||
- [📰 多行文字](#-多行文字)
|
||||
- [🎛 定制](#-定制)
|
||||
- [🎨 自定义颜色](#-自定义颜色)
|
||||
- [从网站 https://flatuicolors.com捕获的图像](#从网站-httpsflatuicolorscom捕获的图像)
|
||||
- [🤓 自定义动画](#-自定义动画)
|
||||
- [👨👧👦 等级制度](#-等级制度)
|
||||
- [📚 文档](#-文档)
|
||||
- [📬 下一步](#-下一步)
|
||||
- [❤️ 特约](#️-特约)
|
||||
- [使用 SwiftPlate 生成的项目](#使用-swiftplate-生成的项目)
|
||||
- [📢 提及](#-提及)
|
||||
- [👨🏻💻 作者](#-作者)
|
||||
- [👮🏻 许可证](#-许可证)
|
||||
|
||||
|
||||
## 🌟 特征
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "SkeletonView"
|
||||
s.version = "1.11.0"
|
||||
s.version = "1.20.0"
|
||||
s.summary = "An elegant way to show users that something is happening and also prepare them to which contents he is waiting"
|
||||
s.description = <<-DESC
|
||||
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.
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
52D6D97B1BEFF229002C0205 = {
|
||||
CreatedOnToolsVersion = 7.1;
|
||||
LastSwiftMigration = 1000;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
F5F899F11FABA607002E8FDA = {
|
||||
CreatedOnToolsVersion = 9.1;
|
||||
@@ -1117,8 +1118,10 @@
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1131,6 +1134,8 @@
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.SkeletonView.SkeletonView-iOS";
|
||||
PRODUCT_NAME = SkeletonView;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -1142,8 +1147,10 @@
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1155,6 +1162,8 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.SkeletonView.SkeletonView-iOS";
|
||||
PRODUCT_NAME = SkeletonView;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -29,6 +29,18 @@ extension CollectionSkeleton where Self: UIScrollView {
|
||||
var estimatedNumberOfRows: Int { return 0 }
|
||||
func addDummyDataSource() {}
|
||||
func removeDummyDataSource(reloadAfter: Bool) {}
|
||||
func disableUserInteraction() { isUserInteractionEnabled = false; isScrollEnabled = false }
|
||||
func enableUserInteraction() { isUserInteractionEnabled = true; isScrollEnabled = true }
|
||||
|
||||
func disableUserInteraction() {
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = false
|
||||
isScrollEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
func enableUserInteraction() {
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = true
|
||||
isScrollEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,20 +13,28 @@ public protocol SkeletonCollectionViewDataSource: UICollectionViewDataSource {
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, cellIdentifierForItemAt indexPath: IndexPath) -> ReusableCellIdentifier
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, supplementaryViewIdentifierOfKind: String, at indexPath: IndexPath) -> ReusableCellIdentifier?
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, skeletonCellForItemAt indexPath: IndexPath) -> UICollectionViewCell?
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath)
|
||||
}
|
||||
|
||||
public extension SkeletonCollectionViewDataSource {
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return skeletonView.estimatedNumberOfRows
|
||||
UICollectionView.automaticNumberOfSkeletonItems
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView,
|
||||
supplementaryViewIdentifierOfKind: String,
|
||||
at indexPath: IndexPath) -> ReusableCellIdentifier? {
|
||||
return nil
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, supplementaryViewIdentifierOfKind: String, at indexPath: IndexPath) -> ReusableCellIdentifier? {
|
||||
nil
|
||||
}
|
||||
|
||||
func numSections(in collectionSkeletonView: UICollectionView) -> Int { return 1 }
|
||||
func numSections(in collectionSkeletonView: UICollectionView) -> Int {
|
||||
1
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, skeletonCellForItemAt indexPath: IndexPath) -> UICollectionViewCell? {
|
||||
nil
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath) { }
|
||||
}
|
||||
|
||||
public protocol SkeletonCollectionViewDelegate: UICollectionViewDelegate { }
|
||||
|
||||
@@ -7,11 +7,20 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
extension UICollectionView: CollectionSkeleton {
|
||||
public static let automaticNumberOfSkeletonItems = -1
|
||||
|
||||
var estimatedNumberOfRows: Int {
|
||||
guard let flowlayout = collectionViewLayout as? UICollectionViewFlowLayout else { return 0 }
|
||||
return Int(ceil(frame.height / flowlayout.itemSize.height))
|
||||
switch flowlayout.scrollDirection {
|
||||
case .vertical:
|
||||
return Int(ceil(frame.height / flowlayout.itemSize.height))
|
||||
case .horizontal:
|
||||
return Int(ceil(frame.width / flowlayout.itemSize.width))
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
var skeletonDataSource: SkeletonCollectionDataSource? {
|
||||
|
||||
@@ -28,16 +28,35 @@ class SkeletonCollectionDataSource: NSObject {
|
||||
// MARK: - UITableViewDataSource
|
||||
extension SkeletonCollectionDataSource: UITableViewDataSource {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return originalTableViewDataSource?.numSections(in: tableView) ?? 0
|
||||
originalTableViewDataSource?.numSections(in: tableView) ?? 0
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return originalTableViewDataSource?.collectionSkeletonView(tableView, numberOfRowsInSection: section) ?? 0
|
||||
guard let originalTableViewDataSource = originalTableViewDataSource else {
|
||||
return 0
|
||||
}
|
||||
|
||||
let numberOfRows = originalTableViewDataSource.collectionSkeletonView(tableView, numberOfRowsInSection: section)
|
||||
|
||||
if numberOfRows == UITableView.automaticNumberOfSkeletonRows {
|
||||
return tableView.estimatedNumberOfRows
|
||||
} else {
|
||||
return numberOfRows
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cellIdentifier = originalTableViewDataSource?.collectionSkeletonView(tableView, cellIdentifierForRowAt: indexPath) ?? ""
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
|
||||
guard let cell = originalTableViewDataSource?.collectionSkeletonView(tableView, skeletonCellForRowAt: indexPath) else {
|
||||
let cellIdentifier = originalTableViewDataSource?.collectionSkeletonView(tableView, cellIdentifierForRowAt: indexPath) ?? ""
|
||||
let fakeCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
|
||||
|
||||
originalTableViewDataSource?.collectionSkeletonView(tableView, prepareCellForSkeleton: fakeCell, at: indexPath)
|
||||
skeletonViewIfContainerSkeletonIsActive(container: tableView, view: fakeCell)
|
||||
|
||||
return fakeCell
|
||||
}
|
||||
|
||||
originalTableViewDataSource?.collectionSkeletonView(tableView, prepareCellForSkeleton: cell, at: indexPath)
|
||||
skeletonViewIfContainerSkeletonIsActive(container: tableView, view: cell)
|
||||
return cell
|
||||
}
|
||||
@@ -46,16 +65,35 @@ extension SkeletonCollectionDataSource: UITableViewDataSource {
|
||||
// MARK: - UICollectionViewDataSource
|
||||
extension SkeletonCollectionDataSource: UICollectionViewDataSource {
|
||||
func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||
return originalCollectionViewDataSource?.numSections(in: collectionView) ?? 0
|
||||
originalCollectionViewDataSource?.numSections(in: collectionView) ?? 0
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return originalCollectionViewDataSource?.collectionSkeletonView(collectionView, numberOfItemsInSection: section) ?? 0
|
||||
guard let originalCollectionViewDataSource = originalCollectionViewDataSource else {
|
||||
return 0
|
||||
}
|
||||
|
||||
let numberOfItems = originalCollectionViewDataSource.collectionSkeletonView(collectionView, numberOfItemsInSection: section)
|
||||
|
||||
if numberOfItems == UICollectionView.automaticNumberOfSkeletonItems {
|
||||
return collectionView.estimatedNumberOfRows
|
||||
} else {
|
||||
return numberOfItems
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cellIdentifier = originalCollectionViewDataSource?.collectionSkeletonView(collectionView, cellIdentifierForItemAt: indexPath) ?? ""
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
|
||||
guard let cell = originalCollectionViewDataSource?.collectionSkeletonView(collectionView, skeletonCellForItemAt: indexPath) else {
|
||||
let cellIdentifier = originalCollectionViewDataSource?.collectionSkeletonView(collectionView, cellIdentifierForItemAt: indexPath) ?? ""
|
||||
let fakeCell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
|
||||
|
||||
originalCollectionViewDataSource?.collectionSkeletonView(collectionView, prepareCellForSkeleton: fakeCell, at: indexPath)
|
||||
skeletonViewIfContainerSkeletonIsActive(container: collectionView, view: fakeCell)
|
||||
|
||||
return fakeCell
|
||||
}
|
||||
|
||||
originalCollectionViewDataSource?.collectionSkeletonView(collectionView, prepareCellForSkeleton: cell, at: indexPath)
|
||||
skeletonViewIfContainerSkeletonIsActive(container: collectionView, view: cell)
|
||||
return cell
|
||||
}
|
||||
@@ -69,7 +107,7 @@ extension SkeletonCollectionDataSource: UICollectionViewDataSource {
|
||||
return view
|
||||
}
|
||||
|
||||
return UICollectionReusableView()
|
||||
return originalCollectionViewDataSource?.collectionView?(collectionView, viewForSupplementaryElementOfKind: kind, at: indexPath) ?? UICollectionReusableView()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,23 +21,11 @@ class SkeletonCollectionDelegate: NSObject {
|
||||
// MARK: - UITableViewDelegate
|
||||
extension SkeletonCollectionDelegate: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
if let viewIdentifier = originalTableViewDelegate?.collectionSkeletonView(tableView, identifierForHeaderInSection: section),
|
||||
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: viewIdentifier) {
|
||||
skeletonViewIfContainerSkeletonIsActive(container: tableView, view: header)
|
||||
return header
|
||||
}
|
||||
|
||||
return nil
|
||||
headerOrFooterView(tableView, for: originalTableViewDelegate?.collectionSkeletonView(tableView, identifierForHeaderInSection: section))
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||
if let viewIdentifier = originalTableViewDelegate?.collectionSkeletonView(tableView, identifierForFooterInSection: section),
|
||||
let footer = tableView.dequeueReusableHeaderFooterView(withIdentifier: viewIdentifier) {
|
||||
skeletonViewIfContainerSkeletonIsActive(container: tableView, view: footer)
|
||||
return footer
|
||||
}
|
||||
|
||||
return nil
|
||||
headerOrFooterView(tableView, for: originalTableViewDelegate?.collectionSkeletonView(tableView, identifierForFooterInSection: section))
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didEndDisplayingHeaderView view: UIView, forSection section: Int) {
|
||||
@@ -54,6 +42,12 @@ extension SkeletonCollectionDelegate: UITableViewDelegate {
|
||||
cell.hideSkeleton()
|
||||
originalTableViewDelegate?.tableView?(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||
}
|
||||
|
||||
private func headerOrFooterView(_ tableView: UITableView, for viewIdentifier: String? ) -> UIView? {
|
||||
guard let viewIdentifier = viewIdentifier, let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: viewIdentifier) else { return nil }
|
||||
skeletonViewIfContainerSkeletonIsActive(container: tableView, view: header)
|
||||
return header
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegate
|
||||
|
||||
@@ -12,11 +12,13 @@ public protocol SkeletonTableViewDataSource: UITableViewDataSource {
|
||||
func numSections(in collectionSkeletonView: UITableView) -> Int
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell?
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath)
|
||||
}
|
||||
|
||||
public extension SkeletonTableViewDataSource {
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return skeletonView.estimatedNumberOfRows
|
||||
return UITableView.automaticNumberOfSkeletonRows
|
||||
}
|
||||
|
||||
func numSections(in collectionSkeletonView: UITableView) -> Int { return 1 }
|
||||
@@ -27,6 +29,12 @@ public extension SkeletonTableViewDataSource {
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, cellIdenfierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier {
|
||||
return collectionSkeletonView(skeletonView, cellIdentifierForRowAt: indexPath)
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell? {
|
||||
nil
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath) { }
|
||||
}
|
||||
|
||||
public protocol SkeletonTableViewDelegate: UITableViewDelegate {
|
||||
|
||||
@@ -11,6 +11,8 @@ import UIKit
|
||||
public typealias ReusableHeaderFooterIdentifier = String
|
||||
|
||||
extension UITableView: CollectionSkeleton {
|
||||
public static let automaticNumberOfSkeletonRows = -1
|
||||
|
||||
var estimatedNumberOfRows: Int {
|
||||
return Int(ceil(frame.height / rowHeight))
|
||||
}
|
||||
|
||||
@@ -125,8 +125,12 @@ extension CALayer {
|
||||
private func calculateNumLines(for config: SkeletonMultilinesLayerConfig) -> Int {
|
||||
let definedNumberOfLines = config.lines
|
||||
let requiredSpaceForEachLine = config.lineHeight + config.multilineSpacing
|
||||
let calculatedNumberOfLines = Int(round(CGFloat(bounds.height - config.paddingInsets.top - config.paddingInsets.bottom) / CGFloat(requiredSpaceForEachLine)))
|
||||
|
||||
let neededLines = round(CGFloat(bounds.height - config.paddingInsets.top - config.paddingInsets.bottom) / CGFloat(requiredSpaceForEachLine))
|
||||
guard neededLines.isNormal else {
|
||||
return 0
|
||||
}
|
||||
|
||||
let calculatedNumberOfLines = Int(neededLines)
|
||||
guard calculatedNumberOfLines > 0 else {
|
||||
return 1
|
||||
}
|
||||
@@ -152,7 +156,8 @@ public extension CALayer {
|
||||
var pulse: CAAnimation {
|
||||
let pulseAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.backgroundColor))
|
||||
pulseAnimation.fromValue = backgroundColor
|
||||
//swiftlint:disable:next force_unwrapping
|
||||
|
||||
// swiftlint:disable:next force_unwrapping
|
||||
pulseAnimation.toValue = UIColor(cgColor: backgroundColor!).complementaryColor.cgColor
|
||||
pulseAnimation.duration = 1
|
||||
pulseAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
||||
|
||||
@@ -4,10 +4,10 @@ import Foundation
|
||||
|
||||
extension Int {
|
||||
var whitespace: String {
|
||||
return whitespaces
|
||||
whitespaces
|
||||
}
|
||||
|
||||
var whitespaces: String {
|
||||
return String(repeating: " ", count: self)
|
||||
String(repeating: " ", count: self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,11 @@ extension UIColor {
|
||||
}
|
||||
|
||||
public var lighter: UIColor {
|
||||
return adjust(by: 1.35)
|
||||
adjust(by: 1.35)
|
||||
}
|
||||
|
||||
public var darker: UIColor {
|
||||
return adjust(by: 0.94)
|
||||
adjust(by: 0.94)
|
||||
}
|
||||
|
||||
func adjust(by percent: CGFloat) -> UIColor {
|
||||
@@ -45,7 +45,7 @@ extension UIColor {
|
||||
}
|
||||
|
||||
func makeGradient() -> [UIColor] {
|
||||
return [self, self.complementaryColor, self]
|
||||
[self, self.complementaryColor, self]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,17 @@ extension UIView {
|
||||
nonContentSizeLayoutConstraints.filter { $0.firstAttribute == NSLayoutConstraint.Attribute.height }
|
||||
}
|
||||
|
||||
var skeletonHeightConstraints: [NSLayoutConstraint] {
|
||||
nonContentSizeLayoutConstraints.filter {
|
||||
$0.firstAttribute == NSLayoutConstraint.Attribute.height
|
||||
&& $0.identifier?.contains("SkeletonView.Constraint.Height") ?? false
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setHeight(equalToConstant constant: CGFloat) -> NSLayoutConstraint {
|
||||
let heightConstraint = heightAnchor.constraint(equalToConstant: constant)
|
||||
heightConstraint.identifier = "SkeletonView.Constraint.Height.\(constant)"
|
||||
NSLayoutConstraint.activate([heightConstraint])
|
||||
return heightConstraint
|
||||
}
|
||||
|
||||
@@ -14,8 +14,11 @@ enum ViewAssociatedKeys {
|
||||
static var labelViewState = "labelViewState"
|
||||
static var imageViewState = "imageViewState"
|
||||
static var buttonViewState = "buttonViewState"
|
||||
static var headerFooterViewState = "headerFooterViewState"
|
||||
static var currentSkeletonConfig = "currentSkeletonConfig"
|
||||
static var skeletonCornerRadius = "skeletonCornerRadius"
|
||||
static var disabledWhenSkeletonIsActive = "disabledWhenSkeletonIsActive"
|
||||
static var delayedShowSkeletonWorkItem = "delayedShowSkeletonWorkItem"
|
||||
}
|
||||
// codebeat:enable[TOO_MANY_IVARS]
|
||||
|
||||
@@ -49,4 +52,13 @@ extension UIView {
|
||||
get { return ao_get(pkey: &ViewAssociatedKeys.isSkeletonAnimated) as? Bool ?? false }
|
||||
set { ao_set(newValue, pkey: &ViewAssociatedKeys.isSkeletonAnimated) }
|
||||
}
|
||||
|
||||
var isSuperviewAStackView: Bool {
|
||||
superview is UIStackView
|
||||
}
|
||||
|
||||
var delayedShowSkeletonWorkItem: DispatchWorkItem? {
|
||||
get { return ao_get(pkey: &ViewAssociatedKeys.delayedShowSkeletonWorkItem) as? DispatchWorkItem }
|
||||
set { ao_setOptional(newValue, pkey: &ViewAssociatedKeys.delayedShowSkeletonWorkItem) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ public extension UIView {
|
||||
get { return hiddenWhenSkeletonIsActive }
|
||||
set { hiddenWhenSkeletonIsActive = newValue }
|
||||
}
|
||||
|
||||
@IBInspectable
|
||||
var isUserInteractionDisabledWhenSkeletonIsActive: Bool {
|
||||
get { return disabledWhenSkeletonIsActive }
|
||||
set { disabledWhenSkeletonIsActive = newValue }
|
||||
}
|
||||
|
||||
@IBInspectable
|
||||
var skeletonCornerRadius: Float {
|
||||
@@ -34,6 +40,11 @@ public extension UIView {
|
||||
get { return ao_get(pkey: &ViewAssociatedKeys.hiddenWhenSkeletonIsActive) as? Bool ?? false }
|
||||
set { ao_set(newValue, pkey: &ViewAssociatedKeys.hiddenWhenSkeletonIsActive) }
|
||||
}
|
||||
|
||||
private var disabledWhenSkeletonIsActive: Bool {
|
||||
get { return ao_get(pkey: &ViewAssociatedKeys.disabledWhenSkeletonIsActive) as? Bool ?? true }
|
||||
set { ao_set(newValue, pkey: &ViewAssociatedKeys.disabledWhenSkeletonIsActive) }
|
||||
}
|
||||
|
||||
private var skeletonableCornerRadius: Float {
|
||||
get { return ao_get(pkey: &ViewAssociatedKeys.skeletonCornerRadius) as? Float ?? 0.0 }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
//Partially copy/pasted from https://github.com/jameslintaylor/AssociatedObjects/blob/master/AssociatedObjects/AssociatedObjects.swift
|
||||
// Partially copy/pasted from https://github.com/jameslintaylor/AssociatedObjects/blob/master/AssociatedObjects/AssociatedObjects.swift
|
||||
enum AssociationPolicy: UInt {
|
||||
// raw values map to objc_AssociationPolicy's raw values
|
||||
case assign = 0
|
||||
@@ -17,7 +17,7 @@ enum AssociationPolicy: UInt {
|
||||
}
|
||||
}
|
||||
|
||||
protocol AssociatedObjects: class { }
|
||||
protocol AssociatedObjects: AnyObject { }
|
||||
|
||||
// transparent wrappers
|
||||
extension AssociatedObjects {
|
||||
|
||||
@@ -10,16 +10,18 @@ import UIKit
|
||||
|
||||
extension UIView {
|
||||
@objc func prepareViewForSkeleton() {
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
startTransition { [weak self] in
|
||||
self?.backgroundColor = .clear
|
||||
self?.isUserInteractionEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UILabel {
|
||||
var desiredHeightBasedOnNumberOfLines: CGFloat {
|
||||
let lineHeight = multilineTextFont?.lineHeight ?? SkeletonAppearance.default.multilineHeight
|
||||
let spaceNeededForEachLine = lineHeight * CGFloat(numberOfLines)
|
||||
let spaceNeededForSpaces = skeletonLineSpacing * CGFloat(numberOfLines - 1)
|
||||
let padding = paddingInsets.top + paddingInsets.bottom
|
||||
@@ -28,7 +30,13 @@ extension UILabel {
|
||||
}
|
||||
|
||||
func updateHeightConstraintsIfNeeded() {
|
||||
guard numberOfLines > 1 else { return }
|
||||
guard numberOfLines > 1 || numberOfLines == 0 else { return }
|
||||
|
||||
// Workaround to simulate content when the label is contained in a `UIStackView`.
|
||||
if isSuperviewAStackView, bounds.height == 0 {
|
||||
// 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.
|
||||
text = " "
|
||||
}
|
||||
|
||||
let desiredHeight = desiredHeightBasedOnNumberOfLines
|
||||
if desiredHeight > definedMaxHeight {
|
||||
@@ -38,15 +46,19 @@ extension UILabel {
|
||||
}
|
||||
}
|
||||
|
||||
func restoreBackupHeightConstraints() {
|
||||
func restoreBackupHeightConstraintsIfNeeded() {
|
||||
guard !backupHeightConstraints.isEmpty else { return }
|
||||
NSLayoutConstraint.deactivate(heightConstraints)
|
||||
NSLayoutConstraint.activate(backupHeightConstraints)
|
||||
backupHeightConstraints.removeAll()
|
||||
}
|
||||
|
||||
override func prepareViewForSkeleton() {
|
||||
backgroundColor = .clear
|
||||
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
resignFirstResponder()
|
||||
startTransition { [weak self] in
|
||||
self?.updateHeightConstraintsIfNeeded()
|
||||
@@ -58,6 +70,11 @@ extension UILabel {
|
||||
extension UITextView {
|
||||
override func prepareViewForSkeleton() {
|
||||
backgroundColor = .clear
|
||||
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
resignFirstResponder()
|
||||
startTransition { [weak self] in
|
||||
self?.textColor = .clear
|
||||
@@ -65,9 +82,26 @@ extension UITextView {
|
||||
}
|
||||
}
|
||||
|
||||
extension UITextField {
|
||||
override func prepareViewForSkeleton() {
|
||||
backgroundColor = .clear
|
||||
resignFirstResponder()
|
||||
|
||||
startTransition { [weak self] in
|
||||
self?.textColor = .clear
|
||||
self?.placeholder = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImageView {
|
||||
override func prepareViewForSkeleton() {
|
||||
backgroundColor = .clear
|
||||
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
startTransition { [weak self] in
|
||||
self?.image = nil
|
||||
}
|
||||
@@ -77,8 +111,23 @@ extension UIImageView {
|
||||
extension UIButton {
|
||||
override func prepareViewForSkeleton() {
|
||||
backgroundColor = .clear
|
||||
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
startTransition { [weak self] in
|
||||
self?.setTitle(nil, for: .normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UITableViewHeaderFooterView {
|
||||
override func prepareViewForSkeleton() {
|
||||
backgroundView?.backgroundColor = .clear
|
||||
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,28 @@ import Foundation
|
||||
|
||||
extension DispatchQueue {
|
||||
private static var _onceTracker = [String]()
|
||||
|
||||
|
||||
class func once(token: String, block: () -> Void) {
|
||||
objc_sync_enter(self); defer { objc_sync_exit(self) }
|
||||
guard !_onceTracker.contains(token) else { return }
|
||||
|
||||
|
||||
_onceTracker.append(token)
|
||||
block()
|
||||
}
|
||||
|
||||
class func removeOnce(token: String, block: () -> Void) {
|
||||
objc_sync_enter(self); defer { objc_sync_exit(self) }
|
||||
guard let index = _onceTracker.firstIndex(of: token) else { return }
|
||||
_onceTracker.remove(at: index)
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
func swizzle(selector originalSelector: Selector, with swizzledSelector: Selector, inClass: AnyClass, usingClass: AnyClass) {
|
||||
guard let originalMethod = class_getInstanceMethod(inClass, originalSelector),
|
||||
let swizzledMethod = class_getInstanceMethod(usingClass, swizzledSelector)
|
||||
else { return }
|
||||
|
||||
|
||||
if class_addMethod(inClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)) {
|
||||
class_replaceMethod(inClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
|
||||
} else {
|
||||
|
||||
@@ -11,8 +11,8 @@ enum MultilineAssociatedKeys {
|
||||
}
|
||||
|
||||
protocol ContainsMultilineText {
|
||||
var multilineTextFont: UIFont? { get }
|
||||
var numLines: Int { get }
|
||||
var lineHeight: CGFloat { get }
|
||||
var numberOfLines: Int { get }
|
||||
var lastLineFillingPercent: Int { get }
|
||||
var multilineCornerRadius: Int { get }
|
||||
var multilineSpacing: CGFloat { get }
|
||||
|
||||
@@ -12,13 +12,13 @@ public extension UILabel {
|
||||
@IBInspectable
|
||||
var linesCornerRadius: Int {
|
||||
get { return multilineCornerRadius }
|
||||
set { multilineCornerRadius = min(newValue, 10) }
|
||||
set { multilineCornerRadius = newValue }
|
||||
}
|
||||
|
||||
@IBInspectable
|
||||
var skeletonLineSpacing: CGFloat {
|
||||
get { return multilineSpacing }
|
||||
set { multilineSpacing = min(newValue, 10) }
|
||||
set { multilineSpacing = newValue }
|
||||
}
|
||||
|
||||
var skeletonPaddingInsets: UIEdgeInsets {
|
||||
@@ -27,15 +27,11 @@ public extension UILabel {
|
||||
}
|
||||
}
|
||||
|
||||
extension UILabel: ContainsMultilineText {
|
||||
var multilineTextFont: UIFont? {
|
||||
return font
|
||||
}
|
||||
|
||||
var numLines: Int {
|
||||
return numberOfLines
|
||||
extension UILabel: ContainsMultilineText {
|
||||
var lineHeight: CGFloat {
|
||||
backupHeightConstraints.first?.constant ?? SkeletonAppearance.default.multilineHeight
|
||||
}
|
||||
|
||||
|
||||
var lastLineFillingPercent: Int {
|
||||
get { return ao_get(pkey: &MultilineAssociatedKeys.lastLineFillingPercent) as? Int ?? SkeletonAppearance.default.multilineLastLineFillPercent }
|
||||
set { ao_set(newValue, pkey: &MultilineAssociatedKeys.lastLineFillingPercent) }
|
||||
|
||||
@@ -12,13 +12,13 @@ public extension UITextView {
|
||||
@IBInspectable
|
||||
var linesCornerRadius: Int {
|
||||
get { return multilineCornerRadius }
|
||||
set { multilineCornerRadius = min(newValue, 10) }
|
||||
set { multilineCornerRadius = newValue }
|
||||
}
|
||||
|
||||
@IBInspectable
|
||||
var skeletonLineSpacing: CGFloat {
|
||||
get { return multilineSpacing }
|
||||
set { multilineSpacing = min(newValue, 10) }
|
||||
set { multilineSpacing = newValue }
|
||||
}
|
||||
|
||||
var skeletonPaddingInsets: UIEdgeInsets {
|
||||
@@ -28,11 +28,19 @@ public extension UITextView {
|
||||
}
|
||||
|
||||
extension UITextView: ContainsMultilineText {
|
||||
var multilineTextFont: UIFont? {
|
||||
font
|
||||
var lineHeight: CGFloat {
|
||||
if let fontLineHeight = font?.lineHeight {
|
||||
if let heightConstraints = heightConstraints.first?.constant {
|
||||
return (fontLineHeight > heightConstraints) ? heightConstraints : fontLineHeight
|
||||
}
|
||||
|
||||
return fontLineHeight
|
||||
}
|
||||
|
||||
return SkeletonAppearance.default.multilineHeight
|
||||
}
|
||||
|
||||
var numLines: Int {
|
||||
var numberOfLines: Int {
|
||||
-1
|
||||
}
|
||||
|
||||
|
||||
@@ -27,12 +27,17 @@ extension UIView: Recoverable {
|
||||
guard let storedViewState = viewState else { return }
|
||||
|
||||
startTransition { [weak self] in
|
||||
self?.layer.cornerRadius = storedViewState.cornerRadius
|
||||
self?.layer.masksToBounds = storedViewState.clipToBounds
|
||||
self?.isUserInteractionEnabled = storedViewState.isUserInteractionsEnabled
|
||||
guard let self = self else { return }
|
||||
|
||||
if self?.backgroundColor == .clear || forced {
|
||||
self?.backgroundColor = storedViewState.backgroundColor
|
||||
self.layer.cornerRadius = storedViewState.cornerRadius
|
||||
self.layer.masksToBounds = storedViewState.clipToBounds
|
||||
|
||||
if self.isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
self.isUserInteractionEnabled = storedViewState.isUserInteractionsEnabled
|
||||
}
|
||||
|
||||
if self.backgroundColor == .clear || forced {
|
||||
self.backgroundColor = storedViewState.backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,12 +57,16 @@ extension UILabel {
|
||||
override func recoverViewState(forced: Bool) {
|
||||
super.recoverViewState(forced: forced)
|
||||
startTransition { [weak self] in
|
||||
guard let storedLabelState = self?.labelState else { return }
|
||||
guard let self = self,
|
||||
let storedLabelState = self.labelState else {
|
||||
return
|
||||
}
|
||||
|
||||
self?.restoreBackupHeightConstraints()
|
||||
NSLayoutConstraint.deactivate(self.skeletonHeightConstraints)
|
||||
self.restoreBackupHeightConstraintsIfNeeded()
|
||||
|
||||
if self?.textColor == .clear || forced {
|
||||
self?.textColor = storedLabelState.textColor
|
||||
if self.textColor == .clear || forced {
|
||||
self.textColor = storedLabelState.textColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,6 +95,33 @@ extension UITextView {
|
||||
}
|
||||
}
|
||||
|
||||
extension UITextField {
|
||||
var textState: RecoverableTextFieldState? {
|
||||
get { return ao_get(pkey: &ViewAssociatedKeys.labelViewState) as? RecoverableTextFieldState }
|
||||
set { ao_setOptional(newValue, pkey: &ViewAssociatedKeys.labelViewState) }
|
||||
}
|
||||
|
||||
override func saveViewState() {
|
||||
super.saveViewState()
|
||||
textState = RecoverableTextFieldState(view: self)
|
||||
}
|
||||
|
||||
override func recoverViewState(forced: Bool) {
|
||||
super.recoverViewState(forced: forced)
|
||||
startTransition { [weak self] in
|
||||
guard let storedLabelState = self?.textState else { return }
|
||||
|
||||
if self?.textColor == .clear || forced {
|
||||
self?.textColor = storedLabelState.textColor
|
||||
}
|
||||
|
||||
if self?.placeholder == nil || forced {
|
||||
self?.placeholder = storedLabelState.placeholder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImageView {
|
||||
var imageState: RecoverableImageViewState? {
|
||||
get { return ao_get(pkey: &ViewAssociatedKeys.imageViewState) as? RecoverableImageViewState }
|
||||
@@ -125,3 +161,22 @@ extension UIButton {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UITableViewHeaderFooterView {
|
||||
var headerFooterState: RecoverableTableViewHeaderFooterViewState? {
|
||||
get { return ao_get(pkey: &ViewAssociatedKeys.headerFooterViewState) as? RecoverableTableViewHeaderFooterViewState }
|
||||
set { ao_setOptional(newValue, pkey: &ViewAssociatedKeys.headerFooterViewState) }
|
||||
}
|
||||
|
||||
override func saveViewState() {
|
||||
super.saveViewState()
|
||||
headerFooterState = RecoverableTableViewHeaderFooterViewState(view: self)
|
||||
}
|
||||
|
||||
override func recoverViewState(forced: Bool) {
|
||||
super.recoverViewState(forced: forced)
|
||||
startTransition { [weak self] in
|
||||
self?.backgroundView?.backgroundColor = self?.headerFooterState?.backgroundViewColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,16 @@ struct RecoverableTextViewState {
|
||||
}
|
||||
}
|
||||
|
||||
struct RecoverableTextFieldState {
|
||||
var textColor: UIColor?
|
||||
var placeholder: String?
|
||||
|
||||
init(view: UITextField) {
|
||||
self.textColor = view.textColor
|
||||
self.placeholder = view.placeholder
|
||||
}
|
||||
}
|
||||
|
||||
struct RecoverableImageViewState {
|
||||
var image: UIImage?
|
||||
|
||||
@@ -49,3 +59,11 @@ struct RecoverableButtonViewState {
|
||||
self.title = view.titleLabel?.text
|
||||
}
|
||||
}
|
||||
|
||||
struct RecoverableTableViewHeaderFooterViewState {
|
||||
var backgroundViewColor: UIColor?
|
||||
|
||||
init(view: UITableViewHeaderFooterView) {
|
||||
self.backgroundViewColor = view.backgroundView?.backgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public class SkeletonAnimationBuilder {
|
||||
public init() { }
|
||||
|
||||
public func makeSlidingAnimation(withDirection direction: GradientDirection, duration: CFTimeInterval = 1.5, autoreverses: Bool = false) -> SkeletonLayerAnimation {
|
||||
return { layer in
|
||||
return { _ in
|
||||
let startPointAnim = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.startPoint))
|
||||
startPointAnim.fromValue = direction.startPoint.from
|
||||
startPointAnim.toValue = direction.startPoint.to
|
||||
|
||||
@@ -83,9 +83,8 @@ struct SkeletonLayer {
|
||||
/// If there is more than one line, or custom preferences have been set for a single line, draw custom layers
|
||||
func addTextLinesIfNeeded() {
|
||||
guard let textView = holderAsTextView else { return }
|
||||
let lineHeight = textView.multilineTextFont?.lineHeight ?? SkeletonAppearance.default.multilineHeight
|
||||
let config = SkeletonMultilinesLayerConfig(lines: textView.numLines,
|
||||
lineHeight: lineHeight,
|
||||
let config = SkeletonMultilinesLayerConfig(lines: textView.numberOfLines,
|
||||
lineHeight: textView.lineHeight,
|
||||
type: type,
|
||||
lastLineFillPercent: textView.lastLineFillingPercent,
|
||||
multilineCornerRadius: textView.multilineCornerRadius,
|
||||
@@ -98,9 +97,8 @@ struct SkeletonLayer {
|
||||
|
||||
func updateLinesIfNeeded() {
|
||||
guard let textView = holderAsTextView else { return }
|
||||
let lineHeight = textView.multilineTextFont?.lineHeight ?? SkeletonAppearance.default.multilineHeight
|
||||
let config = SkeletonMultilinesLayerConfig(lines: textView.numLines,
|
||||
lineHeight: lineHeight,
|
||||
let config = SkeletonMultilinesLayerConfig(lines: textView.numberOfLines,
|
||||
lineHeight: textView.lineHeight,
|
||||
type: type,
|
||||
lastLineFillPercent: textView.lastLineFillingPercent,
|
||||
multilineCornerRadius: textView.multilineCornerRadius,
|
||||
@@ -113,7 +111,7 @@ struct SkeletonLayer {
|
||||
|
||||
var holderAsTextView: ContainsMultilineText? {
|
||||
guard let textView = holder as? ContainsMultilineText,
|
||||
(textView.numLines == -1 || textView.numLines == 0 || textView.numLines > 1 || textView.numLines == 1 && !SkeletonAppearance.default.renderSingleLineAsView) else {
|
||||
(textView.numberOfLines == -1 || textView.numberOfLines == 0 || textView.numberOfLines > 1 || textView.numberOfLines == 1 && !SkeletonAppearance.default.renderSingleLineAsView) else {
|
||||
return nil
|
||||
}
|
||||
return textView
|
||||
|
||||
@@ -9,20 +9,63 @@ public extension UIView {
|
||||
/// - color: The color of the skeleton. Defaults to `SkeletonAppearance.default.tintColor`.
|
||||
/// - transition: The style of the transition when the skeleton appears. Defaults to `.crossDissolve(0.25)`.
|
||||
func showSkeleton(usingColor color: UIColor = SkeletonAppearance.default.tintColor, transition: SkeletonTransitionStyle = .crossDissolve(0.25)) {
|
||||
delayedShowSkeletonWorkItem?.cancel()
|
||||
let config = SkeletonConfig(type: .solid, colors: [color], transition: transition)
|
||||
showSkeleton(skeletonConfig: config)
|
||||
}
|
||||
|
||||
/// Shows the skeleton using the view that calls this method as root view.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - color: The color of the skeleton. Defaults to `SkeletonAppearance.default.tintColor`.
|
||||
/// - animated: If the skeleton is animated or not. Defaults to `true`.
|
||||
/// - delay: The amount of time (measured in seconds) to wait before show the skeleton.
|
||||
/// - transition: The style of the transition when the skeleton appears. Defaults to `.crossDissolve(0.25)`.
|
||||
func showSkeleton(usingColor color: UIColor = SkeletonAppearance.default.tintColor, animated: Bool = true, delay: TimeInterval, transition: SkeletonTransitionStyle = .crossDissolve(0.25)) {
|
||||
delayedShowSkeletonWorkItem?.cancel()
|
||||
|
||||
delayedShowSkeletonWorkItem = DispatchWorkItem { [weak self] in
|
||||
let config = SkeletonConfig(type: .solid, colors: [color], animated: animated, transition: transition)
|
||||
self?.showSkeleton(skeletonConfig: config)
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: delayedShowSkeletonWorkItem!)
|
||||
}
|
||||
|
||||
/// Shows the gradient skeleton without animation using the view that calls this method as root view.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - gradient: The gradient of the skeleton. Defaults to `SkeletonAppearance.default.gradient`.
|
||||
/// - transition: The style of the transition when the skeleton appears. Defaults to `.crossDissolve(0.25)`.
|
||||
func showGradientSkeleton(usingGradient gradient: SkeletonGradient = SkeletonAppearance.default.gradient, transition: SkeletonTransitionStyle = .crossDissolve(0.25)) {
|
||||
delayedShowSkeletonWorkItem?.cancel()
|
||||
let config = SkeletonConfig(type: .gradient, colors: gradient.colors, transition: transition)
|
||||
showSkeleton(skeletonConfig: config)
|
||||
}
|
||||
|
||||
/// Shows the gradient skeleton using the view that calls this method as root view.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - gradient: The gradient of the skeleton. Defaults to `SkeletonAppearance.default.gradient`.
|
||||
/// - animated: If the skeleton is animated or not. Defaults to `true`.
|
||||
/// - delay: The amount of time (measured in seconds) to wait before show the skeleton.
|
||||
/// - transition: The style of the transition when the skeleton appears. Defaults to `.crossDissolve(0.25)`.
|
||||
func showGradientSkeleton(
|
||||
usingGradient gradient: SkeletonGradient = SkeletonAppearance.default.gradient,
|
||||
animated: Bool = true,
|
||||
delay: TimeInterval,
|
||||
transition: SkeletonTransitionStyle = .crossDissolve(0.25)
|
||||
) {
|
||||
delayedShowSkeletonWorkItem?.cancel()
|
||||
|
||||
delayedShowSkeletonWorkItem = DispatchWorkItem { [weak self] in
|
||||
let config = SkeletonConfig(type: .gradient, colors: gradient.colors, animated: animated, transition: transition)
|
||||
self?.showSkeleton(skeletonConfig: config)
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: delayedShowSkeletonWorkItem!)
|
||||
}
|
||||
|
||||
/// Shows the animated skeleton using the view that calls this method as root view.
|
||||
///
|
||||
/// If animation is nil, sliding animation will be used, with direction left to right.
|
||||
@@ -32,6 +75,7 @@ public extension UIView {
|
||||
/// - animation: The animation of the skeleton. Defaults to `nil`.
|
||||
/// - transition: The style of the transition when the skeleton appears. Defaults to `.crossDissolve(0.25)`.
|
||||
func showAnimatedSkeleton(usingColor color: UIColor = SkeletonAppearance.default.tintColor, animation: SkeletonLayerAnimation? = nil, transition: SkeletonTransitionStyle = .crossDissolve(0.25)) {
|
||||
delayedShowSkeletonWorkItem?.cancel()
|
||||
let config = SkeletonConfig(type: .solid, colors: [color], animated: true, animation: animation, transition: transition)
|
||||
showSkeleton(skeletonConfig: config)
|
||||
}
|
||||
@@ -45,6 +89,7 @@ public extension UIView {
|
||||
/// - animation: The animation of the skeleton. Defaults to `nil`.
|
||||
/// - transition: The style of the transition when the skeleton appears. Defaults to `.crossDissolve(0.25)`.
|
||||
func showAnimatedGradientSkeleton(usingGradient gradient: SkeletonGradient = SkeletonAppearance.default.gradient, animation: SkeletonLayerAnimation? = nil, transition: SkeletonTransitionStyle = .crossDissolve(0.25)) {
|
||||
delayedShowSkeletonWorkItem?.cancel()
|
||||
let config = SkeletonConfig(type: .gradient, colors: gradient.colors, animated: true, animation: animation, transition: transition)
|
||||
showSkeleton(skeletonConfig: config)
|
||||
}
|
||||
@@ -75,6 +120,7 @@ public extension UIView {
|
||||
}
|
||||
|
||||
func hideSkeleton(reloadDataAfter reload: Bool = true, transition: SkeletonTransitionStyle = .crossDissolve(0.25)) {
|
||||
delayedShowSkeletonWorkItem?.cancel()
|
||||
flowDelegate?.willBeginHidingSkeletons(rootView: self)
|
||||
recursiveHideSkeleton(reloadDataAfter: reload, transition: transition, root: self)
|
||||
}
|
||||
@@ -136,7 +182,7 @@ extension UIView {
|
||||
private func showSkeletonIfNotActive(skeletonConfig config: SkeletonConfig) {
|
||||
guard !isSkeletonActive else { return }
|
||||
saveViewState()
|
||||
isUserInteractionEnabled = false
|
||||
|
||||
prepareViewForSkeleton()
|
||||
addSkeletonLayer(skeletonConfig: config)
|
||||
}
|
||||
@@ -190,7 +236,8 @@ extension UIView {
|
||||
isHidden = false
|
||||
}
|
||||
currentSkeletonConfig?.transition = transition
|
||||
isUserInteractionEnabled = true
|
||||
unSwizzleLayoutSubviews()
|
||||
unSwizzleTraitCollectionDidChange()
|
||||
removeDummyDataSourceIfNeeded(reloadAfter: reload)
|
||||
subviewsSkeletonables.recursiveSearch(leafBlock: {
|
||||
recoverViewState(forced: false)
|
||||
@@ -234,6 +281,17 @@ extension UIView {
|
||||
}
|
||||
}
|
||||
|
||||
private func unSwizzleLayoutSubviews() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
DispatchQueue.removeOnce(token: "UIView.SkeletonView.swizzleLayoutSubviews") {
|
||||
swizzle(selector: #selector(UIView.skeletonLayoutSubviews),
|
||||
with: #selector(UIView.layoutSubviews),
|
||||
inClass: UIView.self,
|
||||
usingClass: UIView.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func swizzleTraitCollectionDidChange() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
DispatchQueue.once(token: "UIView.SkeletonView.swizzleTraitCollectionDidChange") {
|
||||
@@ -244,6 +302,17 @@ extension UIView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func unSwizzleTraitCollectionDidChange() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
DispatchQueue.removeOnce(token: "UIView.SkeletonView.swizzleTraitCollectionDidChange") {
|
||||
swizzle(selector: #selector(UIView.skeletonTraitCollectionDidChange(_:)),
|
||||
with: #selector(UIView.traitCollectionDidChange(_:)),
|
||||
inClass: UIView.self,
|
||||
usingClass: UIView.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
@@ -254,14 +323,22 @@ extension UIView {
|
||||
.setHolder(self)
|
||||
.build()
|
||||
else { return }
|
||||
|
||||
|
||||
self.skeletonLayer = skeletonLayer
|
||||
layer.insertSublayer(skeletonLayer,
|
||||
at: UInt32.max,
|
||||
transition: config.transition) { [weak self] in
|
||||
if config.animated {
|
||||
self?.startSkeletonAnimation(config.animation)
|
||||
}
|
||||
layer.insertSkeletonLayer(
|
||||
skeletonLayer,
|
||||
atIndex: UInt32.max,
|
||||
transition: config.transition
|
||||
) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
/// Workaround to fix the problem when inserting a sublayer and
|
||||
/// the content offset is modified by the system.
|
||||
(self as? UITextView)?.setContentOffset(.zero, animated: false)
|
||||
|
||||
if config.animated {
|
||||
self.startSkeletonAnimation(config.animation)
|
||||
}
|
||||
}
|
||||
status = .on
|
||||
}
|
||||
|
||||
@@ -4,46 +4,50 @@ import UIKit
|
||||
|
||||
extension UIView {
|
||||
@objc var subviewsSkeletonables: [UIView] {
|
||||
return subviewsToSkeleton.filter { $0.isSkeletonable }
|
||||
subviewsToSkeleton.filter { $0.isSkeletonable }
|
||||
}
|
||||
|
||||
@objc var subviewsToSkeleton: [UIView] {
|
||||
return subviews
|
||||
subviews
|
||||
}
|
||||
}
|
||||
|
||||
extension UITableView {
|
||||
override var subviewsToSkeleton: [UIView] {
|
||||
return visibleCells + visibleSectionHeaders + visibleSectionFooters
|
||||
// on `UIViewController'S onViewDidLoad`, the window is still nil.
|
||||
// Some developer trying to call `view.showAnimatedSkeleton()`
|
||||
// when the request or data is loading which sometimes happens before the ViewDidAppear
|
||||
guard window != nil else { return [] }
|
||||
return subviews
|
||||
}
|
||||
}
|
||||
|
||||
extension UITableViewCell {
|
||||
override var subviewsToSkeleton: [UIView] {
|
||||
return contentView.subviews
|
||||
contentView.subviews
|
||||
}
|
||||
}
|
||||
|
||||
extension UITableViewHeaderFooterView {
|
||||
override var subviewsToSkeleton: [UIView] {
|
||||
return contentView.subviews
|
||||
contentView.subviews
|
||||
}
|
||||
}
|
||||
|
||||
extension UICollectionView {
|
||||
override var subviewsToSkeleton: [UIView] {
|
||||
return subviews
|
||||
subviews
|
||||
}
|
||||
}
|
||||
|
||||
extension UICollectionViewCell {
|
||||
override var subviewsToSkeleton: [UIView] {
|
||||
return contentView.subviews
|
||||
contentView.subviews
|
||||
}
|
||||
}
|
||||
|
||||
extension UIStackView {
|
||||
override var subviewsToSkeleton: [UIView] {
|
||||
return arrangedSubviews
|
||||
arrangedSubviews
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import UIKit
|
||||
|
||||
extension CALayer {
|
||||
func insertSublayer(_ layer: SkeletonLayer, at idx: UInt32, transition: SkeletonTransitionStyle, completion: (() -> Void)? = nil) {
|
||||
insertSublayer(layer.contentLayer, at: idx)
|
||||
func insertSkeletonLayer(_ sublayer: SkeletonLayer, atIndex index: UInt32, transition: SkeletonTransitionStyle, completion: (() -> Void)? = nil) {
|
||||
insertSublayer(sublayer.contentLayer, at: index)
|
||||
switch transition {
|
||||
case .none:
|
||||
completion?()
|
||||
case .crossDissolve(let duration):
|
||||
layer.contentLayer.setOpacity(from: 0, to: 1, duration: duration, completion: completion)
|
||||
sublayer.contentLayer.setOpacity(from: 0, to: 1, duration: duration, completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
default_platform(:ios)
|
||||
podspec_name = "SkeletonView.podspec"
|
||||
|
||||
lane :bump_version do |options|
|
||||
version_bump_podspec(path: @podspec_name, version_number: options[:next_version])
|
||||
end
|
||||
|
||||
lane :release_current do
|
||||
version = version_get_podspec(path: @podspec_name)
|
||||
if git_tag_exists(tag: version)
|
||||
|
||||
@@ -15,6 +15,11 @@ Install _fastlane_ using
|
||||
or alternatively using `brew install fastlane`
|
||||
|
||||
# Available Actions
|
||||
### bump_version
|
||||
```
|
||||
fastlane bump_version
|
||||
```
|
||||
|
||||
### release_current
|
||||
```
|
||||
fastlane release_current
|
||||
|
||||
Reference in New Issue
Block a user