Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12e5688b31 | |||
| 816b2965ff | |||
| e12e4a0fd1 |
@@ -504,6 +504,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 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,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "SkeletonView"
|
||||
s.version = "1.17.1"
|
||||
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.
|
||||
|
||||
@@ -17,6 +17,7 @@ enum ViewAssociatedKeys {
|
||||
static var currentSkeletonConfig = "currentSkeletonConfig"
|
||||
static var skeletonCornerRadius = "skeletonCornerRadius"
|
||||
static var disabledWhenSkeletonIsActive = "disabledWhenSkeletonIsActive"
|
||||
static var delayedShowSkeletonWorkItem = "delayedShowSkeletonWorkItem"
|
||||
}
|
||||
// codebeat:enable[TOO_MANY_IVARS]
|
||||
|
||||
@@ -54,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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user