Compare commits

...

20 Commits

Author SHA1 Message Date
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
Richard L Zarth III 6f78f5c378 Replace SkeletonCollectionDataSource.automaticNumberOfRows with UITableView.automaticNumberOfSkeletonRows and UICollectionView.automaticNumberOfSkeletonItems (#409) 2021-06-11 22:21:27 +02:00
Juanpe Catalán c8fdd6998d fix bug remove constraints wrongly (#406)
* check if the constraints were modified and then restore the original

* identify constraints added by SkeletonView

* move removal of skeleton constraint to recoverable protocol implementation
2021-06-11 11:33:23 +02:00
Juanpe Catalán 134463e529 Update README.md 2021-06-10 19:40:41 +02:00
Juanpe ee59239c59 Bump version 1.17.1 2021-06-10 17:36:12 +00:00
Juanpe Catalán f1e61aa9c0 fix typo isUserInteractionDisabledWhenSkeletonIsActive (#404)
* fix typo

* update README
2021-06-10 19:34:18 +02:00
Juanpe 135778aa1a Bump version 1.17.0 2021-06-10 17:24:00 +00:00
Juanpe Catalán e8d5eb61d8 update README file 2021-06-10 19:19:29 +02:00
Juanpe Catalán c2a029ed51 create disableWhenSkeletonIsActive property 2021-06-10 19:14:05 +02:00
Sam Harrison a1c8276980 Make CI action build targets, not just clean (#403) 2021-06-10 19:01:01 +02:00
Sam Harrison e9ac3a5ab3 Fix TableViewCell skeleton not being removed & automaticNumberOfRows reference (#402)
* Change TableView subviewsToSkeleton to all subviews

* Fix reference to automaticNumberOfRows from pr #401
2021-06-10 18:59:21 +02:00
Richard L Zarth III fb83a62f7b Add SkeletonCollectionDataSource.automaticNumberOfRows Constant (#401)
* Add automaticNumberOfRows constant to SkeletonCollectionDataSource.

* Update tableView(_:numberOfRowsInSection:) and collectionView(_:numberOfItemsInSection:) to use the new automaticNumberOfRows constant.

* Update README.

* Update README.md

Add an **IMPORTANT!** header to the automaticNumberOfRows mention in the README.

Co-authored-by: Juanpe Catalán <juanpecm@gmail.com>

Co-authored-by: Juanpe Catalán <juanpecm@gmail.com>
2021-06-09 18:45:44 +02:00
Juanpe Catalán 12521c1d87 update Info.plist 2021-05-31 17:20:11 +02:00
Juanpe c266035888 Bump version 1.16.0 2021-05-31 15:09:22 +00:00
Juanpe Catalán 74b5172ea5 use the right delegate method for footers (#394) 2021-05-11 19:15:41 +02:00
Michael Henry 318e629d04 update the UILabel placeholder to use an empty space #388 (#389) 2021-05-11 19:02:41 +02:00
StasMalinovsky 62193db76f Added estimated number of rows for UICollectionViewFlowLayout with vertical scroll direction. (#385) 2021-04-30 13:26:25 +02:00
Juanpe 19e7866d3d Bump version 1.15.0 2021-04-13 06:50:02 +00:00
23 changed files with 248 additions and 66 deletions
+1 -1
View File
@@ -16,4 +16,4 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Build
run: xcodebuild clean -target '${{ matrix.build-config['target'] }}' -sdk '${{ matrix.build-config['sdk'] }}' -destination '${{ matrix.build-config['destination'] }}'
run: xcodebuild clean build -target '${{ matrix.build-config['target'] }}' -sdk '${{ matrix.build-config['sdk'] }}' -destination '${{ matrix.build-config['destination'] }}'
+31
View File
@@ -189,6 +189,10 @@ func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection s
// It calculates how many cells need to populate whole tableview
```
> 📣 **IMPORTANT!**
>
> If you return `UITableView.automaticNumberOfSkeletonRows` in the above method, it acts like the default behavior (i.e. it calculates how many cells needed to populate the whole tableview).
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
@@ -491,6 +495,33 @@ Sometimes you wanna hide some view when the animation starts, so there is a quic
view.isHiddenWhenSkeletonIsActive = true // This works only when isSkeletonable = true
```
**Don't modify user interaction when the skeleton is active**
By default, the user interaction is disabled for skeletonized items, but if you don't want to modify the user interaction indicator when skeleton is active, you can use the `isUserInteractionDisabledWhenSkeletonIsActive` property:
```swift
view.isUserInteractionDisabledWhenSkeletonIsActive = false // The view will be active when the skeleton will be active.
```
**Delayed show skeleton**
You can delay the showIf the views are updated quickly you canAn optional delay applied to the transition, like the transition duration.
```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.
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SkeletonView"
s.version = "1.14.0"
s.version = "1.17.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.
+11 -2
View File
@@ -619,6 +619,7 @@
52D6D97B1BEFF229002C0205 = {
CreatedOnToolsVersion = 7.1;
LastSwiftMigration = 1000;
ProvisioningStyle = Automatic;
};
F5F899F11FABA607002E8FDA = {
CreatedOnToolsVersion = 9.1;
@@ -1117,8 +1118,10 @@
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1131,6 +1134,8 @@
ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.SkeletonView.SkeletonView-iOS";
PRODUCT_NAME = SkeletonView;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -1142,8 +1147,10 @@
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1155,6 +1162,8 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.SkeletonView.SkeletonView-iOS";
PRODUCT_NAME = SkeletonView;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 5.0;
@@ -31,12 +31,16 @@ extension CollectionSkeleton where Self: UIScrollView {
func removeDummyDataSource(reloadAfter: Bool) {}
func disableUserInteraction() {
isUserInteractionEnabled = false
isScrollEnabled = false
if isUserInteractionDisabledWhenSkeletonIsActive {
isUserInteractionEnabled = false
isScrollEnabled = false
}
}
func enableUserInteraction() {
isUserInteractionEnabled = true
isScrollEnabled = true
if isUserInteractionDisabledWhenSkeletonIsActive {
isUserInteractionEnabled = true
isScrollEnabled = true
}
}
}
@@ -17,7 +17,7 @@ public protocol SkeletonCollectionViewDataSource: UICollectionViewDataSource {
public extension SkeletonCollectionViewDataSource {
func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return skeletonView.estimatedNumberOfRows
return UICollectionView.automaticNumberOfSkeletonItems
}
func collectionSkeletonView(_ skeletonView: UICollectionView,
@@ -7,11 +7,20 @@
//
import UIKit
extension UICollectionView: CollectionSkeleton {
public static let automaticNumberOfSkeletonItems = -1
var estimatedNumberOfRows: Int {
guard let flowlayout = collectionViewLayout as? UICollectionViewFlowLayout else { return 0 }
return Int(ceil(frame.height / flowlayout.itemSize.height))
switch flowlayout.scrollDirection {
case .vertical:
return Int(ceil(frame.height / flowlayout.itemSize.height))
case .horizontal:
return Int(ceil(frame.width / flowlayout.itemSize.width))
default:
return 0
}
}
var skeletonDataSource: SkeletonCollectionDataSource? {
@@ -32,7 +32,17 @@ extension SkeletonCollectionDataSource: UITableViewDataSource {
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
originalTableViewDataSource?.collectionSkeletonView(tableView, numberOfRowsInSection: section) ?? 0
guard let originalTableViewDataSource = originalTableViewDataSource else {
return 0
}
let numberOfRows = originalTableViewDataSource.collectionSkeletonView(tableView, numberOfRowsInSection: section)
if numberOfRows == UITableView.automaticNumberOfSkeletonRows {
return tableView.estimatedNumberOfRows
} else {
return numberOfRows
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
@@ -50,7 +60,17 @@ extension SkeletonCollectionDataSource: UICollectionViewDataSource {
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
originalCollectionViewDataSource?.collectionSkeletonView(collectionView, numberOfItemsInSection: section) ?? 0
guard let originalCollectionViewDataSource = originalCollectionViewDataSource else {
return 0
}
let numberOfItems = originalCollectionViewDataSource.collectionSkeletonView(collectionView, numberOfItemsInSection: section)
if numberOfItems == UICollectionView.automaticNumberOfSkeletonItems {
return collectionView.estimatedNumberOfRows
} else {
return numberOfItems
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
@@ -25,7 +25,7 @@ extension SkeletonCollectionDelegate: UITableViewDelegate {
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
headerOrFooterView(tableView, for: originalTableViewDelegate?.collectionSkeletonView(tableView, identifierForHeaderInSection: section))
headerOrFooterView(tableView, for: originalTableViewDelegate?.collectionSkeletonView(tableView, identifierForFooterInSection: section))
}
func tableView(_ tableView: UITableView, didEndDisplayingHeaderView view: UIView, forSection section: Int) {
@@ -16,7 +16,7 @@ public protocol SkeletonTableViewDataSource: UITableViewDataSource {
public extension SkeletonTableViewDataSource {
func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int {
return skeletonView.estimatedNumberOfRows
return UITableView.automaticNumberOfSkeletonRows
}
func numSections(in collectionSkeletonView: UITableView) -> Int { return 1 }
@@ -11,6 +11,8 @@ import UIKit
public typealias ReusableHeaderFooterIdentifier = String
extension UITableView: CollectionSkeleton {
public static let automaticNumberOfSkeletonRows = -1
var estimatedNumberOfRows: Int {
return Int(ceil(frame.height / rowHeight))
}
@@ -12,9 +12,17 @@ extension UIView {
nonContentSizeLayoutConstraints.filter { $0.firstAttribute == NSLayoutConstraint.Attribute.height }
}
var skeletonHeightConstraints: [NSLayoutConstraint] {
nonContentSizeLayoutConstraints.filter {
$0.firstAttribute == NSLayoutConstraint.Attribute.height
&& $0.identifier?.contains("SkeletonView.Constraint.Height") ?? false
}
}
@discardableResult
func setHeight(equalToConstant constant: CGFloat) -> NSLayoutConstraint {
let heightConstraint = heightAnchor.constraint(equalToConstant: constant)
heightConstraint.identifier = "SkeletonView.Constraint.Height.\(constant)"
NSLayoutConstraint.activate([heightConstraint])
return heightConstraint
}
@@ -16,6 +16,8 @@ enum ViewAssociatedKeys {
static var buttonViewState = "buttonViewState"
static var currentSkeletonConfig = "currentSkeletonConfig"
static var skeletonCornerRadius = "skeletonCornerRadius"
static var disabledWhenSkeletonIsActive = "disabledWhenSkeletonIsActive"
static var delayedShowSkeletonWorkItem = "delayedShowSkeletonWorkItem"
}
// codebeat:enable[TOO_MANY_IVARS]
@@ -53,4 +55,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) }
}
}
@@ -14,6 +14,12 @@ public extension UIView {
get { return hiddenWhenSkeletonIsActive }
set { hiddenWhenSkeletonIsActive = newValue }
}
@IBInspectable
var isUserInteractionDisabledWhenSkeletonIsActive: Bool {
get { return disabledWhenSkeletonIsActive }
set { disabledWhenSkeletonIsActive = newValue }
}
@IBInspectable
var skeletonCornerRadius: Float {
@@ -34,6 +40,11 @@ public extension UIView {
get { return ao_get(pkey: &ViewAssociatedKeys.hiddenWhenSkeletonIsActive) as? Bool ?? false }
set { ao_set(newValue, pkey: &ViewAssociatedKeys.hiddenWhenSkeletonIsActive) }
}
private var disabledWhenSkeletonIsActive: Bool {
get { return ao_get(pkey: &ViewAssociatedKeys.disabledWhenSkeletonIsActive) as? Bool ?? true }
set { ao_set(newValue, pkey: &ViewAssociatedKeys.disabledWhenSkeletonIsActive) }
}
private var skeletonableCornerRadius: Float {
get { return ao_get(pkey: &ViewAssociatedKeys.skeletonCornerRadius) as? Float ?? 0.0 }
@@ -10,7 +10,10 @@ import UIKit
extension UIView {
@objc func prepareViewForSkeleton() {
isUserInteractionEnabled = false
if isUserInteractionDisabledWhenSkeletonIsActive {
isUserInteractionEnabled = false
}
startTransition { [weak self] in
self?.backgroundColor = .clear
}
@@ -19,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
@@ -32,7 +34,8 @@ extension UILabel {
// Workaround to simulate content when the label is contained in a `UIStackView`.
if isSuperviewAStackView, bounds.height == 0 {
text = "This is a placeholder text to simulate content because it's contained in a stack view in order to prevent that the content size will be zero."
// This is a placeholder text to simulate content because it's contained in a stack view in order to prevent that the content size will be zero.
text = " "
}
let desiredHeight = desiredHeightBasedOnNumberOfLines
@@ -43,10 +46,7 @@ extension UILabel {
}
}
func restoreBackupHeightConstraints() {
heightConstraints.forEach {
removeConstraint($0)
}
func restoreBackupHeightConstraintsIfNeeded() {
guard !backupHeightConstraints.isEmpty else { return }
NSLayoutConstraint.activate(backupHeightConstraints)
backupHeightConstraints.removeAll()
@@ -54,7 +54,11 @@ extension UILabel {
override func prepareViewForSkeleton() {
backgroundColor = .clear
isUserInteractionEnabled = false
if isUserInteractionDisabledWhenSkeletonIsActive {
isUserInteractionEnabled = false
}
resignFirstResponder()
startTransition { [weak self] in
self?.updateHeightConstraintsIfNeeded()
@@ -66,7 +70,11 @@ extension UILabel {
extension UITextView {
override func prepareViewForSkeleton() {
backgroundColor = .clear
isUserInteractionEnabled = false
if isUserInteractionDisabledWhenSkeletonIsActive {
isUserInteractionEnabled = false
}
resignFirstResponder()
startTransition { [weak self] in
self?.textColor = .clear
@@ -89,7 +97,11 @@ extension UITextField {
extension UIImageView {
override func prepareViewForSkeleton() {
backgroundColor = .clear
isUserInteractionEnabled = false
if isUserInteractionDisabledWhenSkeletonIsActive {
isUserInteractionEnabled = false
}
startTransition { [weak self] in
self?.image = nil
}
@@ -99,7 +111,11 @@ extension UIImageView {
extension UIButton {
override func prepareViewForSkeleton() {
backgroundColor = .clear
isUserInteractionEnabled = false
if isUserInteractionDisabledWhenSkeletonIsActive {
isUserInteractionEnabled = false
}
startTransition { [weak self] in
self?.setTitle(nil, for: .normal)
}
@@ -11,8 +11,8 @@ enum MultilineAssociatedKeys {
}
protocol ContainsMultilineText {
var constraintHeight: CGFloat? { get }
var numLines: Int { get }
var lineHeight: CGFloat { get }
var numberOfLines: Int { get }
var lastLineFillingPercent: Int { get }
var multilineCornerRadius: Int { get }
var multilineSpacing: CGFloat { get }
+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
}
+18 -9
View File
@@ -27,12 +27,17 @@ extension UIView: Recoverable {
guard let storedViewState = viewState else { return }
startTransition { [weak self] in
self?.layer.cornerRadius = storedViewState.cornerRadius
self?.layer.masksToBounds = storedViewState.clipToBounds
self?.isUserInteractionEnabled = storedViewState.isUserInteractionsEnabled
guard let self = self else { return }
if self?.backgroundColor == .clear || forced {
self?.backgroundColor = storedViewState.backgroundColor
self.layer.cornerRadius = storedViewState.cornerRadius
self.layer.masksToBounds = storedViewState.clipToBounds
if self.isUserInteractionDisabledWhenSkeletonIsActive {
self.isUserInteractionEnabled = storedViewState.isUserInteractionsEnabled
}
if self.backgroundColor == .clear || forced {
self.backgroundColor = storedViewState.backgroundColor
}
}
}
@@ -52,12 +57,16 @@ extension UILabel {
override func recoverViewState(forced: Bool) {
super.recoverViewState(forced: forced)
startTransition { [weak self] in
guard let storedLabelState = self?.labelState else { return }
guard let self = self,
let storedLabelState = self.labelState else {
return
}
self?.restoreBackupHeightConstraints()
NSLayoutConstraint.deactivate(self.skeletonHeightConstraints)
self.restoreBackupHeightConstraintsIfNeeded()
if self?.textColor == .clear || forced {
self?.textColor = storedLabelState.textColor
if self.textColor == .clear || forced {
self.textColor = storedLabelState.textColor
}
}
}
+5 -7
View File
@@ -83,9 +83,8 @@ struct SkeletonLayer {
/// If there is more than one line, or custom preferences have been set for a single line, draw custom layers
func addTextLinesIfNeeded() {
guard let textView = holderAsTextView else { return }
let lineHeight = textView.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,
@@ -98,9 +97,8 @@ 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,
@@ -113,7 +111,7 @@ struct SkeletonLayer {
var holderAsTextView: ContainsMultilineText? {
guard let textView = holder as? ContainsMultilineText,
(textView.numLines == -1 || textView.numLines == 0 || textView.numLines > 1 || textView.numLines == 1 && !SkeletonAppearance.default.renderSingleLineAsView) else {
(textView.numberOfLines == -1 || textView.numberOfLines == 0 || textView.numberOfLines > 1 || textView.numberOfLines == 1 && !SkeletonAppearance.default.renderSingleLineAsView) else {
return nil
}
return textView
+61 -7
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
}
+1 -1
View File
@@ -18,7 +18,7 @@ extension UITableView {
// Some developer trying to call `view.showAnimatedSkeleton()`
// when the request or data is loading which sometimes happens before the ViewDidAppear
guard window != nil else { return [] }
return visibleCells + visibleSectionHeaders + visibleSectionFooters
return subviews
}
}
+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)
}
}
}