Compare commits

...

1 Commits

Author SHA1 Message Date
Nikita Semenov 4ccfedfa3c fix: code review notes 2023-03-09 12:46:12 +03:00
9 changed files with 118 additions and 38 deletions

View File

@ -33,7 +33,7 @@ open class BaseViewSkeletonsConfiguration {
public var padding: UIEdgeInsets public var padding: UIEdgeInsets
public var shape: Shape public var shape: Shape
public init(padding: UIEdgeInsets = .zero, shape: Shape = .rectangle(cornerRadius: .zero)) { public init(padding: UIEdgeInsets = .edges(5), shape: Shape = .rectangle(cornerRadius: .zero)) {
self.shape = shape self.shape = shape
self.padding = padding self.padding = padding
} }

View File

@ -0,0 +1,37 @@
//
// Copyright (c) 2023 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
open class ContainerViewSkeletonsConfiguration: BaseViewSkeletonsConfiguration {
public var borderWidth: CGFloat
public init(borderWidth: CGFloat = .zero,
padding: UIEdgeInsets = .zero,
shape: Shape = .rectangle(cornerRadius: .zero)) {
self.borderWidth = borderWidth
super.init(padding: padding, shape: shape)
}
}

View File

@ -26,43 +26,43 @@ import UIKit
open class SkeletonsConfiguration { open class SkeletonsConfiguration {
public var viewConfiguration: BaseViewSkeletonsConfiguration public var viewConfiguration: BaseViewSkeletonsConfiguration
public var containerViewConfiguration: ContainerViewSkeletonsConfiguration
public var labelConfiguration: TextSkeletonsConfiguration public var labelConfiguration: TextSkeletonsConfiguration
public var imageViewConfiguration: BaseViewSkeletonsConfiguration public var imageViewConfiguration: BaseViewSkeletonsConfiguration
public var animation: Closure<SkeletonLayer, CAAnimationGroup>? public var animation: Closure<SkeletonLayer, CAAnimationGroup>?
public var skeletonsBackgroundColor: CGColor public var skeletonsBackgroundColor: CGColor
public var skeletonsMovingColor: CGColor public var skeletonsMovingColor: CGColor
public var borderWidth: CGFloat
public weak var configurationDelegate: SkeletonsConfigurationDelegate? public weak var configurationDelegate: SkeletonsConfigurationDelegate?
open var isContainersHidden: Bool { open var isContainersHidden: Bool {
borderWidth == .zero containerViewConfiguration.borderWidth == .zero
} }
// MARK: - Init // MARK: - Init
public init(viewConfiguration: BaseViewSkeletonsConfiguration = .init(), public init(viewConfiguration: BaseViewSkeletonsConfiguration = .init(),
containerViewConfiguration: ContainerViewSkeletonsConfiguration = .init(),
labelConfiguration: TextSkeletonsConfiguration = .init(), labelConfiguration: TextSkeletonsConfiguration = .init(),
imageViewConfiguration: BaseViewSkeletonsConfiguration = .init(shape: .circle), imageViewConfiguration: BaseViewSkeletonsConfiguration = .init(shape: .circle),
animation: Closure<SkeletonLayer, CAAnimationGroup>? = nil, animation: Closure<SkeletonLayer, CAAnimationGroup>? = nil,
skeletonsBackgroundColor: UIColor = .lightGray.withAlphaComponent(0.7), skeletonsBackgroundColor: UIColor = .lightGray.withAlphaComponent(0.7),
borderWidth: CGFloat = .zero,
configurationDelegate: SkeletonsConfigurationDelegate? = nil) { configurationDelegate: SkeletonsConfigurationDelegate? = nil) {
self.viewConfiguration = viewConfiguration self.viewConfiguration = viewConfiguration
self.containerViewConfiguration = containerViewConfiguration
self.labelConfiguration = labelConfiguration self.labelConfiguration = labelConfiguration
self.imageViewConfiguration = imageViewConfiguration self.imageViewConfiguration = imageViewConfiguration
self.animation = animation self.animation = animation
self.skeletonsBackgroundColor = skeletonsBackgroundColor.cgColor self.skeletonsBackgroundColor = skeletonsBackgroundColor.cgColor
self.skeletonsMovingColor = skeletonsBackgroundColor.withAlphaComponent(0.2).cgColor self.skeletonsMovingColor = skeletonsBackgroundColor.withAlphaComponent(0.2).cgColor
self.borderWidth = borderWidth
self.configurationDelegate = configurationDelegate self.configurationDelegate = configurationDelegate
} }
// MARK: - Open methods // MARK: - Open methods
open func createSkeletonLayer(for baseView: UIView?) -> SkeletonLayer { open func createSkeletonLayer(for baseView: UIView) -> SkeletonLayer {
SkeletonLayer(config: self, baseView: baseView) SkeletonLayer(config: self, baseView: baseView)
} }
@ -75,9 +75,9 @@ open class SkeletonsConfiguration {
if !isContainersHidden { if !isContainersHidden {
layer.borderColor = skeletonsBackgroundColor layer.borderColor = skeletonsBackgroundColor
layer.borderWidth = borderWidth layer.borderWidth = containerViewConfiguration.borderWidth
if case let .rectangle(cornerRadius: radius) = viewConfiguration.shape { if case let .rectangle(cornerRadius: radius) = containerViewConfiguration.shape {
layer.cornerRadius = radius layer.cornerRadius = radius
} }
} }

View File

@ -43,7 +43,7 @@ open class TextSkeletonsConfiguration: BaseViewSkeletonsConfiguration {
public init(numberOfLines: Int? = nil, public init(numberOfLines: Int? = nil,
lineHeight: Closure<UIFont?, CGFloat>? = nil, lineHeight: Closure<UIFont?, CGFloat>? = nil,
lineSpacing: Closure<UIFont?, CGFloat>? = nil, lineSpacing: Closure<UIFont?, CGFloat>? = nil,
padding: UIEdgeInsets = .zero, padding: UIEdgeInsets = .edges(5),
shape: Shape = .rectangle(cornerRadius: .zero)) { shape: Shape = .rectangle(cornerRadius: .zero)) {
self.numberOfLines = numberOfLines self.numberOfLines = numberOfLines

View File

@ -23,6 +23,7 @@
import UIKit import UIKit
public protocol SkeletonsPresenter { public protocol SkeletonsPresenter {
var skeletonsHolder: UIView { get }
var skeletonsConfiguration: SkeletonsConfiguration { get } var skeletonsConfiguration: SkeletonsConfiguration { get }
var isSkeletonsHidden: Bool { get } var isSkeletonsHidden: Bool { get }
var viewsToSkeletone: [UIView] { get } var viewsToSkeletone: [UIView] { get }
@ -40,38 +41,37 @@ extension SkeletonsPresenter {
public var skeletonsConfiguration: SkeletonsConfiguration { public var skeletonsConfiguration: SkeletonsConfiguration {
SkeletonsConfiguration() SkeletonsConfiguration()
} }
public var isSkeletonsHidden: Bool {
isSkeletonsHidden(forView: skeletonsHolder)
}
public var viewsToSkeletone: [UIView] {
skeletonsHolder.skeletonableViews
}
public func showSkeletons() {
skeletonsHolder.showSkeletons(viewsToSkeletone: viewsToSkeletone,
skeletonsConfiguration)
}
func isSkeletonsHidden(forView view: UIView) -> Bool {
(view.layer.sublayers ?? []).first { $0 is SkeletonLayer } == nil
}
} }
// MARK: - UIView + SkeletonsPresenter // MARK: - UIView + SkeletonsPresenter
extension SkeletonsPresenter where Self: UIView { extension SkeletonsPresenter where Self: UIView {
public var isSkeletonsHidden: Bool { public var skeletonsHolder: UIView {
(layer.sublayers ?? []).first { $0 is SkeletonLayer } == nil self
}
public var viewsToSkeletone: [UIView] {
skeletonableViews
}
public func showSkeletons() {
showSkeletons(viewsToSkeletone: viewsToSkeletone,
skeletonsConfiguration)
} }
} }
// MARK: - UIViewController + SkeletonsPresenter // MARK: - UIViewController + SkeletonsPresenter
extension SkeletonsPresenter where Self: UIViewController { extension SkeletonsPresenter where Self: UIViewController {
public var isSkeletonsHidden: Bool { public var skeletonsHolder: UIView {
(view.layer.sublayers ?? []).first { $0 is SkeletonLayer } == nil view
}
public var viewsToSkeletone: [UIView] {
view.skeletonableViews
}
public func showSkeletons() {
showSkeletons(viewsToSkeletone: viewsToSkeletone,
skeletonsConfiguration)
} }
} }

View File

@ -63,6 +63,10 @@ open class SkeletonLayer: CAShapeLayer {
public var configuration: SkeletonsConfiguration public var configuration: SkeletonsConfiguration
public weak var baseView: UIView? public weak var baseView: UIView?
public var isAnimating: Bool {
animationLayer.animation(forKey: Constants.animationKeyPath) != nil
}
// MARK: - Init // MARK: - Init
// For debug purposes in Lookin or other programs for view hierarchy inspections // For debug purposes in Lookin or other programs for view hierarchy inspections
@ -72,7 +76,7 @@ open class SkeletonLayer: CAShapeLayer {
super.init(layer: layer) super.init(layer: layer)
} }
public init(config: SkeletonsConfiguration, baseView: UIView?) { public init(config: SkeletonsConfiguration, baseView: UIView) {
self.configuration = config self.configuration = config
self.baseView = baseView self.baseView = baseView
@ -85,6 +89,10 @@ open class SkeletonLayer: CAShapeLayer {
super.init(coder: coder) super.init(coder: coder)
} }
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Open methods // MARK: - Open methods
open func bind(to viewType: ViewType) { open func bind(to viewType: ViewType) {
@ -95,6 +103,13 @@ open class SkeletonLayer: CAShapeLayer {
self?.updateGeometry(viewType: view.viewType) self?.updateGeometry(viewType: view.viewType)
} }
if let _ = configuration.animation?(self) {
NotificationCenter.default.addObserver(self,
selector: #selector(SkeletonLayer.restartAnimationIfNeeded),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
configuration.configurationDelegate?.layerDidConfigured(forViewType: viewType, layer: self) configuration.configurationDelegate?.layerDidConfigured(forViewType: viewType, layer: self)
} }
@ -105,7 +120,7 @@ open class SkeletonLayer: CAShapeLayer {
} }
open func startAnimation() { open func startAnimation() {
guard let animation = configuration.animation?(self) else { guard !isAnimating, let animation = configuration.animation?(self) else {
return return
} }
@ -148,11 +163,20 @@ open class SkeletonLayer: CAShapeLayer {
path = configuration.imageViewConfiguration.drawPath(rect: viewType.view.bounds) path = configuration.imageViewConfiguration.drawPath(rect: viewType.view.bounds)
frame = configuration.imageViewConfiguration.applyPadding(viewFrame: rect) frame = configuration.imageViewConfiguration.applyPadding(viewFrame: rect)
default: case .container(_):
path = configuration.containerViewConfiguration.drawPath(rect: viewType.view.bounds)
frame = configuration.containerViewConfiguration.applyPadding(viewFrame: rect)
case .generic(_):
path = configuration.viewConfiguration.drawPath(rect: viewType.view.bounds) path = configuration.viewConfiguration.drawPath(rect: viewType.view.bounds)
frame = configuration.viewConfiguration.applyPadding(viewFrame: rect) frame = configuration.viewConfiguration.applyPadding(viewFrame: rect)
} }
animationLayer.frame = bounds animationLayer.frame = bounds
} }
@objc private func restartAnimationIfNeeded() {
stopAnimation()
startAnimation()
}
} }

View File

@ -60,7 +60,7 @@ extension UIView {
} }
} }
subviews.forEach { $0.isHidden = false} subviews.forEach { $0.isHidden = false }
} }
public func startAnimation() { public func startAnimation() {
@ -84,7 +84,7 @@ extension UIView {
var subviewSkeletonLayers = [SkeletonLayer]() var subviewSkeletonLayers = [SkeletonLayer]()
if view.isSkeletonsContainer { if view.isSkeletonsContainer {
if conf.borderWidth != .zero { if !conf.isContainersHidden {
skeletonLayer.bind(to: .container(view)) skeletonLayer.bind(to: .container(view))
} }

View File

@ -151,7 +151,7 @@ class CustomConfigurableSkeletonableView: UIView, SkeletonsPresenter {
- `UITextView` - `UITextView`
- `UIImageView` - `UIImageView`
> Для контейнеров доступна только настройка `borderWidth`, а `borderColor` используется тот же, что и для других скелетонов > Для контейнеров в качестве `borderColor` используется тот же цвет, что и для других скелетонов
### Анимация ### Анимация
@ -220,6 +220,14 @@ var confWithLabelSettings: SkeletonsConfiguration {
return .init(labelConfiguration: labelConf) return .init(labelConfiguration: labelConf)
} }
//: Для контейнеров можно настроить `borderWidth`. Стандартно он равняется 0, а значит контейнеры не будут показываться без дополнительной настройки ширины.
var skeletonsConfiguration: SkeletonsConfiguration {
let containerConf = ContainerViewSkeletonsConfiguration(borderWidth: 4,
shape: .rectangle(cornerRadius: 10))
return .init(containerViewConfiguration: containerConf)
}
/*: /*:
### Отступы ### Отступы

View File

@ -157,7 +157,7 @@ class CustomConfigurableSkeletonableView: UIView, SkeletonsPresenter {
- `UITextView` - `UITextView`
- `UIImageView` - `UIImageView`
> Для контейнеров доступна только настройка `borderWidth`, а `borderColor` используется тот же, что и для других скелетонов > Для контейнеров в качестве `borderColor` используется тот же цвет, что и для других скелетонов
### Анимация ### Анимация
@ -233,6 +233,17 @@ var confWithLabelSettings: SkeletonsConfiguration {
} }
``` ```
Для контейнеров можно настроить `borderWidth`. Стандартно он равняется 0, а значит контейнеры не будут показываться без дополнительной настройки ширины.
```swift
var skeletonsConfiguration: SkeletonsConfiguration {
let containerConf = ContainerViewSkeletonsConfiguration(borderWidth: 4,
shape: .rectangle(cornerRadius: 10))
return .init(containerViewConfiguration: containerConf)
}
```
### Отступы ### Отступы
Отступы можно настроить отдельно для `UILabel`, `UITextView`, `UIImageView` и остальных вью. Например, для предыдущего примера можно добавить горизонтальный _padding_ для лейбла: Отступы можно настроить отдельно для `UILabel`, `UITextView`, `UIImageView` и остальных вью. Например, для предыдущего примера можно добавить горизонтальный _padding_ для лейбла: