Compare commits

..

92 Commits

Author SHA1 Message Date
Juanpe Catalán b040f4771c bump version 1.8.5 2020-02-12 01:49:08 +01:00
Juanpe Catalán b938eddb87 fix: remove duplicate import UIKit 2020-02-12 01:48:59 +01:00
Juanpe Catalán 01fe85b95c Merge pull request #227 from diogot/notifications
Post show/update/hide notifications
2020-02-12 01:39:11 +01:00
Juanpe Catalán f538673b53 Update greetings.yml 2020-02-12 01:37:14 +01:00
Juanpe Catalán 898f7e4de4 Delete auto-comment.yml 2020-02-12 01:36:17 +01:00
Juanpe Catalán 570f059615 Update stale.yml 2020-02-12 01:35:52 +01:00
Juanpe Catalán 9a106d1af2 Merge pull request #254 from Juanpe/develop
Merge changes for version 1.8.4
2020-02-12 01:16:35 +01:00
Juanpe Catalán 5544acc63e bump version 1.8.4 2020-02-12 01:14:17 +01:00
Juanpe Catalán 65299378c4 fix: problem with text views that only have one line 2020-02-12 01:13:21 +01:00
Juanpe Catalán d8f1b4c53b Merge pull request #195 from esme-putt/Customise-Single-Line
Customise Single Line
2020-02-12 00:13:39 +01:00
Juanpe Catalán d2d8c1f5db Merge branch 'develop' into Customise-Single-Line 2020-02-12 00:11:28 +01:00
Juanpe Catalán ab385e5afb Merge pull request #234 from AnatoliBenke-Helsana/master
Update SkeletonView.swift
2020-02-12 00:05:32 +01:00
Juanpe Catalán 5d8cd9432e Merge branch 'master' into develop 2020-02-11 14:53:59 +01:00
Juanpe Catalán fbaf2e7b4b Update greetings.yml 2020-02-11 11:38:45 +01:00
Juanpe Catalán 97d83c7038 Create greetings.yml 2020-02-11 11:36:24 +01:00
Juanpe Catalán ce83713240 update staling issues
just stale issues with state equal to awaiting user input
2020-02-11 11:13:32 +01:00
Juanpe Catalán 56d3156f8e Merge branch 'master' into develop 2020-02-11 10:52:04 +01:00
Juanpe Catalán aeb9dcf2c7 Create auto-comment.yml 2020-02-11 10:43:30 +01:00
Juanpe Catalán 9914be0f5b Merge pull request #240 from damien-danglard/master
[fix] Use UIFont.lineHeight for calculate the number of CALayer used in …
2020-02-11 10:33:28 +01:00
Juanpe Catalán 7d0609098c update stale.yml to not stale assigned issues 2020-02-10 00:29:24 +01:00
Diogo Tridapalli ec7a9973c5 Post show/update/hide notifications
Signed-off-by: Diogo Tridapalli <diogot@users.noreply.github.com>
2020-02-03 13:44:59 -03:00
Juanpe Catalán b431aabddb increase pod version 2020-01-31 15:22:54 +01:00
Juanpe Catalán 0091ffc08b Merge branch 'develop' 2020-01-31 14:45:07 +01:00
Juanpe Catalán e948e313a1 Merge pull request #216 from MikeGlotov/fix/preserve-user-interactions-state
Added "isUserInteractionsEnabled" state preservation to UITextView an…
2020-01-30 11:17:43 +01:00
Damien Danglard a7ae5f0f9f [fix] resolve issues from codebeat 2020-01-27 17:11:23 +01:00
Damien Danglard 85cce0cd7c [fix] Use font lineHeight for calculate the number of CALayer used in multiline Skeleton UILabel
close #239
2020-01-24 15:35:29 +01:00
Juanpe Catalán 3afe7286a2 Merge pull request #152 from eduardbosch/feature/fix_skeleton_hierarchy
Fix skeleton hierarchy
2020-01-23 20:38:19 +01:00
AnatoliBenke-Helsana 6f1db7e303 Update SkeletonView.swift
- Fixes for Issue #202 UIView.layoutSubviews swizzle is messing with standard controls
- Due to Swizzling, LayoutSubviews is not called. This fixes the issue.
2020-01-07 13:39:32 +01:00
Juanpe Catalán c1815642db Merge pull request #209 from nikitskynikita/master
Add support skeletonable headers and footers of UITableView and UICollectionView
2019-12-30 20:12:00 +01:00
Juanpe Catalán a6a168a919 Merge pull request #225 from Juanpe/dependabot/bundler/excon-0.71.0
build(deps): bump excon from 0.65.0 to 0.71.0
2019-12-30 20:10:58 +01:00
dependabot[bot] d0dbd1e004 build(deps): bump excon from 0.65.0 to 0.71.0
Bumps [excon](https://github.com/excon/excon) from 0.65.0 to 0.71.0.
- [Release notes](https://github.com/excon/excon/releases)
- [Changelog](https://github.com/excon/excon/blob/master/changelog.txt)
- [Commits](https://github.com/excon/excon/compare/v0.65.0...v0.71.0)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-16 22:14:49 +00:00
Juanpe Catalán 4ade8f8797 Update FUNDING.yml 2019-12-10 17:37:49 +01:00
Nikita Nikitsky 264195bf16 Add support skeletonable headers and footers of UITableView and UICollectionView 2019-12-01 14:37:37 +04:00
Mikhail Glotov 11cc9a3ff7 Added "isUserInteractionsEnabled" state preservation to UITextView and UILabel 2019-11-19 14:26:22 +03:00
Nikita Nikitsky 37ac7d6e5d Fix attributes for tvOS 2019-11-05 14:37:55 +04:00
Juanpe Catalán a1e183a5e1 Merge pull request #199 from Wilsonator5000/feature/dark-mode
Support dark mode
2019-11-04 11:57:38 +01:00
Juanpe Catalán 420785502b Merge pull request #208 from Juanpe/dependabot/bundler/rubyzip-1.3.0
build(deps): bump rubyzip from 1.2.3 to 1.3.0
2019-11-04 11:57:07 +01:00
dependabot[bot] eed18e5372 build(deps): bump rubyzip from 1.2.3 to 1.3.0
Bumps [rubyzip](https://github.com/rubyzip/rubyzip) from 1.2.3 to 1.3.0.
- [Release notes](https://github.com/rubyzip/rubyzip/releases)
- [Changelog](https://github.com/rubyzip/rubyzip/blob/master/Changelog.md)
- [Commits](https://github.com/rubyzip/rubyzip/compare/v1.2.3...v1.3.0)

Signed-off-by: dependabot[bot] <support@github.com>
2019-11-03 14:00:59 +00:00
Wilson Gramer 0a86ac4a9c Merge branch 'master' into feature/dark-mode 2019-10-22 12:53:16 -04:00
Wilson Gramer 264b0a70c2 Update CHANGELOG 2019-10-22 12:51:26 -04:00
Juanpe Catalán 01d68190ab Merge pull request #196 from ashleyng/FixCellDataRefresh
Update tableview cells after hiding skeleton
2019-10-17 09:12:32 +02:00
Juanpe Catalán 2d57efd9cb Update CHANGELOG.md 2019-10-17 09:07:47 +02:00
Wilson Gramer da51a6f673 Update English README 2019-10-13 21:04:45 -04:00
Wilson Gramer f6d4343cc6 Update example to support dark mode 2019-10-13 20:55:53 -04:00
Wilson Gramer c6e20aa1a0 Swizzle traitCollectionDidChange(_:) method in skeleton views
Make skeleton views respond to changes in system appearance by
updating the skeleton view whenever the trait collection changes,
so the colors change automatically with dark mode.
2019-10-13 20:47:03 -04:00
Wilson Gramer 809544066f Make skeleton colors dynamic
If on iOS 13 or higher, returns a UIColor using the
`init(dynamicProvider:)` initializer so it automatically adjusts to
dark mode. Also switches out the `clouds` default color with the
dynamic `skeletonDefault` color.
2019-10-13 20:45:53 -04:00
Ashley Ng 9406e3ef62 refresh tableview after removing skeleton views 2019-10-05 20:49:20 -05:00
Esme Putt 6d4c7d76c3 Added flipper for custom options 2019-10-04 13:28:21 +13:00
Esme Putt f580fdaac2 Changed update function 2019-10-04 13:11:45 +13:00
Esme Putt 4be93db383 Added spacing 2019-10-04 11:52:38 +13:00
Esme Putt ae4ecfa760 Added comments 2019-10-04 11:28:17 +13:00
Esme Putt f60e5cd7ae Add-custom-options-for-single-line 2019-10-04 11:06:30 +13:00
Juanpe Catalán e097385de9 Merge pull request #174 from superhuman/master
Add ability to customize line spacing per label
2019-09-15 10:40:10 +02:00
gshahbazian 84d8971aa2 Add ability to customize line spacing per label
Also customizability of edge insets for skeleton rows
2019-09-13 09:59:17 -07:00
Juanpe Catalán 5384fd34dd Merge branch 'develop' 2019-09-13 10:51:56 +02:00
Juanpe Catalán 53d965e151 feat: update pod version 2019-09-13 10:51:39 +02:00
Juanpe Catalán 04678fc772 feat: change some signature of methods 2019-09-13 10:50:06 +02:00
Juanpe Catalán 46da5ab6fa Merge branch 'master' into develop 2019-09-13 10:42:13 +02:00
Juanpe Catalán 45871be409 Merge pull request #186 from AhmedOS/master
Fixed animation stopping issue after view controller disappears
2019-09-13 10:41:02 +02:00
Juanpe Catalán 93f54dcc4d Merge pull request #182 from yangzhiquan/fix-spelling-error
fix spelling error
2019-09-10 08:31:25 +02:00
yvan 3bf3038941 fix spelling error 2019-09-10 11:27:29 +08:00
Ahmed Osama df1454c749 Improved example project 2019-09-08 05:48:30 +02:00
Ahmed Osama 8212fc1a0b Fixed animation stopping issue after switching tabs 2019-09-08 05:48:30 +02:00
Juanpe Catalán e4b9416667 Merge pull request #167 from Bilue/fix-single-line-labels
Adjust multiline behaviour to properly handle single lines
2019-09-02 17:52:19 +02:00
Juanpe Catalán f6f001068d Update README.md 2019-08-31 14:00:02 +02:00
Juanpe Catalán cb4ddd487a Update README.md 2019-08-31 13:59:37 +02:00
Juanpe Catalán 18cd0f9aba feat: update stale.yml setting stale label 2019-08-27 23:29:03 +02:00
Eduard Bosch Bertran 731509a46f fix: Update examples to show skeletons 2019-08-27 21:17:47 +02:00
Eduard Bosch Bertran 7833c94f2e fix: Stop showing skeleton views when a view is not skeletonable 2019-08-27 21:17:33 +02:00
Juanpe Catalán 71d3e72eec feat: create stale file 2019-08-27 20:21:43 +02:00
Juanpe Catalán a0a2ae760b fix: problem calling layout subviews 2019-08-27 20:04:42 +02:00
Juanpe Catalán 71d40d24b2 feat: update assets 2019-08-27 02:31:59 +02:00
Juanpe Catalán 4236e9d424 feat: update README, include more examples in Hierarchy section 2019-08-27 02:28:19 +02:00
Juanpe Catalán 19b88fce3e feat: update example 2019-08-26 23:27:41 +02:00
Juanpe Catalán b200a1ff3a Merge branch 'master' of https://github.com/Juanpe/SkeletonView 2019-08-26 23:01:10 +02:00
Juanpe Catalán d9408d59b4 feat: update cocoaspec 2019-08-26 23:00:54 +02:00
Juanpe Catalán 5a0f6e2314 Update README.md 2019-08-26 23:00:07 +02:00
Juanpe Catalán 3f7505bed9 Merge branch 'master' of https://github.com/Juanpe/SkeletonView 2019-08-26 22:39:28 +02:00
Juanpe Catalán 5436e44f15 fix: property name 2019-08-26 22:37:11 +02:00
Juanpe Catalán 2f2e542d51 fix: bug #149 animation stopped when modal is presented 2019-08-26 22:34:56 +02:00
Juanpe Catalán 15764debf0 feat: rename alias of delegate 2019-08-26 19:11:28 +02:00
Juanpe Catalán bcd0fa7983 feat: user interaction enabled false 2019-08-26 19:10:44 +02:00
Juanpe Catalán 4cdc5935fc Update CHANGELOG.md 2019-08-25 11:08:42 +02:00
Juanpe Catalán 93769902a3 Merge branch 'master' into develop 2019-08-24 11:46:50 +02:00
Juanpe Catalán a1d54a448d Merge pull request #178 from aadudyrev/completionOnNoneTransition
Fix completion call in .none transition style.
2019-08-23 15:57:20 +02:00
dudyrev cc8d21e7af Fix completion call in .none transition style. 2019-08-23 16:24:47 +03:00
Juanpe Catalán 69a0c8319d Update README.md 2019-08-23 12:58:21 +02:00
Juanpe Catalán 3c173c0a23 Merge branch 'master' into develop 2019-08-22 16:51:16 +02:00
Juanpe Catalán dce910d6d0 feat: update transition gifs 2019-08-22 16:51:04 +02:00
Juanpe Catalán 2f751b9036 Merge branch 'master' into develop 2019-08-22 15:17:28 +02:00
Juanpe Catalán 978fd553e1 Merge branch 'master' into develop 2019-08-22 15:11:15 +02:00
Tom Izaks 5b3bc204bf Adjust multiline behaviour to properly handle single lines 2019-07-09 14:00:48 +10:00
55 changed files with 722 additions and 246 deletions
+1 -9
View File
@@ -1,9 +1 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: skeletonview
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
custom: # Replace with a single custom sponsorship URL
github: [juanpe]
+11
View File
@@ -0,0 +1,11 @@
daysUntilStale: 60
daysUntilClose: 7
onlyLabels:
- awaiting user input
staleLabel: given up
markComment: >
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.
closeComment: false
only: issues
+13
View File
@@ -0,0 +1,13 @@
name: Greetings
on: [pull_request]
jobs:
greeting:
runs-on: ubuntu-latest
steps:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: 'Hi 👋! Thank you for raising your pull request. \n Please make sure you have followed our contributing guidelines. We will review it as soon as possible.'
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+36
View File
@@ -4,7 +4,43 @@ All notable changes to this project will be documented in this file
## Next version
### New
- Support for iOS 13 dark mode. (thanks @Wilsonator5000)
## [1.8.2](https://github.com/Juanpe/SkeletonView/releases/tag/1.8.2)
### New
- Add ability to customize line spacing per label. (thanks @gshahbazian)
## [LayoutSkeleton (1.8.1)](https://github.com/Juanpe/SkeletonView/releases/tag/1.8.1)
### Improvements
- Fix completion call in .none transition style while hide skeletons. (thanks @aadudyrev)
### New
- Swizzle `layoutSubviews` method.
### Improvements
- Fix completion call in .none transition style while hiding skeletons. (thanks @aadudyrev)
- Swift format.
### Bug fixes
- Update layout subviews when the original method is called.
- Issues: [#88, #149]
## [Transitions (1.8)](https://github.com/Juanpe/SkeletonView/releases/tag/1.8)
### New
- Adding swift news to mentioned section (thanks @osterbergmarcus).
- Create `SkeletonTransitionStyle`. Now, you can animate transition when you show or hide skeletons. (thanks @pontusjacobsson)
### Improvements
- Refactor some methods.
### Bug fixes
- Solved issues.
[#175](https://github.com/Juanpe/SkeletonView/issues/175) Swift Package Manager version format
## [Layout update (1.7)](https://github.com/Juanpe/SkeletonView/releases/tag/1.7)
+3
View File
@@ -188,6 +188,9 @@
<constraint firstItem="HKL-L0-T2w" firstAttribute="leading" secondItem="2Gq-Y8-1TU" secondAttribute="leading" id="iIq-cx-paX"/>
</constraints>
<viewLayoutGuide key="safeArea" id="2Gq-Y8-1TU"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSkeletonable" value="YES"/>
</userDefinedRuntimeAttributes>
</view>
<connections>
<outlet property="avatarImage" destination="Ql9-Jy-aWM" id="VoL-by-ygR"/>
+1 -2
View File
@@ -45,7 +45,6 @@ class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.isSkeletonable = true
collectionView.prepareSkeleton(completion: { done in
self.view.showAnimatedSkeleton()
})
@@ -81,7 +80,7 @@ class ViewController: UIViewController {
}
func hideSkeleton() {
view.hideSkeleton(transition: .crossDisolve(transitionDurationStepper.value))
view.hideSkeleton(transition: .crossDissolve(transitionDurationStepper.value))
}
func refreshSkeleton() {
+107 -69
View File
@@ -1,25 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" 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="14490.49"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<!--Item-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="SkeletonViewExample" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="F9K-jU-100" userLabel="ContainerView">
<rect key="frame" x="0.0" y="44" width="375" height="243"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="e9V-mk-xH0">
<rect key="frame" x="45" y="142" width="287" height="78"/>
<constraints>
<constraint firstAttribute="height" constant="78" id="gF5-G1-lKI"/>
</constraints>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. </string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSkeletonable" value="YES"/>
<userDefinedRuntimeAttribute type="number" keyPath="lastLineFillPercent">
<integer key="value" value="40"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="linesCornerRadius">
<integer key="value" value="6"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</textView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="avatar" translatesAutoresizingMaskIntoConstraints="NO" id="nMj-pU-5wJ">
<rect key="frame" x="141" y="20" width="93" height="93"/>
<color key="backgroundColor" red="0.56078431370000004" green="0.59607843140000005" blue="0.7843137255" alpha="0.90709546230000004" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" constant="93" id="gw9-nu-cKo"/>
<constraint firstAttribute="width" constant="93" id="zB6-Lp-IUt"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSkeletonable" value="YES"/>
</userDefinedRuntimeAttributes>
</imageView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="nMj-pU-5wJ" firstAttribute="centerX" secondItem="F9K-jU-100" secondAttribute="centerX" id="9X4-2r-AKx"/>
<constraint firstItem="e9V-mk-xH0" firstAttribute="leading" secondItem="F9K-jU-100" secondAttribute="leading" constant="45" id="HvQ-HY-zYU"/>
<constraint firstItem="e9V-mk-xH0" firstAttribute="centerX" secondItem="F9K-jU-100" secondAttribute="centerX" constant="1" id="KcB-tG-NXa"/>
<constraint firstAttribute="height" constant="243" id="MIj-xq-gr1"/>
<constraint firstItem="e9V-mk-xH0" firstAttribute="top" secondItem="F9K-jU-100" secondAttribute="top" constant="142" id="Wcx-nZ-1lR"/>
<constraint firstAttribute="trailing" secondItem="e9V-mk-xH0" secondAttribute="trailing" constant="43" id="XbU-Og-rht"/>
<constraint firstItem="nMj-pU-5wJ" firstAttribute="top" secondItem="F9K-jU-100" secondAttribute="top" constant="20" id="hQL-cr-MaN"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSkeletonable" value="YES"/>
</userDefinedRuntimeAttributes>
</view>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="UCB-SP-lQk">
<rect key="frame" x="0.0" y="263" width="375" height="244"/>
<rect key="frame" x="0.0" y="287" width="375" height="282"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" red="0.1061807256" green="0.84678786989999999" blue="0.031482450150000001" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<prototypes>
@@ -41,18 +87,21 @@
<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="15" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VhU-1t-AaI">
<rect key="frame" x="118" y="29" width="237" height="20.5"/>
<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="VhU-1t-AaI" userLabel="Label">
<rect key="frame" x="118" y="29" width="237" height="18"/>
<constraints>
<constraint firstAttribute="height" relation="lessThanOrEqual" constant="71" id="HRL-cI-ieC"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSkeletonable" value="YES"/>
<userDefinedRuntimeAttribute type="number" keyPath="linesCornerRadius">
<integer key="value" value="0"/>
<integer key="value" value="5"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="lastLineFillPercent">
<integer key="value" value="20"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</label>
@@ -82,61 +131,14 @@
</userDefinedRuntimeAttributes>
<connections>
<outlet property="dataSource" destination="BYZ-38-t0r" id="Hxi-nC-gbY"/>
<outlet property="delegate" destination="BYZ-38-t0r" id="Z10-Nx-iGb"/>
</connections>
</tableView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="F9K-jU-100">
<rect key="frame" x="0.0" y="20" width="375" height="243"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="e9V-mk-xH0">
<rect key="frame" x="45" y="142" width="287" height="78"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="78" id="gF5-G1-lKI"/>
</constraints>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. </string>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSkeletonable" value="YES"/>
<userDefinedRuntimeAttribute type="number" keyPath="lastLineFillPercent">
<integer key="value" value="40"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="linesCornerRadius">
<integer key="value" value="6"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</textView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="avatar" translatesAutoresizingMaskIntoConstraints="NO" id="nMj-pU-5wJ">
<rect key="frame" x="141" y="20" width="93" height="93"/>
<color key="backgroundColor" red="0.56078431370000004" green="0.59607843140000005" blue="0.7843137255" alpha="0.90709546230000004" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" constant="93" id="gw9-nu-cKo"/>
<constraint firstAttribute="width" constant="93" id="zB6-Lp-IUt"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSkeletonable" value="YES"/>
</userDefinedRuntimeAttributes>
</imageView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="nMj-pU-5wJ" firstAttribute="centerX" secondItem="F9K-jU-100" secondAttribute="centerX" id="9X4-2r-AKx"/>
<constraint firstItem="e9V-mk-xH0" firstAttribute="leading" secondItem="F9K-jU-100" secondAttribute="leading" constant="45" id="HvQ-HY-zYU"/>
<constraint firstItem="e9V-mk-xH0" firstAttribute="centerX" secondItem="F9K-jU-100" secondAttribute="centerX" constant="1" id="KcB-tG-NXa"/>
<constraint firstAttribute="height" constant="243" id="MIj-xq-gr1"/>
<constraint firstItem="e9V-mk-xH0" firstAttribute="top" secondItem="F9K-jU-100" secondAttribute="top" constant="142" id="Wcx-nZ-1lR"/>
<constraint firstAttribute="trailing" secondItem="e9V-mk-xH0" secondAttribute="trailing" constant="43" id="XbU-Og-rht"/>
<constraint firstItem="nMj-pU-5wJ" firstAttribute="top" secondItem="F9K-jU-100" secondAttribute="top" constant="20" id="hQL-cr-MaN"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSkeletonable" value="YES"/>
</userDefinedRuntimeAttributes>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="XgY-1a-UGc">
<rect key="frame" x="0.0" y="507" width="375" height="160"/>
<rect key="frame" x="0.0" y="569" width="375" height="160"/>
<subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="xOL-Sq-r4i">
<rect key="frame" x="20" y="23" width="135" height="29"/>
<rect key="frame" x="20" y="23" width="145" height="32"/>
<segments>
<segment title="Solid"/>
<segment title="Gradient"/>
@@ -158,7 +160,7 @@
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Color" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7CF-rV-pK2">
<rect key="frame" x="20" y="73.5" width="90" height="21"/>
<rect key="frame" x="20" y="73.666666666666629" width="90" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -189,19 +191,19 @@
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Fade Duration: 0 sec" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mrw-PM-jJJ">
<rect key="frame" x="113.5" y="128.5" width="141.5" height="18"/>
<rect key="frame" x="113.66666666666667" y="130" width="141.33333333333331" height="18"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<stepper opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" maximumValue="5" stepValue="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="l4N-LL-ZrJ">
<rect key="frame" x="263" y="123" width="94" height="29"/>
<rect key="frame" x="263" y="123" width="94" height="32"/>
<connections>
<action selector="transitionDurationStepperAction:" destination="BYZ-38-t0r" eventType="valueChanged" id="jPN-df-fNs"/>
</connections>
</stepper>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="l4N-LL-ZrJ" firstAttribute="leading" secondItem="mrw-PM-jJJ" secondAttribute="trailing" constant="8" id="5iU-dO-qVc"/>
<constraint firstItem="mrw-PM-jJJ" firstAttribute="centerY" secondItem="l4N-LL-ZrJ" secondAttribute="centerY" id="9OM-mx-4Jo"/>
@@ -226,7 +228,7 @@
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="F9K-jU-100" firstAttribute="trailing" secondItem="6Tk-OE-BBY" secondAttribute="trailing" id="1es-nY-bd3"/>
<constraint firstItem="F9K-jU-100" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="A3E-iv-1qp"/>
@@ -241,9 +243,10 @@
</constraints>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSkeletonable" value="NO"/>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSkeletonable" value="YES"/>
</userDefinedRuntimeAttributes>
</view>
<tabBarItem key="tabBarItem" title="Item" id="wQY-ap-3n3"/>
<navigationItem key="navigationItem" id="BEI-dU-kr2"/>
<connections>
<outlet property="avatarImage" destination="nMj-pU-5wJ" id="9fa-Z7-vYi"/>
@@ -258,7 +261,42 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-2582" y="-400"/>
<point key="canvasLocation" x="-2682.4000000000001" y="339.90147783251234"/>
</scene>
<!--Item-->
<scene sceneID="Cfc-AT-AS1">
<objects>
<viewController id="dv8-ph-Ehg" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Jwx-gI-Qod">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="Ao1-hk-zrH"/>
</view>
<tabBarItem key="tabBarItem" title="Item" id="iKp-9S-aib"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="M03-a6-GOC" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-1644" y="340"/>
</scene>
<!--Tab Bar Controller-->
<scene sceneID="U6k-MC-AHH">
<objects>
<tabBarController automaticallyAdjustsScrollViewInsets="NO" id="Va7-1y-Tel" sceneMemberID="viewController">
<toolbarItems/>
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="HSI-2O-RyO">
<rect key="frame" x="0.0" y="0.0" width="1000" height="1000"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tabBar>
<connections>
<segue destination="BYZ-38-t0r" kind="relationship" relationship="viewControllers" id="dL3-9L-KNU"/>
<segue destination="dv8-ph-Ehg" kind="relationship" relationship="viewControllers" id="8QB-uV-gaF"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="huq-Fh-0sW" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-2172" y="-555"/>
</scene>
</scenes>
<resources>
+1 -1
View File
@@ -2,4 +2,4 @@
import UIKit
let colors = [(UIColor.turquoise,"turquoise"), (UIColor.emerald,"emerald"), (UIColor.peterRiver,"peterRiver"), (UIColor.amethyst,"amethyst"),(UIColor.wetAsphalt,"wetAsphalt"), (UIColor.nephritis,"nephritis"), (UIColor.belizeHole,"belizeHole"), (UIColor.wisteria,"wisteria"), (UIColor.midnightBlue,"midnightBlue"), (UIColor.sunFlower,"sunFlower"), (UIColor.carrot,"carrot"), (UIColor.alizarin,"alizarin"),(UIColor.clouds,"clouds"), (UIColor.concrete,"concrete"), (UIColor.flatOrange,"flatOrange"), (UIColor.pumpkin,"pumpkin"), (UIColor.pomegranate,"pomegranate"), (UIColor.silver,"silver"), (UIColor.asbestos,"asbestos")]
let colors = [(UIColor.skeletonDefault,"skeletonDefault"),(UIColor.turquoise,"turquoise"), (UIColor.emerald,"emerald"), (UIColor.peterRiver,"peterRiver"), (UIColor.amethyst,"amethyst"),(UIColor.wetAsphalt,"wetAsphalt"), (UIColor.nephritis,"nephritis"), (UIColor.belizeHole,"belizeHole"), (UIColor.wisteria,"wisteria"), (UIColor.midnightBlue,"midnightBlue"), (UIColor.sunFlower,"sunFlower"), (UIColor.carrot,"carrot"), (UIColor.alizarin,"alizarin"),(UIColor.clouds,"clouds"), (UIColor.concrete,"concrete"), (UIColor.flatOrange,"flatOrange"), (UIColor.pumpkin,"pumpkin"), (UIColor.pomegranate,"pomegranate"), (UIColor.silver,"silver"), (UIColor.asbestos,"asbestos")]
+24 -5
View File
@@ -14,6 +14,8 @@ class ViewController: UIViewController {
didSet {
tableview.rowHeight = UITableView.automaticDimension
tableview.estimatedRowHeight = 120.0
tableview.register(UITableViewHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: "HeaderIdentifier")
tableview.register(UITableViewHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: "FooterIdentifier")
}
}
@@ -45,15 +47,11 @@ class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableview.isSkeletonable = true
}
override func viewDidLayoutSubviews() {
view.layoutSkeletonIfNeeded()
view.showAnimatedSkeleton()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
view.showAnimatedSkeleton()
}
@IBAction func changeAnimated(_ sender: Any) {
@@ -182,3 +180,24 @@ extension ViewController: SkeletonTableViewDataSource {
}
}
extension ViewController: SkeletonTableViewDelegate {
func collectionSkeletonView(_ skeletonView: UITableView, identifierForHeaderInSection section: Int) -> ReusableHeaderFooterIdentifier? {
return "HeaderIdentifier"
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderIdentifier")!
header.textLabel?.text = "header => \(section)"
return header
}
func collectionSkeletonView(_ skeletonView: UITableView, identifierForFooterInSection section: Int) -> ReusableHeaderFooterIdentifier? {
return "FooterIdentifier"
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let footer = tableView.dequeueReusableHeaderFooterView(withIdentifier: "FooterIdentifier")!
footer.textLabel?.text = "footer => \(section)"
return footer
}
}
+2 -2
View File
@@ -58,7 +58,7 @@ GEM
dotenv (2.7.4)
emoji_regex (1.0.1)
escape (0.0.4)
excon (0.65.0)
excon (0.71.0)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
@@ -163,7 +163,7 @@ GEM
retriable (3.1.2)
rouge (2.0.7)
ruby-macho (1.4.0)
rubyzip (1.2.3)
rubyzip (1.3.0)
security (0.1.3)
signet (0.11.0)
addressable (~> 2.3)
+40 -9
View File
@@ -22,6 +22,9 @@
<img src="https://img.shields.io/badge/contact-@JuanpeCatalan-blue.svg?style=flat" alt="Twitter: @JuanpeCatalan" />
</a>
<br/>
<a href="https://gitter.im/SkeletonView/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge">
<img src="https://badges.gitter.im/SkeletonView/community.svg?style=flat" />
</a>
<a href="https://twitter.com/intent/tweet?text=Wow%20This%20library%20is%20awesome:&url=https%3A%2F%2Fgithub.com%2FJuanpe%2FSkeletonView">
<img src="https://img.shields.io/twitter/url/https/github.com/Juanpe/SkeletonView.svg?style=social" alt="License" />
</a>
@@ -174,7 +177,7 @@ avatarImageView.isSkeletonable = true
#### Skeleton views layout
Sometimes skeleton layout may not fit your layout because the parent view bounds have changed. For example, rotating the device.
Sometimes skeleton layout may not fit your layout because the parent view bounds have changed. ~For example, rotating the device.~
You can relayout the skeleton views like so:
@@ -184,6 +187,8 @@ override func viewDidLayoutSubviews() {
}
```
⚠️⚠️ You shouldn't call this method. From *version 1.8.1* you don't need to call this method, the library does automatically. So, you can use this method *ONLY* in the cases when you need to update the layout of the skeleton manually.
#### Update skeleton configuration
You can change the skeleton configuration at any time like its colour, animation, etc. with the following methods:
@@ -208,6 +213,8 @@ 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, identifierForHeaderInSection section: Int) -> ReusableHeaderFooterIdentifier?
func collectionSkeletonView(_ skeletonView: UITableView, identifierForFooterInSection section: Int) -> ReusableHeaderFooterIdentifier?
}
```
As you can see, this protocol inherits from ```UITableViewDataSource```, so you can replace this protocol with the skeleton protocol.
@@ -225,6 +232,16 @@ func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection s
// It calculates how many cells need to populate whole tableview
```
``` swift
func collectionSkeletonView(_ skeletonView: UITableView, identifierForHeaderInSection section: Int) -> ReusableHeaderFooterIdentifier?
// Default: nil
```
``` swift
func collectionSkeletonView(_ skeletonView: UITableView, identifierForFooterInSection section: Int) -> ReusableHeaderFooterIdentifier?
// Default: nil
```
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
@@ -321,9 +338,9 @@ Besides, ```SkeletonView``` features 20 flat colors 🤙🏼
Default values:
- **tintColor**: UIColor
- *default: .clouds*
- *default: `.skeletonDefault` (same as `.clouds` but adaptive to dark mode)*
- **gradient**: SkeletonGradient
- *default: SkeletonGradient(baseColor: .clouds)*
- *default: `SkeletonGradient(baseColor: .skeletonDefault)`*
- **multilineHeight**: CGFloat
- *default: 15*
- **multilineSpacing**: CGFloat
@@ -339,6 +356,12 @@ SkeletonAppearance.default.multilineHeight = 20
SkeletonAppearance.default.tintColor = .green
```
You can also specifiy these line appearance properties on a per-label basis:
- **lastLineFillPercent**: Int
- **linesCornerRadius**: Int
- **skeletonLineSpacing**: CGFloat
- **skeletonPaddingInsets**: UIEdgeInsets
### 🤓 Custom animations
@@ -433,14 +456,21 @@ Since ```SkeletonView``` is recursive, and we want skeleton to be very efficient
Because an image is worth a thousand words:
In this example we have a `UIViewController` with a `ContainerView` and a `UITableView`. When the view is ready, we show the skeleton using this method:
```
view.showSkeleton()
```
> ```ìsSkeletonable```= ☠️
| Configuration | Result
|------- | -------
|![](Assets/no_skeletonable.png) | ![](Assets/no_skeletonables_result.png)
|![](Assets/container_no_skeletonable.png) | ![](Assets/no_skeletonables_result.png)
|![](Assets/container_skeletonable.png) | ![](Assets/container_skeletonable_result.png)
|![](Assets/all_skeletonables.png) | ![](Assets/all_skeletonables_result.png)
| Configuration | Result|
|:-------:|:-------:|
|<img src="Assets/no_skeletonable.jpg" width="350"/> | <img src="Assets/no_skeletonables_result.png" width="350"/>|
|<img src="Assets/container_no_skeletonable.jpg" width="350"/> | <img src="Assets/no_skeletonables_result.png" width="350"/>|
|<img src="Assets/container_skeletonable.jpg" width="350"/> | <img src="Assets/container_skeletonable_result.png" width="350"/>|
|<img src="Assets/all_skeletonables.jpg" width="350"/>| <img src="Assets/all_skeletonables_result.png" width="350"/>|
|<img src="Assets/tableview_no_skeletonable.jpg" width="350"/> | <img src="Assets/tableview_no_skeletonable_result.png" height="350"/>|
|<img src="Assets/tableview_skeletonable.jpg" width="350"/> | <img src="Assets/tableview_skeletonable_result.png" height="350"/>|
### 🔬 Debug
@@ -516,6 +546,7 @@ See [all contributors](https://github.com/Juanpe/SkeletonView/graphs/contributor
- [CocoaControls](https://www.cocoacontrols.com/controls/skeletonview)
- [Awesome iOS Newsletter #74](https://ios.libhunt.com/newsletter/74)
- [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)
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SkeletonView"
s.version = "1.8"
s.version = "1.8.5"
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.
+8 -2
View File
@@ -78,6 +78,8 @@
F54CF5E52024CEB000330B0D /* UITableView+CollectionSkeleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F54CF5E22024CEAF00330B0D /* UITableView+CollectionSkeleton.swift */; };
F54CF5E62024CEB000330B0D /* UICollectionView+CollectionSkeleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F54CF5E32024CEB000330B0D /* UICollectionView+CollectionSkeleton.swift */; };
F56B94461FAE20AF0095662F /* PrepareForSkeletonProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F56B94451FAE20AF0095662F /* PrepareForSkeletonProtocol.swift */; };
F570ABF42314629700390248 /* Swizzling.swift in Sources */ = {isa = PBXBuildFile; fileRef = F570ABF32314629700390248 /* Swizzling.swift */; };
F570ABF52314629700390248 /* Swizzling.swift in Sources */ = {isa = PBXBuildFile; fileRef = F570ABF32314629700390248 /* Swizzling.swift */; };
F5804772230ECD0000066D02 /* UIView+IBInspectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5804771230ECD0000066D02 /* UIView+IBInspectable.swift */; };
F5804773230ECD0000066D02 /* UIView+IBInspectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5804771230ECD0000066D02 /* UIView+IBInspectable.swift */; };
F587FB84202CBFC8002DB5FE /* SkeletonFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F587FB83202CBFC8002DB5FE /* SkeletonFlow.swift */; };
@@ -185,6 +187,7 @@
F54CF5E22024CEAF00330B0D /* UITableView+CollectionSkeleton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+CollectionSkeleton.swift"; sourceTree = "<group>"; };
F54CF5E32024CEB000330B0D /* UICollectionView+CollectionSkeleton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+CollectionSkeleton.swift"; sourceTree = "<group>"; };
F56B94451FAE20AF0095662F /* PrepareForSkeletonProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrepareForSkeletonProtocol.swift; sourceTree = "<group>"; };
F570ABF32314629700390248 /* Swizzling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Swizzling.swift; sourceTree = "<group>"; };
F5804771230ECD0000066D02 /* UIView+IBInspectable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+IBInspectable.swift"; sourceTree = "<group>"; };
F587FB83202CBFC8002DB5FE /* SkeletonFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonFlow.swift; sourceTree = "<group>"; };
F587FB85202CEC95002DB5FE /* UIView+UIApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+UIApplicationDelegate.swift"; sourceTree = "<group>"; };
@@ -437,6 +440,7 @@
F5F899D11FAB9630002E8FDA /* AssociationPolicy.swift */,
F56B94451FAE20AF0095662F /* PrepareForSkeletonProtocol.swift */,
F5307E311FB0F42F00EE67C5 /* RecursiveProtocol.swift */,
F570ABF32314629700390248 /* Swizzling.swift */,
);
path = Helpers;
sourceTree = "<group>";
@@ -684,6 +688,7 @@
17DD0E0C207FB28900C56334 /* UICollectionView+CollectionSkeleton.swift in Sources */,
F5804773230ECD0000066D02 /* UIView+IBInspectable.swift in Sources */,
17DD0E13207FB28C00C56334 /* UIView+UIApplicationDelegate.swift in Sources */,
F570ABF52314629700390248 /* Swizzling.swift in Sources */,
F5D3FB0C209DCAA300003FCF /* SubviewsSkeletonables.swift in Sources */,
17DD0E14207FB28F00C56334 /* SkeletonAnimationBuilder.swift in Sources */,
17DD0E11207FB28C00C56334 /* UIView+Frame.swift in Sources */,
@@ -740,6 +745,7 @@
F5F622411FAC6E31007C062A /* UIColor+Skeleton.swift in Sources */,
F5804772230ECD0000066D02 /* UIView+IBInspectable.swift in Sources */,
F587FB84202CBFC8002DB5FE /* SkeletonFlow.swift in Sources */,
F570ABF42314629700390248 /* Swizzling.swift in Sources */,
F5D3FB0B209DCAA300003FCF /* SubviewsSkeletonables.swift in Sources */,
F5F899D21FAB9630002E8FDA /* AssociationPolicy.swift in Sources */,
F54CF5E62024CEB000330B0D /* UICollectionView+CollectionSkeleton.swift in Sources */,
@@ -1038,7 +1044,7 @@
52D6D9911BEFF229002C0205 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
APPLICATION_EXTENSION_API_ONLY = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
@@ -1063,7 +1069,7 @@
52D6D9921BEFF229002C0205 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
APPLICATION_EXTENSION_API_ONLY = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
+5 -2
View File
@@ -9,6 +9,7 @@ public protocol Appearance {
var multilineSpacing: CGFloat { get set }
var multilineLastLineFillPercent: Int { get set }
var multilineCornerRadius: Int { get set }
var renderSingleLineAsView: Bool { get set }
}
public enum SkeletonAppearance {
@@ -19,9 +20,9 @@ public enum SkeletonAppearance {
class SkeletonViewAppearance: Appearance {
static var shared = SkeletonViewAppearance()
var tintColor: UIColor = .clouds
var tintColor: UIColor = .skeletonDefault
var gradient: SkeletonGradient = SkeletonGradient(baseColor: .clouds)
var gradient: SkeletonGradient = SkeletonGradient(baseColor: .skeletonDefault)
var multilineHeight: CGFloat = 15
@@ -30,5 +31,7 @@ class SkeletonViewAppearance: Appearance {
var multilineLastLineFillPercent: Int = 70
var multilineCornerRadius: Int = 0
var renderSingleLineAsView: Bool = false
}
// codebeat:enable[TOO_MANY_IVARS]
@@ -7,8 +7,11 @@ import UIKit
class SkeletonMultilineLayerBuilder {
var skeletonType: SkeletonType?
var index: Int?
var height: CGFloat?
var width: CGFloat?
var cornerRadius: Int?
var multilineSpacing: CGFloat = SkeletonAppearance.default.multilineSpacing
var paddingInsets: UIEdgeInsets = .zero
func setSkeletonType(_ type: SkeletonType) -> SkeletonMultilineLayerBuilder {
self.skeletonType = type
@@ -20,6 +23,11 @@ class SkeletonMultilineLayerBuilder {
return self
}
func setHeight(_ height: CGFloat) -> SkeletonMultilineLayerBuilder {
self.height = height
return self
}
func setWidth(_ width: CGFloat) -> SkeletonMultilineLayerBuilder {
self.width = width
return self
@@ -30,17 +38,28 @@ class SkeletonMultilineLayerBuilder {
return self
}
func setMultilineSpacing(_ spacing: CGFloat) -> SkeletonMultilineLayerBuilder {
self.multilineSpacing = spacing
return self
}
func setPadding(_ insets: UIEdgeInsets) -> SkeletonMultilineLayerBuilder {
self.paddingInsets = insets
return self
}
func build() -> CALayer? {
guard let type = skeletonType,
let index = index,
let width = width,
let height = height,
let radius = cornerRadius
else { return nil }
let layer = type.layer
layer.anchorPoint = .zero
layer.name = CALayer.skeletonSubLayersName
layer.updateLayerFrame(for: index, width: width)
layer.updateLayerFrame(for: index, size: CGSize(width: width, height: height), multilineSpacing: self.multilineSpacing, paddingInsets: paddingInsets)
layer.cornerRadius = CGFloat(radius)
layer.masksToBounds = true
@@ -29,6 +29,6 @@ extension CollectionSkeleton where Self: UIScrollView {
var estimatedNumberOfRows: Int { return 0 }
func addDummyDataSource() {}
func removeDummyDataSource(reloadAfter: Bool) {}
func disableUserInteraction() { isScrollEnabled = false }
func enableUserInteraction() { isScrollEnabled = true }
func disableUserInteraction() { isUserInteractionEnabled = false; isScrollEnabled = false }
func enableUserInteraction() { isUserInteractionEnabled = true; isScrollEnabled = true }
}
@@ -36,7 +36,7 @@ extension SkeletonCollectionDataSource: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = originalTableViewDataSource?.collectionSkeletonView(tableView, cellIdentifierForRowAt: indexPath) ?? ""
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
skeletonCellIfContainerSkeletonIsActive(container: tableView, cell: cell)
skeletonViewIfContainerSkeletonIsActive(container: tableView, view: cell)
return cell
}
}
@@ -54,7 +54,7 @@ extension SkeletonCollectionDataSource: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cellIdentifier = originalCollectionViewDataSource?.collectionSkeletonView(collectionView, cellIdentifierForItemAt: indexPath) ?? ""
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
skeletonCellIfContainerSkeletonIsActive(container: collectionView, cell: cell)
skeletonViewIfContainerSkeletonIsActive(container: collectionView, view: cell)
return cell
}
@@ -63,8 +63,9 @@ extension SkeletonCollectionDataSource: UICollectionViewDataSource {
at indexPath: IndexPath) -> UICollectionReusableView {
if let viewIdentifier = originalCollectionViewDataSource?.collectionSkeletonView(collectionView, supplementaryViewIdentifierOfKind: kind, at: indexPath) {
return collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: viewIdentifier, for: indexPath)
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: viewIdentifier, for: indexPath)
skeletonViewIfContainerSkeletonIsActive(container: collectionView, view: view)
return view
}
return UICollectionReusableView()
@@ -73,12 +74,12 @@ extension SkeletonCollectionDataSource: UICollectionViewDataSource {
}
extension SkeletonCollectionDataSource {
private func skeletonCellIfContainerSkeletonIsActive(container: UIView, cell: UIView) {
private func skeletonViewIfContainerSkeletonIsActive(container: UIView, view: UIView) {
guard container.isSkeletonActive,
let skeletonConfig = container.currentSkeletonConfig else {
return
}
cell.showSkeleton(skeletonConfig: skeletonConfig)
view.showSkeleton(skeletonConfig: skeletonConfig)
}
}
@@ -18,8 +18,41 @@ class SkeletonCollectionDelegate: NSObject {
}
}
// MARK: - UITableViewDataSource
extension SkeletonCollectionDelegate: UITableViewDelegate { }
// 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) {
// MARK: - UICollectionViewDataSource
skeletonViewIfContainerSkeletonIsActive(container: tableView, view: header)
return header
}
return nil
}
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
}
}
// MARK: - UICollectionViewDelegate
extension SkeletonCollectionDelegate: UICollectionViewDelegate { }
extension SkeletonCollectionDelegate {
private func skeletonViewIfContainerSkeletonIsActive(container: UIView, view: UIView) {
guard container.isSkeletonActive,
let skeletonConfig = container.currentSkeletonConfig else {
return
}
view.showSkeleton(skeletonConfig: skeletonConfig)
}
}
@@ -29,4 +29,17 @@ public extension SkeletonTableViewDataSource {
}
}
public protocol SkeletonTableViewDelegate: UITableViewDelegate { }
public protocol SkeletonTableViewDelegate: UITableViewDelegate {
func collectionSkeletonView(_ skeletonView: UITableView, identifierForHeaderInSection section: Int) -> ReusableHeaderFooterIdentifier?
func collectionSkeletonView(_ skeletonView: UITableView, identifierForFooterInSection section: Int) -> ReusableHeaderFooterIdentifier?
}
public extension SkeletonTableViewDelegate {
func collectionSkeletonView(_ skeletonView: UITableView, identifierForHeaderInSection section: Int) -> ReusableHeaderFooterIdentifier? {
return nil
}
func collectionSkeletonView(_ skeletonView: UITableView, identifierForFooterInSection section: Int) -> ReusableHeaderFooterIdentifier? {
return nil
}
}
@@ -8,6 +8,8 @@
import UIKit
public typealias ReusableHeaderFooterIdentifier = String
extension UITableView: CollectionSkeleton {
var estimatedNumberOfRows: Int {
return Int(ceil(frame.height/rowHeight))
@@ -37,6 +39,14 @@ extension UITableView: CollectionSkeleton {
let dataSource = SkeletonCollectionDataSource(tableViewDataSource: originalDataSource,
rowHeight: rowHeight)
self.skeletonDataSource = dataSource
if let originalDelegate = self.delegate as? SkeletonTableViewDelegate,
!(originalDelegate is SkeletonCollectionDelegate)
{
let delegate = SkeletonCollectionDelegate(tableViewDelegate: originalDelegate)
self.skeletonDelegate = delegate
}
reloadData()
}
@@ -53,6 +63,12 @@ extension UITableView: CollectionSkeleton {
restoreRowHeight()
self.skeletonDataSource = nil
self.dataSource = dataSource.originalTableViewDataSource
if let delegate = self.delegate as? SkeletonCollectionDelegate {
self.skeletonDelegate = nil
self.delegate = delegate.originalTableViewDelegate
}
if reloadAfter { self.reloadData() }
}
@@ -11,6 +11,7 @@ import UIKit
extension UIView {
func addDummyDataSourceIfNeeded() {
guard let collection = self as? CollectionSkeleton else { return }
status = .on
collection.addDummyDataSource()
collection.disableUserInteraction()
}
@@ -22,6 +23,7 @@ extension UIView {
func removeDummyDataSourceIfNeeded(reloadAfter reload: Bool = true) {
guard let collection = self as? CollectionSkeleton else { return }
status = .off
collection.removeDummyDataSource(reloadAfter: reload)
collection.enableUserInteraction()
}
+64 -40
View File
@@ -28,6 +28,16 @@ extension CAGradientLayer {
}
}
struct SkeletonMultilinesLayerConfig {
var lines: Int
var lineHeight: CGFloat? = nil
var type: SkeletonType
var lastLineFillPercent: Int
var multilineCornerRadius: Int
var multilineSpacing: CGFloat
var paddingInsets: UIEdgeInsets
}
// MARK: Skeleton sublayers
extension CALayer {
@@ -37,19 +47,22 @@ extension CALayer {
return sublayers?.filter { $0.name == CALayer.skeletonSubLayersName } ?? [CALayer]()
}
func addMultilinesLayers(lines: Int, type: SkeletonType, lastLineFillPercent: Int, multilineCornerRadius: Int) {
let numberOfSublayers = calculateNumLines(maxLines: lines)
func addMultilinesLayers(for config: SkeletonMultilinesLayerConfig) {
let numberOfSublayers = config.lines == 1 ? 1 : calculateNumLines(for: config)
var height = config.lineHeight ?? SkeletonAppearance.default.multilineHeight
if numberOfSublayers == 1 {
height = bounds.height
}
let layerBuilder = SkeletonMultilineLayerBuilder()
.setSkeletonType(type)
.setCornerRadius(multilineCornerRadius)
.setSkeletonType(config.type)
.setCornerRadius(config.multilineCornerRadius)
.setMultilineSpacing(config.multilineSpacing)
.setPadding(config.paddingInsets)
.setHeight(height)
(0..<numberOfSublayers).forEach { index in
var width = getLineWidth(index: index, numberOfSublayers: numberOfSublayers, lastLineFillPercent: lastLineFillPercent)
if index == numberOfSublayers - 1 && numberOfSublayers != 1 {
width = width * CGFloat(lastLineFillPercent) / 100;
}
let width = calculatedWidthForLine(at: index, totalLines: numberOfSublayers, lastLineFillPercent: config.lastLineFillPercent, paddingInsets: config.paddingInsets)
if let layer = layerBuilder
.setIndex(index)
.setWidth(width)
@@ -58,34 +71,41 @@ extension CALayer {
}
}
}
func updateMultilinesLayers(lastLineFillPercent: Int) {
func updateMultilinesLayers(for config: SkeletonMultilinesLayerConfig) {
let currentSkeletonSublayers = skeletonSublayers
let numberOfSublayers = currentSkeletonSublayers.count
let lastLineFillPercent = config.lastLineFillPercent
let paddingInsets = config.paddingInsets
let multilineSpacing = config.multilineSpacing
var height = config.lineHeight ?? SkeletonAppearance.default.multilineHeight
if numberOfSublayers == 1 {
height = bounds.height
}
for (index, layer) in currentSkeletonSublayers.enumerated() {
let width = getLineWidth(index: index, numberOfSublayers: numberOfSublayers, lastLineFillPercent: lastLineFillPercent)
layer.updateLayerFrame(for: index, width: width)
let width = calculatedWidthForLine(at: index, totalLines: numberOfSublayers, lastLineFillPercent: lastLineFillPercent, paddingInsets: paddingInsets)
layer.updateLayerFrame(for: index, size: CGSize(width: width, height: height), multilineSpacing: multilineSpacing, paddingInsets: paddingInsets)
}
}
private func getLineWidth(index: Int, numberOfSublayers: Int, lastLineFillPercent: Int) -> CGFloat {
var width = bounds.width
if index == numberOfSublayers - 1 && numberOfSublayers != 1 {
width = width * CGFloat(lastLineFillPercent) / 100;
private func calculatedWidthForLine(at index: Int, totalLines: Int, lastLineFillPercent: Int, paddingInsets: UIEdgeInsets) -> CGFloat {
var width = bounds.width - paddingInsets.left - paddingInsets.right
if index == totalLines - 1 && totalLines != 1 {
width = width * CGFloat(lastLineFillPercent) / 100
}
return width
}
func updateLayerFrame(for index: Int, width: CGFloat) {
let spaceRequiredForEachLine = SkeletonAppearance.default.multilineHeight + SkeletonAppearance.default.multilineSpacing
frame = CGRect(x: 0.0, y: CGFloat(index) * spaceRequiredForEachLine, width: width, height: SkeletonAppearance.default.multilineHeight)
func updateLayerFrame(for index: Int, size: CGSize, multilineSpacing: CGFloat, paddingInsets: UIEdgeInsets) {
let spaceRequiredForEachLine = SkeletonAppearance.default.multilineHeight + multilineSpacing
frame = CGRect(x: paddingInsets.left, y: CGFloat(index) * spaceRequiredForEachLine + paddingInsets.top, width: size.width, height: size.height)
}
private func calculateNumLines(maxLines: Int) -> Int {
let spaceRequitedForEachLine = SkeletonAppearance.default.multilineHeight + SkeletonAppearance.default.multilineSpacing
var numberOfSublayers = Int(round(CGFloat(bounds.height)/CGFloat(spaceRequitedForEachLine)))
if maxLines != 0, maxLines <= numberOfSublayers { numberOfSublayers = maxLines }
private func calculateNumLines(for config: SkeletonMultilinesLayerConfig) -> Int {
let requiredSpaceForEachLine = (config.lineHeight ?? SkeletonAppearance.default.multilineHeight) + config.multilineSpacing
var numberOfSublayers = Int(round(CGFloat(bounds.height - config.paddingInsets.top - config.paddingInsets.bottom)/CGFloat(requiredSpaceForEachLine)))
if config.lines != 0, config.lines <= numberOfSublayers { numberOfSublayers = config.lines }
return numberOfSublayers
}
}
@@ -100,6 +120,7 @@ public extension CALayer {
pulseAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
pulseAnimation.autoreverses = true
pulseAnimation.repeatCount = .infinity
pulseAnimation.isRemovedOnCompletion = false
return pulseAnimation
}
@@ -117,15 +138,19 @@ public extension CALayer {
animGroup.duration = 1.5
animGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
animGroup.repeatCount = .infinity
animGroup.isRemovedOnCompletion = false
return animGroup
}
func playAnimation(_ anim: SkeletonLayerAnimation, key: String) {
func playAnimation(_ anim: SkeletonLayerAnimation, key: String, completion: (() -> Void)? = nil) {
skeletonSublayers.recursiveSearch(leafBlock: {
DispatchQueue.main.async { CATransaction.begin() }
DispatchQueue.main.async { CATransaction.setCompletionBlock(completion) }
add(anim(self), forKey: key)
DispatchQueue.main.async { CATransaction.commit() }
}) {
$0.playAnimation(anim, key: key)
$0.playAnimation(anim, key: key, completion: completion)
}
}
@@ -139,16 +164,15 @@ public extension CALayer {
}
extension CALayer {
func setOpacity(from: Int, to: Int, duration: TimeInterval, completion: (() -> Void)?) {
CATransaction.begin()
let animation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
animation.fromValue = from
animation.toValue = to
animation.duration = duration
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
// layer.contentLayer.opacity = 1
CATransaction.setCompletionBlock(completion)
add(animation, forKey: "setOpacityAnimation")
CATransaction.commit()
}
func setOpacity(from: Int, to: Int, duration: TimeInterval, completion: (() -> Void)?) {
DispatchQueue.main.async { CATransaction.begin() }
let animation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
animation.fromValue = from
animation.toValue = to
animation.duration = duration
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
DispatchQueue.main.async { CATransaction.setCompletionBlock(completion) }
add(animation, forKey: "setOpacityAnimation")
DispatchQueue.main.async { CATransaction.commit() }
}
}
+21 -1
View File
@@ -21,7 +21,13 @@ extension UIColor {
}
public var complementaryColor: UIColor {
return isLight() ? darker : lighter
if #available(iOS 13, tvOS 13, *) {
return UIColor { traitCollection in
return self.isLight() ? self.darker : self.lighter
}
} else {
return isLight() ? darker : lighter
}
}
public var lighter: UIColor {
@@ -58,11 +64,25 @@ public extension UIColor {
static var carrot = UIColor(0xe67e22)
static var alizarin = UIColor(0xe74c3c)
static var clouds = UIColor(0xecf0f1)
static var darkClouds = UIColor(0x1c2325)
static var concrete = UIColor(0x95a5a6)
static var flatOrange = UIColor(0xf39c12)
static var pumpkin = UIColor(0xd35400)
static var pomegranate = UIColor(0xc0392b)
static var silver = UIColor(0xbdc3c7)
static var asbestos = UIColor(0x7f8c8d)
static var skeletonDefault: UIColor {
if #available(iOS 13, tvOS 13, *) {
return UIColor { traitCollection in
switch traitCollection.userInterfaceStyle {
case .dark: return .darkClouds
default: return .clouds
}
}
} else {
return .clouds
}
}
}
// codebeat:enable[TOO_MANY_IVARS]
+1 -1
View File
@@ -42,7 +42,7 @@ extension UIView {
set { ao_set(newValue ?? .off, pkey: &ViewAssociatedKeys.status) }
}
var skeletonIsAnimated: Bool! {
var isSkeletonAnimated: Bool! {
get { return ao_get(pkey: &ViewAssociatedKeys.isSkeletonAnimated) as? Bool ?? false }
set { ao_set(newValue ?? false, pkey: &ViewAssociatedKeys.isSkeletonAnimated) }
}
@@ -13,7 +13,7 @@ public extension UIView {
return status == .on || (subviewsSkeletonables.first(where: { $0.isSkeletonActive }) != nil)
}
fileprivate var skeletonable: Bool! {
private var skeletonable: Bool! {
get { return ao_get(pkey: &ViewAssociatedKeys.skeletonable) as? Bool ?? false }
set { ao_set(newValue ?? false, pkey: &ViewAssociatedKeys.skeletonable) }
}
@@ -12,12 +12,14 @@ extension UIView {
enum Constants {
static let becomeActiveNotification = UIApplication.didBecomeActiveNotification
static let enterForegroundNotification = UIApplication.didEnterBackgroundNotification
static let willTerminateNotification = UIApplication.willTerminateNotification
static let needAnimatedSkeletonKey = "needAnimateSkeleton"
}
func addAppNotificationsObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: Constants.becomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: Constants.enterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willTerminateNotification), name: Constants.enterForegroundNotification, object: nil)
}
func removeAppNoticationsObserver() {
@@ -32,6 +34,10 @@ extension UIView {
}
@objc func appDidEnterBackground() {
UserDefaults.standard.set((isSkeletonActive && skeletonIsAnimated), forKey: Constants.needAnimatedSkeletonKey)
UserDefaults.standard.set((isSkeletonActive && isSkeletonAnimated), forKey: Constants.needAnimatedSkeletonKey)
}
@objc func willTerminateNotification() {
UserDefaults.standard.set(false, forKey: Constants.needAnimatedSkeletonKey)
}
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright © 2019 SkeletonView. All rights reserved.
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()
}
}
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 {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
@@ -5,12 +5,17 @@ import UIKit
enum MultilineAssociatedKeys {
static var lastLineFillingPercent = "lastLineFillingPercent"
static var multilineCornerRadius = "multilineCornerRadius"
static var multilineSpacing = "multilineSpacing"
static var paddingInsets = "paddingInsets"
}
protocol ContainsMultilineText {
var multilineTextFont: UIFont? { get }
var numLines: Int { get }
var lastLineFillingPercent: Int { get }
var multilineCornerRadius: Int { get }
var multilineSpacing: CGFloat { get }
var paddingInsets: UIEdgeInsets { get }
}
extension ContainsMultilineText {
@@ -13,9 +13,23 @@ public extension UILabel {
get { return multilineCornerRadius }
set { multilineCornerRadius = min(newValue, 10) }
}
@IBInspectable
var skeletonLineSpacing: CGFloat {
get { return multilineSpacing }
set { multilineSpacing = min(newValue, 10) }
}
@IBInspectable
var skeletonPaddingInsets: UIEdgeInsets {
get { return paddingInsets }
set { paddingInsets = newValue }
}
}
extension UILabel: ContainsMultilineText {
var multilineTextFont: UIFont? {
return font
}
var numLines: Int {
return numberOfLines
}
@@ -29,4 +43,14 @@ extension UILabel: ContainsMultilineText {
get { return ao_get(pkey: &MultilineAssociatedKeys.multilineCornerRadius) as? Int ?? SkeletonAppearance.default.multilineCornerRadius }
set { ao_set(newValue, pkey: &MultilineAssociatedKeys.multilineCornerRadius) }
}
var multilineSpacing: CGFloat {
get { return ao_get(pkey: &MultilineAssociatedKeys.multilineSpacing) as? CGFloat ?? SkeletonAppearance.default.multilineSpacing }
set { ao_set(newValue, pkey: &MultilineAssociatedKeys.multilineSpacing) }
}
var paddingInsets: UIEdgeInsets {
get { return ao_get(pkey: &MultilineAssociatedKeys.paddingInsets) as? UIEdgeInsets ?? .zero }
set { ao_set(newValue, pkey: &MultilineAssociatedKeys.paddingInsets) }
}
}
@@ -14,9 +14,25 @@ public extension UITextView {
get { return multilineCornerRadius }
set { multilineCornerRadius = min(newValue, 10) }
}
@IBInspectable
var skeletonLineSpacing: CGFloat {
get { return multilineSpacing }
set { multilineSpacing = min(newValue, 10) }
}
@IBInspectable
var skeletonPaddingInsets: UIEdgeInsets {
get { return paddingInsets }
set { paddingInsets = newValue }
}
}
extension UITextView: ContainsMultilineText {
var multilineTextFont: UIFont? {
return font
}
var lastLineFillingPercent: Int {
get {
let defaultValue = SkeletonAppearance.default.multilineLastLineFillPercent
@@ -32,4 +48,14 @@ extension UITextView: ContainsMultilineText {
}
set { ao_set(newValue, pkey: &MultilineAssociatedKeys.multilineCornerRadius) }
}
var multilineSpacing: CGFloat {
get { return ao_get(pkey: &MultilineAssociatedKeys.multilineSpacing) as? CGFloat ?? SkeletonAppearance.default.multilineSpacing }
set { ao_set(newValue, pkey: &MultilineAssociatedKeys.multilineSpacing) }
}
var paddingInsets: UIEdgeInsets {
get { return ao_get(pkey: &MultilineAssociatedKeys.paddingInsets) as? UIEdgeInsets ?? .zero }
set { ao_set(newValue, pkey: &MultilineAssociatedKeys.paddingInsets) }
}
}
+2
View File
@@ -53,6 +53,7 @@ extension UILabel{
startTransition { [weak self] in
self?.textColor = self?.labelState?.textColor
self?.text = self?.labelState?.text
self?.isUserInteractionEnabled = self?.labelState?.isUserInteractionsEnabled ?? false
}
}
}
@@ -73,6 +74,7 @@ extension UITextView{
startTransition { [weak self] in
self?.textColor = self?.textState?.textColor
self?.text = self?.textState?.text
self?.isUserInteractionEnabled = self?.textState?.isUserInteractionsEnabled ?? false
}
}
}
@@ -23,15 +23,18 @@ struct RecoverableViewState {
struct RecoverableTextViewState {
var text: String?
var textColor: UIColor?
var isUserInteractionsEnabled: Bool
init(view: UILabel) {
self.textColor = view.textColor
self.text = view.text
self.isUserInteractionsEnabled = view.isUserInteractionEnabled
}
init(view: UITextView) {
self.textColor = view.textColor
self.text = view.text
self.isUserInteractionsEnabled = view.isUserInteractionEnabled
}
}
+1
View File
@@ -78,6 +78,7 @@ public class SkeletonAnimationBuilder {
animGroup.duration = duration
animGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
animGroup.repeatCount = .infinity
animGroup.isRemovedOnCompletion = false
return animGroup
}
+33 -19
View File
@@ -3,43 +3,57 @@
import UIKit
protocol SkeletonFlowDelegate {
func willBeginShowingSkeletons(withRootView rootView: UIView)
func didShowSkeletons(withRootView rootView: UIView)
func willBeginUpdatingSkeletons(withRootView rootView: UIView)
func didUpdateSkeletons(withRootView rootView: UIView)
func willBeginLayingSkeletonsIfNeeded(withRootView: UIView)
func didLayoutSkeletonsIfNeeded(withRootView: UIView)
func willBeginHidingSkeletons(withRootView rootView: UIView)
func didHideSkeletons(withRootView rootView: UIView)
func willBeginShowingSkeletons(rootView: UIView)
func didShowSkeletons(rootView: UIView)
func willBeginUpdatingSkeletons(rootView: UIView)
func didUpdateSkeletons(rootView: UIView)
func willBeginLayingSkeletonsIfNeeded(rootView: UIView)
func didLayoutSkeletonsIfNeeded(rootView: UIView)
func willBeginHidingSkeletons(rootView: UIView)
func didHideSkeletons(rootView: UIView)
}
class SkeletonFlowHandler: SkeletonFlowDelegate {
func willBeginShowingSkeletons(withRootView rootView: UIView) {
func willBeginShowingSkeletons(rootView: UIView) {
NotificationCenter.default.post(name: .willBeginShowingSkeletons, object: rootView, userInfo: nil)
rootView.addAppNotificationsObservers()
}
func didShowSkeletons(withRootView rootView: UIView) {
func didShowSkeletons(rootView: UIView) {
printSkeletonHierarchy(in: rootView)
}
func willBeginUpdatingSkeletons(withRootView rootView: UIView) {
NotificationCenter.default.post(name: .didShowSkeletons, object: rootView, userInfo: nil)
}
func didUpdateSkeletons(withRootView rootView: UIView) {
func willBeginUpdatingSkeletons(rootView: UIView) {
NotificationCenter.default.post(name: .willBeginUpdatingSkeletons, object: rootView, userInfo: nil)
}
func willBeginLayingSkeletonsIfNeeded(withRootView: UIView) {
func didUpdateSkeletons(rootView: UIView) {
NotificationCenter.default.post(name: .didUpdateSkeletons, object: rootView, userInfo: nil)
}
func didLayoutSkeletonsIfNeeded(withRootView: UIView) {
func willBeginLayingSkeletonsIfNeeded(rootView: UIView) {
}
func willBeginHidingSkeletons(withRootView rootView: UIView) {
func didLayoutSkeletonsIfNeeded(rootView: UIView) {
}
func willBeginHidingSkeletons(rootView: UIView) {
NotificationCenter.default.post(name: .willBeginHidingSkeletons, object: rootView, userInfo: nil)
rootView.removeAppNoticationsObserver()
}
func didHideSkeletons(withRootView rootView: UIView) {
func didHideSkeletons(rootView: UIView) {
rootView.flowDelegate = nil
NotificationCenter.default.post(name: .didHideSkeletons, object: rootView, userInfo: nil)
}
}
public extension Notification.Name {
static let willBeginShowingSkeletons = Notification.Name("willBeginShowingSkeletons")
static let didShowSkeletons = Notification.Name("didShowSkeletons")
static let willBeginUpdatingSkeletons = Notification.Name("willBeginUpdatingSkeletons")
static let didUpdateSkeletons = Notification.Name("didUpdateSkeletons")
static let willBeginHidingSkeletons = Notification.Name("willBeginHidingSkeletons")
static let didHideSkeletons = Notification.Name("didHideSkeletons")
}
+44 -17
View File
@@ -50,7 +50,7 @@ struct SkeletonLayer {
self.maskLayer = type.layer
self.maskLayer.anchorPoint = .zero
self.maskLayer.bounds = holder.maxBoundsEstimated
addMultilinesIfNeeded()
addTextLinesIfNeeded()
self.maskLayer.tint(withColors: colors)
}
@@ -63,38 +63,65 @@ struct SkeletonLayer {
if let bounds = holder?.maxBoundsEstimated {
maskLayer.bounds = bounds
}
updateMultilinesIfNeeded()
updateLinesIfNeeded()
}
func removeLayer(transition: SkeletonTransitionStyle, completion: (() -> Void)? = nil) {
switch transition {
case .none:
maskLayer.removeFromSuperlayer()
completion?()
case .crossDissolve(let duration):
maskLayer.setOpacity(from: 1, to: 0, duration: duration) {
self.maskLayer.removeFromSuperlayer()
completion?()
}
maskLayer.setOpacity(from: 1, to: 0, duration: duration) {
self.maskLayer.removeFromSuperlayer()
completion?()
}
}
}
func addMultilinesIfNeeded() {
guard let multiLineView = holder as? ContainsMultilineText else { return }
maskLayer.addMultilinesLayers(lines: multiLineView.numLines, type: type, lastLineFillPercent: multiLineView.lastLineFillingPercent, multilineCornerRadius: multiLineView.multilineCornerRadius)
}
func updateMultilinesIfNeeded() {
guard let multiLineView = holder as? ContainsMultilineText else { return }
maskLayer.updateMultilinesLayers(lastLineFillPercent: multiLineView.lastLineFillingPercent)
/// 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 config = SkeletonMultilinesLayerConfig(lines: textView.numLines,
lineHeight: textView.multilineTextFont?.lineHeight,
type: type,
lastLineFillPercent: textView.lastLineFillingPercent,
multilineCornerRadius: textView.multilineCornerRadius,
multilineSpacing: textView.multilineSpacing,
paddingInsets: textView.paddingInsets)
maskLayer.addMultilinesLayers(for: config)
}
func updateLinesIfNeeded() {
guard let textView = holderAsTextView else { return }
let config = SkeletonMultilinesLayerConfig(lines: textView.numLines,
lineHeight: textView.multilineTextFont?.lineHeight,
type: type,
lastLineFillPercent: textView.lastLineFillingPercent,
multilineCornerRadius: textView.multilineCornerRadius,
multilineSpacing: textView.multilineSpacing,
paddingInsets: textView.paddingInsets)
maskLayer.updateMultilinesLayers(for: config)
}
var holderAsTextView: ContainsMultilineText? {
guard let textView = holder as? ContainsMultilineText,
(textView.numLines == 0 || textView.numLines > 1 || textView.numLines == 1 && !SkeletonAppearance.default.renderSingleLineAsView) else {
return nil
}
return textView
}
}
extension SkeletonLayer {
func start(_ anim: SkeletonLayerAnimation? = nil) {
func start(_ anim: SkeletonLayerAnimation? = nil, completion: (() -> Void)? = nil) {
let animation = anim ?? type.layerAnimation
contentLayer.playAnimation(animation, key: "skeletonAnimation")
contentLayer.playAnimation(animation, key: "skeletonAnimation", completion: completion)
}
func stopAnimation() {
contentLayer.stopAnimation(forKey: "skeletonAnimation")
}
+106 -51
View File
@@ -3,21 +3,47 @@
import UIKit
public extension UIView {
/// Shows the skeleton without animation using the view that calls this method as root view.
///
/// - Parameters:
/// - color: The color of the skeleton. Defaults to `SkeletonAppearance.default.tintColor`.
/// - transition: The style of the transition when the skeleton appears. Defaults to `.none`.
func showSkeleton(usingColor color: UIColor = SkeletonAppearance.default.tintColor, transition: SkeletonTransitionStyle = .none) {
let config = SkeletonConfig(type: .solid, colors: [color], transition: transition)
showSkeleton(skeletonConfig: config)
}
/// 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 `.none`.
func showGradientSkeleton(usingGradient gradient: SkeletonGradient = SkeletonAppearance.default.gradient, transition: SkeletonTransitionStyle = .none) {
let config = SkeletonConfig(type: .gradient, colors: gradient.colors, transition: transition)
showSkeleton(skeletonConfig: config)
}
/// 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.
///
/// - Parameters:
/// - color: The color of skeleton. Defaults to `SkeletonAppearance.default.tintColor`.
/// - animation: The animation of the skeleton. Defaults to `nil`.
/// - transition: The style of the transition when the skeleton appears. Defaults to `.none`.
func showAnimatedSkeleton(usingColor color: UIColor = SkeletonAppearance.default.tintColor, animation: SkeletonLayerAnimation? = nil, transition: SkeletonTransitionStyle = .none) {
let config = SkeletonConfig(type: .solid, colors: [color], animated: true, animation: animation, transition: transition)
showSkeleton(skeletonConfig: config)
}
/// Shows the gradient skeleton without animation using the view that calls this method as root view.
///
/// If animation is nil, sliding animation will be used, with direction left to right.
///
/// - Parameters:
/// - gradient: The gradient of the skeleton. Defaults to `SkeletonAppearance.default.gradient`.
/// - animation: The animation of the skeleton. Defaults to `nil`.
/// - transition: The style of the transition when the skeleton appears. Defaults to `.none`.
func showAnimatedGradientSkeleton(usingGradient gradient: SkeletonGradient = SkeletonAppearance.default.gradient, animation: SkeletonLayerAnimation? = nil, transition: SkeletonTransitionStyle = .none) {
let config = SkeletonConfig(type: .gradient, colors: gradient.colors, animated: true, animation: animation, transition: transition)
showSkeleton(skeletonConfig: config)
@@ -44,29 +70,22 @@ public extension UIView {
}
func layoutSkeletonIfNeeded() {
guard let flowDelegate = flowDelegate else { return }
flowDelegate.willBeginLayingSkeletonsIfNeeded(withRootView: self)
flowDelegate?.willBeginLayingSkeletonsIfNeeded(rootView: self)
recursiveLayoutSkeletonIfNeeded(root: self)
}
func hideSkeleton(reloadDataAfter reload: Bool = true, transition: SkeletonTransitionStyle = .none) {
if var config = currentSkeletonConfig {
config.transition = transition
updateSkeleton(skeletonConfig: config)
}
flowDelegate?.willBeginHidingSkeletons(withRootView: self)
recursiveHideSkeleton(reloadDataAfter: reload, root: self)
flowDelegate?.willBeginHidingSkeletons(rootView: self)
recursiveHideSkeleton(reloadDataAfter: reload, transition: transition, root: self)
}
func startSkeletonAnimation(_ anim: SkeletonLayerAnimation? = nil) {
skeletonIsAnimated = true
subviewsSkeletonables.recursiveSearch(leafBlock: startSkeletonLayerAnimationBlock(anim)) { subview in
subview.startSkeletonAnimation(anim)
}
}
func stopSkeletonAnimation() {
skeletonIsAnimated = false
subviewsSkeletonables.recursiveSearch(leafBlock: stopSkeletonLayerAnimationBlock) { subview in
subview.stopSkeletonAnimation()
}
@@ -74,19 +93,29 @@ public extension UIView {
}
extension UIView {
@objc func skeletonLayoutSubviews() {
skeletonLayoutSubviews()
guard isSkeletonActive else { return }
layoutSkeletonIfNeeded()
}
@objc func skeletonTraitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
guard isSkeletonActive, let config = currentSkeletonConfig else { return }
updateSkeleton(skeletonConfig: config)
}
func showSkeleton(skeletonConfig config: SkeletonConfig) {
skeletonIsAnimated = config.animated
isSkeletonAnimated = config.animated
flowDelegate = SkeletonFlowHandler()
flowDelegate?.willBeginShowingSkeletons(withRootView: self)
flowDelegate?.willBeginShowingSkeletons(rootView: self)
recursiveShowSkeleton(skeletonConfig: config, root: self)
}
fileprivate func recursiveShowSkeleton(skeletonConfig config: SkeletonConfig, root: UIView? = nil) {
layoutIfNeeded()
private func recursiveShowSkeleton(skeletonConfig config: SkeletonConfig, root: UIView? = nil) {
guard !isSkeletonActive && isSkeletonable else { return }
currentSkeletonConfig = config
swizzleLayoutSubviews()
swizzleTraitCollectionDidChange()
addDummyDataSourceIfNeeded()
subviewsSkeletonables.recursiveSearch(leafBlock: {
showSkeletonIfNotActive(skeletonConfig: config)
@@ -95,94 +124,117 @@ extension UIView {
}
if let root = root {
flowDelegate?.didShowSkeletons(withRootView: root)
flowDelegate?.didShowSkeletons(rootView: root)
}
}
fileprivate func showSkeletonIfNotActive(skeletonConfig config: SkeletonConfig) {
private func showSkeletonIfNotActive(skeletonConfig config: SkeletonConfig) {
guard !isSkeletonActive else { return }
isUserInteractionEnabled = false
saveViewState()
isUserInteractionEnabled = false
prepareViewForSkeleton()
addSkeletonLayer(skeletonConfig: config)
}
func updateSkeleton(skeletonConfig config: SkeletonConfig) {
guard let flowDelegate = flowDelegate else { return }
skeletonIsAnimated = config.animated
flowDelegate.willBeginUpdatingSkeletons(withRootView: self)
isSkeletonAnimated = config.animated
flowDelegate?.willBeginUpdatingSkeletons(rootView: self)
recursiveUpdateSkeleton(skeletonConfig: config, root: self)
}
fileprivate func recursiveUpdateSkeleton(skeletonConfig config: SkeletonConfig, root: UIView? = nil) {
layoutIfNeeded()
private func recursiveUpdateSkeleton(skeletonConfig config: SkeletonConfig, root: UIView? = nil) {
guard isSkeletonActive else { return }
currentSkeletonConfig = config
updateDummyDataSourceIfNeeded()
subviewsSkeletonables.recursiveSearch(leafBlock: {
guard isSkeletonActive else { return }
if skeletonLayer?.type != config.type {
hideSkeleton()
}
if isSkeletonActive {
updateSkeletonLayer(skeletonConfig: config)
removeSkeletonLayer()
addSkeletonLayer(skeletonConfig: config)
} else {
showSkeletonIfNotActive(skeletonConfig: config)
updateSkeletonLayer(skeletonConfig: config)
}
}) { subview in
subview.recursiveUpdateSkeleton(skeletonConfig: config)
}
if let root = root {
flowDelegate?.didUpdateSkeletons(withRootView: root)
flowDelegate?.didUpdateSkeletons(rootView: root)
}
}
fileprivate func recursiveLayoutSkeletonIfNeeded(root: UIView? = nil) {
layoutIfNeeded()
private func recursiveLayoutSkeletonIfNeeded(root: UIView? = nil) {
subviewsSkeletonables.recursiveSearch(leafBlock: {
guard isSkeletonable, isSkeletonActive else { return }
layoutSkeletonLayerIfNeeded()
if let config = currentSkeletonConfig, config.animated, !isSkeletonAnimated {
startSkeletonAnimation(config.animation)
}
}) { subview in
subview.recursiveLayoutSkeletonIfNeeded()
}
if let root = root {
flowDelegate?.didLayoutSkeletonsIfNeeded(withRootView: root)
flowDelegate?.didLayoutSkeletonsIfNeeded(rootView: root)
}
}
fileprivate func recursiveHideSkeleton(reloadDataAfter reload: Bool, root: UIView? = nil) {
removeDummyDataSourceIfNeeded(reloadAfter: reload)
private func recursiveHideSkeleton(reloadDataAfter reload: Bool, transition: SkeletonTransitionStyle, root: UIView? = nil) {
guard isSkeletonActive else { return }
currentSkeletonConfig?.transition = transition
isUserInteractionEnabled = true
removeDummyDataSourceIfNeeded(reloadAfter: reload)
subviewsSkeletonables.recursiveSearch(leafBlock: {
recoverViewState(forced: false)
removeSkeletonLayer()
}) { subview in
subview.recursiveHideSkeleton(reloadDataAfter: reload)
subview.recursiveHideSkeleton(reloadDataAfter: reload, transition: transition)
}
if let root = root {
flowDelegate?.didHideSkeletons(withRootView: root)
flowDelegate?.didHideSkeletons(rootView: root)
}
}
fileprivate func startSkeletonLayerAnimationBlock(_ anim: SkeletonLayerAnimation? = nil) -> VoidBlock {
private func startSkeletonLayerAnimationBlock(_ anim: SkeletonLayerAnimation? = nil) -> VoidBlock {
return {
self.isSkeletonAnimated = true
guard let layer = self.skeletonLayer else { return }
layer.start(anim)
layer.start(anim) { [weak self] in
self?.isSkeletonAnimated = false
}
}
}
fileprivate var stopSkeletonLayerAnimationBlock: VoidBlock {
private var stopSkeletonLayerAnimationBlock: VoidBlock {
return {
self.isSkeletonAnimated = false
guard let layer = self.skeletonLayer else { return }
layer.stopAnimation()
}
}
private func swizzleLayoutSubviews() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
DispatchQueue.once(token: "UIView.SkeletonView.swizzleLayoutSubviews") {
swizzle(selector: #selector(UIView.layoutSubviews),
with: #selector(UIView.skeletonLayoutSubviews),
inClass: UIView.self,
usingClass: UIView.self)
self.layoutSkeletonIfNeeded()
}
}
}
private func swizzleTraitCollectionDidChange() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
DispatchQueue.once(token: "UIView.SkeletonView.swizzleTraitCollectionDidChange") {
swizzle(selector: #selector(UIView.traitCollectionDidChange(_:)),
with: #selector(UIView.skeletonTraitCollectionDidChange(_:)),
inClass: UIView.self,
usingClass: UIView.self)
}
}
}
}
extension UIView {
@@ -197,9 +249,9 @@ extension UIView {
self.skeletonLayer = skeletonLayer
layer.insertSublayer(skeletonLayer,
at: UInt32.max,
transition: config.transition) {
transition: config.transition) { [weak self] in
if config.animated {
skeletonLayer.start(config.animation)
self?.startSkeletonAnimation(config.animation)
}
}
status = .on
@@ -208,8 +260,11 @@ extension UIView {
func updateSkeletonLayer(skeletonConfig config: SkeletonConfig) {
guard let skeletonLayer = skeletonLayer else { return }
skeletonLayer.update(usingColors: config.colors)
if config.animated { skeletonLayer.start(config.animation) }
else { skeletonLayer.stopAnimation() }
if config.animated {
startSkeletonAnimation(config.animation)
} else {
skeletonLayer.stopAnimation()
}
}
func layoutSkeletonLayerIfNeeded() {
+6
View File
@@ -24,6 +24,12 @@ extension UITableViewCell {
}
}
extension UITableViewHeaderFooterView {
override var subviewsToSkeleton: [UIView] {
return contentView.subviews
}
}
extension UICollectionView {
override var subviewsToSkeleton: [UIView] {
return subviews
@@ -7,6 +7,7 @@ extension CALayer {
insertSublayer(layer.contentLayer, at: idx)
switch transition {
case .none:
completion?()
break
case .crossDissolve(let duration):
layer.contentLayer.setOpacity(from: 0, to: 1, duration: duration, completion: completion)