Compare commits

...

24 Commits

Author SHA1 Message Date
Sofia Silva ae44d9f07a Align last line of skeleton view according to text alignment (#431)
* feature: align last line of skeleton view according to text alignment

* code review changes
2021-08-17 19:03:13 +02:00
Juanpe Catalán 25c47d3029 update file header 2021-08-02 19:10:46 +02:00
Juanpe 11c90283ff Bump version 1.21.2 2021-07-23 13:54:10 +00:00
Juanpe Catalán 49fafccd3f set status off before the transition is finished (#423) 2021-07-23 15:52:52 +02:00
Juanpe 1f70ee4573 Bump version 1.21.1 2021-07-13 11:51:26 +00:00
dependabot[bot] d13369a0dd Bump addressable from 2.6.0 to 2.8.0 (#420)
Bumps [addressable](https://github.com/sporkmonger/addressable) from 2.6.0 to 2.8.0.
- [Release notes](https://github.com/sporkmonger/addressable/releases)
- [Changelog](https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sporkmonger/addressable/compare/addressable-2.6.0...addressable-2.8.0)

---
updated-dependencies:
- dependency-name: addressable
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-13 13:50:06 +02:00
Juanpe Catalán 35d63041d2 Update README.md 2021-07-05 20:01:41 +02:00
Juanpe 4514b509cc Bump version 1.21.0 2021-07-02 14:10:57 +00:00
Guglielmo Faglioni 795fd7d0f1 Fallback to non-skeleton Header (#416)
Sometimes my collectionview's header doesn't need to be 'skeletoned' while the content does.  

When  collectionSkeletonView(_ skeletonView: UICollectionView, supplementaryViewIdentifierOfKind: String, at indexPath: IndexPath) -> ReusableCellIdentifier? returns nil, it's best if we fallback to the non-skeleton header.
2021-07-02 16:05:16 +02:00
Juanpe Catalán 6cc6f5aa80 Update README.md 2021-06-29 11:08:52 +02:00
Juanpe Catalán 3ecc3c3b39 Update README.md 2021-06-29 11:06:05 +02:00
Juanpe Catalán 511535921f Update README.md 2021-06-29 11:05:20 +02:00
Juanpe a6d1ae0b95 Bump version 1.20.0 2021-06-28 15:47:21 +00:00
Juanpe Catalán 220fc4016d Fixed background color warning in UITableViewHeaderFooterView (#417) 2021-06-28 17:45:59 +02:00
Juanpe Catalán ee94dd8aec Update README.md 2021-06-24 16:00:38 +02:00
Juanpe Catalán be2aa4f4ab create CD workflow 2021-06-24 13:26:25 +02:00
Juanpe 55f16d9d51 Bump version 1.19.0 2021-06-24 06:21:47 +00:00
Nemanja Markicevic 9fccaf4fbd Add prepareCellForSkeleton for UITableView and UICollectionView (#415)
* Add prepareCellForSkeleton for UITableView and UICollectionView

* Update README to add collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath)

* Update README.md

Co-authored-by: Juanpe Catalán <juanpecm@gmail.com>
2021-06-24 08:09:36 +02:00
Juanpe Catalán 58959a5f9b Add both skeletonCellForRowAt and skeletonCellForItemAt methods (#414)
* add skeletonCellForIndexPath

* included in README file the new data source methods

* fix typo in the README file
2021-06-23 17:49:31 +02:00
Juanpe Catalán 5838f7881b update README 2021-06-22 13:09:46 +02:00
Juanpe 41173471f6 Bump version 1.18.0 2021-06-22 10:59:04 +00:00
Juanpe Catalán 12e5688b31 Delay the presentation of the skeletons (#411) 2021-06-22 12:54:33 +02:00
Juanpe Catalán 816b2965ff Improved the algorithm that calculates the number of skeleton lines for UITextViews (#410) 2021-06-22 11:23:08 +02:00
Juanpe e12e4a0fd1 Bump version 1.17.2 2021-06-11 20:24:41 +00:00
25 changed files with 379 additions and 86 deletions
+64
View File
@@ -0,0 +1,64 @@
name: CD
on:
pull_request:
branches: [main]
types: [closed]
jobs:
release_version:
if: github.event.pull_request.milestone == null && github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup fastlane
run: brew install fastlane
- name: Publish release
id: publish_release
uses: release-drafter/release-drafter@v5
with:
publish: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update podspec
run: fastlane bump_version next_version:${{ steps.publish_release.outputs.tag_name }}
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
branch: 'main'
commit_message: 'Bump version ${{ steps.publish_release.outputs.tag_name }}'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Cocoapods
env:
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
run: |
set -eo pipefail
pod lib lint --allow-warnings
pod trunk push --allow-warnings
- name: Communicate on PR released
uses: unsplash/comment-on-pr@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
msg: |
Congratulations! 🎉 This was released as part of [SkeletonView ${{ steps.publish_release.outputs.tag_name }}](${{ steps.publish_release.outputs.html_url }}) 🚀
- name: Tweet the release
uses: ethomson/send-tweet-action@v1
with:
consumer-key: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
consumer-secret: ${{ secrets.TWITTER_CONSUMER_API_SECRET }}
access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }}
access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
status: |
🎉 New release ${{ steps.publish_release.outputs.tag_name }} is out 🚀
Check out all the changes here:
${{ steps.publish_release.outputs.html_url }}
+11 -1
View File
@@ -174,6 +174,12 @@ extension ViewController: SkeletonCollectionViewDataSource {
func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionSkeletonView(_ skeletonView: UICollectionView, skeletonCellForItemAt indexPath: IndexPath) -> UICollectionViewCell? {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as? CollectionViewCell
cell?.isSkeletonable = indexPath.row != 0
return cell
}
// MARK: - UICollectionViewDataSource
@@ -184,6 +190,10 @@ extension ViewController: SkeletonCollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
return cell
}
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath) {
let cell = cell as? CollectionViewCell
cell?.isSkeletonable = indexPath.row != 0
}
}
+4 -4
View File
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Va7-1y-Tel">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" 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="17703"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -50,7 +50,7 @@
<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="3" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CJW-A4-Fb8">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="3" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CJW-A4-Fb8">
<rect key="frame" x="166" y="27" width="166" height="12"/>
<color key="backgroundColor" red="0.92156862750000001" green="0.16862745100000001" blue="0.54901960780000003" alpha="1" colorSpace="calibratedRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="10"/>
@@ -89,7 +89,7 @@
<color key="separatorColor" red="0.1061807256" green="0.84678786989999999" blue="0.031482450150000001" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="CellIdentifier" rowHeight="120" id="2dN-Bd-tdy" customClass="Cell" customModule="SkeletonViewExample" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="120"/>
<rect key="frame" x="0.0" y="24.333333969116211" width="375" height="120"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="2dN-Bd-tdy" id="7IN-F3-Mr6">
<rect key="frame" x="0.0" y="0.0" width="375" height="120"/>
+11
View File
@@ -183,6 +183,17 @@ extension ViewController: SkeletonTableViewDataSource {
cell.label1.text = "cell -> \(indexPath.row)"
return cell
}
func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell? {
let cell = skeletonView.dequeueReusableCell(withIdentifier: "CellIdentifier", for: indexPath) as? Cell
cell?.textField.isHidden = indexPath.row == 0
return cell
}
func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath) {
let cell = cell as? Cell
cell?.textField.isHidden = indexPath.row == 0
}
}
extension ViewController: SkeletonTableViewDelegate {
+2 -2
View File
@@ -7,8 +7,8 @@ GEM
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
babosa (1.0.2)
claide (1.0.2)
+57 -20
View File
@@ -4,10 +4,12 @@
<a href="https://github.com/Juanpe/SkeletonView/actions?query=workflow%3ACI">
<img src="https://github.com/Juanpe/SkeletonView/workflows/CI/badge.svg">
</a>
<a href="https://codebeat.co/projects/github-com-juanpe-skeletonview-master"><img alt="codebeat badge" src="https://codebeat.co/badges/f854fdfd-31e5-4689-ba04-075d83653e60" /></a>
<img src="http://img.shields.io/badge/dependency%20manager-swiftpm%2Bcocoapods%2Bcarthage-green" />
<img src="https://img.shields.io/badge/platforms-ios%2Btvos-green" />
<a href="https://badge.bow-swift.io/recipe?name=SkeletonView&description=An%20elegant%20way%20to%20show%20users%20that%20something%20is%20happening%20and%20also%20prepare%20them%20to%20which%20contents%20he%20is%20waiting&url=https://github.com/juanpe/skeletonview&owner=Juanpe&avatar=https://avatars0.githubusercontent.com/u/1409041?v=4&tag=1.8.7"><img src="https://raw.githubusercontent.com/bow-swift/bow-art/master/badges/nef-playgrounds-badge.svg" alt="SkeletonView Playground" style="height:20px"></a>
<a href="https://codebeat.co/projects/github-com-juanpe-skeletonview-main"><img alt="codebeat badge" src="https://codebeat.co/badges/1f37bbab-a1c8-4a4a-94d7-f21740d461e9" /></a>
<a href="https://cocoapods.org/pods/SkeletonView"><img src="https://img.shields.io/cocoapods/v/SkeletonView.svg?style=flat"></a>
<a href="https://github.com/Carthage/Carthage/"><img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat"></a>
<a href="https://swift.org/package-manager/"><img src="https://img.shields.io/badge/SPM-supported-Green.svg?style=flat"></a>
<img src="https://img.shields.io/badge/platforms-iOS_tvOS-green" />
<a href="https://badge.bow-swift.io/recipe?name=SkeletonView&description=An%20elegant%20way%20to%20show%20users%20that%20something%20is%20happening%20and%20also%20prepare%20them%20to%20which%20contents%20he%20is%20waiting&url=https://github.com/juanpe/skeletonview&owner=Juanpe&avatar=https://avatars0.githubusercontent.com/u/1409041?v=4&tag=1.20.0"><img src="https://raw.githubusercontent.com/bow-swift/bow-art/master/badges/nef-playgrounds-badge.svg" alt="SkeletonView Playground" style="height:20px"></a>
</p>
<p align="center">
@@ -37,12 +39,12 @@ Enjoy it! 🙂
- [🔠 Texts](#-texts)
- [🦋 Appearance](#-appearance)
- [🎨 Custom colors](#-custom-colors)
- [Image captured from website https://flatuicolors.com](#image-captured-from-website-httpsflatuicolorscom)
- [🏃‍♀️ Animations](#-animations)
- [🏄 Transitions](#-transitions)
- [✨ Miscellaneous](#-miscellaneous)
- [❤️ Contributing](#-contributing)
- [📢 Mentions](#-mentions)
- [🏆 Sponsors](#-sponsors)
- [👨🏻‍💻 Author](#-author)
- [👮🏻 License](#-license)
@@ -169,19 +171,16 @@ If you want to show the skeleton in a ```UITableView```, you need to conform to
``` swift
public protocol SkeletonTableViewDataSource: UITableViewDataSource {
func numSections(in collectionSkeletonView: UITableView) -> Int
func numSections(in collectionSkeletonView: UITableView) -> Int // Default: 1
func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int
func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier
func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell? // Default: nil
func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath)
}
```
As you can see, this protocol inherits from ```UITableViewDataSource```, so you can replace this protocol with the skeleton protocol.
This protocol has a default implementation:
``` swift
func numSections(in collectionSkeletonView: UITableView) -> Int
// Default: 1
```
This protocol has a default implementation for some methods. For example, the number of rows for each section is calculated in runtime:
``` swift
func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int
@@ -194,17 +193,30 @@ func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection s
> If you return `UITableView.automaticNumberOfSkeletonRows` in the above method, it acts like the default behavior (i.e. it calculates how many cells needed to populate the whole tableview).
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
```
**Example**
``` swift
func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier {
return "CellIdentifier"
}
```
By default, the library dequeues the cells from each indexPath, but you can also do this if you want to make some changes before the skeleton appears:
``` swift
func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell? {
let cell = skeletonView.dequeueReusableCell(withIdentifier: "CellIdentifier", for: indexPath) as? Cell
cell?.textField.isHidden = indexPath.row == 0
return cell
}
```
If you prefer to leave the deque part to the library you can configure the cell using this method:
``` swift
func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath) {
let cell = cell as? Cell
cell?.textField.isHidden = indexPath.row == 0
}
```
Besides, you can skeletonize both the headers and footers. You need to conform to `SkeletonTableViewDelegate` protocol.
```swift
@@ -232,10 +244,12 @@ For `UICollectionView`, you need to conform to `SkeletonCollectionViewDataSource
``` swift
public protocol SkeletonCollectionViewDataSource: UICollectionViewDataSource {
func numSections(in collectionSkeletonView: UICollectionView) -> Int // default: 1
func numSections(in collectionSkeletonView: UICollectionView) -> Int // default: 1
func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int
func collectionSkeletonView(_ skeletonView: UICollectionView, cellIdentifierForItemAt indexPath: IndexPath) -> ReusableCellIdentifier
func collectionSkeletonView(_ skeletonView: UICollectionView, supplementaryViewIdentifierOfKind: String, at indexPath: IndexPath) -> ReusableCellIdentifier? // default: nil
func collectionSkeletonView(_ skeletonView: UICollectionView, skeletonCellForItemAt indexPath: IndexPath) -> UICollectionViewCell? // default: nil
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath)
}
```
@@ -504,6 +518,24 @@ By default, the user interaction is disabled for skeletonized items, but if you
view.isUserInteractionDisabledWhenSkeletonIsActive = false // The view will be active when the skeleton will be active.
```
**Delayed show skeleton**
You can delay the presentation of the skeleton if the views update quickly.
```swift
func showSkeleton(usingColor: UIColor,
animated: Bool,
delay: TimeInterval,
transition: SkeletonTransitionStyle)
```
```swift
func showGradientSkeleton(usingGradient: SkeletonGradient,
animated: Bool,
delay: TimeInterval,
transition: SkeletonTransitionStyle)
```
**Debug**
To facilitate the debug tasks when something is not working fine. **`SkeletonView`** has some new tools.
@@ -533,11 +565,11 @@ Then, when the skeleton appears, you can see the view hierarchy in the Xcode con
* iOS 9.0+
* tvOS 9.0+
* Swift 5
* Swift 5.3
## ❤️ Contributing
This is an open source project, so feel free to contribute. How?
- Open an [issue](https://github.com/Juanpe/SkeletonView/issues/new).
- Send feedback via [email](mailto://juanpecatalan.com).
- Propose your own fixes, suggestions and open a pull request with the changes.
@@ -563,7 +595,12 @@ For more information, please read the [contributing guidelines](https://github.c
- [Swift News #36](https://www.youtube.com/watch?v=mAGpsQiy6so)
- [Best iOS articles, new tools & more](https://medium.com/flawless-app-stories/best-ios-articles-new-tools-more-fcbe673e10d)
## 🏆 Sponsors
Open-source projects cannot live long without your help. If you find **SkeletonView** is useful, please consider supporting this
project by becoming a sponsor.
Become a sponsor through [GitHub Sponsors](https://github.com/sponsors/Juanpe) :heart:
## 👨🏻‍💻 Author
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SkeletonView"
s.version = "1.17.1"
s.version = "1.21.2"
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.
@@ -3,6 +3,17 @@
<plist version="1.0">
<dict>
<key>FILEHEADER</key>
<string> ___COPYRIGHT___</string>
<string>
// Copyright SkeletonView. All Rights Reserved.
//
// Licensed under the MIT License (the &quot;License&quot;);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// ___FILENAME___
//
// Created by ___FULLUSERNAME___ on ___DATE___.</string>
</dict>
</plist>
@@ -27,8 +27,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -38,8 +36,8 @@
ReferencedContainer = "container:SkeletonView.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -61,8 +59,6 @@
ReferencedContainer = "container:SkeletonView.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -12,6 +12,7 @@ class SkeletonMultilineLayerBuilder {
var cornerRadius: Int?
var multilineSpacing: CGFloat = SkeletonAppearance.default.multilineSpacing
var paddingInsets: UIEdgeInsets = .zero
var alignment: NSTextAlignment = .natural
var isRTL: Bool = false
@discardableResult
@@ -55,6 +56,12 @@ class SkeletonMultilineLayerBuilder {
self.paddingInsets = insets
return self
}
@discardableResult
func setAlignment(_ alignment: NSTextAlignment) -> SkeletonMultilineLayerBuilder {
self.alignment = alignment
return self
}
@discardableResult
func setIsRTL(_ isRTL: Bool) -> SkeletonMultilineLayerBuilder {
@@ -74,9 +81,11 @@ class SkeletonMultilineLayerBuilder {
layer.anchorPoint = .zero
layer.name = CALayer.skeletonSubLayersName
layer.updateLayerFrame(for: index,
totalLines: layer.skeletonSublayers.count,
size: CGSize(width: width, height: height),
multilineSpacing: multilineSpacing,
paddingInsets: paddingInsets,
alignment: alignment,
isRTL: isRTL)
layer.cornerRadius = CGFloat(radius)
@@ -13,20 +13,28 @@ public protocol SkeletonCollectionViewDataSource: UICollectionViewDataSource {
func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int
func collectionSkeletonView(_ skeletonView: UICollectionView, cellIdentifierForItemAt indexPath: IndexPath) -> ReusableCellIdentifier
func collectionSkeletonView(_ skeletonView: UICollectionView, supplementaryViewIdentifierOfKind: String, at indexPath: IndexPath) -> ReusableCellIdentifier?
func collectionSkeletonView(_ skeletonView: UICollectionView, skeletonCellForItemAt indexPath: IndexPath) -> UICollectionViewCell?
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath)
}
public extension SkeletonCollectionViewDataSource {
func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return UICollectionView.automaticNumberOfSkeletonItems
UICollectionView.automaticNumberOfSkeletonItems
}
func collectionSkeletonView(_ skeletonView: UICollectionView,
supplementaryViewIdentifierOfKind: String,
at indexPath: IndexPath) -> ReusableCellIdentifier? {
return nil
func collectionSkeletonView(_ skeletonView: UICollectionView, supplementaryViewIdentifierOfKind: String, at indexPath: IndexPath) -> ReusableCellIdentifier? {
nil
}
func numSections(in collectionSkeletonView: UICollectionView) -> Int { return 1 }
func numSections(in collectionSkeletonView: UICollectionView) -> Int {
1
}
func collectionSkeletonView(_ skeletonView: UICollectionView, skeletonCellForItemAt indexPath: IndexPath) -> UICollectionViewCell? {
nil
}
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath) { }
}
public protocol SkeletonCollectionViewDelegate: UICollectionViewDelegate { }
@@ -46,8 +46,17 @@ 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)
guard let cell = originalTableViewDataSource?.collectionSkeletonView(tableView, skeletonCellForRowAt: indexPath) else {
let cellIdentifier = originalTableViewDataSource?.collectionSkeletonView(tableView, cellIdentifierForRowAt: indexPath) ?? ""
let fakeCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
originalTableViewDataSource?.collectionSkeletonView(tableView, prepareCellForSkeleton: fakeCell, at: indexPath)
skeletonViewIfContainerSkeletonIsActive(container: tableView, view: fakeCell)
return fakeCell
}
originalTableViewDataSource?.collectionSkeletonView(tableView, prepareCellForSkeleton: cell, at: indexPath)
skeletonViewIfContainerSkeletonIsActive(container: tableView, view: cell)
return cell
}
@@ -74,8 +83,17 @@ 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)
guard let cell = originalCollectionViewDataSource?.collectionSkeletonView(collectionView, skeletonCellForItemAt: indexPath) else {
let cellIdentifier = originalCollectionViewDataSource?.collectionSkeletonView(collectionView, cellIdentifierForItemAt: indexPath) ?? ""
let fakeCell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
originalCollectionViewDataSource?.collectionSkeletonView(collectionView, prepareCellForSkeleton: fakeCell, at: indexPath)
skeletonViewIfContainerSkeletonIsActive(container: collectionView, view: fakeCell)
return fakeCell
}
originalCollectionViewDataSource?.collectionSkeletonView(collectionView, prepareCellForSkeleton: cell, at: indexPath)
skeletonViewIfContainerSkeletonIsActive(container: collectionView, view: cell)
return cell
}
@@ -89,7 +107,7 @@ extension SkeletonCollectionDataSource: UICollectionViewDataSource {
return view
}
return UICollectionReusableView()
return originalCollectionViewDataSource?.collectionView?(collectionView, viewForSupplementaryElementOfKind: kind, at: indexPath) ?? UICollectionReusableView()
}
}
@@ -12,6 +12,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, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell?
func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath)
}
public extension SkeletonTableViewDataSource {
@@ -27,6 +29,12 @@ public extension SkeletonTableViewDataSource {
func collectionSkeletonView(_ skeletonView: UITableView, cellIdenfierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier {
return collectionSkeletonView(skeletonView, cellIdentifierForRowAt: indexPath)
}
func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell? {
nil
}
func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath) { }
}
public protocol SkeletonTableViewDelegate: UITableViewDelegate {
+26 -7
View File
@@ -36,6 +36,7 @@ struct SkeletonMultilinesLayerConfig {
var multilineCornerRadius: Int
var multilineSpacing: CGFloat
var paddingInsets: UIEdgeInsets
var alignment: NSTextAlignment
var isRTL: Bool
/// Returns padding insets taking into account if the RTL is activated
@@ -69,6 +70,7 @@ extension CALayer {
.setMultilineSpacing(config.multilineSpacing)
.setPadding(config.paddingInsets)
.setHeight(height)
.setAlignment(config.alignment)
.setIsRTL(config.isRTL)
(0..<numberOfSublayers).forEach { index in
@@ -97,10 +99,13 @@ extension CALayer {
for (index, layer) in currentSkeletonSublayers.enumerated() {
let width = calculatedWidthForLine(at: index, totalLines: numberOfSublayers, lastLineFillPercent: lastLineFillPercent, paddingInsets: paddingInsets)
layer.updateLayerFrame(for: index,
totalLines: numberOfSublayers,
size: CGSize(width: width, height: height),
multilineSpacing: multilineSpacing,
paddingInsets: paddingInsets,
isRTL: config.isRTL)
alignment: config.alignment,
isRTL: config.isRTL
)
}
}
@@ -112,14 +117,18 @@ extension CALayer {
return width
}
func updateLayerFrame(for index: Int, size: CGSize, multilineSpacing: CGFloat, paddingInsets: UIEdgeInsets, isRTL: Bool) {
func updateLayerFrame(for index: Int, totalLines: Int, size: CGSize, multilineSpacing: CGFloat, paddingInsets: UIEdgeInsets, alignment: NSTextAlignment, isRTL: Bool) {
let spaceRequiredForEachLine = size.height + multilineSpacing
let newFrame = CGRect(x: paddingInsets.left,
y: CGFloat(index) * spaceRequiredForEachLine + paddingInsets.top,
width: size.width,
height: size.height)
frame = flipRectForRTLIfNeeded(newFrame, isRTL: isRTL)
if index == totalLines - 1 && totalLines != 1 {
frame = alignLayerFrame(newFrame, alignment: alignment, isRTL: isRTL)
} else {
frame = newFrame
}
}
private func calculateNumLines(for config: SkeletonMultilinesLayerConfig) -> Int {
@@ -141,12 +150,22 @@ extension CALayer {
return calculatedNumberOfLines
}
private func flipRectForRTLIfNeeded(_ rect: CGRect, isRTL: Bool) -> CGRect {
private func alignLayerFrame(_ rect: CGRect, alignment: NSTextAlignment, isRTL: Bool) -> CGRect {
var newRect = rect
if isRTL {
switch alignment {
case .natural where isRTL,
.right:
newRect.origin.x = (superlayer?.bounds.width ?? 0) - rect.origin.x - rect.width
case .center:
newRect.origin.x = rect.origin.x + ((superlayer?.bounds.width ?? 0) - rect.width) / 2
case .natural, .left, .justified:
break
@unknown default:
break
}
return newRect
}
}
@@ -14,9 +14,11 @@ enum ViewAssociatedKeys {
static var labelViewState = "labelViewState"
static var imageViewState = "imageViewState"
static var buttonViewState = "buttonViewState"
static var headerFooterViewState = "headerFooterViewState"
static var currentSkeletonConfig = "currentSkeletonConfig"
static var skeletonCornerRadius = "skeletonCornerRadius"
static var disabledWhenSkeletonIsActive = "disabledWhenSkeletonIsActive"
static var delayedShowSkeletonWorkItem = "delayedShowSkeletonWorkItem"
}
// codebeat:enable[TOO_MANY_IVARS]
@@ -54,4 +56,9 @@ extension UIView {
var isSuperviewAStackView: Bool {
superview is UIStackView
}
var delayedShowSkeletonWorkItem: DispatchWorkItem? {
get { return ao_get(pkey: &ViewAssociatedKeys.delayedShowSkeletonWorkItem) as? DispatchWorkItem }
set { ao_setOptional(newValue, pkey: &ViewAssociatedKeys.delayedShowSkeletonWorkItem) }
}
}
@@ -22,7 +22,6 @@ extension UIView {
extension UILabel {
var desiredHeightBasedOnNumberOfLines: CGFloat {
let lineHeight = constraintHeight ?? SkeletonAppearance.default.multilineHeight
let spaceNeededForEachLine = lineHeight * CGFloat(numberOfLines)
let spaceNeededForSpaces = skeletonLineSpacing * CGFloat(numberOfLines - 1)
let padding = paddingInsets.top + paddingInsets.bottom
@@ -122,3 +121,13 @@ extension UIButton {
}
}
}
extension UITableViewHeaderFooterView {
override func prepareViewForSkeleton() {
backgroundView?.backgroundColor = .clear
if isUserInteractionDisabledWhenSkeletonIsActive {
isUserInteractionEnabled = false
}
}
}
@@ -11,8 +11,9 @@ enum MultilineAssociatedKeys {
}
protocol ContainsMultilineText {
var constraintHeight: CGFloat? { get }
var numLines: Int { get }
var lineHeight: CGFloat { get }
var numberOfLines: Int { get }
var textAlignment: NSTextAlignment { get }
var lastLineFillingPercent: Int { get }
var multilineCornerRadius: Int { get }
var multilineSpacing: CGFloat { get }
+4 -8
View File
@@ -27,15 +27,11 @@ public extension UILabel {
}
}
extension UILabel: ContainsMultilineText {
var constraintHeight: CGFloat? {
backupHeightConstraints.first?.constant
extension UILabel: ContainsMultilineText {
var lineHeight: CGFloat {
backupHeightConstraints.first?.constant ?? SkeletonAppearance.default.multilineHeight
}
var numLines: Int {
return numberOfLines
}
var lastLineFillingPercent: Int {
get { return ao_get(pkey: &MultilineAssociatedKeys.lastLineFillingPercent) as? Int ?? SkeletonAppearance.default.multilineLastLineFillPercent }
set { ao_set(newValue, pkey: &MultilineAssociatedKeys.lastLineFillingPercent) }
+11 -3
View File
@@ -28,11 +28,19 @@ public extension UITextView {
}
extension UITextView: ContainsMultilineText {
var constraintHeight: CGFloat? {
heightConstraints.first?.constant
var lineHeight: CGFloat {
if let fontLineHeight = font?.lineHeight {
if let heightConstraints = heightConstraints.first?.constant {
return (fontLineHeight > heightConstraints) ? heightConstraints : fontLineHeight
}
return fontLineHeight
}
return SkeletonAppearance.default.multilineHeight
}
var numLines: Int {
var numberOfLines: Int {
-1
}
+19
View File
@@ -161,3 +161,22 @@ extension UIButton {
}
}
}
extension UITableViewHeaderFooterView {
var headerFooterState: RecoverableTableViewHeaderFooterViewState? {
get { return ao_get(pkey: &ViewAssociatedKeys.headerFooterViewState) as? RecoverableTableViewHeaderFooterViewState }
set { ao_setOptional(newValue, pkey: &ViewAssociatedKeys.headerFooterViewState) }
}
override func saveViewState() {
super.saveViewState()
headerFooterState = RecoverableTableViewHeaderFooterViewState(view: self)
}
override func recoverViewState(forced: Bool) {
super.recoverViewState(forced: forced)
startTransition { [weak self] in
self?.backgroundView?.backgroundColor = self?.headerFooterState?.backgroundViewColor
}
}
}
@@ -59,3 +59,11 @@ struct RecoverableButtonViewState {
self.title = view.titleLabel?.text
}
}
struct RecoverableTableViewHeaderFooterViewState {
var backgroundViewColor: UIColor?
init(view: UITableViewHeaderFooterView) {
self.backgroundViewColor = view.backgroundView?.backgroundColor
}
}
+7 -7
View File
@@ -83,14 +83,14 @@ struct SkeletonLayer {
/// If there is more than one line, or custom preferences have been set for a single line, draw custom layers
func addTextLinesIfNeeded() {
guard let textView = holderAsTextView else { return }
let lineHeight = textView.constraintHeight ?? SkeletonAppearance.default.multilineHeight
let config = SkeletonMultilinesLayerConfig(lines: textView.numLines,
lineHeight: lineHeight,
let config = SkeletonMultilinesLayerConfig(lines: textView.numberOfLines,
lineHeight: textView.lineHeight,
type: type,
lastLineFillPercent: textView.lastLineFillingPercent,
multilineCornerRadius: textView.multilineCornerRadius,
multilineSpacing: textView.multilineSpacing,
paddingInsets: textView.paddingInsets,
alignment: textView.textAlignment,
isRTL: holder?.isRTL ?? false)
maskLayer.addMultilinesLayers(for: config)
@@ -98,14 +98,14 @@ struct SkeletonLayer {
func updateLinesIfNeeded() {
guard let textView = holderAsTextView else { return }
let lineHeight = textView.constraintHeight ?? SkeletonAppearance.default.multilineHeight
let config = SkeletonMultilinesLayerConfig(lines: textView.numLines,
lineHeight: lineHeight,
let config = SkeletonMultilinesLayerConfig(lines: textView.numberOfLines,
lineHeight: textView.lineHeight,
type: type,
lastLineFillPercent: textView.lastLineFillingPercent,
multilineCornerRadius: textView.multilineCornerRadius,
multilineSpacing: textView.multilineSpacing,
paddingInsets: textView.paddingInsets,
alignment: textView.textAlignment,
isRTL: holder?.isRTL ?? false)
maskLayer.updateMultilinesLayers(for: config)
@@ -113,7 +113,7 @@ struct SkeletonLayer {
var holderAsTextView: ContainsMultilineText? {
guard let textView = holder as? ContainsMultilineText,
(textView.numLines == -1 || textView.numLines == 0 || textView.numLines > 1 || textView.numLines == 1 && !SkeletonAppearance.default.renderSingleLineAsView) else {
(textView.numberOfLines == -1 || textView.numberOfLines == 0 || textView.numberOfLines > 1 || textView.numberOfLines == 1 && !SkeletonAppearance.default.renderSingleLineAsView) else {
return nil
}
return textView
+62 -8
View File
@@ -9,20 +9,63 @@ public extension UIView {
/// - color: The color of the skeleton. Defaults to `SkeletonAppearance.default.tintColor`.
/// - transition: The style of the transition when the skeleton appears. Defaults to `.crossDissolve(0.25)`.
func showSkeleton(usingColor color: UIColor = SkeletonAppearance.default.tintColor, transition: SkeletonTransitionStyle = .crossDissolve(0.25)) {
delayedShowSkeletonWorkItem?.cancel()
let config = SkeletonConfig(type: .solid, colors: [color], transition: transition)
showSkeleton(skeletonConfig: config)
}
/// Shows the skeleton using the view that calls this method as root view.
///
/// - Parameters:
/// - color: The color of the skeleton. Defaults to `SkeletonAppearance.default.tintColor`.
/// - animated: If the skeleton is animated or not. Defaults to `true`.
/// - delay: The amount of time (measured in seconds) to wait before show the skeleton.
/// - transition: The style of the transition when the skeleton appears. Defaults to `.crossDissolve(0.25)`.
func showSkeleton(usingColor color: UIColor = SkeletonAppearance.default.tintColor, animated: Bool = true, delay: TimeInterval, transition: SkeletonTransitionStyle = .crossDissolve(0.25)) {
delayedShowSkeletonWorkItem?.cancel()
delayedShowSkeletonWorkItem = DispatchWorkItem { [weak self] in
let config = SkeletonConfig(type: .solid, colors: [color], animated: animated, transition: transition)
self?.showSkeleton(skeletonConfig: config)
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: delayedShowSkeletonWorkItem!)
}
/// Shows the gradient skeleton without animation using the view that calls this method as root view.
///
/// - Parameters:
/// - gradient: The gradient of the skeleton. Defaults to `SkeletonAppearance.default.gradient`.
/// - transition: The style of the transition when the skeleton appears. Defaults to `.crossDissolve(0.25)`.
func showGradientSkeleton(usingGradient gradient: SkeletonGradient = SkeletonAppearance.default.gradient, transition: SkeletonTransitionStyle = .crossDissolve(0.25)) {
delayedShowSkeletonWorkItem?.cancel()
let config = SkeletonConfig(type: .gradient, colors: gradient.colors, transition: transition)
showSkeleton(skeletonConfig: config)
}
/// Shows the gradient skeleton using the view that calls this method as root view.
///
/// - Parameters:
/// - gradient: The gradient of the skeleton. Defaults to `SkeletonAppearance.default.gradient`.
/// - animated: If the skeleton is animated or not. Defaults to `true`.
/// - delay: The amount of time (measured in seconds) to wait before show the skeleton.
/// - transition: The style of the transition when the skeleton appears. Defaults to `.crossDissolve(0.25)`.
func showGradientSkeleton(
usingGradient gradient: SkeletonGradient = SkeletonAppearance.default.gradient,
animated: Bool = true,
delay: TimeInterval,
transition: SkeletonTransitionStyle = .crossDissolve(0.25)
) {
delayedShowSkeletonWorkItem?.cancel()
delayedShowSkeletonWorkItem = DispatchWorkItem { [weak self] in
let config = SkeletonConfig(type: .gradient, colors: gradient.colors, animated: animated, transition: transition)
self?.showSkeleton(skeletonConfig: config)
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: delayedShowSkeletonWorkItem!)
}
/// Shows the animated skeleton using the view that calls this method as root view.
///
/// If animation is nil, sliding animation will be used, with direction left to right.
@@ -32,6 +75,7 @@ public extension UIView {
/// - animation: The animation of the skeleton. Defaults to `nil`.
/// - transition: The style of the transition when the skeleton appears. Defaults to `.crossDissolve(0.25)`.
func showAnimatedSkeleton(usingColor color: UIColor = SkeletonAppearance.default.tintColor, animation: SkeletonLayerAnimation? = nil, transition: SkeletonTransitionStyle = .crossDissolve(0.25)) {
delayedShowSkeletonWorkItem?.cancel()
let config = SkeletonConfig(type: .solid, colors: [color], animated: true, animation: animation, transition: transition)
showSkeleton(skeletonConfig: config)
}
@@ -45,6 +89,7 @@ public extension UIView {
/// - animation: The animation of the skeleton. Defaults to `nil`.
/// - transition: The style of the transition when the skeleton appears. Defaults to `.crossDissolve(0.25)`.
func showAnimatedGradientSkeleton(usingGradient gradient: SkeletonGradient = SkeletonAppearance.default.gradient, animation: SkeletonLayerAnimation? = nil, transition: SkeletonTransitionStyle = .crossDissolve(0.25)) {
delayedShowSkeletonWorkItem?.cancel()
let config = SkeletonConfig(type: .gradient, colors: gradient.colors, animated: true, animation: animation, transition: transition)
showSkeleton(skeletonConfig: config)
}
@@ -75,6 +120,7 @@ public extension UIView {
}
func hideSkeleton(reloadDataAfter reload: Bool = true, transition: SkeletonTransitionStyle = .crossDissolve(0.25)) {
delayedShowSkeletonWorkItem?.cancel()
flowDelegate?.willBeginHidingSkeletons(rootView: self)
recursiveHideSkeleton(reloadDataAfter: reload, transition: transition, root: self)
}
@@ -277,14 +323,22 @@ extension UIView {
.setHolder(self)
.build()
else { return }
self.skeletonLayer = skeletonLayer
layer.insertSublayer(skeletonLayer,
at: UInt32.max,
transition: config.transition) { [weak self] in
if config.animated {
self?.startSkeletonAnimation(config.animation)
}
layer.insertSkeletonLayer(
skeletonLayer,
atIndex: UInt32.max,
transition: config.transition
) { [weak self] in
guard let self = self else { return }
/// Workaround to fix the problem when inserting a sublayer and
/// the content offset is modified by the system.
(self as? UITextView)?.setContentOffset(.zero, animated: false)
if config.animated {
self.startSkeletonAnimation(config.animation)
}
}
status = .on
}
@@ -309,9 +363,9 @@ extension UIView {
let skeletonLayer = skeletonLayer,
let transitionStyle = currentSkeletonConfig?.transition else { return }
skeletonLayer.stopAnimation()
status = .off
skeletonLayer.removeLayer(transition: transitionStyle) {
self.skeletonLayer = nil
self.status = .off
self.currentSkeletonConfig = nil
}
}
+3 -3
View File
@@ -3,13 +3,13 @@
import UIKit
extension CALayer {
func insertSublayer(_ layer: SkeletonLayer, at idx: UInt32, transition: SkeletonTransitionStyle, completion: (() -> Void)? = nil) {
insertSublayer(layer.contentLayer, at: idx)
func insertSkeletonLayer(_ sublayer: SkeletonLayer, atIndex index: UInt32, transition: SkeletonTransitionStyle, completion: (() -> Void)? = nil) {
insertSublayer(sublayer.contentLayer, at: index)
switch transition {
case .none:
completion?()
case .crossDissolve(let duration):
layer.contentLayer.setOpacity(from: 0, to: 1, duration: duration, completion: completion)
sublayer.contentLayer.setOpacity(from: 0, to: 1, duration: duration, completion: completion)
}
}
}
+1 -1
View File
@@ -28,6 +28,6 @@ fastlane release_current
----
This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).