Compare commits
1 Commits
master
...
feature/sk
| Author | SHA1 | Date |
|---|---|---|
|
|
4ccfedfa3c |
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
/*:
|
/*:
|
||||||
### Отступы
|
### Отступы
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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_ для лейбла:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue