Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a106d1af2 | |||
| 5544acc63e | |||
| 65299378c4 | |||
| d8f1b4c53b | |||
| d2d8c1f5db | |||
| ab385e5afb | |||
| 5d8cd9432e | |||
| fbaf2e7b4b | |||
| 97d83c7038 | |||
| ce83713240 | |||
| 56d3156f8e | |||
| aeb9dcf2c7 | |||
| 9914be0f5b | |||
| 7d0609098c | |||
| b431aabddb | |||
| 0091ffc08b | |||
| e948e313a1 | |||
| a7ae5f0f9f | |||
| 85cce0cd7c | |||
| 3afe7286a2 | |||
| 6f1db7e303 | |||
| c1815642db | |||
| a6a168a919 | |||
| d0dbd1e004 | |||
| 4ade8f8797 | |||
| 264195bf16 | |||
| 11cc9a3ff7 | |||
| 37ac7d6e5d | |||
| a1e183a5e1 | |||
| 420785502b | |||
| eed18e5372 | |||
| 0a86ac4a9c | |||
| 264b0a70c2 | |||
| 01d68190ab | |||
| 2d57efd9cb | |||
| da51a6f673 | |||
| f6d4343cc6 | |||
| c6e20aa1a0 | |||
| 809544066f | |||
| 9406e3ef62 | |||
| 6d4c7d76c3 | |||
| f580fdaac2 | |||
| 4be93db383 | |||
| ae4ecfa760 | |||
| f60e5cd7ae | |||
| e097385de9 | |||
| 84d8971aa2 | |||
| 731509a46f | |||
| 7833c94f2e |
+1
-9
@@ -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]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
issuesOpened: >
|
||||
Thank you for raising an issue. We will try and get back to you as soon as possible.
|
||||
|
||||
Please make sure you have given us as much context as possible.
|
||||
|
||||
pullRequestOpened: >
|
||||
Thank you for raising your pull request.
|
||||
|
||||
Please make sure you have followed our contributing guidelines. We will review it as soon as possible.
|
||||
+2
-7
@@ -1,15 +1,10 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- work in progress
|
||||
onlyLabels:
|
||||
- awaiting user input
|
||||
staleLabel: given up
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
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.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Greetings
|
||||
|
||||
on: [pull_request, issues]
|
||||
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: 'Hi '
|
||||
pr-message: 'Hi! Message that will be displayed on users'
|
||||
|
||||
@@ -3,9 +3,31 @@ 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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -45,7 +45,6 @@ class ViewController: UIViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.isSkeletonable = true
|
||||
collectionView.prepareSkeleton(completion: { done in
|
||||
self.view.showAnimatedSkeleton()
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Va7-1y-Tel">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<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="14824"/>
|
||||
<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>
|
||||
@@ -13,19 +13,19 @@
|
||||
<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="0.0" width="375" height="243"/>
|
||||
<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"/>
|
||||
<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>
|
||||
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
@@ -50,7 +50,7 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<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"/>
|
||||
@@ -65,7 +65,7 @@
|
||||
</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="243" width="375" height="215"/>
|
||||
<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>
|
||||
@@ -87,12 +87,12 @@
|
||||
<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="VhU-1t-AaI" userLabel="Label">
|
||||
<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>
|
||||
@@ -131,10 +131,11 @@
|
||||
</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="XgY-1a-UGc">
|
||||
<rect key="frame" x="0.0" y="458" 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="145" height="32"/>
|
||||
@@ -159,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"/>
|
||||
@@ -190,7 +191,7 @@
|
||||
</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="130" 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"/>
|
||||
@@ -202,7 +203,7 @@
|
||||
</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"/>
|
||||
@@ -227,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"/>
|
||||
@@ -242,7 +243,7 @@
|
||||
</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"/>
|
||||
@@ -260,16 +261,16 @@
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-2682" y="340"/>
|
||||
<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="667"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="Ao1-hk-zrH"/>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" title="Item" id="iKp-9S-aib"/>
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,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
@@ -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)
|
||||
|
||||
@@ -213,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.
|
||||
@@ -230,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
|
||||
@@ -326,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
|
||||
@@ -344,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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "SkeletonView"
|
||||
s.version = "1.8.2"
|
||||
s.version = "1.8.4"
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
|
||||
@@ -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,15 +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
|
||||
let width = calculatedWidthForLine(at: index, totalLines: numberOfSublayers, lastLineFillPercent: lastLineFillPercent)
|
||||
let width = calculatedWidthForLine(at: index, totalLines: numberOfSublayers, lastLineFillPercent: config.lastLineFillPercent, paddingInsets: config.paddingInsets)
|
||||
if let layer = layerBuilder
|
||||
.setIndex(index)
|
||||
.setWidth(width)
|
||||
@@ -54,33 +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 = calculatedWidthForLine(at: index, totalLines: 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 calculatedWidthForLine(at index: Int, totalLines: Int, lastLineFillPercent: Int) -> CGFloat {
|
||||
var width = bounds.width
|
||||
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 requiredSpaceForEachLine = SkeletonAppearance.default.multilineHeight + SkeletonAppearance.default.multilineSpacing
|
||||
var numberOfSublayers = Int(round(CGFloat(bounds.height)/CGFloat(requiredSpaceForEachLine)))
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -139,15 +164,15 @@ public extension CALayer {
|
||||
}
|
||||
|
||||
extension CALayer {
|
||||
func setOpacity(from: Int, to: Int, duration: TimeInterval, completion: (() -> Void)?) {
|
||||
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)
|
||||
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")
|
||||
add(animation, forKey: "setOpacityAnimation")
|
||||
DispatchQueue.main.async { CATransaction.commit() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+39
-21
@@ -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,7 +63,7 @@ struct SkeletonLayer {
|
||||
if let bounds = holder?.maxBoundsEstimated {
|
||||
maskLayer.bounds = bounds
|
||||
}
|
||||
updateMultilinesIfNeeded()
|
||||
updateLinesIfNeeded()
|
||||
}
|
||||
|
||||
func removeLayer(transition: SkeletonTransitionStyle, completion: (() -> Void)? = nil) {
|
||||
@@ -72,29 +72,47 @@ struct SkeletonLayer {
|
||||
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?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a multiLineViewHolder only if the current holder implements the `ContainsMultilineText` protocol,
|
||||
/// and actually displays multiple lines of text.
|
||||
private var multiLineViewHolder: ContainsMultilineText? {
|
||||
guard let multiLineView = holder as? ContainsMultilineText,
|
||||
multiLineView.numLines != 1 else { return nil }
|
||||
return multiLineView
|
||||
}
|
||||
/// 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)
|
||||
|
||||
func addMultilinesIfNeeded() {
|
||||
guard let multiLineView = multiLineViewHolder else { return }
|
||||
maskLayer.addMultilinesLayers(lines: multiLineView.numLines, type: type, lastLineFillPercent: multiLineView.lastLineFillingPercent, multilineCornerRadius: multiLineView.multilineCornerRadius)
|
||||
maskLayer.addMultilinesLayers(for: config)
|
||||
}
|
||||
|
||||
func updateMultilinesIfNeeded() {
|
||||
guard let multiLineView = multiLineViewHolder else { return }
|
||||
maskLayer.updateMultilinesLayers(lastLineFillPercent: multiLineView.lastLineFillingPercent)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +121,7 @@ extension SkeletonLayer {
|
||||
let animation = anim ?? type.layerAnimation
|
||||
contentLayer.playAnimation(animation, key: "skeletonAnimation", completion: completion)
|
||||
}
|
||||
|
||||
|
||||
func stopAnimation() {
|
||||
contentLayer.stopAnimation(forKey: "skeletonAnimation")
|
||||
}
|
||||
|
||||
@@ -94,9 +94,15 @@ 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) {
|
||||
isSkeletonAnimated = config.animated
|
||||
@@ -106,9 +112,10 @@ extension UIView {
|
||||
}
|
||||
|
||||
private func recursiveShowSkeleton(skeletonConfig config: SkeletonConfig, root: UIView? = nil) {
|
||||
guard !isSkeletonActive else { return }
|
||||
guard !isSkeletonActive && isSkeletonable else { return }
|
||||
currentSkeletonConfig = config
|
||||
swizzleLayoutSubviews()
|
||||
swizzleTraitCollectionDidChange()
|
||||
addDummyDataSourceIfNeeded()
|
||||
subviewsSkeletonables.recursiveSearch(leafBlock: {
|
||||
showSkeletonIfNotActive(skeletonConfig: config)
|
||||
@@ -123,8 +130,8 @@ extension UIView {
|
||||
|
||||
private func showSkeletonIfNotActive(skeletonConfig config: SkeletonConfig) {
|
||||
guard !isSkeletonActive else { return }
|
||||
isUserInteractionEnabled = false
|
||||
saveViewState()
|
||||
isUserInteractionEnabled = false
|
||||
prepareViewForSkeleton()
|
||||
addSkeletonLayer(skeletonConfig: config)
|
||||
}
|
||||
@@ -173,16 +180,16 @@ extension UIView {
|
||||
|
||||
private func recursiveHideSkeleton(reloadDataAfter reload: Bool, transition: SkeletonTransitionStyle, root: UIView? = nil) {
|
||||
guard isSkeletonActive else { return }
|
||||
removeDummyDataSourceIfNeeded(reloadAfter: reload)
|
||||
currentSkeletonConfig?.transition = transition
|
||||
isUserInteractionEnabled = true
|
||||
removeDummyDataSourceIfNeeded(reloadAfter: reload)
|
||||
subviewsSkeletonables.recursiveSearch(leafBlock: {
|
||||
recoverViewState(forced: false)
|
||||
removeSkeletonLayer()
|
||||
}) { subview in
|
||||
subview.recursiveHideSkeleton(reloadDataAfter: reload, transition: transition)
|
||||
}
|
||||
|
||||
|
||||
if let root = root {
|
||||
flowDelegate?.didHideSkeletons(rootView: root)
|
||||
}
|
||||
@@ -208,7 +215,7 @@ extension UIView {
|
||||
|
||||
private func swizzleLayoutSubviews() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
DispatchQueue.once(token: "UIView.SkeletonView.swizzle") {
|
||||
DispatchQueue.once(token: "UIView.SkeletonView.swizzleLayoutSubviews") {
|
||||
swizzle(selector: #selector(UIView.layoutSubviews),
|
||||
with: #selector(UIView.skeletonLayoutSubviews),
|
||||
inClass: UIView.self,
|
||||
@@ -217,6 +224,17 @@ extension UIView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -24,6 +24,12 @@ extension UITableViewCell {
|
||||
}
|
||||
}
|
||||
|
||||
extension UITableViewHeaderFooterView {
|
||||
override var subviewsToSkeleton: [UIView] {
|
||||
return contentView.subviews
|
||||
}
|
||||
}
|
||||
|
||||
extension UICollectionView {
|
||||
override var subviewsToSkeleton: [UIView] {
|
||||
return subviews
|
||||
|
||||
Reference in New Issue
Block a user