Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1e61aa9c0 | |||
| 135778aa1a | |||
| e8d5eb61d8 | |||
| c2a029ed51 | |||
| a1c8276980 | |||
| e9ac3a5ab3 | |||
| fb83a62f7b | |||
| 12521c1d87 | |||
| c266035888 | |||
| 74b5172ea5 | |||
| 318e629d04 | |||
| 62193db76f | |||
| 19e7866d3d | |||
| 36668f450b | |||
| 1bdb3b9c72 | |||
| 6c4e9091a7 |
@@ -16,4 +16,4 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: xcodebuild clean -target '${{ matrix.build-config['target'] }}' -sdk '${{ matrix.build-config['sdk'] }}' -destination '${{ matrix.build-config['destination'] }}'
|
||||
run: xcodebuild clean build -target '${{ matrix.build-config['target'] }}' -sdk '${{ matrix.build-config['sdk'] }}' -destination '${{ matrix.build-config['destination'] }}'
|
||||
|
||||
@@ -189,6 +189,10 @@ func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection s
|
||||
// It calculates how many cells need to populate whole tableview
|
||||
```
|
||||
|
||||
> 📣 **IMPORTANT!**
|
||||
>
|
||||
> If you return `SkeletonCollectionDataSource.automaticNumberOfRows` in the above method, it acts like the default behavior (i.e. it calculates how many cells needed to populate the whole tableview).
|
||||
|
||||
There is only one method you need to implement to let Skeleton know the cell identifier. This method doesn't have default implementation:
|
||||
``` swift
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier
|
||||
@@ -491,6 +495,15 @@ Sometimes you wanna hide some view when the animation starts, so there is a quic
|
||||
view.isHiddenWhenSkeletonIsActive = true // This works only when isSkeletonable = true
|
||||
```
|
||||
|
||||
**Don't modify user interaction when the skeleton is active**
|
||||
|
||||
|
||||
By default, user interaction is disabled for skeletonized items, but if you don't want to modify the user interaction indicator when skeleton is active, you can use the `isUserInteractionDisabledWhenSkeletonIsActive` property:
|
||||
|
||||
```swift
|
||||
view.isUserInteractionDisabledWhenSkeletonIsActive = false // The view will be active when the skeleton will be active.
|
||||
```
|
||||
|
||||
**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.13.0"
|
||||
s.version = "1.17.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.
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
52D6D97B1BEFF229002C0205 = {
|
||||
CreatedOnToolsVersion = 7.1;
|
||||
LastSwiftMigration = 1000;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
F5F899F11FABA607002E8FDA = {
|
||||
CreatedOnToolsVersion = 9.1;
|
||||
@@ -1117,8 +1118,10 @@
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1131,6 +1134,8 @@
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.SkeletonView.SkeletonView-iOS";
|
||||
PRODUCT_NAME = SkeletonView;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -1142,8 +1147,10 @@
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1155,6 +1162,8 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.SkeletonView.SkeletonView-iOS";
|
||||
PRODUCT_NAME = SkeletonView;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -31,12 +31,16 @@ extension CollectionSkeleton where Self: UIScrollView {
|
||||
func removeDummyDataSource(reloadAfter: Bool) {}
|
||||
|
||||
func disableUserInteraction() {
|
||||
isUserInteractionEnabled = false
|
||||
isScrollEnabled = false
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = false
|
||||
isScrollEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
func enableUserInteraction() {
|
||||
isUserInteractionEnabled = true
|
||||
isScrollEnabled = true
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = true
|
||||
isScrollEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public protocol SkeletonCollectionViewDataSource: UICollectionViewDataSource {
|
||||
|
||||
public extension SkeletonCollectionViewDataSource {
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return skeletonView.estimatedNumberOfRows
|
||||
return SkeletonCollectionDataSource.automaticNumberOfRows
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView,
|
||||
|
||||
@@ -7,11 +7,18 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
extension UICollectionView: CollectionSkeleton {
|
||||
var estimatedNumberOfRows: Int {
|
||||
guard let flowlayout = collectionViewLayout as? UICollectionViewFlowLayout else { return 0 }
|
||||
return Int(ceil(frame.height / flowlayout.itemSize.height))
|
||||
switch flowlayout.scrollDirection {
|
||||
case .vertical:
|
||||
return Int(ceil(frame.height / flowlayout.itemSize.height))
|
||||
case .horizontal:
|
||||
return Int(ceil(frame.width / flowlayout.itemSize.width))
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
var skeletonDataSource: SkeletonCollectionDataSource? {
|
||||
|
||||
@@ -11,6 +11,8 @@ import UIKit
|
||||
public typealias ReusableCellIdentifier = String
|
||||
|
||||
class SkeletonCollectionDataSource: NSObject {
|
||||
static let automaticNumberOfRows = -1
|
||||
|
||||
weak var originalTableViewDataSource: SkeletonTableViewDataSource?
|
||||
weak var originalCollectionViewDataSource: SkeletonCollectionViewDataSource?
|
||||
var rowHeight: CGFloat = 0.0
|
||||
@@ -32,7 +34,17 @@ extension SkeletonCollectionDataSource: UITableViewDataSource {
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
originalTableViewDataSource?.collectionSkeletonView(tableView, numberOfRowsInSection: section) ?? 0
|
||||
guard let originalTableViewDataSource = originalTableViewDataSource else {
|
||||
return 0
|
||||
}
|
||||
|
||||
let numberOfRows = originalTableViewDataSource.collectionSkeletonView(tableView, numberOfRowsInSection: section)
|
||||
|
||||
if numberOfRows == Self.automaticNumberOfRows {
|
||||
return tableView.estimatedNumberOfRows
|
||||
} else {
|
||||
return numberOfRows
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
@@ -50,7 +62,17 @@ extension SkeletonCollectionDataSource: UICollectionViewDataSource {
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
originalCollectionViewDataSource?.collectionSkeletonView(collectionView, numberOfItemsInSection: section) ?? 0
|
||||
guard let originalCollectionViewDataSource = originalCollectionViewDataSource else {
|
||||
return 0
|
||||
}
|
||||
|
||||
let numberOfItems = originalCollectionViewDataSource.collectionSkeletonView(collectionView, numberOfItemsInSection: section)
|
||||
|
||||
if numberOfItems == Self.automaticNumberOfRows {
|
||||
return collectionView.estimatedNumberOfRows
|
||||
} else {
|
||||
return numberOfItems
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
|
||||
@@ -25,7 +25,7 @@ extension SkeletonCollectionDelegate: UITableViewDelegate {
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||
headerOrFooterView(tableView, for: originalTableViewDelegate?.collectionSkeletonView(tableView, identifierForHeaderInSection: section))
|
||||
headerOrFooterView(tableView, for: originalTableViewDelegate?.collectionSkeletonView(tableView, identifierForFooterInSection: section))
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didEndDisplayingHeaderView view: UIView, forSection section: Int) {
|
||||
|
||||
@@ -16,7 +16,7 @@ public protocol SkeletonTableViewDataSource: UITableViewDataSource {
|
||||
|
||||
public extension SkeletonTableViewDataSource {
|
||||
func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return skeletonView.estimatedNumberOfRows
|
||||
return SkeletonCollectionDataSource.automaticNumberOfRows
|
||||
}
|
||||
|
||||
func numSections(in collectionSkeletonView: UITableView) -> Int { return 1 }
|
||||
|
||||
@@ -125,8 +125,12 @@ extension CALayer {
|
||||
private func calculateNumLines(for config: SkeletonMultilinesLayerConfig) -> Int {
|
||||
let definedNumberOfLines = config.lines
|
||||
let requiredSpaceForEachLine = config.lineHeight + config.multilineSpacing
|
||||
let calculatedNumberOfLines = Int(round(CGFloat(bounds.height - config.paddingInsets.top - config.paddingInsets.bottom) / CGFloat(requiredSpaceForEachLine)))
|
||||
|
||||
let neededLines = round(CGFloat(bounds.height - config.paddingInsets.top - config.paddingInsets.bottom) / CGFloat(requiredSpaceForEachLine))
|
||||
guard neededLines.isNormal else {
|
||||
return 0
|
||||
}
|
||||
|
||||
let calculatedNumberOfLines = Int(neededLines)
|
||||
guard calculatedNumberOfLines > 0 else {
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ enum ViewAssociatedKeys {
|
||||
static var buttonViewState = "buttonViewState"
|
||||
static var currentSkeletonConfig = "currentSkeletonConfig"
|
||||
static var skeletonCornerRadius = "skeletonCornerRadius"
|
||||
static var disabledWhenSkeletonIsActive = "disabledWhenSkeletonIsActive"
|
||||
}
|
||||
// codebeat:enable[TOO_MANY_IVARS]
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ public extension UIView {
|
||||
get { return hiddenWhenSkeletonIsActive }
|
||||
set { hiddenWhenSkeletonIsActive = newValue }
|
||||
}
|
||||
|
||||
@IBInspectable
|
||||
var isUserInteractionDisabledWhenSkeletonIsActive: Bool {
|
||||
get { return disabledWhenSkeletonIsActive }
|
||||
set { disabledWhenSkeletonIsActive = newValue }
|
||||
}
|
||||
|
||||
@IBInspectable
|
||||
var skeletonCornerRadius: Float {
|
||||
@@ -34,6 +40,11 @@ public extension UIView {
|
||||
get { return ao_get(pkey: &ViewAssociatedKeys.hiddenWhenSkeletonIsActive) as? Bool ?? false }
|
||||
set { ao_set(newValue, pkey: &ViewAssociatedKeys.hiddenWhenSkeletonIsActive) }
|
||||
}
|
||||
|
||||
private var disabledWhenSkeletonIsActive: Bool {
|
||||
get { return ao_get(pkey: &ViewAssociatedKeys.disabledWhenSkeletonIsActive) as? Bool ?? true }
|
||||
set { ao_set(newValue, pkey: &ViewAssociatedKeys.disabledWhenSkeletonIsActive) }
|
||||
}
|
||||
|
||||
private var skeletonableCornerRadius: Float {
|
||||
get { return ao_get(pkey: &ViewAssociatedKeys.skeletonCornerRadius) as? Float ?? 0.0 }
|
||||
|
||||
@@ -10,7 +10,10 @@ import UIKit
|
||||
|
||||
extension UIView {
|
||||
@objc func prepareViewForSkeleton() {
|
||||
isUserInteractionEnabled = false
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
startTransition { [weak self] in
|
||||
self?.backgroundColor = .clear
|
||||
}
|
||||
@@ -32,7 +35,8 @@ extension UILabel {
|
||||
|
||||
// Workaround to simulate content when the label is contained in a `UIStackView`.
|
||||
if isSuperviewAStackView, bounds.height == 0 {
|
||||
text = "This is a placeholder text to simulate content because it's contained in a stack view in order to prevent that the content size will be zero."
|
||||
// This is a placeholder text to simulate content because it's contained in a stack view in order to prevent that the content size will be zero.
|
||||
text = " "
|
||||
}
|
||||
|
||||
let desiredHeight = desiredHeightBasedOnNumberOfLines
|
||||
@@ -54,7 +58,11 @@ extension UILabel {
|
||||
|
||||
override func prepareViewForSkeleton() {
|
||||
backgroundColor = .clear
|
||||
isUserInteractionEnabled = false
|
||||
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
resignFirstResponder()
|
||||
startTransition { [weak self] in
|
||||
self?.updateHeightConstraintsIfNeeded()
|
||||
@@ -66,7 +74,11 @@ extension UILabel {
|
||||
extension UITextView {
|
||||
override func prepareViewForSkeleton() {
|
||||
backgroundColor = .clear
|
||||
isUserInteractionEnabled = false
|
||||
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
resignFirstResponder()
|
||||
startTransition { [weak self] in
|
||||
self?.textColor = .clear
|
||||
@@ -89,7 +101,11 @@ extension UITextField {
|
||||
extension UIImageView {
|
||||
override func prepareViewForSkeleton() {
|
||||
backgroundColor = .clear
|
||||
isUserInteractionEnabled = false
|
||||
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
startTransition { [weak self] in
|
||||
self?.image = nil
|
||||
}
|
||||
@@ -99,7 +115,11 @@ extension UIImageView {
|
||||
extension UIButton {
|
||||
override func prepareViewForSkeleton() {
|
||||
backgroundColor = .clear
|
||||
isUserInteractionEnabled = false
|
||||
|
||||
if isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
startTransition { [weak self] in
|
||||
self?.setTitle(nil, for: .normal)
|
||||
}
|
||||
|
||||
@@ -27,12 +27,17 @@ extension UIView: Recoverable {
|
||||
guard let storedViewState = viewState else { return }
|
||||
|
||||
startTransition { [weak self] in
|
||||
self?.layer.cornerRadius = storedViewState.cornerRadius
|
||||
self?.layer.masksToBounds = storedViewState.clipToBounds
|
||||
self?.isUserInteractionEnabled = storedViewState.isUserInteractionsEnabled
|
||||
guard let self = self else { return }
|
||||
|
||||
if self?.backgroundColor == .clear || forced {
|
||||
self?.backgroundColor = storedViewState.backgroundColor
|
||||
self.layer.cornerRadius = storedViewState.cornerRadius
|
||||
self.layer.masksToBounds = storedViewState.clipToBounds
|
||||
|
||||
if self.isUserInteractionDisabledWhenSkeletonIsActive {
|
||||
self.isUserInteractionEnabled = storedViewState.isUserInteractionsEnabled
|
||||
}
|
||||
|
||||
if self.backgroundColor == .clear || forced {
|
||||
self.backgroundColor = storedViewState.backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,11 @@ extension UIView {
|
||||
|
||||
extension UITableView {
|
||||
override var subviewsToSkeleton: [UIView] {
|
||||
visibleCells + visibleSectionHeaders + visibleSectionFooters
|
||||
// on `UIViewController'S onViewDidLoad`, the window is still nil.
|
||||
// Some developer trying to call `view.showAnimatedSkeleton()`
|
||||
// when the request or data is loading which sometimes happens before the ViewDidAppear
|
||||
guard window != nil else { return [] }
|
||||
return subviews
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user