Compare commits

..

3 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
10 changed files with 111 additions and 32 deletions
+18
View File
@@ -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 -1
View File
@@ -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 }
+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
}
+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
}
+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)
}
}
}