Compare commits

...

44 Commits

Author SHA1 Message Date
André Pacheco Neves 739a4f5d78 Fix skeletons interfering with attributed strings in text-based views (#520)
When enabling skeleton mode in a text-based view (`UILabel`,
`UITextView`, `UITextField`), it sets the `textColor` to `.clear`,
which is fine when `text` is used, but causes problems when
`attributedText` is used, as it effectively "resets" the string to have
a single color.

Additionally, when a `UILabel` is nested inside a `UIStackView` a
dummy string `" "` was set on the label's `text` so that it didn't have
a 0 height content size. However, this workaround didn't consider the
case where the label already had a non-empty text, meaning that this
(intrusive) `text = " "` broke existing code by clearing the label's
contents.

By improving the corresponding `RecoverableXState` structs, we are able
to preserve each element's contents and state as skeleton is disabled.

Fixes #518.

## Changes

- Create new `RecoverableLabelState` containing a `attributedText` and
`text`, and use it on `UILabel`.

- Update `RecoverableTextViewState` and `RecoverableTextFieldState` to
have a `attributedText`.

- Check if `UILabel`'s `text` is empty before setting dummy value when
enabling skeleton mode in a label nested inside a `UIStackView`.
2022-10-20 12:59:00 +02:00
Sebastian Nicklas 0058d510a9 The translation was written in an overly polite tone, which sounded kind (#519)
of strange in the context of library documentation.

Co-authored-by: Sebastian Nicklas <sebastian@iconicfinance.io>
2022-10-20 12:55:18 +02:00
Lucas Zischka 6a51bc8cc0 Add German translation (#515)
* Setup german translation file

- Create copy of english readme
- Link to it as German translation

* Translate header

* Translate Features

* Translate Installation

* Whoops i translated in the wrong file, switch them out

* Translate Usage

* Translate Collections

* Translate Texts

* Translate Appearance

* Better translation for Solid

* Even better translation for Solid

* Translate Custom colors

* Add back german translation link to readme

got lost in commit d6726af

* Translate Animations

* Fix linking

* Fix code formating

* Another code formatting fix

* Fix Asset linking

* Translate Transitions

* Translate Miscellaneous

* Translate the rest
2022-10-04 10:16:34 +02:00
Juanpe b6b0fc894c Bump version 1.30.3 2022-09-29 09:09:01 +00:00
Michał Zaborowski cb4d48d603 Update lifecycle handlers (#514) 2022-09-28 17:34:42 +02:00
Juanpe 6604aae989 Bump version 1.30.2 2022-09-05 07:27:37 +00:00
Alexander ca7d2edcee Fix "complementaryColor" color isn't changed after updating the application theme (#496) 2022-09-05 09:26:20 +02:00
Juanpe Catalán 3688549eb8 Update Stale action 2022-08-29 08:48:23 +02:00
Juanpe Catalán f27a4f69ff Create needs-attention.yml 2022-08-29 08:40:38 +02:00
Juanpe 2ac515b97e Bump version 1.30.1 2022-08-16 08:36:34 +00:00
Tom Cheung 969f068480 Bugfix/tableview insetgrouped (#494)
* Fixed TableView skeleton cannot hide if style is insetGrouped

* Fixed show skeleton button not visible

* Update the fix base on PR comments
2022-08-16 10:35:13 +02:00
Juanpe Catalán fbc2e6f8d2 Update README.md 2022-08-11 21:00:45 +02:00
Juanpe Catalán 8be6c81729 Revert changes in the Package file 2022-08-11 20:16:10 +02:00
Juanpe Catalán 87c4706be8 Testing XCFramework 2022-08-11 20:13:19 +02:00
Juanpe b4d6dc2602 Bump version 1.30.0 2022-08-11 16:33:23 +00:00
Carlos Enumo f859d197f3 Add prepareViewForSkeleton to SkeletonCollectionViewDataSource (#505) 2022-08-11 18:32:08 +02:00
Juanpe 3a11e233c7 Bump version 1.29.4 2022-08-11 16:15:54 +00:00
dependabot[bot] ddd4fdbb1f Bump tzinfo from 1.2.5 to 1.2.10 (#507)
Bumps [tzinfo](https://github.com/tzinfo/tzinfo) from 1.2.5 to 1.2.10.
- [Release notes](https://github.com/tzinfo/tzinfo/releases)
- [Changelog](https://github.com/tzinfo/tzinfo/blob/master/CHANGES.md)
- [Commits](https://github.com/tzinfo/tzinfo/compare/v1.2.5...v1.2.10)

---
updated-dependencies:
- dependency-name: tzinfo
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-11 18:14:28 +02:00
Juanpe Catalán 00394e0b9f Add Swift Package Index badge for platforms 2022-06-01 08:35:50 +02:00
Juanpe daea88df8f Bump version 1.29.3 2022-06-01 06:32:58 +00:00
dependabot[bot] b640c1d8ee Bump cocoapods-downloader from 1.2.2 to 1.6.3 (#497)
Bumps [cocoapods-downloader](https://github.com/CocoaPods/cocoapods-downloader) from 1.2.2 to 1.6.3.
- [Release notes](https://github.com/CocoaPods/cocoapods-downloader/releases)
- [Changelog](https://github.com/CocoaPods/cocoapods-downloader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/CocoaPods/cocoapods-downloader/compare/1.2.2...1.6.3)

---
updated-dependencies:
- dependency-name: cocoapods-downloader
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-01 08:31:25 +02:00
Juanpe 2eaf72ed0e Bump version 1.29.2 2022-01-18 14:21:59 +00:00
Juanpe Catalán 9e61ff3a85 Fix crash when text is empty (#483) 2022-01-18 15:20:32 +01:00
Juanpe Catalán 204b871f17 Update CD.yml 2022-01-18 14:56:28 +01:00
Juanpe Catalán ecb41f1885 Update stale.yml 2022-01-15 10:17:58 +01:00
Juanpe dffbd8cbfd Bump version 1.29.1 2022-01-10 15:33:01 +00:00
Juanpe Catalán 458e5fc29b Fix animation when the transition is .none (#481) 2022-01-10 16:32:00 +01:00
Juanpe a6f335df01 Bump version 1.29.0 2022-01-10 10:24:35 +00:00
Juanpe Catalán 106692fbfb Create new `SkeletonGradient constructor (#480) 2022-01-10 11:23:07 +01:00
Juanpe Catalán cb4dcfd07a Update CD.yml 2022-01-10 11:11:19 +01:00
Juanpe 419172104d Bump version 1.28.0 2022-01-10 10:09:04 +00:00
Juanpe Catalán b40a771510 Improved number of lines calculation (#479)
* Create SkeletonTextNumberOfLines

* Update README format
2022-01-10 11:07:31 +01:00
Juanpe Catalán 73b137cda6 Update CD.yml 2022-01-10 11:07:16 +01:00
Juanpe add7401058 Bump version 1.27.0 2022-01-08 12:40:21 +00:00
Tom Cheung 947e2a63c2 Add skeletonCornerRadius in SkeletonAppearance (#478)
* Add skeletonCornerRadius in SkeletonAppearance

* Remove unnecessary type casting
2022-01-08 13:39:03 +01:00
Juanpe Catalán c5462de1db Update CD.yml 2022-01-08 11:25:33 +01:00
Juanpe 00b5d614b6 Bump version 1.26.2 2022-01-08 10:22:27 +00:00
Nick Thompson 8ba48071de Fix last line text alignment when using padding insets (#477) 2022-01-08 11:20:59 +01:00
Jackson a0980380f2 Add ExampleViewController for programatically creating ViewController (#475)
* Add ExampleViewController for programatically creating view

* Update UITextViewByCodeViewController
2022-01-07 17:05:02 +01:00
Juanpe Catalán dd80fabeea Rename pod_trunk to pod_trunk.yml 2022-01-07 17:04:21 +01:00
Juanpe Catalán d89a70052c Create pod trunk workflow 2022-01-07 17:03:43 +01:00
Juanpe cd12c64e18 Bump version 1.26.1 2022-01-07 15:56:40 +00:00
Juanpe Catalán 2edd715a87 Fix skeleton flow notifications triggered multiple times (#476)
* Fix notification names

* Fix problem with multiple notifications
2022-01-07 16:55:19 +01:00
Juanpe f3cc8f0aba Bump version 1.26.0 2021-11-23 10:41:10 +00:00
42 changed files with 1115 additions and 142 deletions
-10
View File
@@ -1,10 +0,0 @@
daysUntilStale: 7
daysUntilClose: 7
onlyLabels:
- awaiting user input
staleLabel: given up
markComment: >
🤖 This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions 🙂
closeComment: false
+6 -11
View File
@@ -12,8 +12,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Setup fastlane
run: brew install fastlane
- name: Setup Environment
run: |
brew install fastlane
brew install cocoapods
- name: Publish release
id: publish_release
@@ -32,9 +34,10 @@ jobs:
branch: 'main'
commit_message: 'Bump version ${{ steps.publish_release.outputs.tag_name }}'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Cocoapods
continue-on-error: true
env:
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
run: |
@@ -42,14 +45,6 @@ jobs:
pod lib lint --allow-warnings
pod trunk push --allow-warnings
- name: Communicate on PR released
uses: unsplash/comment-on-pr@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
msg: |
Congratulations! 🎉 This was released as part of [SkeletonView ${{ steps.publish_release.outputs.tag_name }}](${{ steps.publish_release.outputs.html_url }}) 🚀
- name: Tweet the release
uses: ethomson/send-tweet-action@v1
with:
+18
View File
@@ -0,0 +1,18 @@
name: Issue Needs Attention
# This workflow is triggered on issue comments.
on:
issue_comment:
types: created
jobs:
applyNeedsAttentionLabel:
name: Apply Needs Attention Label
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Apply Needs Attention Label
uses: hramos/needs-attention@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
response-required-label: 'awaiting user info'
needs-attention-label: 'needs triage'
+16
View File
@@ -0,0 +1,16 @@
name: Pod trunk
on: [workflow_dispatch]
jobs:
release_version:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v2
- name: Deploy to Cocoapods
env:
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
run: |
set -eo pipefail
pod lib lint --allow-warnings
pod trunk push --allow-warnings
+18
View File
@@ -0,0 +1,18 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 5 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
with:
close-issue-message: 'Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please feel free to create a new issue with up-to-date information.'
stale-issue-message: '🤖 This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions 🙂'
days-before-stale: 5
days-before-close: 3
enable-statistics: true
operations-per-run: 60
only-labels: 'awaiting user input'
@@ -2,13 +2,8 @@
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
"platform" : "universal",
"reference" : "systemBlueColor"
},
"idiom" : "universal"
},
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Va7-1y-Tel">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19455" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Va7-1y-Tel">
<device id="retina5_9" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -12,7 +12,7 @@
<!--Item-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="SkeletonViewExample" customModuleProvider="target" sceneMemberID="viewController">
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="iOS_Example" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -88,8 +88,8 @@
<rect key="frame" x="0.0" y="287" width="375" height="282"/>
<color key="separatorColor" red="0.1061807256" green="0.84678786989999999" blue="0.031482450150000001" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="CellIdentifier" rowHeight="120" id="2dN-Bd-tdy" customClass="Cell" customModule="SkeletonViewExample" customModuleProvider="target">
<rect key="frame" x="0.0" y="24.333333969116211" width="375" height="120"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="CellIdentifier" rowHeight="120" id="2dN-Bd-tdy" customClass="Cell" customModule="iOS_Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="44.666666030883789" width="375" height="120"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="2dN-Bd-tdy" id="7IN-F3-Mr6">
<rect key="frame" x="0.0" y="0.0" width="375" height="120"/>
@@ -299,7 +299,7 @@
<!--Item-->
<scene sceneID="Cfc-AT-AS1">
<objects>
<viewController id="dv8-ph-Ehg" sceneMemberID="viewController">
<viewController id="dv8-ph-Ehg" customClass="UITextViewByCodeViewController" customModule="iOS_Example" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Jwx-gI-Qod">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -5,12 +5,12 @@ import UIKit
class HeaderFooterSection: UITableViewHeaderFooterView {
lazy var titleLabel: UILabel = {
let label = UILabel()
let label = UILabel()
label.text = " "
label.isSkeletonable = true
label.linesCornerRadius = 5
label.linesCornerRadius = 10
return label
}()
@@ -0,0 +1,44 @@
// Copyright © 2022 SkeletonView. All rights reserved.
import UIKit
import SkeletonView
class UITextViewByCodeViewController: UIViewController {
lazy var textView: UITextView = {
let tv = UITextView()
tv.text = " "
tv.linesCornerRadius = 10
tv.isSkeletonable = true
tv.translatesAutoresizingMaskIntoConstraints = false
return tv
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupElementsConstraints()
showSkeletonForElements()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
func setupUI() {
view.addSubview(textView)
}
func setupElementsConstraints() {
textView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 10).isActive = true
textView.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor, constant: 10).isActive = true
textView.rightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true
textView.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
func showSkeletonForElements() {
textView.showSkeleton()
}
}
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
E21D8BB727888D050041DBCE /* UITextViewByCodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21D8BB627888D050041DBCE /* UITextViewByCodeViewController.swift */; };
F556F5C026CD20A300A80B83 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F556F5B726CD20A300A80B83 /* ViewController.swift */; };
F556F5C126CD20A300A80B83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F556F5B826CD20A300A80B83 /* Assets.xcassets */; };
F556F5C226CD20A300A80B83 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F556F5B926CD20A300A80B83 /* LaunchScreen.storyboard */; };
@@ -57,6 +58,7 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
E21D8BB627888D050041DBCE /* UITextViewByCodeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextViewByCodeViewController.swift; sourceTree = "<group>"; };
F556F59F26CD201B00A80B83 /* iOS Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS Example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
F556F5B726CD20A300A80B83 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
F556F5B826CD20A300A80B83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -106,6 +108,7 @@
F556F6EC26CE813F00A80B83 /* Constants.swift */,
F556F6ED26CE813F00A80B83 /* HeaderFooterSection.swift */,
F556F5B726CD20A300A80B83 /* ViewController.swift */,
E21D8BB627888D050041DBCE /* UITextViewByCodeViewController.swift */,
F556F5B826CD20A300A80B83 /* Assets.xcassets */,
F556F5B926CD20A300A80B83 /* LaunchScreen.storyboard */,
F556F5BB26CD20A300A80B83 /* Main.storyboard */,
@@ -239,6 +242,7 @@
F556F6EE26CE813F00A80B83 /* Cell.swift in Sources */,
F556F5C026CD20A300A80B83 /* ViewController.swift in Sources */,
F556F6F026CE813F00A80B83 /* HeaderFooterSection.swift in Sources */,
E21D8BB727888D050041DBCE /* UITextViewByCodeViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+2 -2
View File
@@ -36,7 +36,7 @@ GEM
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.2.2)
cocoapods-downloader (1.6.3)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.0)
@@ -182,7 +182,7 @@ GEM
tty-screen (0.7.0)
tty-spinner (0.9.1)
tty-cursor (~> 0.7)
tzinfo (1.2.5)
tzinfo (1.2.10)
thread_safe (~> 0.1)
uber (0.1.0)
unf (0.1.4)
+1 -1
View File
@@ -1,4 +1,4 @@
// swift-tools-version:5.0
// swift-tools-version:5.3
import PackageDescription
+35 -15
View File
@@ -8,7 +8,7 @@
<a href="https://cocoapods.org/pods/SkeletonView"><img src="https://img.shields.io/cocoapods/v/SkeletonView.svg?style=flat"></a>
<a href="https://github.com/Carthage/Carthage/"><img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat"></a>
<a href="https://swift.org/package-manager/"><img src="https://img.shields.io/badge/SPM-supported-Green.svg?style=flat"></a>
<img src="https://img.shields.io/badge/platforms-iOS_tvOS-green" />
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FJuanpe%2FSkeletonView%2Fbadge%3Ftype%3Dplatforms"/>
<a href="https://badge.bow-swift.io/recipe?name=SkeletonView&description=An%20elegant%20way%20to%20show%20users%20that%20something%20is%20happening%20and%20also%20prepare%20them%20to%20which%20contents%20he%20is%20waiting&url=https://github.com/juanpe/skeletonview&owner=Juanpe&avatar=https://avatars0.githubusercontent.com/u/1409041?v=4&tag=1.20.0"><img src="https://raw.githubusercontent.com/bow-swift/bow-art/master/badges/nef-playgrounds-badge.svg" alt="SkeletonView Playground" style="height:20px"></a>
</p>
@@ -21,7 +21,7 @@
• <a href="#-contributing">Contributing</a>
</p>
**🌎 README is available in other languages: [🇪🇸](Translations/README_es.md) . [🇨🇳](Translations/README_zh.md) . [🇧🇷](Translations/README_pt-br.md) . [🇰🇷](Translations/README_ko.md) . [🇫🇷](Translations/README_fr.md)**
**🌎 README is available in other languages: [🇪🇸](Translations/README_es.md) . [🇨🇳](Translations/README_zh.md) . [🇧🇷](Translations/README_pt-br.md) . [🇰🇷](Translations/README_ko.md) . [🇫🇷](Translations/README_fr.md) . [🇩🇪](Translations/README_de.md)**
Today almost all apps have async processes, such as API requests, long running processes, etc. While the processes are working, usually developers place a loading view to show users that something is going on.
@@ -90,6 +90,10 @@ dependencies: [
]
```
> 📣 **IMPORTANT!**
>
> Since version 1.30.0, `SkeletonView` supports **XCFrameworks**, so if you want to install it as a **XCFramework**, please use [this repo](https://github.com/Juanpe/SkeletonView-XCFramework.git) instead.
## 🐒 Usage
@@ -250,6 +254,7 @@ public protocol SkeletonCollectionViewDataSource: UICollectionViewDataSource {
func collectionSkeletonView(_ skeletonView: UICollectionView, supplementaryViewIdentifierOfKind: String, at indexPath: IndexPath) -> ReusableCellIdentifier? // default: nil
func collectionSkeletonView(_ skeletonView: UICollectionView, skeletonCellForItemAt indexPath: IndexPath) -> UICollectionViewCell? // default: nil
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath)
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareViewForSkeleton view: UICollectionReusableView, at indexPath: IndexPath)
}
```
@@ -261,7 +266,6 @@ The rest of the process is the same as ```UITableView```
![](Assets/multilines2.png)
When using elements with text, ```SkeletonView``` draws lines to simulate text.
Besides, you can decide how many lines you want. If ```numberOfLines``` is set to zero, it will calculate how many lines needed to populate the whole skeleton and it will be drawn. Instead, if you set it to one, two or any number greater than zero, it will only draw this number of lines.
You can set some properties for multilines elements.
@@ -272,6 +276,34 @@ You can set some properties for multilines elements.
| **skeletonLineSpacing** | `CGFloat` | `10` | ![](Assets/multiline_lineSpacing.png)
| **skeletonPaddingInsets** | `UIEdgeInsets` | `.zero` | ![](Assets/multiline_insets.png)
| **skeletonTextLineHeight** | `SkeletonTextLineHeight` | `.fixed(15)` | ![](Assets/multiline_lineHeight.png)
| **skeletonTextNumberOfLines** | `SkeletonTextNumberOfLines` | `.inherited` | ![](Assets/multiline_corner.png)
<br />
To modify the percent or radius **using code**, set the properties:
```swift
descriptionTextView.lastLineFillPercent = 50
descriptionTextView.linesCornerRadius = 5
```
Or, if you prefer use **IB/Storyboard**:
![](Assets/multiline_customize.png)
<br />
**How to define the number of lines?**
By default, the number of lines is the same as the value of the `numberOfLines` property. And, if it's set to **zero**, it'll calculate how many lines are needed to populate the whole skeleton and draw it.
However, if you want to set a specific number of skeleton lines you can do it by setting the `skeletonTextNumberOfLines` property. This property has two possible values, `inherited` which returns `numberOfLines` value and `custom(Int)` which returns the specific number of lines specified as the associated value.
For example:
```swift
label.skeletonTextNumberOfLines = 3 // .custom(3)
```
<br />
@@ -288,18 +320,6 @@ You can set some properties for multilines elements.
> as the last line.
<br />
To modify the percent or radius **using code**, set the properties:
```swift
descriptionTextView.lastLineFillPercent = 50
descriptionTextView.linesCornerRadius = 5
```
Or, if you prefer use **IB/Storyboard**:
![](Assets/multiline_customize.png)
### 🦋 Appearance
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SkeletonView"
s.version = "1.25.2"
s.version = "1.30.3"
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.
+6
View File
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
F5225F2A278C2BCE0061A9B0 /* SkeletonTextNumberOfLines.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5225F29278C2BCE0061A9B0 /* SkeletonTextNumberOfLines.swift */; };
F5225F2B278C2BCE0061A9B0 /* SkeletonTextNumberOfLines.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5225F29278C2BCE0061A9B0 /* SkeletonTextNumberOfLines.swift */; };
F53D731826D399E100249D46 /* SkeletonTreeNode+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53D731726D399E100249D46 /* SkeletonTreeNode+Extensions.swift */; };
F53D731926D399E100249D46 /* SkeletonTreeNode+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53D731726D399E100249D46 /* SkeletonTreeNode+Extensions.swift */; };
F53D731B26D3A35100249D46 /* SkeletonExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53D731A26D3A35100249D46 /* SkeletonExtended.swift */; };
@@ -151,6 +153,7 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
F5225F29278C2BCE0061A9B0 /* SkeletonTextNumberOfLines.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonTextNumberOfLines.swift; sourceTree = "<group>"; };
F53D731726D399E100249D46 /* SkeletonTreeNode+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SkeletonTreeNode+Extensions.swift"; sourceTree = "<group>"; };
F53D731A26D3A35100249D46 /* SkeletonExtended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonExtended.swift; sourceTree = "<group>"; };
F53D732226D3C3A800249D46 /* UILabel+SKExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+SKExtensions.swift"; sourceTree = "<group>"; };
@@ -519,6 +522,7 @@
F556F6B826CE262700A80B83 /* GradientDirection.swift */,
F556F6C126CE27FD00A80B83 /* SkeletonType.swift */,
F5C84883274BB6F000004D1A /* SkeletonTextLineHeight.swift */,
F5225F29278C2BCE0061A9B0 /* SkeletonTextNumberOfLines.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -834,6 +838,7 @@
F556F58A26CD1F3900A80B83 /* SkeletonGradient.swift in Sources */,
F556F6C326CE27FD00A80B83 /* SkeletonType.swift in Sources */,
F556F58B26CD1F3900A80B83 /* SkeletonLayer.swift in Sources */,
F5225F2B278C2BCE0061A9B0 /* SkeletonTextNumberOfLines.swift in Sources */,
F556F6A226CD566C00A80B83 /* UIView+SKExtensions.swift in Sources */,
F556F69326CD506C00A80B83 /* Deprecated.swift in Sources */,
F556F6BA26CE262700A80B83 /* GradientDirection.swift in Sources */,
@@ -917,6 +922,7 @@
F556F6C226CE27FD00A80B83 /* SkeletonType.swift in Sources */,
OBJ_123 /* SkeletonLayer.swift in Sources */,
F556F6E026CE367600A80B83 /* UIView+SkeletonView.swift in Sources */,
F5225F2A278C2BCE0061A9B0 /* SkeletonTextNumberOfLines.swift in Sources */,
F556F6A126CD566C00A80B83 /* UIView+SKExtensions.swift in Sources */,
F556F69226CD506C00A80B83 /* Deprecated.swift in Sources */,
F556F6B926CE262700A80B83 /* GradientDirection.swift in Sources */,
@@ -37,5 +37,7 @@ public class SkeletonViewAppearance {
public var renderSingleLineAsView: Bool = false
public var skeletonCornerRadius: Float = 0
}
// codebeat:enable[TOO_MANY_IVARS]
@@ -15,6 +15,7 @@ public protocol SkeletonCollectionViewDataSource: UICollectionViewDataSource {
func collectionSkeletonView(_ skeletonView: UICollectionView, supplementaryViewIdentifierOfKind: String, at indexPath: IndexPath) -> ReusableCellIdentifier?
func collectionSkeletonView(_ skeletonView: UICollectionView, skeletonCellForItemAt indexPath: IndexPath) -> UICollectionViewCell?
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath)
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareViewForSkeleton view: UICollectionReusableView, at indexPath: IndexPath)
}
public extension SkeletonCollectionViewDataSource {
@@ -35,6 +36,8 @@ public extension SkeletonCollectionViewDataSource {
}
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath) { }
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareViewForSkeleton view: UICollectionReusableView, at indexPath: IndexPath) { }
}
public protocol SkeletonCollectionViewDelegate: UICollectionViewDelegate { }
@@ -29,4 +29,8 @@ public struct SkeletonGradient {
}
}
public init(colors: [UIColor]) {
self.gradientColors = colors
}
}
@@ -0,0 +1,32 @@
//
// Copyright SkeletonView. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// SkeletonTextNumberOfLines.swift
//
// Created by Juanpe Catalán on 10/1/22.
import UIKit
public enum SkeletonTextNumberOfLines: Equatable, ExpressibleByIntegerLiteral {
/// Returns `numberOfLines` value.
case inherited
/// Returns the specific number of lines specified as the associated value.
case custom(Int)
}
public extension SkeletonTextNumberOfLines {
init(integerLiteral value: Int) {
self = .custom(value)
}
}
@@ -15,6 +15,7 @@ import UIKit
public extension UILabel {
/// Defines the skeleton paddings.
var skeletonPaddingInsets: UIEdgeInsets {
get {
paddingInsets
@@ -24,6 +25,8 @@ public extension UILabel {
}
}
/// Defines the logic for calculating the height of the skeleton lines.
/// Default: `SkeletonAppearance.default.textLineHeight`
var skeletonTextLineHeight: SkeletonTextLineHeight {
get {
textLineHeight
@@ -33,4 +36,15 @@ public extension UILabel {
}
}
/// Defines the logic for calculating the number of lines of the skeleton.
/// Default: `inherited`
var skeletonTextNumberOfLines: SkeletonTextNumberOfLines {
get {
skeletonNumberOfLines
}
set {
skeletonNumberOfLines = newValue
}
}
}
@@ -15,6 +15,7 @@ import UIKit
public extension UITextView {
/// Defines the skeleton paddings.
var skeletonPaddingInsets: UIEdgeInsets {
get {
paddingInsets
@@ -24,6 +25,8 @@ public extension UITextView {
}
}
/// Defines the logic for calculating the height of the skeleton lines.
/// Default: `SkeletonAppearance.default.textLineHeight`
var skeletonTextLineHeight: SkeletonTextLineHeight {
get {
textLineHeight
@@ -33,4 +36,15 @@ public extension UITextView {
}
}
/// Defines the logic for calculating the number of lines of the skeleton.
/// Default: `inherited`
var skeletonTextNumberOfLines: SkeletonTextNumberOfLines {
get {
skeletonNumberOfLines
}
set {
skeletonNumberOfLines = newValue
}
}
}
@@ -51,13 +51,13 @@ extension SkeletonCollectionDataSource: UITableViewDataSource {
let fakeCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
originalTableViewDataSource?.collectionSkeletonView(tableView, prepareCellForSkeleton: fakeCell, at: indexPath)
skeletonViewIfContainerSkeletonIsActive(container: tableView, view: fakeCell)
skeletonizeViewIfContainerSkeletonIsActive(container: tableView, view: fakeCell)
return fakeCell
}
originalTableViewDataSource?.collectionSkeletonView(tableView, prepareCellForSkeleton: cell, at: indexPath)
skeletonViewIfContainerSkeletonIsActive(container: tableView, view: cell)
skeletonizeViewIfContainerSkeletonIsActive(container: tableView, view: cell)
return cell
}
}
@@ -88,13 +88,13 @@ extension SkeletonCollectionDataSource: UICollectionViewDataSource {
let fakeCell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
originalCollectionViewDataSource?.collectionSkeletonView(collectionView, prepareCellForSkeleton: fakeCell, at: indexPath)
skeletonViewIfContainerSkeletonIsActive(container: collectionView, view: fakeCell)
skeletonizeViewIfContainerSkeletonIsActive(container: collectionView, view: fakeCell)
return fakeCell
}
originalCollectionViewDataSource?.collectionSkeletonView(collectionView, prepareCellForSkeleton: cell, at: indexPath)
skeletonViewIfContainerSkeletonIsActive(container: collectionView, view: cell)
skeletonizeViewIfContainerSkeletonIsActive(container: collectionView, view: cell)
return cell
}
@@ -103,7 +103,9 @@ extension SkeletonCollectionDataSource: UICollectionViewDataSource {
at indexPath: IndexPath) -> UICollectionReusableView {
if let viewIdentifier = originalCollectionViewDataSource?.collectionSkeletonView(collectionView, supplementaryViewIdentifierOfKind: kind, at: indexPath) {
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: viewIdentifier, for: indexPath)
skeletonViewIfContainerSkeletonIsActive(container: collectionView, view: view)
originalCollectionViewDataSource?.collectionSkeletonView(collectionView, prepareViewForSkeleton: view, at: indexPath)
skeletonizeViewIfContainerSkeletonIsActive(container: collectionView, view: view)
return view
}
@@ -113,12 +115,15 @@ extension SkeletonCollectionDataSource: UICollectionViewDataSource {
}
extension SkeletonCollectionDataSource {
private func skeletonViewIfContainerSkeletonIsActive(container: UIView, view: UIView) {
private func skeletonizeViewIfContainerSkeletonIsActive(container: UIView, view: UIView) {
guard container.sk.isSkeletonActive,
let skeletonConfig = container._currentSkeletonConfig else {
return
}
view.showSkeleton(skeletonConfig: skeletonConfig)
view.showSkeleton(
skeletonConfig: skeletonConfig,
notifyDelegate: false
)
}
}
@@ -9,17 +9,23 @@
import UIKit
class SkeletonCollectionDelegate: NSObject {
weak var originalTableViewDelegate: SkeletonTableViewDelegate?
weak var originalCollectionViewDelegate: SkeletonCollectionViewDelegate?
init(tableViewDelegate: SkeletonTableViewDelegate? = nil, collectionViewDelegate: SkeletonCollectionViewDelegate? = nil) {
init(
tableViewDelegate: SkeletonTableViewDelegate? = nil,
collectionViewDelegate: SkeletonCollectionViewDelegate? = nil
) {
self.originalTableViewDelegate = tableViewDelegate
self.originalCollectionViewDelegate = collectionViewDelegate
}
}
// MARK: - UITableViewDelegate
extension SkeletonCollectionDelegate: UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
headerOrFooterView(tableView, for: originalTableViewDelegate?.collectionSkeletonView(tableView, identifierForHeaderInSection: section))
}
@@ -42,24 +48,40 @@ extension SkeletonCollectionDelegate: UITableViewDelegate {
cell.hideSkeleton()
originalTableViewDelegate?.tableView?(tableView, didEndDisplaying: cell, forRowAt: indexPath)
}
private func headerOrFooterView(_ tableView: UITableView, for viewIdentifier: String? ) -> UIView? {
guard let viewIdentifier = viewIdentifier, let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: viewIdentifier) else { return nil }
skeletonViewIfContainerSkeletonIsActive(container: tableView, view: header)
return header
}
}
// MARK: - UICollectionViewDelegate
extension SkeletonCollectionDelegate: UICollectionViewDelegate { }
extension SkeletonCollectionDelegate {
private func skeletonViewIfContainerSkeletonIsActive(container: UIView, view: UIView) {
private extension SkeletonCollectionDelegate {
func skeletonizeViewIfContainerSkeletonIsActive(container: UIView, view: UIView) {
guard container.sk.isSkeletonActive,
let skeletonConfig = container._currentSkeletonConfig else {
let skeletonConfig = container._currentSkeletonConfig
else {
return
}
view.showSkeleton(skeletonConfig: skeletonConfig)
view.showSkeleton(
skeletonConfig: skeletonConfig,
notifyDelegate: false
)
}
func headerOrFooterView(_ tableView: UITableView, for viewIdentifier: String? ) -> UIView? {
guard let viewIdentifier = viewIdentifier,
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: viewIdentifier)
else {
return nil
}
skeletonizeViewIfContainerSkeletonIsActive(
container: tableView,
view: header
)
return header
}
}
@@ -24,23 +24,38 @@ struct RecoverableViewState {
}
struct RecoverableTextViewState {
struct RecoverableLabelState {
var attributedText: NSAttributedString? // we mess with `textColor`, which impacts attributed string if defined
var text: String? // we mess with `text` if the label is within a `UIStackView`
var textColor: UIColor?
init(view: UILabel) {
if let attributedText = view.attributedText {
self.attributedText = attributedText
} else {
self.text = view.text
}
self.textColor = view.textColor
}
}
struct RecoverableTextViewState {
var attributedText: NSAttributedString? // we mess with `textColor`, which impacts attributed string if defined
var textColor: UIColor?
init(view: UITextView) {
self.attributedText = view.attributedText
self.textColor = view.textColor
}
}
struct RecoverableTextFieldState {
var attributedText: NSAttributedString? // we mess with `textColor`, which impacts attributed string if defined
var textColor: UIColor?
var placeholder: String?
init(view: UITextField) {
self.attributedText = view.attributedText
self.textColor = view.textColor
self.placeholder = view.placeholder
}
@@ -28,12 +28,12 @@ struct SkeletonLayer {
self.maskLayer.bounds = holder.definedMaxBounds
self.maskLayer.cornerRadius = CGFloat(holder.skeletonCornerRadius)
addTextLinesIfNeeded()
self.maskLayer.tint(withColors: colors)
self.maskLayer.tint(withColors: colors, traitCollection: holder.traitCollection)
}
func update(usingColors colors: [UIColor]) {
layoutIfNeeded()
maskLayer.tint(withColors: colors)
maskLayer.tint(withColors: colors, traitCollection: holder?.traitCollection)
}
func layoutIfNeeded() {
@@ -59,7 +59,7 @@ 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 config = SkeletonMultilinesLayerConfig(lines: textView.numberOfLines,
let config = SkeletonMultilinesLayerConfig(lines: textView.estimatedNumberOfLines,
lineHeight: textView.estimatedLineHeight,
type: type,
lastLineFillPercent: textView.lastLineFillingPercent,
@@ -75,7 +75,7 @@ struct SkeletonLayer {
func updateLinesIfNeeded() {
guard let textView = holderAsTextView else { return }
let config = SkeletonMultilinesLayerConfig(lines: textView.numberOfLines,
let config = SkeletonMultilinesLayerConfig(lines: textView.estimatedNumberOfLines,
lineHeight: textView.estimatedLineHeight,
type: type,
lastLineFillPercent: textView.lastLineFillingPercent,
@@ -91,7 +91,7 @@ struct SkeletonLayer {
var holderAsTextView: SkeletonTextNode? {
guard let textView = holder as? SkeletonTextNode,
(textView.numberOfLines == -1 || textView.numberOfLines == 0 || textView.numberOfLines > 1 || textView.numberOfLines == 1 && !SkeletonAppearance.default.renderSingleLineAsView) else {
(textView.estimatedNumberOfLines == -1 || textView.estimatedNumberOfLines == 0 || textView.estimatedNumberOfLines > 1 || textView.estimatedNumberOfLines == 1 && !SkeletonAppearance.default.renderSingleLineAsView) else {
return nil
}
return textView
@@ -47,14 +47,14 @@ extension UIView: Recoverable {
extension UILabel {
var labelState: RecoverableTextViewState? {
get { return ao_get(pkey: &ViewAssociatedKeys.labelViewState) as? RecoverableTextViewState }
var labelState: RecoverableLabelState? {
get { return ao_get(pkey: &ViewAssociatedKeys.labelViewState) as? RecoverableLabelState }
set { ao_setOptional(newValue, pkey: &ViewAssociatedKeys.labelViewState) }
}
override func saveViewState() {
super.saveViewState()
labelState = RecoverableTextViewState(view: self)
labelState = RecoverableLabelState(view: self)
}
override func recoverViewState(forced: Bool) {
@@ -70,6 +70,11 @@ extension UILabel {
if self.textColor == .clear || forced {
self.textColor = storedLabelState.textColor
if let attributedText = storedLabelState.attributedText {
self.attributedText = attributedText
} else {
self.text = storedLabelState.text
}
}
}
}
@@ -95,6 +100,9 @@ extension UITextView {
if self?.textColor == .clear || forced {
self?.textColor = storedLabelState.textColor
if let attributedText = storedLabelState.attributedText {
self?.attributedText = attributedText
}
}
}
}
@@ -120,6 +128,9 @@ extension UITextField {
if self?.textColor == .clear || forced {
self?.textColor = storedLabelState.textColor
if let attributedText = storedLabelState.attributedText {
self?.attributedText = attributedText
}
}
if self?.placeholder == nil || forced {
@@ -17,7 +17,7 @@ protocol SkeletonTextNode {
var textLineHeight: SkeletonTextLineHeight { get }
var estimatedLineHeight: CGFloat { get }
var numberOfLines: Int { get }
var estimatedNumberOfLines: Int { get }
var textAlignment: NSTextAlignment { get }
var lastLineFillingPercent: Int { get }
var multilineCornerRadius: Int { get }
@@ -35,6 +35,7 @@ enum SkeletonTextNodeAssociatedKeys {
static var paddingInsets = "paddingInsets"
static var backupHeightConstraints = "backupHeightConstraints"
static var textLineHeight = "textLineHeight"
static var skeletonNumberOfLines = "skeletonNumberOfLines"
}
@@ -48,11 +49,11 @@ extension UILabel: SkeletonTextNode {
return fontLineHeight ?? SkeletonAppearance.default.multilineHeight
case .relativeToConstraints:
guard let constraintsLineHeight = heightConstraints.first?.constant,
numberOfLines != 0 else {
estimatedNumberOfLines != 0 else {
return SkeletonAppearance.default.multilineHeight
}
return constraintsLineHeight / CGFloat(numberOfLines)
return constraintsLineHeight / CGFloat(estimatedNumberOfLines)
}
}
@@ -61,6 +62,20 @@ extension UILabel: SkeletonTextNode {
set { ao_set(newValue, pkey: &SkeletonTextNodeAssociatedKeys.textLineHeight) }
}
var skeletonNumberOfLines: SkeletonTextNumberOfLines {
get { return ao_get(pkey: &SkeletonTextNodeAssociatedKeys.skeletonNumberOfLines) as? SkeletonTextNumberOfLines ?? SkeletonTextNumberOfLines.inherited }
set { ao_set(newValue, pkey: &SkeletonTextNodeAssociatedKeys.skeletonNumberOfLines) }
}
var estimatedNumberOfLines: Int {
switch skeletonNumberOfLines {
case .inherited:
return numberOfLines
case .custom(let lines):
return lines >= 0 ? lines : 1
}
}
var lastLineFillingPercent: Int {
get { return ao_get(pkey: &SkeletonTextNodeAssociatedKeys.lastLineFillingPercent) as? Int ?? SkeletonAppearance.default.multilineLastLineFillPercent }
set { ao_set(newValue, pkey: &SkeletonTextNodeAssociatedKeys.lastLineFillingPercent) }
@@ -91,9 +106,11 @@ extension UILabel: SkeletonTextNode {
}
var fontLineHeight: CGFloat? {
if let attributes = attributedText?.attributes(at: 0, effectiveRange: nil),
let fontAttribute = attributes.first(where: { $0.key == .font }) {
return fontAttribute.value as? CGFloat ?? font.lineHeight
if let attributedText = attributedText,
attributedText.length > 0 {
let attributes = attributedText.attributes(at: 0, effectiveRange: nil)
let fontAttribute = attributes.first(where: { $0.key == .font })
return fontAttribute?.value as? CGFloat ?? font.lineHeight
} else {
return font.lineHeight
}
@@ -119,8 +136,18 @@ extension UITextView: SkeletonTextNode {
set { ao_set(newValue, pkey: &SkeletonTextNodeAssociatedKeys.textLineHeight) }
}
var numberOfLines: Int {
-1
var skeletonNumberOfLines: SkeletonTextNumberOfLines {
get { return ao_get(pkey: &SkeletonTextNodeAssociatedKeys.skeletonNumberOfLines) as? SkeletonTextNumberOfLines ?? SkeletonTextNumberOfLines.inherited }
set { ao_set(newValue, pkey: &SkeletonTextNodeAssociatedKeys.skeletonNumberOfLines) }
}
var estimatedNumberOfLines: Int {
switch skeletonNumberOfLines {
case .inherited:
return -1
case .custom(let lines):
return lines >= -1 ? lines : 1
}
}
var lastLineFillingPercent: Int {
@@ -154,9 +181,11 @@ extension UITextView: SkeletonTextNode {
}
var fontLineHeight: CGFloat? {
if let attributes = attributedText?.attributes(at: 0, effectiveRange: nil),
let fontAttribute = attributes.first(where: { $0.key == .font }) {
return fontAttribute.value as? CGFloat ?? font?.lineHeight
if let attributedText = attributedText,
attributedText.length > 0 {
let attributes = attributedText.attributes(at: 0, effectiveRange: nil)
let fontAttribute = attributes.first(where: { $0.key == .font })
return fontAttribute?.value as? CGFloat ?? font?.lineHeight
} else {
return font?.lineHeight
}
@@ -21,7 +21,18 @@ extension UITableView {
// 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
var result = [UIView]()
for subview in subviews {
if String(describing: type(of: subview)) == "UITableViewWrapperView" {
result.append(contentsOf: subview.subviews)
} else {
result.append(subview)
}
}
return result
}
}
@@ -21,15 +21,15 @@ class SkeletonFlowHandler: SkeletonFlowDelegate {
func didShowSkeletons(rootView: UIView) {
skeletonLog(rootView.sk.skeletonTreeDescription)
NotificationCenter.default.post(name: .skeletonWillAppearNotification, object: rootView, userInfo: nil)
NotificationCenter.default.post(name: .skeletonDidAppearNotification, object: rootView, userInfo: nil)
}
func willBeginUpdatingSkeletons(rootView: UIView) {
NotificationCenter.default.post(name: .skeletonWillAppearNotification, object: rootView, userInfo: nil)
NotificationCenter.default.post(name: .skeletonWillUpdateNotification, object: rootView, userInfo: nil)
}
func didUpdateSkeletons(rootView: UIView) {
NotificationCenter.default.post(name: .skeletonWillAppearNotification, object: rootView, userInfo: nil)
NotificationCenter.default.post(name: .skeletonDidUpdateNotification, object: rootView, userInfo: nil)
}
func willBeginLayingSkeletonsIfNeeded(rootView: UIView) {
@@ -15,11 +15,15 @@ import UIKit
extension CAGradientLayer {
override func tint(withColors colors: [UIColor]) {
override func tint(withColors colors: [UIColor], traitCollection: UITraitCollection?) {
skeletonSublayers.recursiveSearch(leafBlock: {
self.colors = colors.map { $0.cgColor }
if #available(iOS 13.0, tvOS 13, *), let traitCollection = traitCollection {
self.colors = colors.map { $0.resolvedColor(with: traitCollection).cgColor }
} else {
self.colors = colors.map { $0.cgColor }
}
}) {
$0.tint(withColors: colors)
$0.tint(withColors: colors, traitCollection: traitCollection)
}
}
@@ -35,11 +39,15 @@ extension CALayer {
return sublayers?.filter { $0.name == Constants.skeletonSubLayersName } ?? [CALayer]()
}
@objc func tint(withColors colors: [UIColor]) {
@objc func tint(withColors colors: [UIColor], traitCollection: UITraitCollection?) {
skeletonSublayers.recursiveSearch(leafBlock: {
backgroundColor = colors.first?.cgColor
if #available(iOS 13.0, tvOS 13, *), let traitCollection = traitCollection {
backgroundColor = colors.first?.resolvedColor(with: traitCollection).cgColor
} else {
backgroundColor = colors.first?.cgColor
}
}) {
$0.tint(withColors: colors)
$0.tint(withColors: colors, traitCollection: traitCollection)
}
}
@@ -78,7 +86,7 @@ extension CALayer {
insertSublayer(sublayer.contentLayer, at: index)
switch transition {
case .none:
completion?()
DispatchQueue.main.async { completion?() }
case .crossDissolve(let duration):
sublayer.contentLayer.setOpacity(from: 0, to: 1, duration: duration, completion: completion)
}
@@ -88,15 +96,16 @@ extension CALayer {
private extension CALayer {
func alignLayerFrame(_ rect: CGRect, alignment: NSTextAlignment, isRTL: Bool) -> CGRect {
func alignLayerFrame(_ rect: CGRect, paddingInsets: UIEdgeInsets, alignment: NSTextAlignment, isRTL: Bool) -> CGRect {
var newRect = rect
let superlayerWidth = (superlayer?.bounds.width ?? 0)
switch alignment {
case .natural where isRTL,
.right:
newRect.origin.x = (superlayer?.bounds.width ?? 0) - rect.origin.x - rect.width
newRect.origin.x = superlayerWidth - rect.width - paddingInsets.right
case .center:
newRect.origin.x = rect.origin.x + ((superlayer?.bounds.width ?? 0) - rect.width) / 2
newRect.origin.x = (superlayerWidth + paddingInsets.left - paddingInsets.right - rect.width) / 2
case .natural, .left, .justified:
break
@unknown default:
@@ -211,7 +220,7 @@ extension CALayer {
height: size.height)
if index == totalLines - 1 {
frame = alignLayerFrame(newFrame, alignment: alignment, isRTL: isRTL)
frame = alignLayerFrame(newFrame, paddingInsets: paddingInsets, alignment: alignment, isRTL: isRTL)
} else {
frame = newFrame
}
@@ -43,7 +43,13 @@ public extension UIColor {
}
var complementaryColor: UIColor {
isLight ? darker : lighter
if #available(iOS 13, tvOS 13, *) {
return UIColor { _ in
self.isLight ? self.darker : self.lighter
}
} else {
return isLight ? darker : lighter
}
}
var lighter: UIColor {
@@ -16,18 +16,18 @@ import UIKit
extension UILabel {
var desiredHeightBasedOnNumberOfLines: CGFloat {
let spaceNeededForEachLine = estimatedLineHeight * CGFloat(numberOfLines)
let spaceNeededForSpaces = skeletonLineSpacing * CGFloat(numberOfLines - 1)
let spaceNeededForEachLine = estimatedLineHeight * CGFloat(estimatedNumberOfLines)
let spaceNeededForSpaces = skeletonLineSpacing * CGFloat(estimatedNumberOfLines - 1)
let padding = paddingInsets.top + paddingInsets.bottom
return spaceNeededForEachLine + spaceNeededForSpaces + padding
}
func updateHeightConstraintsIfNeeded() {
guard numberOfLines > 1 || numberOfLines == 0 else { return }
guard estimatedNumberOfLines > 1 || estimatedNumberOfLines == 0 else { return }
// Workaround to simulate content when the label is contained in a `UIStackView`.
if isSuperviewAStackView, bounds.height == 0 {
if isSuperviewAStackView, bounds.height == 0, (text?.isEmpty ?? true) {
// 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 = " "
}
@@ -22,11 +22,11 @@ extension UIView {
func startObservingAppLifecycleNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: .applicationDidBecomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: .applicationDidEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willTerminateNotification), name: .applicationDidEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willTerminateNotification), name: .applicationWillTerminateNotification, object: nil)
}
func stopObservingAppLifecycleNotications() {
NotificationCenter.default.removeObserver(self, name: .applicationDidEnterForegroundNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: .applicationDidBecomeActiveNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: .applicationDidEnterForegroundNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: .applicationWillTerminateNotification, object: nil)
}
@@ -88,7 +88,7 @@ extension UIView {
}
var _skeletonableCornerRadius: Float {
get { return ao_get(pkey: &ViewAssociatedKeys.skeletonCornerRadius) as? Float ?? 0.0 }
get { return ao_get(pkey: &ViewAssociatedKeys.skeletonCornerRadius) as? Float ?? SkeletonViewAppearance.shared.skeletonCornerRadius }
set { ao_set(newValue, pkey: &ViewAssociatedKeys.skeletonCornerRadius) }
}
}
@@ -15,16 +15,30 @@ import UIKit
extension UIView {
func showSkeleton(skeletonConfig config: SkeletonConfig) {
func showSkeleton(
skeletonConfig config: SkeletonConfig,
notifyDelegate: Bool = true
) {
_isSkeletonAnimated = config.animated
_flowDelegate = SkeletonFlowHandler()
_flowDelegate?.willBeginShowingSkeletons(rootView: self)
if notifyDelegate {
_flowDelegate = SkeletonFlowHandler()
_flowDelegate?.willBeginShowingSkeletons(rootView: self)
}
recursiveShowSkeleton(skeletonConfig: config, root: self)
}
func updateSkeleton(skeletonConfig config: SkeletonConfig) {
func updateSkeleton(
skeletonConfig config: SkeletonConfig,
notifyDelegate: Bool = true
) {
_isSkeletonAnimated = config.animated
_flowDelegate?.willBeginUpdatingSkeletons(rootView: self)
if notifyDelegate {
_flowDelegate?.willBeginUpdatingSkeletons(rootView: self)
}
recursiveUpdateSkeleton(skeletonConfig: config, root: self)
}
+671
View File
@@ -0,0 +1,671 @@
![](../Assets/header2.jpg)
<p align="center">
<a href="https://github.com/Juanpe/SkeletonView/actions?query=workflow%3ACI">
<img src="https://github.com/Juanpe/SkeletonView/workflows/CI/badge.svg">
</a>
<a href="https://codebeat.co/projects/github-com-juanpe-skeletonview-main"><img alt="codebeat badge" src="https://codebeat.co/badges/1f37bbab-a1c8-4a4a-94d7-f21740d461e9" /></a>
<a href="https://cocoapods.org/pods/SkeletonView"><img src="https://img.shields.io/cocoapods/v/SkeletonView.svg?style=flat"></a>
<a href="https://github.com/Carthage/Carthage/"><img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat"></a>
<a href="https://swift.org/package-manager/"><img src="https://img.shields.io/badge/SPM-supported-Green.svg?style=flat"></a>
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FJuanpe%2FSkeletonView%2Fbadge%3Ftype%3Dplatforms"/>
<a href="https://badge.bow-swift.io/recipe?name=SkeletonView&description=An%20elegant%20way%20to%20show%20users%20that%20something%20is%20happening%20and%20also%20prepare%20them%20to%20which%20contents%20he%20is%20waiting&url=https://github.com/juanpe/skeletonview&owner=Juanpe&avatar=https://avatars0.githubusercontent.com/u/1409041?v=4&tag=1.20.0"><img src="https://raw.githubusercontent.com/bow-swift/bow-art/master/badges/nef-playgrounds-badge.svg" alt="SkeletonView Playground" style="height:20px"></a>
</p>
<p align="center">
<a href="#-funktionen">Funktionen</a>
• <a href="#-anleitungen">Anleitungen</a>
• <a href="#-installation">Installation</a>
• <a href="#-verwendung">Verwendung</a>
• <a href="#-sonstiges">Sonstiges</a>
• <a href="#-beitragen">Beitragen</a>
</p>
**🌎 README ist auch in anderen Sprachen verfügbar: [🇬🇧](../README.md) . [🇪🇸](README_es.md) . [🇨🇳](README_zh.md) . [🇧🇷](README_pt-br.md) . [🇰🇷](README_ko.md) . [🇫🇷](README_fr.md)**
Heutzutage haben fast alle Anwendungen async-Prozesse, z.B. API-Anfragen, lang laufende Prozesse, usw. Während die Prozesse arbeiten, platzieren die Entwickler in der Regel eine Ladeansicht, um den Benutzern zu zeigen, dass im Hintergrund etwas vor sich geht.
**SkeletonView** wurde entwickelt, um dieses Bedürfnis zu befriedigen, indem auf eine elegante Art und Weise den Nutzern gezeigt wird, dass etwas passiert und sie gleichzeitig darauf vorbereitet, welche Inhalte sie erwarten.
Viel Spaß damit! 🙂
##
- [🌟 Funktionen](#-funktionen)
- [🎬 Anleitungen](#-anleitungen)
- [📲 Installation](#-installation)
- [🐒 Verwendung](#-verwendung)
- [🌿 Sammlungen](#-sammlungen)
- [🔠 Texte](#-texte)
- [🦋 Erscheinungsbild](#-erscheinungsbild)
- [🎨 Benutzerdefinierte Farben](#-benutzerdefinierte-farben)
- [🏃‍♀️ Animationen](#-animationen)
- [🏄 Übergänge](#-übergänge)
- [✨ Sonstiges](#-sonstiges)
- [❤️ Beitragen](#-beitragen)
- [📢 Erwähnungen](#-erwähnungen)
- [🏆 Sponsoren](#-sponsoren)
- [👨🏻‍💻 Autor](#-autor)
- [👮🏻 Lizenz](#-lizenz)
## 🌟 Funktionen
* Einfach zu benutzen
* Alle UIViews sind skelettierbar
* Vollständig anpassbar
* Universal (iPhone & iPad)
* Freundlicher interface builder
* Einfache Swift-Syntax
* Leicht lesbarer code
## 🎬 Anleitungen
| [![](https://img.youtube.com/vi/75kgOhWsPNA/maxresdefault.jpg)](https://youtu.be/75kgOhWsPNA)|[![](https://img.youtube.com/vi/MVCiM_VdxVA/maxresdefault.jpg)](https://youtu.be/MVCiM_VdxVA)|[![](https://img.youtube.com/vi/Qq3Evspeea8/maxresdefault.jpg)](https://youtu.be/Qq3Evspeea8)|[![](https://img.youtube.com/vi/Zx1Pg1gPfxA/maxresdefault.jpg)](https://www.youtube.com/watch?v=Zx1Pg1gPfxA)
|:---: | :---: | :---: | :---:
|[**SkeletonView Guides - Getting started**](https://youtu.be/75kgOhWsPNA)|[**How to Create Loading View with Skeleton View in Swift 5.2**](https://youtu.be/MVCiM_VdxVA) by iKh4ever Studio|[**Create Skeleton Loading View in App (Swift 5) - Xcode 11, 2020**](https://youtu.be/Qq3Evspeea8) by iOS Academy| [**Cómo crear una ANIMACIÓN de CARGA de DATOS en iOS**](https://www.youtube.com/watch?v=Zx1Pg1gPfxA) by MoureDev
## 📲 Installation
* [CocoaPods](https://guides.cocoapods.org/using/using-cocoapods.html):
```ruby
pod 'SkeletonView'
```
* [Carthage](https://github.com/Carthage/Carthage):
```ruby
github "Juanpe/SkeletonView"
```
* [Swift Package Manager](https://swift.org/package-manager/):
```swift
dependencies: [
.package(url: "https://github.com/Juanpe/SkeletonView.git", from: "1.7.0")
]
```
> 📣 **WICHTIG!**
>
> Seit Version 1.30.0 unterstützt `SkeletonView` **XCFrameworks**, wenn du es also als **XCFramework** installieren möchtest, verwende bitte stattdessen [dieses Repo](https://github.com/Juanpe/SkeletonView-XCFramework.git).
## 🐒 Verwendung
Nur **3** Schritte sind erforderlich, um `SkeletonView` zu verwenden:
1️⃣ Importiere SkeletonView an der richtigen Stelle.
```swift
import SkeletonView
```
2️⃣ Lege nun fest, welche Ansichten `skelettierbar` sein sollen. Dies kannst du auf zwei Arten erreichen:
**Durch code:**
```swift
avatarImageView.isSkeletonable = true
```
**Durch IB/Storyboards:**
![](../Assets/storyboard.png)
3️⃣ Sobald du die Views eingestellt hast, kannst du das **Skelett** anzeigen. Dazu hast du **4** Auswahlmöglichkeiten:
```swift
(1) view.showSkeleton() // Einfarbig
(2) view.showGradientSkeleton() // Farbverlauf
(3) view.showAnimatedSkeleton() // Einfarbig animiert
(4) view.showAnimatedGradientSkeleton() // Farbverlauf animiert
```
**Vorschau**
<table>
<tr>
<td width="25%">
<center>Einfarbig</center>
</td>
<td width="25%">
<center>Farbverlauf</center>
</td>
<td width="25%">
<center>Einfarbig animiert</center>
</td>
<td width="25%">
<center>Farbverlauf animiert</center>
</td>
</tr>
<tr>
<td width="25%">
<img src="../Assets/solid.png"></img>
</td>
<td width="25%">
<img src="../Assets/gradient.png"></img>
</td>
<td width="25%">
<img src="../Assets/solid_animated.gif"></img>
</td>
<td width="25%">
<img src="../Assets/gradient_animated.gif"></img>
</td>
</tr>
</table>
> 📣 **WICHTIG!**
>
> `SkeletonView` ist rekursiv, wenn du also das Skelett in allen skelettierbaren Views anzeigen willst, musst du nur die show-Methode in der Haupt-Container-View aufrufen. Zum Beispiel mit `UIViewControllers`.
### 🌿 Sammlungen
```SkeletonView``` ist kompatibel mit ```UITableView``` und ```UICollectionView```.
**UITableView**
Wenn du das Skelett in ```UITableView```'s anzeigen willst, müssen diese dem ```SkeletonTableViewDataSource```-Protokoll entsprechen.
``` swift
public protocol SkeletonTableViewDataSource: UITableViewDataSource {
func numSections(in collectionSkeletonView: UITableView) -> Int // Standard: 1
func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int
func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier
func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell? // Standard: nil
func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath)
}
```
Wie du sehen kannst, erbt dieses Protokoll von ```UITableViewDataSource```, so dass du dieses Protokoll durch das Skelettprotokoll ersetzen kannst.
Dieses Protokoll hat eine Standardimplementierung für einige Methoden. Zum Beispiel wird die Anzahl der Zeilen für jeden Abschnitt in Echtzeit berechnet:
``` swift
func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int
// Standard:
// Es wird berechnet, wie viele Zellen benötigt werden, um die gesamte Tabellenansicht zu füllen
```
> 📣 **WICHTIG!**
>
> Wenn du in der obigen Methode `UITableView.automaticNumberOfSkeletonRows` zurückgibst, verhält es sich wie das Standardverhalten (d.h. es wird berechnet, wie viele Zellen benötigt werden, um den gesamten Tableview zu füllen).
Es gibt nur eine Methode, die du implementieren musst, damit Skeleton die Zellen ID kennt. Diese Methode hat keine Standardimplementierung:
``` swift
func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier {
return "CellIdentifier"
}
```
Standardmäßig entfernt die library die Zellen aus jedem indexPath, aber du kannst dies auch tun, wenn du einige Änderungen vornehmen möchtest, bevor das Skelett erscheint:
``` swift
func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell? {
let cell = skeletonView.dequeueReusableCell(withIdentifier: "CellIdentifier", for: indexPath) as? Cell
cell?.textField.isHidden = indexPath.row == 0
return cell
}
```
Wenn du es vorziehst, den deque-Teil der Bibliothek zu überlassen, kannst du die Zelle mit dieser Methode konfigurieren:
``` swift
func collectionSkeletonView(_ skeletonView: UITableView, prepareCellForSkeleton cell: UITableViewCell, at indexPath: IndexPath) {
let cell = cell as? Cell
cell?.textField.isHidden = indexPath.row == 0
}
```
Außerdem kannst du sowohl die Kopf- als auch die Fußzeilen skelettieren. Diese müssen nur dem Protokoll "SkeletonTableViewDelegate" entsprechen.
```swift
public protocol SkeletonTableViewDelegate: UITableViewDelegate {
func collectionSkeletonView(_ skeletonView: UITableView, identifierForHeaderInSection section: Int) -> ReusableHeaderFooterIdentifier? // standard: nil
func collectionSkeletonView(_ skeletonView: UITableView, identifierForFooterInSection section: Int) -> ReusableHeaderFooterIdentifier? // standard: nil
}
```
> 📣 **WICHTIG!**
>
> 1️⃣ Wenn du größenvariable Zellen verwendest (**`tableView.rowHeight = UITableViewAutomaticDimension`**), ist es zwingend erforderlich, die **`estimatedRowHeight`** zu definieren.
>
> 2️⃣ Wenn man Elemente in einer **`UITableViewCell`** hinzufügt, sollte man sie dem **`contentView`** hinzufügen und nicht direkt in der Zelle.
>
> ```swift
> self.contentView.addSubview(titleLabel) ✅
> self.addSubview(titleLabel) ❌
> ```
**UICollectionView**
Für `UICollectionView` musst du dem Protokoll `SkeletonCollectionViewDataSource` entsprechen.
``` swift
public protocol SkeletonCollectionViewDataSource: UICollectionViewDataSource {
func numSections(in collectionSkeletonView: UICollectionView) -> Int // standard: 1
func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int
func collectionSkeletonView(_ skeletonView: UICollectionView, cellIdentifierForItemAt indexPath: IndexPath) -> ReusableCellIdentifier
func collectionSkeletonView(_ skeletonView: UICollectionView, supplementaryViewIdentifierOfKind: String, at indexPath: IndexPath) -> ReusableCellIdentifier? // standard: nil
func collectionSkeletonView(_ skeletonView: UICollectionView, skeletonCellForItemAt indexPath: IndexPath) -> UICollectionViewCell? // standard: nil
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareCellForSkeleton cell: UICollectionViewCell, at indexPath: IndexPath)
func collectionSkeletonView(_ skeletonView: UICollectionView, prepareViewForSkeleton view: UICollectionReusableView, at indexPath: IndexPath)
}
```
Der Rest des Prozesses ist derselbe wie bei ```UITableView```
### 🔠 Texte
![](../Assets/multilines2.png)
Wenn Elemente mit Text verwendet werden, zeichnet ```SkeletonView``` Linien, um Text zu simulieren.
Du kannst einige Variablen für mehrzeilige Elemente einstellen.
| Variable | Typ | Standard | Vorschau
| ------- | ------- |------- | -------
| **lastLineFillPercent** | `CGFloat` | `70`| ![](../Assets/multiline_lastline.png)
| **linesCornerRadius** | `Int` | `0` | ![](../Assets/multiline_corner.png)
| **skeletonLineSpacing** | `CGFloat` | `10` | ![](../Assets/multiline_lineSpacing.png)
| **skeletonPaddingInsets** | `UIEdgeInsets` | `.zero` | ![](../Assets/multiline_insets.png)
| **skeletonTextLineHeight** | `SkeletonTextLineHeight` | `.fixed(15)` | ![](../Assets/multiline_lineHeight.png)
| **skeletonTextNumberOfLines** | `SkeletonTextNumberOfLines` | `.inherited` | ![](../Assets/multiline_corner.png)
<br />
Um den Prozentsatz oder den Radius **mit Hilfe von Code** zu ändern, lege diese Variablen fest:
```swift
descriptionTextView.lastLineFillPercent = 50
descriptionTextView.linesCornerRadius = 5
```
Oder, wenn du es vorziehst, verwende **IB/Storyboard**:
![](../Assets/multiline_customize.png)
<br />
**Wie kann die Anzahl der Zeilen festgelegt werden?**
Standardmäßig entspricht die Anzahl der Linien dem Wert der Variable `numberOfLines`. Und wenn es auf **null** gesetzt ist, wird berechnet, wie viele Linien benötigt werden, um das gesamte Skelett zu füllen und es zu zeichnen.
Wenn du jedoch eine bestimmte Anzahl von Zeilen für das Skelett festlegen möchtest, kannst du dies mit der Variable `skeletonTextNumberOfLines` tun. Diese Variable hat zwei mögliche Werte: `inherited`, der den Wert `numberOfLines` zurückgibt, und `custom(Int)`, der die spezifische Anzahl von Zeilen zurückgibt, die als zugehöriger Wert angegeben wurde.
Zum Beispiel:
```swift
label.skeletonTextNumberOfLines = 3 // .custom(3)
```
<br />
> **⚠️ VERALTET!**
>
> **useFontLineHeight** wurde abgeschafft. Du kannst stattdessen **skeletonTextLineHeight** verwenden:
>
> ```swift
> descriptionTextView.skeletonTextLineHeight = .relativeToFont
> ```
> **📣 WICHTIG!**
>
> Bitte beachte, dass bei Ansichten ohne mehrere Zeilen die einzelne Zeile
> als letzte Zeile betrachtet wird.
### 🦋 Erscheinungsbild
Die Skelette haben ein Standardaussehen. Wenn du also die Farbe, den Farbverlauf oder Mehrlinien-Eigenschaften nicht angibst, verwendet `SkeletonView` die Standardwerte.
Standardwerte:
- **tintColor**: `UIColor`
- *standard: `.skeletonDefault` (gleich wie `.clouds`, aber anpassungsfähig an den dunklen Modus)*
- **gradient**: SkeletonGradient
- *standard: `SkeletonGradient(baseColor: .skeletonDefault)`*
- **multilineHeight**: `CGFloat`
- *standard: 15*
- **multilineSpacing**: `CGFloat`
- *standard: 10*
- **multilineLastLineFillPercent**: `Int`
- *standard: 70*
- **multilineCornerRadius**: `Int`
- *standard: 0*
- **skeletonCornerRadius**: `CGFloat` (IBInspectable)(Macht ihre Skelettansicht mit Ecken)
- *standard: 0*
Um diese Standardwerte zu erhalten, kannst du `SkeletonAppearance.default` verwenden. Mit dieser Variable kannst du auch die Werte einstellen:
```swift
SkeletonAppearance.default.multilineHeight = 20
SkeletonAppearance.default.tintColor = .green
```
> **⚠️ VERALTET!**
>
> **useFontLineHeight** wurde abgeschafft. Du kannst stattdessen **textLineHeight** verwenden:
>
> ```swift
> SkeletonAppearance.default.textLineHeight = .relativeToFont
> ```
### 🎨 Benutzerdefinierte Farben
Du kannst entscheiden, mit welcher Farbe das Skelett eingefärbt wird. Du brauchst nur die gewünschte Farbe oder den gewünschten Farbverlauf als Parameter zu übergeben.
**Verwendung von Volltonfarben**
```swift
view.showSkeleton(usingColor: UIColor.gray) // Einfarbig
// oder
view.showSkeleton(usingColor: UIColor(red: 25.0, green: 30.0, blue: 255.0, alpha: 1.0))
```
**Verwendung von Farbverläufen**
```swift
let gradient = SkeletonGradient(baseColor: UIColor.midnightBlue)
view.showGradientSkeleton(usingGradient: gradient) // Farbverlauf
```
Außerdem bietet **SkeletonView** 20 flache Farben 🤙🏼.
```UIColor.turquoise, UIColor.greenSea, UIColor.sunFlower, UIColor.flatOrange ...```
![](../Assets/flatcolors.png)
###### Bild von der Website [https://flatuicolors.com](https://flatuicolors.com) entnommen
### 🏃‍♀️ Animationen
**SkeletonView** hat zwei eingebaute Animationen, *pulse* für einfarbige Skelette und *sliding* für Farbverläufe.
Außerdem ist es sehr einfach, eine eigene Skelettanimationen zu erstellen.
Skeleton bietet die Funktion `showAnimatedSkeleton`, die eine Closure ```SkeletonLayerAnimation``` besitzt, in der du deine eigene Animation definieren kannst.
```swift
public typealias SkeletonLayerAnimation = (CALayer) -> CAAnimation
```
Du kannst die Funktion wie folgt aufrufen:
```swift
view.showAnimatedSkeleton { (layer) -> CAAnimation in
let animation = CAAnimation()
// Passe hier ihre Animation an
return animation
}
```
Es ist ein ```SkeletonAnimationBuilder``` verfügbar. Es ist ein Builder um ```SkeletonLayerAnimation``` zu erstellen.
Heute kann man **Gleitanimationen** für Farbverläufe erstellen, indem man die **Richtung** und die **Dauer** der Animation festlegt (Standard = 1,5s).
```swift
// func makeSlidingAnimation(withDirection direction: GradientDirection, duration: CFTimeInterval = 1.5) -> SkeletonLayerAnimation
let animation = SkeletonAnimationBuilder().makeSlidingAnimation(withDirection: .leftToRight)
view.showAnimatedGradientSkeleton(usingGradient: gradient, animation: animation)
```
```GradientDirection``` ist ein enum, mit den folgenden cases:
| Richtung | Vorschau
|------- | -------
| .leftRight | ![](../Assets/sliding_left_to_right.gif)
| .rightLeft | ![](../Assets/sliding_right_to_left.gif)
| .topBottom | ![](../Assets/sliding_top_to_bottom.gif)
| .bottomTop | ![](../Assets/sliding_bottom_to_top.gif)
| .topLeftBottomRight | ![](../Assets/sliding_topLeft_to_bottomRight.gif)
| .bottomRightTopLeft | ![](../Assets/sliding_bottomRight_to_topLeft.gif)
> **😉 TRICK!**
>
> Es gibt noch eine andere Möglichkeit, Schiebeanimationen zu erstellen, indem man einfach diese Abkürzung benutzt:
>
> ```swift
> let animation = GradientDirection.leftToRight.slidingAnimation()
> ```
### 🏄 Übergänge
**SkeletonView** hat eingebaute Übergänge, um die Skelette auf eine *ruhigere* Weise **ein- und auszublenden** 🤙.
Um den Übergang zu benutzen, füge einfach den Parameter ```transition``` zu ihrer Funktion ```showSkeleton()``` oder ```hideSkeleton()``` mit der Übergangszeit hinzu, wie hier:
```swift
view.showSkeleton(transition: .crossDissolve(0.25)) //Einblenden des Skeleton mit Querauflösen-Übergang mit 0,25 Sekunden Übergangszeit
view.hideSkeleton(transition: .crossDissolve(0.25)) //Ausblenden des Skeleton mit Querauflösen-Übergang mit 0,25 Sekunden Übergangszeit
```
Der Standardwert ist `crossDissolve(0.25)`
**Vorschau**
<table>
<tr>
<td width="50%">
<center>Keinen</center>
</td>
<td width="50%">
<center>Querauflösen</center>
</td>
</tr>
<tr>
<td width="50%">
<img src="../Assets/skeleton_transition_nofade.gif"></img>
</td>
<td width="50%">
<img src="../Assets/skeleton_transition_fade.gif"></img>
</td>
</tr>
</table>
## ✨ Sonstiges
**Hierarchie**
Da ```SkeletonView``` rekursiv ist, und wir wollen, dass Skeleton sehr effizient ist, wollen wir die Rekursion so schnell wie möglich beenden. Aus diesem Grund musst du die Container-Ansicht auf `Skeletonable` setzen, denn Skeleton wird aufhören, nach `skeletonable` Unteransichten zu suchen, sobald eine Ansicht nicht skelettierbar ist, und damit die Rekursion beenden.
Denn ein Bild sagt mehr als tausend Worte:
In diesem Beispiel haben wir einen `UIViewController` mit einem `ContainerView` und einem `UITableView`. Wenn der View fertig ist, zeigen wir das Skelett mit dieser Methode:
```
view.showSkeleton()
```
> ```isSkeletonable```= ☠️
| Konfiguration | Ergebnis|
|:-------:|:-------:|
|<img src="../Assets/no_skeletonable.jpg" width="350"/> | <img src="../Assets/no_skeletonables_result.png" width="350"/>|
|<img src="../Assets/container_no_skeletonable.jpg" width="350"/> | <img src="../Assets/no_skeletonables_result.png" width="350"/>|
|<img src="../Assets/container_skeletonable.jpg" width="350"/> | <img src="../Assets/container_skeletonable_result.png" width="350"/>|
|<img src="../Assets/all_skeletonables.jpg" width="350"/>| <img src="../Assets/all_skeletonables_result.png" width="350"/>|
|<img src="../Assets/tableview_no_skeletonable.jpg" width="350"/> | <img src="../Assets/tableview_no_skeletonable_result.png" height="350"/>|
|<img src="../Assets/tableview_skeletonable.jpg" width="350"/> | <img src="../Assets/tableview_skeletonable_result.png" height="350"/>|
**Skelettansichten-Layout**
Manchmal kann es vorkommen, dass das Skelett-Layout nicht zu ihrem Layout passt, weil sich die Grenzen der übergeordneten Ansicht geändert haben. ~Zum Beispiel, wenn du das Gerät drehst.
Du kannst die Skelettansichten wie folgt neu anordnen:
```swift
override func viewDidLayoutSubviews() {
view.layoutSkeletonIfNeeded()
}
```
> 📣 **WICHTIG!**
>
> Du solltest diese Methode nicht aufrufen. Ab **Version 1.8.1** brauchst du diese Methode nicht mehr aufzurufen, die Bibliothek macht das automatisch. Du kannst diese Methode also **NUR** in den Fällen verwenden, in denen du das Layout des Skeletts manuell aktualisieren musst.
**Skelett aktualisieren**
Du kannst die Konfiguration des Skeletts jederzeit ändern, wie z.B. seine Farbe, Animation, etc. mit den folgenden Methoden:
```swift
(1) view.updateSkeleton() // Einfarbig
(2) view.updateGradientSkeleton() // Farbverlauf
(3) view.updateAnimatedSkeleton() // Einfarbig animiert
(4) view.updateAnimatedGradientSkeleton() // Farbverlauf animiert
```
**Ausblenden von Ansichten, wenn die Animation beginnt**
Manchmal möchte man einige Ansichten ausblenden, wenn die Animation beginnt. Dafür gibt es eine praktische Variable, die man benutzen kann:
```swift
view.isHiddenWhenSkeletonIsActive = true // Dies funktioniert nur, wenn isSkeletonable = true
```
**Benutzerinteraktion nicht ändern, wenn das Skelett aktiv ist**
Standardmäßig ist die Benutzerinteraktion für skelettierte Elemente deaktiviert, aber wenn du den Indikator für die Benutzerinteraktion nicht ändern willst, wenn das Skelett aktiv ist, kannst du die Variable `isUserInteractionDisabledWhenSkeletonIsActive` verwenden:
```swift
view.isUserInteractionDisabledWhenSkeletonIsActive = false // Die Ansicht wird aktiv sein, wenn das Skelett aktiv ist.
```
**Zeilenhöhe der Schriftart für die Skelettlinien in Labels nicht verwenden**
`False`, um die automatische Anpassung des Skeletts an die Schrifthöhe für ein `UILabel` oder `UITextView` zu deaktivieren. Standardmäßig wird die Höhe der Skelettlinien automatisch an die Schrifthöhe angepasst, um den Text im Label-Rect genauer wiederzugeben, anstatt die Bounding Box zu verwenden.
```swift
label.useFontLineHeight = false
```
**Skelett verzögert anzeigen**
Du kannst die Darstellung des Skeletts verzögern, wenn die Ansichten schnell aktualisiert werden.
```swift
func showSkeleton(usingColor: UIColor,
animated: Bool,
delay: TimeInterval,
transition: SkeletonTransitionStyle)
```
```swift
func showGradientSkeleton(usingGradient: SkeletonGradient,
animated: Bool,
delay: TimeInterval,
transition: SkeletonTransitionStyle)
```
**Debug**
Um die Debug-Aufgaben zu erleichtern, wenn etwas nicht richtig funktioniert, hat **`SkeletonView`** einige neue Werkzeuge.
Erstens, `UIView` hat eine Variable mit seinen Skelett-Informationen zur Verfügung:
```swift
var sk.skeletonTreeDescription: String
```
Außerdem kannst du den neuen **Debug-Modus** aktivieren. Füge einfach die Umgebungsvariable `SKELETON_DEBUG` hinzu um ihn zu aktivieren.
![](../Assets/debug_mode.png)
Wenn das Skelett dann erscheint, kannst du die Ansichtshierarchie in der Xcode-Konsole sehen.
```
{
"type" : "UIView", // UITableView, UILabel...
"isSkeletonable" : true,
"reference" : "0x000000014751ce30",
"children" : [
{
"type" : "UIView",
"isSkeletonable" : true,
"children" : [ ... ],
"reference" : "0x000000014751cfa0"
}
]
}
```
**Unterstützte OS & SDK-Versionen**
- iOS 9.0+
- tvOS 9.0+
- Swift 5.3
## ❤️ Beitragen
Dies ist ein Open-Source-Projekt, du kannst also gerne dazu beitragen. Wie?
- Eröffne ein [issue](https://github.com/Juanpe/SkeletonView/issues/new).
- Sende Feedback über [email](mailto://juanpecatalan.com).
- Schlage deine eigenen Korrekturen und Vorschläge vor und öffne einen Pull Request mit den Änderungen.
Siehe [alle Mitwirkenden](https://github.com/Juanpe/SkeletonView/graphs/contributors)
Für weitere Informationen lies bitte die [contributing guidelines](https://github.com/Juanpe/SkeletonView/blob/main/CONTRIBUTING.md).
## 📢 Erwähnungen
- [iOS Dev Weekly #327](https://iosdevweekly.com/issues/327#start)
- [Hacking with Swift Articles](https://www.hackingwithswift.com/articles/40/skeletonview-makes-loading-content-beautiful)
- [Top 10 Swift Articles November](https://medium.mybridge.co/swift-top-10-articles-for-the-past-month-v-nov-2017-dfed7861cd65)
- [30 Amazing iOS Swift Libraries (v2018)](https://medium.mybridge.co/30-amazing-ios-swift-libraries-for-the-past-year-v-2018-7cf15027eee9)
- [AppCoda Weekly #44](http://digest.appcoda.com/issues/appcoda-weekly-issue-44-81899)
- [iOS Cookies Newsletter #103](https://us11.campaign-archive.com/?u=cd1f3ed33c6527331d82107ba&id=48131a516d)
- [Swift Developments Newsletter #113](https://andybargh.com/swiftdevelopments-113/)
- [iOS Goodies #204](http://ios-goodies.com/post/167557280951/week-204)
- [Swift Weekly #96](http://digest.swiftweekly.com/issues/swift-weekly-issue-96-81759)
- [CocoaControls](https://www.cocoacontrols.com/controls/skeletonview)
- [Awesome iOS Newsletter #74](https://ios.libhunt.com/newsletter/74)
- [Swift News #36](https://www.youtube.com/watch?v=mAGpsQiy6so)
- [Best iOS articles, new tools & more](https://medium.com/flawless-app-stories/best-ios-articles-new-tools-more-fcbe673e10d)
## 🏆 Sponsoren
Open-Source-Projekte leben nicht lange ohne ihre Hilfe. Wenn du **SkeletonView** nützlich findest, ziehe bitte in Betracht, dieses
Projekt zu unterstützen, indem du ein Sponsor wirst.
Werde Sponsor über [GitHub Sponsors] (<https://github.com/sponsors/Juanpe>) :heart:
## 👨🏻‍💻 Autor
[Juanpe Catalán](http://www.twitter.com/JuanpeCatalan)
<a class="bmc-button" target="_blank" href="https://www.buymeacoffee.com/CDou4xtIK"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy me a coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;"><span style="margin-left:5px"></span></a>
## 👮🏻 Lizenz
```
MIT License
Copyright (c) 2017 Juanpe Catalán
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.
```
+1 -1
View File
@@ -18,7 +18,7 @@
• <a href="#-contribuir">Contribuir</a>
</p>
**🌎 README está disponible en estos idiomas: [🇬🇧](../README.md) . [🇨🇳](README_zh.md) . [🇧🇷](README_pt-br.md) . [🇰🇷](README_ko.md) . [🇫🇷](README_fr.md)**
**🌎 README está disponible en estos idiomas: [🇬🇧](../README.md) . [🇨🇳](README_zh.md) . [🇧🇷](README_pt-br.md) . [🇰🇷](README_ko.md) . [🇫🇷](README_fr.md) . [🇩🇪](README_de.md)**
Hoy en día, La mayoría de las apps tiene procesos asíncronos, como peticiones a una API, procesos que tardan mucho tiempo, etc. Mientras estos procesos se están ejecutando, se suele mostrar un aburrido spinner indicando que algo está pasando.
+1 -1
View File
@@ -24,7 +24,7 @@
</p>
**🌎 Traductions: [🇬🇧](../README.md) . [🇨🇳](README_zh.md) . [🇧🇷](README_pt-br.md) . [🇰🇷](README_ko.md) . [🇫🇷](README_fr.md)**
**🌎 Traductions: [🇬🇧](../README.md) . [🇨🇳](README_zh.md) . [🇧🇷](README_pt-br.md) . [🇰🇷](README_ko.md) . [🇫🇷](README_fr.md) . [🇩🇪](README_de.md)**
Aujourd'hui, presque toutes les applications ont des processus asynchrones, tels que les requêtes Api, les processus de longue durée, etc. Et pendant que les processus fonctionnent, les développeurs affichent généralement une vue de chargement pour montrer aux utilisateurs que quelque chose se passe.
+1 -1
View File
@@ -23,7 +23,7 @@
</a>
</p>
**🌎 번역에 도움을 주신분들: [🇬🇧](../README.md) . [🇨🇳](README_zh.md) . [🇧🇷](README_pt-br.md) . [🇰🇷](README_ko.md) . [🇫🇷](README_fr.md)**
**🌎 번역에 도움을 주신분들: [🇬🇧](../README.md) . [🇨🇳](README_zh.md) . [🇧🇷](README_pt-br.md) . [🇰🇷](README_ko.md) . [🇫🇷](README_fr.md) . [🇩🇪](README_de.md)**
오늘날 거의 대부분의 앱들은 비동기 방식의 API 호출을 사용하는 프로세스를 가지고 있습니다.
프로세스가 작동하는동안 개발자들은 작업이 실행되고 있다는것을 사용자들에게 보여주기 위해서 로딩 뷰를 배치합니다.
+1 -1
View File
@@ -23,7 +23,7 @@
</a>
</p>
**🌎 Traduções: [🇬🇧](../README.md) . [🇨🇳](README_zh.md) . [🇧🇷](README_pt-br.md) . [🇰🇷](README_ko.md) . [🇫🇷](README_fr.md)**
**🌎 Traduções: [🇬🇧](../README.md) . [🇨🇳](README_zh.md) . [🇧🇷](README_pt-br.md) . [🇰🇷](README_ko.md) . [🇫🇷](README_fr.md) . [🇩🇪](README_de.md)**
Hoje, quase todos os apps têm processos assíncronos, como requisições de API, processos longos, etc. E enquanto os processos estão ocorrendo, normalmente os desenvolvedores usam uma view que mostra os usuarios que algo está ocorrendo.
+1 -1
View File
@@ -23,7 +23,7 @@
</a>
</p>
**🌎 翻译: [🇬🇧](../README.md) . [🇨🇳](README_zh.md) . [🇧🇷](README_pt-br.md) . [🇰🇷](README_ko.md) . [🇫🇷](README_fr.md)**
**🌎 翻译: [🇬🇧](../README.md) . [🇨🇳](README_zh.md) . [🇧🇷](README_pt-br.md) . [🇰🇷](README_ko.md) . [🇫🇷](README_fr.md) . [🇩🇪](README_de.md)**
今天,几乎所有的应用程序都有异步流程,例如:Api请求、长时间运行的流程等。虽然流程正在运行,但通常开发人员会设置一个加载视图来向用户显示正在发生的事情。
+18 -13
View File
@@ -1,33 +1,38 @@
fastlane documentation
================
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```
```sh
xcode-select --install
```
Install _fastlane_ using
```
[sudo] gem install fastlane -NV
```
or alternatively using `brew install fastlane`
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
### bump_version
```
fastlane bump_version
```sh
[bundle exec] fastlane bump_version
```
### release_current
```sh
[bundle exec] fastlane release_current
```
fastlane release_current
```
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).