diff --git a/README.md b/README.md index 9e1cb04c..78bbc14b 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This repository contains the following frameworks: ```sh cd TIModuleName -nef plaground --name TIModuleName --cocoapods --custom-podfile PlaygroundPodfile +nef playground --name TIModuleName --cocoapods --custom-podfile PlaygroundPodfile ``` See example of `PlaygroundPodfile` in `TIFoundationUtils` @@ -72,7 +72,7 @@ ${SRCROOT}/TIModuleName/TIModuleName.app" ```ruby sources = 'your_sources_expression' - if File.basename(Dir.getwd) == s.name # installing using :path => + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => s.source_files = sources s.exclude_files = s.name + '.app' else diff --git a/TIUIElements/PlaygroundPodfile b/TIUIElements/PlaygroundPodfile new file mode 100644 index 00000000..95938d81 --- /dev/null +++ b/TIUIElements/PlaygroundPodfile @@ -0,0 +1,10 @@ +ENV["DEVELOPMENT_INSTALL"] = "true" + +target 'TIUIElements' do + platform :ios, 11.0 + use_frameworks! + + pod 'TIUIElements', :path => '../../../../TIUIElements/TIUIElements.podspec' + pod 'TIUIKitCore', :path => '../../../../TIUIKitCore/TIUIKitCore.podspec' + pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' +end diff --git a/TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift b/TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift index d96b9a74..d8908764 100644 --- a/TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift +++ b/TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift @@ -92,7 +92,6 @@ open class SkeletonLayer: CAShapeLayer { updateGeometry(viewType: viewType) viewBoundsObservation = viewType.view.observe(\.frame, options: [.new]) { [weak self] view, _ in - view.isHidden = true self?.updateGeometry(viewType: view.viewType) } diff --git a/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift b/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift index 2946332c..307d7df7 100644 --- a/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift +++ b/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift @@ -217,7 +217,6 @@ open class StatefulButton: UIButton { } else { updateAppearance(to: .disabled) } - } private func updateAppearance(to state: State) { diff --git a/TIUIElements/TIUIElements.app/.gitignore b/TIUIElements/TIUIElements.app/.gitignore new file mode 100644 index 00000000..b7fe13ce --- /dev/null +++ b/TIUIElements/TIUIElements.app/.gitignore @@ -0,0 +1,4 @@ +# gitignore nef files +**/build/ +**/nef/ +LICENSE \ No newline at end of file diff --git a/TIUIElements/TIUIElements.app/Contents/Info.plist b/TIUIElements/TIUIElements.app/Contents/Info.plist new file mode 100644 index 00000000..831ea97a --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + launcher + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon + CFBundleIdentifier + com.fortysevendeg.nef + CFBundleInfoDictionaryVersion + 6.0 + CFBundleSupportedPlatforms + + MacOSX + + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + 10.14 + NSHumanReadableCopyright + Copyright © 2019 The nef Authors. All rights reserved. + + diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/.gitignore b/TIUIElements/TIUIElements.app/Contents/MacOS/.gitignore new file mode 100644 index 00000000..18bd1f3b --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/.gitignore @@ -0,0 +1,26 @@ +## gitignore nef files +**/build/ +**/nef/ +LICENSE + +## User data +**/xcuserdata/ +podfile.lock +**.DS_Store + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## CocoaPods +**Pods** + +## Carthage +**Carthage** + +## SPM +.build +.swiftpm +swiftpm diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/Podfile b/TIUIElements/TIUIElements.app/Contents/MacOS/Podfile new file mode 100644 index 00000000..ab33d1bc --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/Podfile @@ -0,0 +1,10 @@ +ENV["DEVELOPMENT_INSTALL"] = "true" + +target 'TIUIElements' do + platform :ios, 11.0 + use_frameworks! + + pod 'TIUIElements', :path => '../../../../TIUIElements/TIUIElements.podspec' + pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' + pod 'TIUIKitCore', :path => '../../../../TIUIKitCore/TIUIKitCore.podspec' +end diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift new file mode 100644 index 00000000..fb8e5596 --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift @@ -0,0 +1,304 @@ +/*: + # Skeletons API + + При импорте _TIUIElements_ вы можете использовать API для показа скелетонов. + + ## Принцип работы + + При использовании методов показа скелетонов: + 1. происходит скрытие всех subview в иерархии той view, на которой был вызван метод + 2. далее происходит проход по view, которые можно сконвертировать в скелетоны (список либо определяется пользователем, либо конвертация будет происходить автоматически), создается `CALayer` типа `SkeletonLayer`, представляющий конвертируемую view + 3. поверх view с которой начался показ, добавляются все созданные `SkeletonLayer` + + > Таким образом скелетоны не модифицируют размеры view и не изменяют ее положение + + ## Как начать пользоваться + + Базовая настройка для показа скелетонов не требуется. `UIView` и `UIViewController` уже имеют все необходимые методы для работы: + - `showSkeletons(viewsToSkeletone:_:)` : используется для показа скелетонов, если была передана конфигурация анимации, то она запустится автоматически. `viewsToSkeletone` - опциональный массив `UIView`, определяющий, какие вью будут конвертироваться в скелетоны. Если nil, то проход будет осуществляться по списку subview + - `hideSkeletons()` : используется для скрытия скелетонов + - `startAnimation()` : используется для старта анимации на скелетонах (если анимания не была сконфигурирована в методе `showSkeletons(viewsToSkeletone:_:)` то ничего не произойдет) + - `stopAnimation()` : используется для остановки анимации на скелетонах + */ +import TIUIKitCore +import TIUIElements +import UIKit + +class CanShowAndHideSkeletons: BaseInitializableViewController { + + private let imageView = UIImageView(image: UIImage(systemName: "apple.logo")) + private let label = UILabel() + private let button = UIButton(type: .custom) + + override func addViews() { + super.addViews() + + view.addSubviews(imageView, label, button) + } + + override func configureLayout() { + super.configureLayout() + + [imageView, label, button] + .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + imageView.heightAnchor.constraint(equalToConstant: 40), + imageView.widthAnchor.constraint(equalToConstant: 40), + imageView.centerYAnchor.constraint(equalTo: label.centerYAnchor), + imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + + label.topAnchor.constraint(equalTo: view.topAnchor), + label.trailingAnchor.constraint(equalTo: view.trailingAnchor), + label.leadingAnchor.constraint(equalTo: imageView.trailingAnchor), + label.heightAnchor.constraint(equalToConstant: 60), + + button.leadingAnchor.constraint(equalTo: view.leadingAnchor), + button.topAnchor.constraint(equalTo: label.bottomAnchor), + button.trailingAnchor.constraint(equalTo: view.trailingAnchor), + button.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + override func bindViews() { + super.bindViews() + + button.addTarget(self, action: #selector(toggleSkeletons), for: .touchUpInside) + } + + override func configureAppearance() { + super.configureAppearance() + + label.text = "Hello from SkeletonableViewController" + button.setTitle("show skeletons", for: .normal) + + let textAttributes = BaseTextAttributes(font: .systemFont(ofSize: 25), color: .black, alignment: .natural, isMultiline: false) + + view.configureUIView(appearance: UIView.DefaultAppearance(backgroundColor: .white)) + + label.configureUILabel(appearance: UILabel.DefaultAppearance.make { + $0.textAttributes = textAttributes + }) + + button.configureUIButton(appearance: UILabel.DefaultAppearance.make { + $0.textAttributes = textAttributes + }) + } + + @objc private func toggleSkeletons() { + // Т.к. передается nil, скелетонится будут все subview (в данном случае view.subview == [button, label, imageView]) + showSkeletons(viewsToSkeletone: nil, .init()) + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { [weak self] in + self?.hideSkeletons() + } + } +} + +/*: + ## Skeletonable + + Если необходимо изменить список конвертируемых в скелетоны view у какой-нибудь из отдельных view в иерархии, можно подписать его под протокол `Skeletonable` + */ +extension UITableViewCell: Skeletonable { + public var viewsToSkeletone: [UIView] { + contentView.subviews + } +} + +/*: + ## SkeletonsPresenter + + Чтобы не приходилось постоянно передавать в методы необходимые параметры для конфигурации можно соответствовать протоколу `SkeletonsPresenter`. Протокол дает возможность определять свойства для конфигурации скелетонов внутри view или viewController, вызывать метод `showSkeletons()` без передачи каких-либо параметров + + Перепишем _CanShowAndHideSkeletons_ под использование протокола + */ + +class CanShowAndHideWithSkeletonsPresenter: CanShowAndHideSkeletons, SkeletonsPresenter { + var skeletonsConfiguration: SkeletonsConfiguration { + SkeletonsConfiguration(skeletonsBackgroundColor: .gray) + } +} + +let canShowAndHideController = CanShowAndHideWithSkeletonsPresenter() + +//: Skeletons will be shown with custom configuration +canShowAndHideController.showSkeletons() + +/*: + ## Конфигурация внешнего вида + + Для конфигурации скелетонов существует класс `SkeletonsConfiguration` + */ +class CustomConfigurableSkeletonableView: UIView, SkeletonsPresenter { + var skeletonsConfiguration: SkeletonsConfiguration { + .init(skeletonsBackgroundColor: .blue) + } +} + +/*: + Возможные опции для настройки: + + - анимация + - цвет + - форма + - отступы + + При этом все view делятся на: + - `UIView` с subviews (контейнеры) + - `UIView` без subviews + - `UILabel` + - `UITextView` + - `UIImageView` + + > Для контейнеров доступна только настройка `borderWidth`, а `borderColor` используется тот же, что и для других скелетонов + + ### Анимация + + `SkeletonsConfiguration` для настройки анимации принимает тип `(SkeletonsLayer) -> CAAnimationGroup`. Это означает, что при необходимости вы можете создать любую необходимую анимацию. Анимация будет применена к каждому скелетону. + + Однако для удобства существует уже определенный класс `SkeletonsAnimationBuilder` со статическим методом `createDirectionalGradientAnimation(_:)` для создания анимаций в одну из сторон: + + ```swift + public enum SkeletonsAnimationDirection { + case leftToRight + case rightToLeft + case topToBottom + case bottomToTop + case topLeftToBottomRight + case topRightToBottomLeft + case bottomLeftToTopRight + case bottomRightToTopLeft + } + ``` + */ +let confWithLeftToRightAnim = SkeletonsConfiguration(animation: { _ in + let animConfig = DirectionalSkeletonsAnimationConfiguration(direction: .leftToRight, duration: 1.5) + return SkeletonsAnimationBuilder.createDirectionalGradientAnimation(animConfig) +}) + +let confWithTopToBottomAnim = SkeletonsConfiguration(animation: { _ in + let animConfig = DirectionalSkeletonsAnimationConfiguration(direction: .topToBottom, duration: 1.5) + return SkeletonsAnimationBuilder.createDirectionalGradientAnimation(animConfig) +}) + +/*: + ### Цвет + + За настройку цвета отвечает параметр `skeletonsBackgroundColor`: основной цвет скелетонов, им будт заливаться фон и выделяться _border_ + */ +let confWithRedBackgroundColor = SkeletonsConfiguration(skeletonsBackgroundColor: .red) + +/*: + ### Форма + + Форму можно настраивать отдельно для `UILabel`, `UITextView`, `UIImageView` и остальных вью. Например, картинки можно сделать круглыми, а лейблы прямоугольные с закругленными краями: + */ +var confWithShape: SkeletonsConfiguration { + let labelConf = TextSkeletonsConfiguration(shape: .rectangle(cornerRadius: 10)) + let imageConf = BaseViewSkeletonsConfiguration(shape: .circle) + + return .init(labelConfiguration: labelConf, + imageViewConfiguration: imageConf) +} + +//: Для `UILabel` и `UITextView` есть возможность настроить высоту каждой строчки, расстояние между ними и их количество. +var confWithLabelSettings: SkeletonsConfiguration { + let labelConf = TextSkeletonsConfiguration(numberOfLines: 3, + lineHeight: { font in + if let font = font { + return font.pointSize + } + return 10 + + }, lineSpacing: { font in + if let font = font { + return font.xHeight + } + return 5 + }) + return .init(labelConfiguration: labelConf) +} + +/*: + ### Отступы + + Отступы можно настроить отдельно для `UILabel`, `UITextView`, `UIImageView` и остальных вью. Например, для предыдущего примера можно добавить горизонтальный _padding_ для лейбла: + */ +var confWithPadding: SkeletonsConfiguration { + let labelConf = TextSkeletonsConfiguration(padding: .horizontal(left: 15), shape: .rectangle(cornerRadius: 10)) + let imageConf = BaseViewSkeletonsConfiguration(shape: .circle) + + return .init(labelConfiguration: labelConf, + imageViewConfiguration: imageConf) +} + +/*: + ## Что если нужно больше? + + Если стандартной настройки не хватает, то в конфигуратор можно передать объект, соответствующий протоколу `SkeletonsConfigurationDelegate` через который можно настроить слой скелетона для каждой вью отдельно + + ```swift + public protocol SkeletonsConfigurationDelegate: AnyObject { + func layerDidConfigured(forViewType type: SkeletonLayer.ViewType, layer: SkeletonLayer) + } + ``` + */ +class SkeletonsConfDelegate: SkeletonsConfigurationDelegate { + func layerDidConfigured(forViewType type: SkeletonLayer.ViewType, layer: SkeletonLayer) { + if case .imageView(_) = type { + layer.frame = .init(x: layer.frame.minX - 20, + y: layer.frame.minY - 20, + width: layer.frame.width, + height: layer.frame.height) + } + } +} + +let delegate = SkeletonsConfDelegate() +let confWithDelegate = SkeletonsConfiguration(configurationDelegate: delegate) + +/*: + ### Особенности + + Т.к. размеры view на основе которой строятся скелетоны не модифицируются, может возникнуть ситуация, когда одни скелетоны перекрывают другие. Например, когда размер view меньше ее скелетонов. В таких случаях как раз может помочь установка позиции или размеров в методе делегата `SkeletonsConfigurationDelegate` + + Также в качестве способа обойти такую ситуацию можно передавать во view моковые данные для увеличения ее размеров, чтобы размеры были хотя бы примерно похожи на размер скелетонов, как в примере: + */ +extension DefaultTitleSubtitleView: SkeletonsPresenter { + public var skeletonsConfiguration: SkeletonsConfiguration { + .init(labelConfiguration: .init(numberOfLines: 3)) + } +} + +let titleSubtitleView = DefaultTitleSubtitleView() +titleSubtitleView.configure(appearance: .make { + $0.titleAppearance.update { + $0.textAttributes = .init(font: .systemFont(ofSize: 15), color: .black, alignment: .natural, isMultiline: true) + } + $0.subtitleAppearance.update { + $0.textAttributes = .init(font: .systemFont(ofSize: 15), color: .black, alignment: .natural, isMultiline: true) + } +}) +titleSubtitleView.configure(with: .init(title: "very very long mock string to make multiple lines", + subtitle: "very very long mock string to make multiple lines")) + +titleSubtitleView.showSkeletons() + +DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { + titleSubtitleView.configure(with: .init(title: "normal data from a request", + subtitle: "normal data from a request")) + titleSubtitleView.hideSkeletons() +} + +//: ## Тестовый сконфигурированный контроллер +import PlaygroundSupport + +canShowAndHideController.view.frame = .init(origin: .zero, size: .init(width: 250, height: 100)) + +canShowAndHideController.hideSkeletons() +confWithLeftToRightAnim.labelConfiguration = .init(numberOfLines: 2) + +PlaygroundPage.current.liveView = canShowAndHideController + +canShowAndHideController.showSkeletons(viewsToSkeletone: nil, confWithLeftToRightAnim) diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/contents.xcplayground b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/contents.xcplayground new file mode 100644 index 00000000..3debe4b3 --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/contents.xcplayground @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/project.pbxproj b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/project.pbxproj new file mode 100644 index 00000000..52cfb0fd --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/project.pbxproj @@ -0,0 +1,395 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + B295B0E33533FFC3D833A6CB /* Pods_TIUIElements.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8E0E28C6F64363C77CAE4662 /* Pods_TIUIElements.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2E10916FC4CAF67D840FC3D2 /* Pods-TIUIElements.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Target Support Files/Pods-TIUIElements/Pods-TIUIElements.release.xcconfig"; sourceTree = ""; }; + 8BACBE8322576CAD00266845 /* TIUIElements.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TIUIElements.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8BACBE8622576CAD00266845 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8E0E28C6F64363C77CAE4662 /* Pods_TIUIElements.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TIUIElements.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CB08C5B0C7051DCB015D3D9F /* Pods-TIUIElements.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIUIElements.debug.xcconfig"; path = "Target Support Files/Pods-TIUIElements/Pods-TIUIElements.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8BACBE8022576CAD00266845 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B295B0E33533FFC3D833A6CB /* Pods_TIUIElements.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6FA8D567F06C39C360B32325 /* Pods */ = { + isa = PBXGroup; + children = ( + 2E10916FC4CAF67D840FC3D2 /* Pods-TIUIElements.release.xcconfig */, + CB08C5B0C7051DCB015D3D9F /* Pods-TIUIElements.debug.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 7672C2F734E0BBEC76B58962 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8E0E28C6F64363C77CAE4662 /* Pods_TIUIElements.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 8B39A26221D40F8700DE2643 = { + isa = PBXGroup; + children = ( + 8BACBE8422576CAD00266845 /* TIUIElements */, + 8B39A26C21D40F8700DE2643 /* Products */, + 6FA8D567F06C39C360B32325 /* Pods */, + 7672C2F734E0BBEC76B58962 /* Frameworks */, + ); + sourceTree = ""; + }; + 8B39A26C21D40F8700DE2643 /* Products */ = { + isa = PBXGroup; + children = ( + 8BACBE8322576CAD00266845 /* TIUIElements.framework */, + ); + name = Products; + sourceTree = ""; + }; + 8BACBE8422576CAD00266845 /* TIUIElements */ = { + isa = PBXGroup; + children = ( + 8BACBE8622576CAD00266845 /* Info.plist */, + ); + path = TIUIElements; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 8BACBE7E22576CAD00266845 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 8BACBE8222576CAD00266845 /* TIUIElements */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIUIElements" */; + buildPhases = ( + 9D2C80D787CBCD9B1EA728B0 /* [CP] Check Pods Manifest.lock */, + 8BACBE7E22576CAD00266845 /* Headers */, + 8BACBE7F22576CAD00266845 /* Sources */, + 8BACBE8022576CAD00266845 /* Frameworks */, + 8BACBE8122576CAD00266845 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TIUIElements; + productName = TIUIElements2; + productReference = 8BACBE8322576CAD00266845 /* TIUIElements.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8B39A26321D40F8700DE2643 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1010; + LastUpgradeCheck = 1200; + ORGANIZATIONNAME = "47 Degrees"; + TargetAttributes = { + 8BACBE8222576CAD00266845 = { + CreatedOnToolsVersion = 10.1; + }; + }; + }; + buildConfigurationList = 8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TIUIElements" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8B39A26221D40F8700DE2643; + productRefGroup = 8B39A26C21D40F8700DE2643 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8BACBE8222576CAD00266845 /* TIUIElements */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8BACBE8122576CAD00266845 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 9D2C80D787CBCD9B1EA728B0 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-TIUIElements-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8BACBE7F22576CAD00266845 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 8B39A27721D40F8800DE2643 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8B39A27821D40F8800DE2643 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 8BACBE8822576CAD00266845 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CB08C5B0C7051DCB015D3D9F /* Pods-TIUIElements.debug.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + CURRENT_TIUIElements_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/TIUIElements/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TIUIElements; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 8BACBE8922576CAD00266845 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2E10916FC4CAF67D840FC3D2 /* Pods-TIUIElements.release.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + CURRENT_TIUIElements_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/TIUIElements/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TIUIElements; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TIUIElements" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8B39A27721D40F8800DE2643 /* Debug */, + 8B39A27821D40F8800DE2643 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIUIElements" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8BACBE8822576CAD00266845 /* Debug */, + 8BACBE8922576CAD00266845 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8B39A26321D40F8700DE2643 /* Project object */; +} diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..4fdc4a67 --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/xcshareddata/xcschemes/TIUIElements.xcscheme b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/xcshareddata/xcschemes/TIUIElements.xcscheme new file mode 100644 index 00000000..2ac02e93 --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/xcshareddata/xcschemes/TIUIElements.xcscheme @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcworkspace/contents.xcworkspacedata b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..313f1fa5 --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements/Info.plist b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements/Info.plist new file mode 100644 index 00000000..98d14f60 --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + NSHumanReadableCopyright + Copyright © 2019. The nef authors. + + diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/launcher b/TIUIElements/TIUIElements.app/Contents/MacOS/launcher new file mode 100755 index 00000000..dedc792f --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/launcher @@ -0,0 +1,6 @@ +#!/bin/bash + +workspace="TIUIElements.xcworkspace" +workspacePath=$(echo "$0" | rev | cut -f2- -d '/' | rev) + +open "`pwd`/$workspacePath/$workspace" diff --git a/TIUIElements/TIUIElements.app/Contents/Resources/AppIcon.icns b/TIUIElements/TIUIElements.app/Contents/Resources/AppIcon.icns new file mode 100644 index 00000000..32814f1c Binary files /dev/null and b/TIUIElements/TIUIElements.app/Contents/Resources/AppIcon.icns differ diff --git a/TIUIElements/TIUIElements.app/Contents/Resources/Assets.car b/TIUIElements/TIUIElements.app/Contents/Resources/Assets.car new file mode 100644 index 00000000..79d9ea89 Binary files /dev/null and b/TIUIElements/TIUIElements.app/Contents/Resources/Assets.car differ diff --git a/TIUIElements/TIUIElements.playground b/TIUIElements/TIUIElements.playground new file mode 120000 index 00000000..254203c3 --- /dev/null +++ b/TIUIElements/TIUIElements.playground @@ -0,0 +1 @@ +TIUIElements.app/Contents/MacOS/TIUIElements.playground \ No newline at end of file diff --git a/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift b/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift index 4a602848..bc589798 100644 --- a/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift +++ b/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift @@ -157,6 +157,9 @@ open class BaseTextAttributes { var configuration = UIButton.Configuration.plain() if let title = string { + button.setTitle(nil, for: .normal) + button.setAttributedTitle(nil, for: .normal) + configuration.attributedTitle = attributedString(for: title) button.configuration = configuration } diff --git a/docs/tiuielements/skeletons.md b/docs/tiuielements/skeletons.md new file mode 100644 index 00000000..41da669d --- /dev/null +++ b/docs/tiuielements/skeletons.md @@ -0,0 +1,323 @@ + +# Skeletons API + + При импорте _TIUIElements_ вы можете использовать API для показа скелетонов. + +## Принцип работы + + При использовании методов показа скелетонов: + 1. происходит скрытие всех subview в иерархии той view, на которой был вызван метод + 2. далее происходит проход по view, которые можно сконвертировать в скелетоны (список либо определяется пользователем, либо конвертация будет происходить автоматически), создается `CALayer` типа `SkeletonLayer`, представляющий конвертируемую view + 3. поверх view с которой начался показ, добавляются все созданные `SkeletonLayer` + + > Таким образом скелетоны не модифицируют размеры view и не изменяют ее положение + +## Как начать пользоваться + + Базовая настройка для показа скелетонов не требуется. `UIView` и `UIViewController` уже имеют все необходимые методы для работы: + - `showSkeletons(viewsToSkeletone:_:)` : используется для показа скелетонов, если была передана конфигурация анимации, то она запустится автоматически. `viewsToSkeletone` - опциональный массив `UIView`, определяющий, какие вью будут конвертироваться в скелетоны. Если nil, то проход будет осуществляться по списку subview + - `hideSkeletons()` : используется для скрытия скелетонов + - `startAnimation()` : используется для старта анимации на скелетонах (если анимания не была сконфигурирована в методе `showSkeletons(viewsToSkeletone:_:)` то ничего не произойдет) + - `stopAnimation()` : используется для остановки анимации на скелетонах + +```swift +import TIUIKitCore +import TIUIElements +import UIKit + +class CanShowAndHideSkeletons: BaseInitializableViewController { + + private let imageView = UIImageView(image: UIImage(systemName: "apple.logo")) + private let label = UILabel() + private let button = UIButton(type: .custom) + + override func addViews() { + super.addViews() + + view.addSubviews(imageView, label, button) + } + + override func configureLayout() { + super.configureLayout() + + [imageView, label, button] + .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + imageView.heightAnchor.constraint(equalToConstant: 40), + imageView.widthAnchor.constraint(equalToConstant: 40), + imageView.centerYAnchor.constraint(equalTo: label.centerYAnchor), + imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + + label.topAnchor.constraint(equalTo: view.topAnchor), + label.trailingAnchor.constraint(equalTo: view.trailingAnchor), + label.leadingAnchor.constraint(equalTo: imageView.trailingAnchor), + label.heightAnchor.constraint(equalToConstant: 60), + + button.leadingAnchor.constraint(equalTo: view.leadingAnchor), + button.topAnchor.constraint(equalTo: label.bottomAnchor), + button.trailingAnchor.constraint(equalTo: view.trailingAnchor), + button.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + override func bindViews() { + super.bindViews() + + button.addTarget(self, action: #selector(toggleSkeletons), for: .touchUpInside) + } + + override func configureAppearance() { + super.configureAppearance() + + label.text = "Hello from SkeletonableViewController" + button.setTitle("show skeletons", for: .normal) + + let textAttributes = BaseTextAttributes(font: .systemFont(ofSize: 25), color: .black, alignment: .natural, isMultiline: false) + + view.configureUIView(appearance: UIView.DefaultAppearance(backgroundColor: .white)) + + label.configureUILabel(appearance: UILabel.DefaultAppearance.make { + $0.textAttributes = textAttributes + }) + + button.configureUIButton(appearance: UILabel.DefaultAppearance.make { + $0.textAttributes = textAttributes + }) + } + + @objc private func toggleSkeletons() { + // Т.к. передается nil, скелетонится будут все subview (в данном случае view.subview == [button, label, imageView]) + showSkeletons(viewsToSkeletone: nil, .init()) + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { [weak self] in + self?.hideSkeletons() + } + } +} +``` + +## Skeletonable + + Если необходимо изменить список конвертируемых в скелетоны view у какой-нибудь из отдельных view в иерархии, можно подписать его под протокол `Skeletonable` + +```swift +extension UITableViewCell: Skeletonable { + public var viewsToSkeletone: [UIView] { + contentView.subviews + } +} +``` + +## SkeletonsPresenter + + Чтобы не приходилось постоянно передавать в методы необходимые параметры для конфигурации можно соответствовать протоколу `SkeletonsPresenter`. Протокол дает возможность определять свойства для конфигурации скелетонов внутри view или viewController, вызывать метод `showSkeletons()` без передачи каких-либо параметров + + Перепишем _CanShowAndHideSkeletons_ под использование протокола + +```swift +class CanShowAndHideWithSkeletonsPresenter: CanShowAndHideSkeletons, SkeletonsPresenter { + var skeletonsConfiguration: SkeletonsConfiguration { + SkeletonsConfiguration(skeletonsBackgroundColor: .gray) + } +} + +let canShowAndHideController = CanShowAndHideWithSkeletonsPresenter() +``` + +Skeletons will be shown with custom configuration + +```swift +canShowAndHideController.showSkeletons() +``` + +## Конфигурация внешнего вида + + Для конфигурации скелетонов существует класс `SkeletonsConfiguration` + +```swift +class CustomConfigurableSkeletonableView: UIView, SkeletonsPresenter { + var skeletonsConfiguration: SkeletonsConfiguration { + .init(skeletonsBackgroundColor: .blue) + } +} +``` + + Возможные опции для настройки: + + - анимация + - цвет + - форма + - отступы + + При этом все view делятся на: + - `UIView` с subviews (контейнеры) + - `UIView` без subviews + - `UILabel` + - `UITextView` + - `UIImageView` + + > Для контейнеров доступна только настройка `borderWidth`, а `borderColor` используется тот же, что и для других скелетонов + +### Анимация + + `SkeletonsConfiguration` для настройки анимации принимает тип `(SkeletonsLayer) -> CAAnimationGroup`. Это означает, что при необходимости вы можете создать любую необходимую анимацию. Анимация будет применена к каждому скелетону. + + Однако для удобства существует уже определенный класс `SkeletonsAnimationBuilder` со статическим методом `createDirectionalGradientAnimation(_:)` для создания анимаций в одну из сторон: + + ```swift + public enum SkeletonsAnimationDirection { + case leftToRight + case rightToLeft + case topToBottom + case bottomToTop + case topLeftToBottomRight + case topRightToBottomLeft + case bottomLeftToTopRight + case bottomRightToTopLeft + } + ``` + +```swift +let confWithLeftToRightAnim = SkeletonsConfiguration(animation: { _ in + let animConfig = DirectionalSkeletonsAnimationConfiguration(direction: .leftToRight, duration: 1.5) + return SkeletonsAnimationBuilder.createDirectionalGradientAnimation(animConfig) +}) + +let confWithTopToBottomAnim = SkeletonsConfiguration(animation: { _ in + let animConfig = DirectionalSkeletonsAnimationConfiguration(direction: .topToBottom, duration: 1.5) + return SkeletonsAnimationBuilder.createDirectionalGradientAnimation(animConfig) +}) +``` + +### Цвет + + За настройку цвета отвечает параметр `skeletonsBackgroundColor`: основной цвет скелетонов, им будт заливаться фон и выделяться _border_ + +```swift +let confWithRedBackgroundColor = SkeletonsConfiguration(skeletonsBackgroundColor: .red) +``` + +### Форма + + Форму можно настраивать отдельно для `UILabel`, `UITextView`, `UIImageView` и остальных вью. Например, картинки можно сделать круглыми, а лейблы прямоугольные с закругленными краями: + +```swift +var confWithShape: SkeletonsConfiguration { + let labelConf = TextSkeletonsConfiguration(shape: .rectangle(cornerRadius: 10)) + let imageConf = BaseViewSkeletonsConfiguration(shape: .circle) + + return .init(labelConfiguration: labelConf, + imageViewConfiguration: imageConf) +} +``` + +Для `UILabel` и `UITextView` есть возможность настроить высоту каждой строчки, расстояние между ними и их количество. + +```swift +var confWithLabelSettings: SkeletonsConfiguration { + let labelConf = TextSkeletonsConfiguration(numberOfLines: 3, + lineHeight: { font in + if let font = font { + return font.pointSize + } + return 10 + + }, lineSpacing: { font in + if let font = font { + return font.xHeight + } + return 5 + }) + return .init(labelConfiguration: labelConf) +} +``` + +### Отступы + + Отступы можно настроить отдельно для `UILabel`, `UITextView`, `UIImageView` и остальных вью. Например, для предыдущего примера можно добавить горизонтальный _padding_ для лейбла: + +```swift +var confWithPadding: SkeletonsConfiguration { + let labelConf = TextSkeletonsConfiguration(padding: .horizontal(left: 15), shape: .rectangle(cornerRadius: 10)) + let imageConf = BaseViewSkeletonsConfiguration(shape: .circle) + + return .init(labelConfiguration: labelConf, + imageViewConfiguration: imageConf) +} +``` + +## Что если нужно больше? + + Если стандартной настройки не хватает, то в конфигуратор можно передать объект, соответствующий протоколу `SkeletonsConfigurationDelegate` через который можно настроить слой скелетона для каждой вью отдельно + + ```swift + public protocol SkeletonsConfigurationDelegate: AnyObject { + func layerDidConfigured(forViewType type: SkeletonLayer.ViewType, layer: SkeletonLayer) + } + ``` + +```swift +class SkeletonsConfDelegate: SkeletonsConfigurationDelegate { + func layerDidConfigured(forViewType type: SkeletonLayer.ViewType, layer: SkeletonLayer) { + if case .imageView(_) = type { + layer.frame = .init(x: layer.frame.minX - 20, + y: layer.frame.minY - 20, + width: layer.frame.width, + height: layer.frame.height) + } + } +} + +let delegate = SkeletonsConfDelegate() +let confWithDelegate = SkeletonsConfiguration(configurationDelegate: delegate) +``` + +### Особенности + + Т.к. размеры view на основе которой строятся скелетоны не модифицируются, может возникнуть ситуация, когда одни скелетоны перекрывают другие. Например, когда размер view меньше ее скелетонов. В таких случаях как раз может помочь установка позиции или размеров в методе делегата `SkeletonsConfigurationDelegate` + + Также в качестве способа обойти такую ситуацию можно передавать во view моковые данные для увеличения ее размеров, чтобы размеры были хотя бы примерно похожи на размер скелетонов, как в примере: + +```swift +extension DefaultTitleSubtitleView: SkeletonsPresenter { + public var skeletonsConfiguration: SkeletonsConfiguration { + .init(labelConfiguration: .init(numberOfLines: 3)) + } +} + +let titleSubtitleView = DefaultTitleSubtitleView() +titleSubtitleView.configure(appearance: .make { + $0.titleAppearance.update { + $0.textAttributes = .init(font: .systemFont(ofSize: 15), color: .black, alignment: .natural, isMultiline: true) + } + $0.subtitleAppearance.update { + $0.textAttributes = .init(font: .systemFont(ofSize: 15), color: .black, alignment: .natural, isMultiline: true) + } +}) +titleSubtitleView.configure(with: .init(title: "very very long mock string to make multiple lines", + subtitle: "very very long mock string to make multiple lines")) + +titleSubtitleView.showSkeletons() + +DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { + titleSubtitleView.configure(with: .init(title: "normal data from a request", + subtitle: "normal data from a request")) + titleSubtitleView.hideSkeletons() +} +``` + +## Тестовый сконфигурированный контроллер + +```swift +import PlaygroundSupport + +canShowAndHideController.view.frame = .init(origin: .zero, size: .init(width: 250, height: 100)) + +canShowAndHideController.hideSkeletons() +confWithLeftToRightAnim.labelConfiguration = .init(numberOfLines: 2) + +PlaygroundPage.current.liveView = canShowAndHideController + +canShowAndHideController.showSkeletons(viewsToSkeletone: nil, confWithLeftToRightAnim) +``` diff --git a/project-scripts/gen_docs_from_playgrounds.sh b/project-scripts/gen_docs_from_playgrounds.sh index 9e5c172a..1c5e3242 100755 --- a/project-scripts/gen_docs_from_playgrounds.sh +++ b/project-scripts/gen_docs_from_playgrounds.sh @@ -7,9 +7,10 @@ # SRCROOT - path to project folder. # -PLAYGROUNDS="${SRCROOT}/TIFoundationUtils/TIFoundationUtils.app" +PLAYGROUNDS="${SRCROOT}/TIFoundationUtils/TIFoundationUtils.app +${SRCROOT}/TIUIElements/TIUIElements.app" for playground_path in ${PLAYGROUNDS}; do nef compile --project ${playground_path} - nef markdown --project ${playground_path} --output ../docs -done \ No newline at end of file + nef markdown --project ${playground_path} --output ${SRCROOT}/docs +done