Compare commits

..

8 Commits

Author SHA1 Message Date
Juanpe Catalán 01733b358c bump version 1.10.0 2020-10-02 17:15:47 +02:00
Juanpe Catalán 027143499c Merge pull request #337 from Juanpe/feature/support_RTL
RTL support
2020-10-02 17:14:24 +02:00
Juanpe Catalán ae1b5ee790 fix: add condition for tvOS 10.0 2020-10-02 17:08:25 +02:00
Juanpe Catalán 0c0a0ea451 doc: Update CHANGELOG file 2020-10-02 17:01:40 +02:00
Juanpe Catalán 8d090904b4 feat: support RTL 2020-10-02 16:47:00 +02:00
Juanpe Catalán 8d00d5fa16 Merge pull request #336 from Juanpe/fix/replacing_text_wrongly_after_hide
Not replace text when the skeleton disappears
2020-10-02 14:00:28 +02:00
Juanpe Catalán f2bc1cab27 doc: update CHANGELOG file 2020-10-02 13:39:20 +02:00
Juanpe Catalán 83b99f16b1 fix: modified how the skeleton save and restore the original view state of the elements 2020-10-02 13:23:26 +02:00
12 changed files with 140 additions and 71 deletions
+14 -7
View File
@@ -7,11 +7,23 @@ disabled_rules:
- identifier_name
- multiple_closures_with_trailing_closure
- class_delegate_protocol
- force_unwrapping
- force_try
- force_cast
- function_parameter_count
- discouraged_optional_collection
- shorthand_operator
- reduce_boolean
- weak_delegate
- nesting
- closure_end_indentation
- function_default_parameter_at_end
- unowned_variable_capture
- legacy_constructor
opt_in_rules:
- multiline_arguments
- multiline_parameters
- closure_spacing
- closure_end_indentation
- closure_body_length
- collection_alignment
- contains_over_filter_is_empty
@@ -27,8 +39,6 @@ opt_in_rules:
- file_name_no_space
- first_where
- flatmap_over_map_reduce
- force_unwrapping
- function_default_parameter_at_end
- implicitly_unwrapped_optional
- joined_default_parameter
- last_where
@@ -43,7 +53,6 @@ opt_in_rules:
- sorted_first_last
- switch_case_on_newline
- unneeded_parentheses_in_closure_argument
- unowned_variable_capture
- unused_declaration
- unused_import
- vertical_whitespace_opening_braces
@@ -51,12 +60,10 @@ opt_in_rules:
- enum_case_associated_values_counts
- legacy_multiple
- legacy_random
force_cast: error
force_unwrapping: error
indentation: 2
file_length:
- 2500
- 3000
large_tuple:
- 5
- 6
- 6
+4
View File
@@ -7,6 +7,10 @@ All notable changes to this project will be documented in this file
* [**327**](https://github.com/Juanpe/SkeletonView/pull/327): Add SwiftLint - [@Juanpe](https://github.com/Juanpe)
* [**329**](https://github.com/Juanpe/SkeletonView/pull/329): Spanish README 🇪🇸 - [@Juanpe](https://github.com/Juanpe)
#### 🩹 Bug fixes
* [**336**](https://github.com/Juanpe/SkeletonView/pull/336): Not replace text when the skeleton disappears. Solved issues: [#296](https://github.com/Juanpe/SkeletonView/issues/296), [#330](https://github.com/Juanpe/SkeletonView/issues/330) - [@Juanpe](https://github.com/Juanpe)
* [**337**](https://github.com/Juanpe/SkeletonView/pull/337): RTL support. Solved issues: [#143](https://github.com/Juanpe/SkeletonView/issues/143) - [@Juanpe](https://github.com/Juanpe)
## 📦 [1.9](https://github.com/Juanpe/SkeletonView/releases/tag/1.9)
#### 🩹 Bug fixes
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SkeletonView"
s.version = "1.9"
s.version = "1.10.0"
s.summary = "An elegant way to show users that something is happening and also prepare them to which contents he is waiting"
s.description = <<-DESC
Today almost all apps have async processes, as API requests, long runing processes, etc. And while the processes are working, usually developers place a loading view to show users that something is going on.
@@ -9,25 +9,30 @@ class SkeletonLayerBuilder {
var colors: [UIColor] = []
var holder: UIView?
@discardableResult
func setSkeletonType(_ type: SkeletonType) -> SkeletonLayerBuilder {
self.skeletonType = type
return self
}
@discardableResult
func addColor(_ color: UIColor) -> SkeletonLayerBuilder {
return addColors([color])
}
@discardableResult
func addColors(_ colors: [UIColor]) -> SkeletonLayerBuilder {
self.colors.append(contentsOf: colors)
return self
}
@discardableResult
func setHolder(_ holder: UIView) -> SkeletonLayerBuilder {
self.holder = holder
return self
}
@discardableResult
func build() -> SkeletonLayer? {
guard let type = skeletonType,
let holder = holder
@@ -12,41 +12,55 @@ class SkeletonMultilineLayerBuilder {
var cornerRadius: Int?
var multilineSpacing: CGFloat = SkeletonAppearance.default.multilineSpacing
var paddingInsets: UIEdgeInsets = .zero
var isRTL: Bool = false
@discardableResult
func setSkeletonType(_ type: SkeletonType) -> SkeletonMultilineLayerBuilder {
self.skeletonType = type
return self
}
@discardableResult
func setIndex(_ index: Int) -> SkeletonMultilineLayerBuilder {
self.index = index
return self
}
func setHeight(_ height: CGFloat) -> SkeletonMultilineLayerBuilder {
self.height = height
return self
}
@discardableResult
func setHeight(_ height: CGFloat) -> SkeletonMultilineLayerBuilder {
self.height = height
return self
}
@discardableResult
func setWidth(_ width: CGFloat) -> SkeletonMultilineLayerBuilder {
self.width = width
return self
}
@discardableResult
func setCornerRadius(_ radius: Int) -> SkeletonMultilineLayerBuilder {
self.cornerRadius = radius
return self
}
@discardableResult
func setMultilineSpacing(_ spacing: CGFloat) -> SkeletonMultilineLayerBuilder {
self.multilineSpacing = spacing
return self
}
@discardableResult
func setPadding(_ insets: UIEdgeInsets) -> SkeletonMultilineLayerBuilder {
self.paddingInsets = insets
return self
}
@discardableResult
func setIsRTL(_ isRTL: Bool) -> SkeletonMultilineLayerBuilder {
self.isRTL = isRTL
return self
}
func build() -> CALayer? {
guard let type = skeletonType,
@@ -59,7 +73,11 @@ class SkeletonMultilineLayerBuilder {
let layer = type.layer
layer.anchorPoint = .zero
layer.name = CALayer.skeletonSubLayersName
layer.updateLayerFrame(for: index, size: CGSize(width: width, height: height), multilineSpacing: self.multilineSpacing, paddingInsets: paddingInsets)
layer.updateLayerFrame(for: index,
size: CGSize(width: width, height: height),
multilineSpacing: multilineSpacing,
paddingInsets: paddingInsets,
isRTL: isRTL)
layer.cornerRadius = CGFloat(radius)
layer.masksToBounds = true
+49 -22
View File
@@ -36,6 +36,15 @@ struct SkeletonMultilinesLayerConfig {
var multilineCornerRadius: Int
var multilineSpacing: CGFloat
var paddingInsets: UIEdgeInsets
var isRTL: Bool
/// Returns padding insets taking into account if the RTL is activated
var calculatedPaddingInsets: UIEdgeInsets {
UIEdgeInsets(top: paddingInsets.top,
left: paddingInsets.right,
bottom: paddingInsets.bottom,
right: paddingInsets.left)
}
}
// MARK: Skeleton sublayers
@@ -60,6 +69,7 @@ extension CALayer {
.setMultilineSpacing(config.multilineSpacing)
.setPadding(config.paddingInsets)
.setHeight(height)
.setIsRTL(config.isRTL)
(0..<numberOfSublayers).forEach { index in
let width = calculatedWidthForLine(at: index, totalLines: numberOfSublayers, lastLineFillPercent: config.lastLineFillPercent, paddingInsets: config.paddingInsets)
@@ -76,7 +86,7 @@ extension CALayer {
let currentSkeletonSublayers = skeletonSublayers
let numberOfSublayers = currentSkeletonSublayers.count
let lastLineFillPercent = config.lastLineFillPercent
let paddingInsets = config.paddingInsets
let paddingInsets = config.calculatedPaddingInsets
let multilineSpacing = config.multilineSpacing
var height = config.lineHeight ?? SkeletonAppearance.default.multilineHeight
@@ -86,7 +96,11 @@ extension CALayer {
for (index, layer) in currentSkeletonSublayers.enumerated() {
let width = calculatedWidthForLine(at: index, totalLines: numberOfSublayers, lastLineFillPercent: lastLineFillPercent, paddingInsets: paddingInsets)
layer.updateLayerFrame(for: index, size: CGSize(width: width, height: height), multilineSpacing: multilineSpacing, paddingInsets: paddingInsets)
layer.updateLayerFrame(for: index,
size: CGSize(width: width, height: height),
multilineSpacing: multilineSpacing,
paddingInsets: paddingInsets,
isRTL: config.isRTL)
}
}
@@ -98,9 +112,14 @@ extension CALayer {
return width
}
func updateLayerFrame(for index: Int, size: CGSize, multilineSpacing: CGFloat, paddingInsets: UIEdgeInsets) {
func updateLayerFrame(for index: Int, size: CGSize, multilineSpacing: CGFloat, paddingInsets: UIEdgeInsets, isRTL: Bool) {
let spaceRequiredForEachLine = SkeletonAppearance.default.multilineHeight + multilineSpacing
frame = CGRect(x: paddingInsets.left, y: CGFloat(index) * spaceRequiredForEachLine + paddingInsets.top, width: size.width, height: size.height - paddingInsets.bottom - paddingInsets.top)
let newFrame = CGRect(x: paddingInsets.left,
y: CGFloat(index) * spaceRequiredForEachLine + paddingInsets.top,
width: size.width,
height: size.height - paddingInsets.bottom - paddingInsets.top)
frame = flipRectForRTLIfNeeded(newFrame, isRTL: isRTL)
}
private func calculateNumLines(for config: SkeletonMultilinesLayerConfig) -> Int {
@@ -109,6 +128,14 @@ extension CALayer {
if config.lines != 0, config.lines <= numberOfSublayers { numberOfSublayers = config.lines }
return numberOfSublayers
}
private func flipRectForRTLIfNeeded(_ rect: CGRect, isRTL: Bool) -> CGRect {
var newRect = rect
if isRTL {
newRect.origin.x = (superlayer?.bounds.width ?? 0) - rect.origin.x - rect.width
}
return newRect
}
}
// MARK: Animations
@@ -126,24 +153,24 @@ public extension CALayer {
return pulseAnimation
}
var sliding: CAAnimation {
let startPointAnim = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.startPoint))
startPointAnim.fromValue = CGPoint(x: -1, y: 0.5)
startPointAnim.toValue = CGPoint(x: 1, y: 0.5)
let endPointAnim = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.endPoint))
endPointAnim.fromValue = CGPoint(x: 0, y: 0.5)
endPointAnim.toValue = CGPoint(x: 2, y: 0.5)
let animGroup = CAAnimationGroup()
animGroup.animations = [startPointAnim, endPointAnim]
animGroup.duration = 1.5
animGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
animGroup.repeatCount = .infinity
animGroup.isRemovedOnCompletion = false
return animGroup
}
// var sliding: CAAnimation {
// let startPointAnim = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.startPoint))
// startPointAnim.fromValue = CGPoint(x: -1, y: 0.5)
// startPointAnim.toValue = CGPoint(x: 1, y: 0.5)
//
// let endPointAnim = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.endPoint))
// endPointAnim.fromValue = CGPoint(x: 0, y: 0.5)
// endPointAnim.toValue = CGPoint(x: 2, y: 0.5)
//
// let animGroup = CAAnimationGroup()
// animGroup.animations = [startPointAnim, endPointAnim]
// animGroup.duration = 1.5
// animGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
// animGroup.repeatCount = .infinity
// animGroup.isRemovedOnCompletion = false
//
// return animGroup
// }
func playAnimation(_ anim: SkeletonLayerAnimation, key: String, completion: (() -> Void)? = nil) {
skeletonSublayers.recursiveSearch(leafBlock: {
+8
View File
@@ -50,4 +50,12 @@ extension UIView {
var nonContentSizeLayoutConstraints: [NSLayoutConstraint] {
return constraints.filter({ "\(type(of: $0))" != "NSContentSizeLayoutConstraint" })
}
var isRTL: Bool {
if #available(iOS 10.0, *), #available(tvOS 10.0, *) {
return effectiveUserInterfaceLayoutDirection == .rightToLeft
} else {
return false
}
}
}
@@ -12,6 +12,7 @@ extension UIView {
@objc func prepareViewForSkeleton() {
startTransition { [weak self] in
self?.backgroundColor = .clear
self?.isUserInteractionEnabled = false
}
}
}
@@ -50,7 +51,6 @@ extension UIButton {
backgroundColor = .clear
startTransition { [weak self] in
self?.setTitle(nil, for: .normal)
self?.isUserInteractionEnabled = false
}
}
}
+19 -13
View File
@@ -24,14 +24,15 @@ extension UIView: Recoverable {
}
@objc func recoverViewState(forced: Bool) {
guard let safeViewState = viewState else { return }
guard let storedViewState = viewState else { return }
startTransition { [weak self] in
self?.layer.cornerRadius = safeViewState.cornerRadius
self?.layer.masksToBounds = safeViewState.clipToBounds
self?.layer.cornerRadius = storedViewState.cornerRadius
self?.layer.masksToBounds = storedViewState.clipToBounds
self?.isUserInteractionEnabled = storedViewState.isUserInteractionsEnabled
if safeViewState.backgroundColor != self?.backgroundColor || forced {
self?.backgroundColor = safeViewState.backgroundColor
if self?.backgroundColor == .clear || forced {
self?.backgroundColor = storedViewState.backgroundColor
}
}
}
@@ -51,9 +52,11 @@ extension UILabel {
override func recoverViewState(forced: Bool) {
super.recoverViewState(forced: forced)
startTransition { [weak self] in
self?.textColor = self?.labelState?.textColor
self?.text = self?.labelState?.text
self?.isUserInteractionEnabled = self?.labelState?.isUserInteractionsEnabled ?? false
guard let storedLabelState = self?.labelState else { return }
if self?.textColor == .clear || forced {
self?.textColor = storedLabelState.textColor
}
}
}
}
@@ -72,9 +75,11 @@ extension UITextView {
override func recoverViewState(forced: Bool) {
super.recoverViewState(forced: forced)
startTransition { [weak self] in
self?.textColor = self?.textState?.textColor
self?.text = self?.textState?.text
self?.isUserInteractionEnabled = self?.textState?.isUserInteractionsEnabled ?? false
guard let storedLabelState = self?.textState else { return }
if self?.textColor == .clear || forced {
self?.textColor = storedLabelState.textColor
}
}
}
}
@@ -112,8 +117,9 @@ extension UIButton {
override func recoverViewState(forced: Bool) {
super.recoverViewState(forced: forced)
startTransition { [weak self] in
self?.setTitle(self?.buttonState?.title, for: .normal)
self?.isUserInteractionEnabled = self?.buttonState?.isUserInteractionsEnabled ?? false
if self?.title(for: .normal) == nil {
self?.setTitle(self?.buttonState?.title, for: .normal)
}
}
}
}
@@ -12,29 +12,25 @@ struct RecoverableViewState {
var backgroundColor: UIColor?
var cornerRadius: CGFloat
var clipToBounds: Bool
var isUserInteractionsEnabled: Bool
init(view: UIView) {
self.backgroundColor = view.backgroundColor
self.clipToBounds = view.layer.masksToBounds
self.cornerRadius = view.layer.cornerRadius
self.isUserInteractionsEnabled = view.isUserInteractionEnabled
}
}
struct RecoverableTextViewState {
var text: String?
var textColor: UIColor?
var isUserInteractionsEnabled: Bool
init(view: UILabel) {
self.textColor = view.textColor
self.text = view.text
self.isUserInteractionsEnabled = view.isUserInteractionEnabled
}
init(view: UITextView) {
self.textColor = view.textColor
self.text = view.text
self.isUserInteractionsEnabled = view.isUserInteractionEnabled
}
}
@@ -48,10 +44,8 @@ struct RecoverableImageViewState {
struct RecoverableButtonViewState {
var title: String?
var isUserInteractionsEnabled: Bool
init(view: UIButton) {
self.title = view.titleLabel?.text
self.isUserInteractionsEnabled = view.isUserInteractionEnabled
}
}
+6 -8
View File
@@ -22,14 +22,12 @@ struct SkeletonConfig {
/// Transition style
var transition: SkeletonTransitionStyle
init(
type: SkeletonType,
colors: [UIColor],
gradientDirection: GradientDirection? = nil,
animated: Bool = false,
animation: SkeletonLayerAnimation? = nil,
transition: SkeletonTransitionStyle = .crossDissolve(0.25)
) {
init(type: SkeletonType,
colors: [UIColor],
gradientDirection: GradientDirection? = nil,
animated: Bool = false,
animation: SkeletonLayerAnimation? = nil,
transition: SkeletonTransitionStyle = .crossDissolve(0.25)) {
self.type = type
self.colors = colors
self.gradientDirection = gradientDirection
+7 -5
View File
@@ -23,12 +23,12 @@ public enum SkeletonType {
}
}
var layerAnimation: SkeletonLayerAnimation {
func defaultLayerAnimation(isRTL: Bool) -> SkeletonLayerAnimation {
switch self {
case .solid:
return { $0.pulse }
case .gradient:
return { $0.sliding }
return { SkeletonAnimationBuilder().makeSlidingAnimation(withDirection: isRTL ? .rightLeft : .leftRight) }()
}
}
}
@@ -90,7 +90,8 @@ struct SkeletonLayer {
lastLineFillPercent: textView.lastLineFillingPercent,
multilineCornerRadius: textView.multilineCornerRadius,
multilineSpacing: textView.multilineSpacing,
paddingInsets: textView.paddingInsets)
paddingInsets: textView.paddingInsets,
isRTL: holder?.isRTL ?? false)
maskLayer.addMultilinesLayers(for: config)
}
@@ -103,7 +104,8 @@ struct SkeletonLayer {
lastLineFillPercent: textView.lastLineFillingPercent,
multilineCornerRadius: textView.multilineCornerRadius,
multilineSpacing: textView.multilineSpacing,
paddingInsets: textView.paddingInsets)
paddingInsets: textView.paddingInsets,
isRTL: holder?.isRTL ?? false)
maskLayer.updateMultilinesLayers(for: config)
}
@@ -119,7 +121,7 @@ struct SkeletonLayer {
extension SkeletonLayer {
func start(_ anim: SkeletonLayerAnimation? = nil, completion: (() -> Void)? = nil) {
let animation = anim ?? type.layerAnimation
let animation = anim ?? type.defaultLayerAnimation(isRTL: holder?.isRTL ?? false)
contentLayer.playAnimation(animation, key: "skeletonAnimation", completion: completion)
}