Merge branch 'feature/placeholder_api' into 'master'
feat: placeholder api See merge request touchinstinct/LeadKit!11
This commit is contained in:
commit
29d7a6ca65
|
|
@ -1,5 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
### 1.40.0
|
||||
|
||||
- **Added**: `PlaceholderFactory` for creating `DefaultPlaceholderView` views
|
||||
- **Added**: `DefaultPlaceholderImageView`
|
||||
|
||||
### 1.39.0
|
||||
|
||||
- **Added**: UIButton Appearance model
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIAppleMapUtils'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIAuth'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Login, registration, confirmation and other related actions'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIDeveloperUtils'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Universal web view API'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIEcommerce'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Cart, products, promocodes, bonuses and other related actions'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIFoundationUtils'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Set of helpers for Foundation framework classes.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIGoogleMapUtils'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIKeychainUtils'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Set of helpers for Keychain classes.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIMapUtils'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Set of helpers for map objects clustering and interacting.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIMoyaNetworking'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Moya + Swagger network service.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TINetworking'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Swagger-frendly networking layer helpers.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TINetworkingCache'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Caching results of EndpointRequests.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIPagination'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Generic pagination component.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TISwiftUICore'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Core UI elements: protocols, views and helpers.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TISwiftUtils'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Bunch of useful helpers for Swift development.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TITableKitUtils'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Set of helpers for TableKit classes.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -138,6 +138,10 @@ extension UIView {
|
|||
}
|
||||
}
|
||||
|
||||
open class BaseWrappedAppearance<Layout: WrappedViewLayout>: BaseAppearance<Layout> {
|
||||
|
||||
}
|
||||
|
||||
public final class DefaultWrappedViewHolderAppearance<SubviewAppearance: WrappedViewAppearance,
|
||||
Layout: ViewLayout>: BaseWrappedViewHolderAppearance<SubviewAppearance, Layout>,
|
||||
WrappedViewHolderAppearance {
|
||||
|
|
@ -146,7 +150,7 @@ extension UIView {
|
|||
}
|
||||
}
|
||||
|
||||
public final class DefaultWrappedAppearance: BaseAppearance<DefaultWrappedLayout>, WrappedViewAppearance {
|
||||
public final class DefaultWrappedAppearance: BaseWrappedAppearance<DefaultWrappedLayout>, WrappedViewAppearance {
|
||||
public static var defaultAppearance: Self {
|
||||
Self()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import class UIKit.NSLayoutConstraint
|
||||
|
||||
public struct SubviewConstraints {
|
||||
public var centerXConstraint: NSLayoutConstraint?
|
||||
public var centerYConstraint: NSLayoutConstraint?
|
||||
public var leadingConstraint: NSLayoutConstraint?
|
||||
public var topConstraint: NSLayoutConstraint?
|
||||
public var trailingConstraint: NSLayoutConstraint?
|
||||
public var bottomConstraint: NSLayoutConstraint?
|
||||
public var widthConstraint: NSLayoutConstraint?
|
||||
public var heightConstraint: NSLayoutConstraint?
|
||||
|
||||
public var constraints: [NSLayoutConstraint] {
|
||||
[
|
||||
centerXConstraint,
|
||||
leadingConstraint,
|
||||
topConstraint,
|
||||
trailingConstraint,
|
||||
bottomConstraint,
|
||||
widthConstraint,
|
||||
heightConstraint
|
||||
]
|
||||
.compactMap { $0 }
|
||||
}
|
||||
|
||||
public var sizeConstraints: [NSLayoutConstraint] {
|
||||
[widthConstraint, heightConstraint]
|
||||
.compactMap { $0 }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class DefaultPlaceholderLocalizationProvider: PlaceholderLocalizationProvider {
|
||||
|
||||
public enum Defaults {
|
||||
public static var errorKey: String {
|
||||
"common_placeholder_title_error"
|
||||
}
|
||||
|
||||
public static var loadingDataErrorKey: String {
|
||||
"common_placeholder_title_loading_data"
|
||||
}
|
||||
|
||||
public static var emptyStateKey: String {
|
||||
"common_placeholder_title_empty_state"
|
||||
}
|
||||
|
||||
public static var repeatKey: String {
|
||||
"common_global_repeat"
|
||||
}
|
||||
}
|
||||
|
||||
public var bundle: Bundle
|
||||
public var tableName: String?
|
||||
|
||||
// MARK: - PlaceholderLocalizationProvider
|
||||
|
||||
open var errorTitle: String {
|
||||
bundle.localizedString(forKey: Defaults.errorKey, value: "An error has occured.", table: tableName)
|
||||
}
|
||||
|
||||
open var loadingDataErrorTitle: String {
|
||||
bundle.localizedString(forKey: Defaults.loadingDataErrorKey, value: "Failed to load data.", table: tableName)
|
||||
}
|
||||
|
||||
open var emptyStateTitle: String {
|
||||
bundle.localizedString(forKey: Defaults.emptyStateKey, value: "The list is empty.", table: tableName)
|
||||
}
|
||||
|
||||
open var repeatButtonTitle: String {
|
||||
bundle.localizedString(forKey: Defaults.repeatKey, value: "Repeat", table: tableName)
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(bundle: Bundle = .main, tableName: String? = nil) {
|
||||
self.bundle = bundle
|
||||
self.tableName = tableName
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
public protocol PlaceholderLocalizationProvider {
|
||||
var errorTitle: String { get }
|
||||
var loadingDataErrorTitle: String { get }
|
||||
var emptyStateTitle: String { get }
|
||||
var repeatButtonTitle: String { get }
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
open class PlaceholderFactory {
|
||||
public enum Defaults {
|
||||
public static var errorStyleImageName: String {
|
||||
"placeholder_error_icon"
|
||||
}
|
||||
|
||||
public static var loadingDataErrorImageName: String {
|
||||
"placeholder_loading_data_icon"
|
||||
}
|
||||
|
||||
public static var emptyStateImageName: String {
|
||||
"placeholder_empty_state_icon"
|
||||
}
|
||||
}
|
||||
|
||||
public var localizationProvider: PlaceholderLocalizationProvider
|
||||
|
||||
public init(localizationProvider: PlaceholderLocalizationProvider = DefaultPlaceholderLocalizationProvider()) {
|
||||
self.localizationProvider = localizationProvider
|
||||
}
|
||||
|
||||
// MARK: - Default styles creation
|
||||
|
||||
open func errorStyle() -> DefaultPlaceholderStyle {
|
||||
.defaultStyle { style in
|
||||
style.image = UIImage(named: Defaults.errorStyleImageName)
|
||||
style.titleSubtitle = DefaultTitleSubtitleViewModel(title: localizationProvider.errorTitle)
|
||||
}
|
||||
.updateAppearance { placeholder in
|
||||
placeholder.backgroundColor = .white
|
||||
|
||||
placeholder.imageViewAppearance.layout {
|
||||
$0.size = .fixedHeight(250)
|
||||
$0.centerOffset = .centerVertical(-125)
|
||||
}
|
||||
|
||||
placeholder.textViewAppearance = Self.defaultTitlesAppearance { titleSubtitle in
|
||||
titleSubtitle.layout {
|
||||
$0.spacing = 8
|
||||
$0.insets = .vertical(23)
|
||||
}
|
||||
}
|
||||
|
||||
placeholder.controlsViewAppearance.layout {
|
||||
$0.insets = .horizontal(66)
|
||||
$0.size = .fixedHeight(52)
|
||||
}
|
||||
}
|
||||
.withButton { buttonStyle in
|
||||
buttonStyle.titles = [.normal: localizationProvider.repeatButtonTitle]
|
||||
buttonStyle.appearance = [.normal: Self.defaultButtonAppearance]
|
||||
}
|
||||
}
|
||||
|
||||
open func loadingDataErrorStyle() -> DefaultPlaceholderStyle {
|
||||
errorStyle().update { placeholder in
|
||||
placeholder.image = UIImage(named: Defaults.loadingDataErrorImageName)
|
||||
placeholder.titleSubtitle = DefaultTitleSubtitleViewModel(title: localizationProvider.loadingDataErrorTitle)
|
||||
}
|
||||
}
|
||||
|
||||
open func emptyStateStyle() -> DefaultPlaceholderStyle {
|
||||
errorStyle().update { placeholder in
|
||||
placeholder.image = UIImage(named: Defaults.emptyStateImageName)
|
||||
placeholder.titleSubtitle = DefaultTitleSubtitleViewModel(title: localizationProvider.emptyStateTitle)
|
||||
placeholder.buttonsStyles = []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Placeholder creation
|
||||
|
||||
open func createEmptyStatePlaceholder() -> DefaultPlaceholderView {
|
||||
createImageStylePlaceholder(emptyStateStyle())
|
||||
}
|
||||
|
||||
open func createErrorPlaceholder() -> DefaultPlaceholderView {
|
||||
createImageStylePlaceholder(errorStyle())
|
||||
}
|
||||
|
||||
open func createLoadingDataErrorPlaceholder() -> DefaultPlaceholderView {
|
||||
createImageStylePlaceholder(loadingDataErrorStyle())
|
||||
}
|
||||
|
||||
// MARK: - Helper methods
|
||||
|
||||
open func createImageStylePlaceholder(_ style: DefaultPlaceholderStyle = .defaultStyle) -> DefaultPlaceholderView {
|
||||
let view = DefaultPlaceholderView()
|
||||
view.apply(style: style)
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private configurations
|
||||
|
||||
private extension PlaceholderFactory {
|
||||
static var defaultButtonAppearance: UIButton.DefaultAppearance {
|
||||
.make {
|
||||
$0.border.cornerRadius = 25
|
||||
$0.border.roundedCorners = .allCorners
|
||||
$0.backgroundColor = UIColor(red: 0.892, green: 0.906, blue: 0.92, alpha: 0.5)
|
||||
$0.textAttributes = .init(font: .systemFont(ofSize: 20, weight: .bold),
|
||||
color: .black,
|
||||
alignment: .natural,
|
||||
isMultiline: false)
|
||||
}
|
||||
}
|
||||
|
||||
static var defaultTitlesAppearance: DefaultTitleSubtitleView.Appearance {
|
||||
.make { titleSubtitle in
|
||||
titleSubtitle.titleAppearance { title in
|
||||
title.textAttributes = Self.defaultTextAttributes
|
||||
}
|
||||
|
||||
titleSubtitle.subtitleAppearance { subtitle in
|
||||
subtitle.textAttributes = Self.defaultTextAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var defaultTextAttributes: BaseTextAttributes {
|
||||
.init(font: .systemFont(ofSize: 20, weight: .light),
|
||||
color: .black,
|
||||
alignment: .center,
|
||||
isMultiline: false)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
open class BasePlaceholderStyle<Appearance: ViewAppearance> {
|
||||
|
||||
public var titleSubtitle: DefaultTitleSubtitleViewModel
|
||||
public var controlsViewAxis: NSLayoutConstraint.Axis
|
||||
public var appearance: Appearance
|
||||
public var buttonsStyles: [PlaceholderButtonStyle]
|
||||
|
||||
public init(titleSubtitle: DefaultTitleSubtitleViewModel = .init(),
|
||||
appearance: Appearance = .defaultAppearance,
|
||||
controlsViewAxis: NSLayoutConstraint.Axis = .vertical,
|
||||
buttonsStyles: [PlaceholderButtonStyle] = []) {
|
||||
|
||||
self.titleSubtitle = titleSubtitle
|
||||
self.appearance = appearance
|
||||
self.controlsViewAxis = controlsViewAxis
|
||||
self.buttonsStyles = buttonsStyles
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
public final class DefaultPlaceholderStyle: BasePlaceholderStyle<DefaultPlaceholderView.Appearance>,
|
||||
PlaceholderStyle {
|
||||
|
||||
static public var defaultStyle: Self {
|
||||
.init()
|
||||
}
|
||||
|
||||
public var image: UIImage?
|
||||
|
||||
public init(image: UIImage? = nil,
|
||||
titleSubtitle: DefaultTitleSubtitleViewModel = .init(),
|
||||
appearance: DefaultPlaceholderView.Appearance = .defaultAppearance,
|
||||
controlsViewAxis: NSLayoutConstraint.Axis = .vertical,
|
||||
buttonsStyles: [PlaceholderButtonStyle] = []) {
|
||||
|
||||
self.image = image
|
||||
|
||||
super.init(titleSubtitle: titleSubtitle,
|
||||
appearance: appearance,
|
||||
controlsViewAxis: controlsViewAxis,
|
||||
buttonsStyles: buttonsStyles)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
|
||||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
open class PlaceholderButtonStyle {
|
||||
|
||||
public var titles: UIControl.StateTitles
|
||||
public var images: UIControl.StateImages
|
||||
public var appearance: StatefulButton.StateAppearance
|
||||
public var action: UIButton.Action?
|
||||
|
||||
public init(titles: UIControl.StateTitles = [:],
|
||||
images: UIControl.StateImages = [:],
|
||||
appearance: StatefulButton.StateAppearance = [:],
|
||||
action: UIButton.Action? = nil) {
|
||||
|
||||
self.titles = titles
|
||||
self.images = images
|
||||
self.appearance = appearance
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
|
||||
public extension UIButton {
|
||||
typealias Action = (target: Any?, action: Selector, event: UIControl.Event)
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TISwiftUtils
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
public protocol PlaceholderStyle: AnyObject {
|
||||
associatedtype PlaceholderAppearance: ViewAppearance
|
||||
|
||||
static var defaultStyle: Self { get }
|
||||
|
||||
var titleSubtitle: DefaultTitleSubtitleViewModel { get set }
|
||||
var controlsViewAxis: NSLayoutConstraint.Axis { get set }
|
||||
var appearance: PlaceholderAppearance { get set }
|
||||
var buttonsStyles: [PlaceholderButtonStyle] { get set }
|
||||
}
|
||||
|
||||
// MARK: - Builder methods
|
||||
|
||||
public extension PlaceholderStyle {
|
||||
static func callAsFunction(_ builder: ParameterClosure<Self>) -> Self {
|
||||
make(builder)
|
||||
}
|
||||
|
||||
static func make(_ builder: ParameterClosure<Self>) -> Self {
|
||||
let style = Self.defaultStyle
|
||||
builder(style)
|
||||
|
||||
return style
|
||||
}
|
||||
|
||||
func callAsFunction(_ builder: ParameterClosure<Self>) -> Self {
|
||||
update(builder)
|
||||
}
|
||||
|
||||
func update(_ builder: ParameterClosure<Self>) -> Self {
|
||||
builder(self)
|
||||
return self
|
||||
}
|
||||
|
||||
func updateAppearance(_ builder: ParameterClosure<PlaceholderAppearance>) -> Self {
|
||||
builder(appearance)
|
||||
return self
|
||||
}
|
||||
|
||||
func withButton(_ builder: ParameterClosure<PlaceholderButtonStyle>) -> Self {
|
||||
let buttonStyle = PlaceholderButtonStyle()
|
||||
|
||||
builder(buttonStyle)
|
||||
buttonsStyles.append(buttonStyle)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func withButtons(_ amount: Int,
|
||||
axis: NSLayoutConstraint.Axis,
|
||||
_ builder: (Int, PlaceholderButtonStyle) -> Void) -> Self {
|
||||
|
||||
controlsViewAxis = axis
|
||||
|
||||
for index in 0..<amount {
|
||||
if let buttonStyle = buttonsStyles[safe: index] {
|
||||
builder(index, buttonStyle)
|
||||
|
||||
} else {
|
||||
let buttonStyle = PlaceholderButtonStyle()
|
||||
|
||||
builder(index, buttonStyle)
|
||||
buttonsStyles.append(buttonStyle)
|
||||
}
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
open class BasePlaceholderImageView<Placeholder: UIView>: UIImageView,
|
||||
InitializableViewProtocol {
|
||||
|
||||
public let placeholderView = Placeholder()
|
||||
|
||||
public var placeholderConstraints: SubviewConstraints?
|
||||
|
||||
open override var image: UIImage? {
|
||||
get {
|
||||
super.image
|
||||
}
|
||||
set {
|
||||
placeholderView.isHidden = newValue != nil
|
||||
|
||||
super.image = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
override public init(image: UIImage?) {
|
||||
super.init(image: image)
|
||||
|
||||
initializeView()
|
||||
}
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
initializeView()
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
initializeView()
|
||||
}
|
||||
|
||||
// MARK: - InitializableViewProtocol
|
||||
|
||||
open func addViews() {
|
||||
addSubview(placeholderView)
|
||||
}
|
||||
|
||||
open func configureLayout() {
|
||||
placeholderView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let placeholderConstraints = SubviewConstraints(
|
||||
centerXConstraint: placeholderView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
centerYConstraint: placeholderView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
leadingConstraint: placeholderView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
topConstraint: placeholderView.topAnchor.constraint(equalTo: topAnchor),
|
||||
trailingConstraint: placeholderView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
bottomConstraint: placeholderView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
widthConstraint: placeholderView.widthAnchor.constraint(equalToConstant: .zero),
|
||||
heightConstraint: placeholderView.heightAnchor.constraint(equalToConstant: .zero))
|
||||
|
||||
NSLayoutConstraint.activate(placeholderConstraints.constraints)
|
||||
NSLayoutConstraint.deactivate(placeholderConstraints.sizeConstraints)
|
||||
|
||||
self.placeholderConstraints = placeholderConstraints
|
||||
}
|
||||
|
||||
open func bindViews() {
|
||||
// override in subviews
|
||||
}
|
||||
|
||||
open func configureAppearance() {
|
||||
// override in subviews
|
||||
}
|
||||
|
||||
open func localize() {
|
||||
// override in subviews
|
||||
}
|
||||
|
||||
// MARK: - Open methods
|
||||
|
||||
open func configureBasePlaceholder(appearance: BaseWrappedViewHolderAppearance<UIView.DefaultWrappedAppearance, some WrappedViewLayout>) {
|
||||
configureUIView(appearance: appearance)
|
||||
|
||||
placeholderView.configureUIView(appearance: appearance.subviewAppearance)
|
||||
configurePlaceholderLayout(layout: appearance.subviewAppearance.layout)
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func configurePlaceholderLayout(layout: some WrappedViewLayout) {
|
||||
layout.setupSize(widthConstraint: placeholderConstraints?.widthConstraint,
|
||||
heightConstraint: placeholderConstraints?.heightConstraint)
|
||||
|
||||
layout.setupCenterYOffset(centerYConstraint: placeholderConstraints?.centerYConstraint,
|
||||
topConstraint: placeholderConstraints?.topConstraint,
|
||||
bottomConstraint: placeholderConstraints?.bottomConstraint)
|
||||
|
||||
layout.setupCenterXOffset(centerXConstraint: placeholderConstraints?.centerXConstraint,
|
||||
leadingConstraint: placeholderConstraints?.leadingConstraint,
|
||||
trailingConstraint: placeholderConstraints?.trailingConstraint)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,272 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TISwiftUtils
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
open class BasePlaceholderView<ImageView: UIView>: BaseInitializableView {
|
||||
|
||||
public let imageView = ImageView()
|
||||
public let textView = DefaultTitleSubtitleView()
|
||||
public let controlsStackView = UIStackView()
|
||||
|
||||
public var imageViewConstraints: SubviewConstraints?
|
||||
public var textViewConstraints: SubviewConstraints?
|
||||
public var controlsViewConstraints: SubviewConstraints?
|
||||
|
||||
public var keyboardDidShownObserver: NSObjectProtocol?
|
||||
public var keyboardDidHiddenObserver: NSObjectProtocol?
|
||||
|
||||
open var isImageViewHidden: Bool {
|
||||
imageView.isHidden
|
||||
}
|
||||
|
||||
open var isControlsViewHidden: Bool {
|
||||
controlsStackView.isHidden || controlsStackView.arrangedSubviews.isEmpty
|
||||
}
|
||||
|
||||
// MARK: - Deinit
|
||||
|
||||
deinit {
|
||||
if let keyboardDidShownObserver = keyboardDidShownObserver {
|
||||
NotificationCenter.default.removeObserver(keyboardDidShownObserver)
|
||||
}
|
||||
|
||||
if let keyboardDidHiddenObserver = keyboardDidHiddenObserver {
|
||||
NotificationCenter.default.removeObserver(keyboardDidHiddenObserver)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BaseInitializableView
|
||||
|
||||
open override func addViews() {
|
||||
super.addViews()
|
||||
|
||||
addSubviews(imageView, textView, controlsStackView)
|
||||
}
|
||||
|
||||
open override func configureLayout() {
|
||||
super.configureLayout()
|
||||
|
||||
[imageView, textView, controlsStackView]
|
||||
.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
|
||||
|
||||
imageViewConstraints = .init(
|
||||
centerXConstraint: imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
centerYConstraint: imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
leadingConstraint: imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
topConstraint: imageView.topAnchor.constraint(equalTo: topAnchor),
|
||||
trailingConstraint: imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
bottomConstraint: imageView.bottomAnchor.constraint(equalTo: textView.topAnchor),
|
||||
widthConstraint: imageView.widthAnchor.constraint(equalToConstant: .zero),
|
||||
heightConstraint: imageView.heightAnchor.constraint(equalToConstant: .zero))
|
||||
|
||||
textViewConstraints = .init(
|
||||
centerXConstraint: textView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
centerYConstraint: textView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
leadingConstraint: textView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
topConstraint: textView.topAnchor.constraint(equalTo: imageView.bottomAnchor),
|
||||
trailingConstraint: textView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
bottomConstraint: textView.bottomAnchor.constraint(lessThanOrEqualTo: controlsStackView.topAnchor))
|
||||
|
||||
controlsViewConstraints = .init(
|
||||
centerXConstraint: controlsStackView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
leadingConstraint: controlsStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
topConstraint: controlsStackView.topAnchor.constraint(equalTo: textView.bottomAnchor),
|
||||
trailingConstraint: controlsStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
bottomConstraint: controlsStackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor),
|
||||
widthConstraint: controlsStackView.widthAnchor.constraint(equalToConstant: .zero),
|
||||
heightConstraint: controlsStackView.heightAnchor.constraint(equalToConstant: .zero))
|
||||
|
||||
NSLayoutConstraint.activate(
|
||||
(imageViewConstraints?.constraints ?? [])
|
||||
+ (textViewConstraints?.constraints ?? [])
|
||||
+ (controlsViewConstraints?.constraints ?? [])
|
||||
)
|
||||
|
||||
NSLayoutConstraint.deactivate(
|
||||
(imageViewConstraints?.sizeConstraints ?? [])
|
||||
+ (controlsViewConstraints?.sizeConstraints ?? [])
|
||||
)
|
||||
}
|
||||
|
||||
open override func bindViews() {
|
||||
super.bindViews()
|
||||
|
||||
keyboardDidShownObserver = NotificationCenter.default
|
||||
.addObserver(forName: UIResponder.keyboardDidShowNotification,
|
||||
object: nil,
|
||||
queue: .main) { [weak self] notification in
|
||||
self?.configureLayoutForKeyboard(notification, isKeyboardHidden: false)
|
||||
}
|
||||
|
||||
keyboardDidHiddenObserver = NotificationCenter.default
|
||||
.addObserver(forName: UIResponder.keyboardDidHideNotification,
|
||||
object: nil,
|
||||
queue: .main) { [weak self] notification in
|
||||
self?.configureLayoutForKeyboard(notification, isKeyboardHidden: true)
|
||||
}
|
||||
}
|
||||
|
||||
open override func configureAppearance() {
|
||||
super.configureAppearance()
|
||||
|
||||
controlsStackView.distribution = .equalSpacing
|
||||
}
|
||||
|
||||
// MARK: - Open methods
|
||||
|
||||
open func applyBaseStyle(style: BasePlaceholderStyle<some BaseAppearance<some WrappedViewAppearance> & ViewAppearance>) {
|
||||
textView.configure(with: style.titleSubtitle)
|
||||
controlsStackView.axis = style.controlsViewAxis
|
||||
|
||||
style.buttonsStyles.forEach {
|
||||
let button = StatefulButton(type: .custom)
|
||||
|
||||
button.set(titles: $0.titles)
|
||||
button.set(images: $0.images)
|
||||
button.set(appearance: $0.appearance)
|
||||
|
||||
if let action = $0.action {
|
||||
button.addTarget(action.target, action: action.action, for: action.event)
|
||||
}
|
||||
|
||||
controlsStackView.addArrangedSubview(button)
|
||||
}
|
||||
|
||||
configureAppearance(appearance: style.appearance)
|
||||
}
|
||||
|
||||
open func addAction(_ action: UIButton.Action,
|
||||
forButtonAtIndex index: Int) {
|
||||
|
||||
let button = controlsStackView.arrangedSubviews[safe: index] as? UIButton
|
||||
|
||||
button?.addTarget(action.target, action: action.action, for: action.event)
|
||||
}
|
||||
|
||||
open func configureAppearance(appearance: BaseAppearance<some WrappedViewAppearance>) {
|
||||
configureImageViewLayout(layout: appearance.imageViewAppearance.layout)
|
||||
configureTextViewLayout(layout: appearance.textViewAppearance.layout)
|
||||
configureControlsViewLayout(layout: appearance.controlsViewAppearance.layout)
|
||||
|
||||
configureUIView(appearance: appearance)
|
||||
textView.configure(appearance: appearance.textViewAppearance)
|
||||
controlsStackView.configureUIView(appearance: appearance.controlsViewAppearance)
|
||||
}
|
||||
|
||||
open func configureLayoutForKeyboard(_ notification: Notification, isKeyboardHidden: Bool) {
|
||||
let multiplier = isKeyboardHidden ? 1.0 : -1.0
|
||||
|
||||
if let height = getKeyboardHeight(notification) {
|
||||
controlsViewConstraints?.bottomConstraint?.constant = multiplier * height / 2
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func configureImageViewLayout(layout: WrappedViewLayout) {
|
||||
guard !isImageViewHidden, let imageViewConstraints = imageViewConstraints else {
|
||||
NSLayoutConstraint.deactivate(imageViewConstraints?.constraints ?? [])
|
||||
return
|
||||
}
|
||||
|
||||
configureLayout(layout: layout, constraints: imageViewConstraints)
|
||||
}
|
||||
|
||||
private func configureTextViewLayout(layout: WrappedViewLayout) {
|
||||
if isImageViewHidden {
|
||||
self.textViewConstraints?.topConstraint?.isActive = false
|
||||
self.textViewConstraints?.topConstraint = textView.topAnchor.constraint(equalTo: topAnchor)
|
||||
}
|
||||
|
||||
if isControlsViewHidden {
|
||||
self.textViewConstraints?.bottomConstraint?.isActive = false
|
||||
self.textViewConstraints?.bottomConstraint = textView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor)
|
||||
}
|
||||
|
||||
if let textViewConstraints = textViewConstraints {
|
||||
configureLayout(layout: layout, constraints: textViewConstraints)
|
||||
}
|
||||
}
|
||||
|
||||
private func configureControlsViewLayout(layout: SpacedWrappedViewLayout) {
|
||||
guard !isControlsViewHidden, let controlsViewConstraints = controlsViewConstraints else {
|
||||
NSLayoutConstraint.deactivate(controlsViewConstraints?.constraints ?? [])
|
||||
return
|
||||
}
|
||||
|
||||
configureLayout(layout: layout, constraints: controlsViewConstraints)
|
||||
controlsStackView.spacing = layout.spacing
|
||||
}
|
||||
|
||||
private func configureLayout(layout: WrappedViewLayout, constraints: SubviewConstraints) {
|
||||
layout.setupSize(widthConstraint: constraints.widthConstraint, heightConstraint: constraints.heightConstraint)
|
||||
|
||||
layout.setupCenterYOffset(centerYConstraint: constraints.centerYConstraint,
|
||||
topConstraint: constraints.topConstraint,
|
||||
bottomConstraint: constraints.bottomConstraint)
|
||||
|
||||
layout.setupCenterXOffset(centerXConstraint: constraints.centerXConstraint,
|
||||
leadingConstraint: constraints.leadingConstraint,
|
||||
trailingConstraint: constraints.trailingConstraint)
|
||||
}
|
||||
|
||||
private func getKeyboardHeight(_ notification: Notification) -> CGFloat? {
|
||||
guard let userInfo = notification.userInfo else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BaseAppearance + Appearance
|
||||
|
||||
extension BasePlaceholderView {
|
||||
|
||||
open class BaseAppearance<ImageViewAppearance: WrappedViewAppearance>: UIView.BaseAppearance<UIView.DefaultWrappedLayout> {
|
||||
|
||||
public var imageViewAppearance: ImageViewAppearance
|
||||
public var textViewAppearance: DefaultTitleSubtitleView.Appearance
|
||||
public var controlsViewAppearance: UIView.DefaultSpacedWrappedAppearance
|
||||
|
||||
public init(layout: UIView.DefaultWrappedLayout = .defaultLayout,
|
||||
backgroundColor: UIColor = .clear,
|
||||
border: UIViewBorder = .init(),
|
||||
shadow: UIViewShadow? = nil,
|
||||
imageViewAppearance: ImageViewAppearance = .defaultAppearance,
|
||||
textViewAppearance: DefaultTitleSubtitleView.Appearance = .defaultAppearance,
|
||||
controlsViewAppearance: UIView.DefaultSpacedWrappedAppearance = .defaultAppearance) {
|
||||
|
||||
self.imageViewAppearance = imageViewAppearance
|
||||
self.textViewAppearance = textViewAppearance
|
||||
self.controlsViewAppearance = controlsViewAppearance
|
||||
|
||||
super.init(layout: layout,
|
||||
backgroundColor: backgroundColor,
|
||||
border: border,
|
||||
shadow: shadow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
final public class DefaultPlaceholderImageView: BasePlaceholderImageView<UIImageView>, AppearanceConfigurable {
|
||||
|
||||
public enum Defaults {
|
||||
public static var placeholderImageName: String {
|
||||
"global_image_placeholder_icon"
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Appearance = UIView.DefaultWrappedViewHolderAppearance<UIView.DefaultWrappedAppearance, UIView.DefaultWrappedLayout>
|
||||
|
||||
public var placeholderImage: UIImage? {
|
||||
get {
|
||||
placeholderView.image
|
||||
}
|
||||
set {
|
||||
placeholderView.image = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(image: UIImage? = nil, placeholderImage: UIImage? = .init(named: Defaults.placeholderImageName)) {
|
||||
super.init(image: image)
|
||||
|
||||
self.placeholderImage = placeholderImage
|
||||
}
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
initializeView()
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
// MARK: - BaseInitializableViewProtocol
|
||||
|
||||
public override func configureAppearance() {
|
||||
super.configureAppearance()
|
||||
|
||||
placeholderView.contentMode = .scaleAspectFit
|
||||
}
|
||||
|
||||
// MARK: - AppearanceConfigurable
|
||||
|
||||
public func configure(appearance: Appearance) {
|
||||
configureBasePlaceholder(appearance: appearance)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
public final class DefaultPlaceholderView: BasePlaceholderView<UIImageView>, AppearanceConfigurable {
|
||||
|
||||
public override var isImageViewHidden: Bool {
|
||||
super.isImageViewHidden || imageView.image == nil
|
||||
}
|
||||
|
||||
public func apply(style: DefaultPlaceholderStyle) {
|
||||
imageView.image = style.image
|
||||
|
||||
super.applyBaseStyle(style: style)
|
||||
|
||||
configureImageSizeConstraints(size: imageView.image?.size ?? .zero)
|
||||
}
|
||||
|
||||
// MARK: - AppearanceConfigurable
|
||||
|
||||
public func configure(appearance: Appearance) {
|
||||
configureAppearance(appearance: appearance)
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func configureImageSizeConstraints(size: CGSize) {
|
||||
guard size != .zero else {
|
||||
return
|
||||
}
|
||||
|
||||
if size.height.isFinite, size.height > .zero {
|
||||
imageViewConstraints?.heightConstraint?.constant = size.height
|
||||
}
|
||||
|
||||
if size.width.isFinite, size.width > .zero {
|
||||
imageViewConstraints?.widthConstraint?.constant = size.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Default appearance model
|
||||
|
||||
extension DefaultPlaceholderView {
|
||||
public final class Appearance: BaseAppearance<UIView.DefaultWrappedAppearance>, ViewAppearance {
|
||||
public static var defaultAppearance: Self {
|
||||
.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,571 @@
|
|||
/*:
|
||||
# Placeholder API
|
||||
|
||||
_TIUIElements_ добавляет `DefaultPlaceholderView` - плейсхолдер. Он представляет собой 3 вертикально расположенные view: `UIImageView`, `DefaultTitleSubtitleView`, `UIStackView`.
|
||||
|
||||
> `UIStackView` используется для добавления кнопок в различном количестве и с необходимым расположением (горизонтальным/вертикальным)
|
||||
|
||||
## Принцип работы
|
||||
|
||||
Для создания и конфигурации плейсхолдера существует фабрика `PlaceholderFactory`. Для этого следует воспользоваться методом `createImageStylePlaceholder(_:)`.
|
||||
*/
|
||||
import TIUIElements
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
let factory = PlaceholderFactory()
|
||||
let defaultPlaceholderView = factory.createLoadingDataErrorPlaceholder()
|
||||
|
||||
/*:
|
||||
## Конфигурация
|
||||
|
||||
Метод `createImageStylePlaceholder(_:)` принимает в себя аргумент типа `DefaultPlaceholderStyle`, позволяющий полностью сконфигурировать создаваемую view
|
||||
*/
|
||||
let styleWithText = DefaultPlaceholderStyle(titleSubtitle: .init(title: "Server error has occured!"))
|
||||
let placeholderWithErrorText = factory.createImageStylePlaceholder(styleWithText)
|
||||
|
||||
/*:
|
||||
Возможные опции для конфигурации:
|
||||
|
||||
- картинка
|
||||
- текст (_title_, _subtitle_)
|
||||
- внешний вид плейсхолдера в целом и каждой отдельной view
|
||||
- расположение view внутри плейсхолдера относительно друг друга
|
||||
- расоложение кнопок внутри *stackView* (горизонтальное/вертикальное)
|
||||
- внешний вид кнопок
|
||||
- текст и картинка кнопок
|
||||
- действия кнопок
|
||||
|
||||
> Стоит учитывать, что если картинка плейсхолдера не указывается, то `UIImageView` будет скрыт. Тот же принцип касается и кнопок - если вы не добавили стилей кнопок, то `UIStackView` скрывается.
|
||||
*/
|
||||
let customStyle = DefaultPlaceholderStyle(
|
||||
image: UIImage(named: "proj-placeholder-image"),
|
||||
titleSubtitle: .init(title: "An error has occured", subtitle: "Please, reload the page"),
|
||||
appearance: .make { placeholder in
|
||||
placeholder.backgroundColor = .blue
|
||||
|
||||
placeholder.textViewAppearance { textView in
|
||||
textView.titleAppearance { title in
|
||||
title.textAttributes = .init(font: .systemFont(ofSize: 20),
|
||||
color: .black,
|
||||
alignment: .natural,
|
||||
isMultiline: false)
|
||||
}
|
||||
textView.subtitleAppearance = textView.titleAppearance
|
||||
}
|
||||
|
||||
}, buttonsStyles: [
|
||||
.init(titles: [.normal: "Reload"],
|
||||
appearance: [
|
||||
.normal: UIButton.DefaultAppearance(textAttributes: .init(font: .systemFont(ofSize: 20),
|
||||
color: .black,
|
||||
alignment: .natural,
|
||||
isMultiline: false))
|
||||
])
|
||||
])
|
||||
|
||||
let placeholderView = factory.createImageStylePlaceholder(customStyle)
|
||||
|
||||
/*:
|
||||
### Методы конфигурации
|
||||
|
||||
Как видно из прошлого примера даже самая простая настройка может выглядеть очень громоздко. Для улучшения читаемости и простоты переиспользования, `DefaultPlaceholderStyle` соответствует протоколу `PlaceholderStyle`, который добавляет следующие методы для конструирования стилей плейсхолдеров:
|
||||
|
||||
- `make(_:)`: статический метод для создания стиля. Принимает в себя функцию с переменной типа создоваемого стиля
|
||||
- `update(_:)`: метод для изменения уже существующего стиля. Принимает в себя функцию с переменной типа создаваемого стиля
|
||||
- `updateAppearance(_:)`: метод для изменения внешнего вида плейсхолдера. Принимает в себя функцию с переменной типа `ViewAppearance`. В случае `DefaultPlaceholderStyle` это `DefaultPlaceholderView.Appearance`
|
||||
- `withButton(_:)`: метод для добавления новой кнопки. Принимает в себя фунцию с переменной типа `PlaceholderButtonStyle`.
|
||||
- `withButtons(_:axis:_:)`: метод для добавления/изменения срузу нескольких кнопок. Первым агрументом указывается количество кнопок, вторым их расположение, третьим - функция двух переменных, где первая переменная - это номер (индекс) кнопки, вторая - `PlaceholderButtonStyle`
|
||||
*/
|
||||
let styleFromMake = DefaultPlaceholderStyle.make { style in
|
||||
style.image = UIImage(named: "proj-placeholder-image")
|
||||
style.titleSubtitle = .init(title: "An error has occured", subtitle: "Please, reload the page")
|
||||
}
|
||||
|
||||
let styleWithUpdatedImage = styleFromMake.update { style in
|
||||
style.image = UIImage(named: "proj-other-placeholder-image")
|
||||
}
|
||||
|
||||
let styleWithAppearance = styleFromMake.updateAppearance { placeholder in
|
||||
placeholder.backgroundColor = .blue
|
||||
|
||||
placeholder.textViewAppearance { textView in
|
||||
textView.titleAppearance { title in
|
||||
title.textAttributes = .init(font: .systemFont(ofSize: 20),
|
||||
color: .black,
|
||||
alignment: .natural,
|
||||
isMultiline: false)
|
||||
}
|
||||
|
||||
textView.subtitleAppearance = textView.titleAppearance
|
||||
}
|
||||
}
|
||||
|
||||
let styleWithButton = styleWithAppearance.withButton { buttonStyle in
|
||||
buttonStyle.titles = [.normal: "Reload"]
|
||||
buttonStyle.appearance = [.normal: UIButton.DefaultAppearance.make { button in
|
||||
button.textAttributes = .init(font: .systemFont(ofSize: 20),
|
||||
color: .black,
|
||||
alignment: .natural,
|
||||
isMultiline: false)
|
||||
}]
|
||||
}
|
||||
|
||||
//: Стоит обратить внимание на то, что если бы метод использовался на `styleWithButton`, то у уже добавленной кнопки (она была бы с индексом 0) сохранился стиль, так что его можно было либо дополнить, либо переопределить
|
||||
let styleWithTwoButtons = styleWithAppearance.withButtons(2, axis: .vertical) { index, buttonStyle in
|
||||
if index == .zero {
|
||||
buttonStyle.titles = [.normal: "Reload"]
|
||||
buttonStyle.appearance = [.normal: UIButton.DefaultAppearance.make { button in
|
||||
button.textAttributes = .init(font: .systemFont(ofSize: 20),
|
||||
color: .black,
|
||||
alignment: .natural,
|
||||
isMultiline: false)
|
||||
}]
|
||||
}
|
||||
|
||||
if index == 1 {
|
||||
buttonStyle.titles = [.normal: "Wait"]
|
||||
buttonStyle.appearance = [.normal: UIButton.DefaultAppearance.make { button in
|
||||
button.textAttributes = .init(font: .systemFont(ofSize: 20),
|
||||
color: .black,
|
||||
alignment: .natural,
|
||||
isMultiline: false)
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
/*:
|
||||
### Стандартные стили
|
||||
|
||||
Для удобства у `PlaceholderFactory` есть несколько стандартных стилей, которые можно использовать для ускоренной разработки и изменять под свои нужды при необходимости:
|
||||
|
||||
- `errorStyle`
|
||||
- `loadingDataErrorStyle`
|
||||
- `emptyStateStyle`
|
||||
|
||||
У `PlaceholderFactory` есть готовые методы для создания плейсхолдера с такими стилями
|
||||
*/
|
||||
|
||||
//: 
|
||||
let errorStylePlaceholder = factory.createErrorPlaceholder()
|
||||
|
||||
//: 
|
||||
let loadingDataErrorStylePlaceholder = factory.createLoadingDataErrorPlaceholder()
|
||||
|
||||
//: 
|
||||
let emptyStateStylePlaceholder = factory.createEmptyStatePlaceholder()
|
||||
|
||||
/*:
|
||||
### Кастомизация стандартных стилей
|
||||
|
||||
Стили `errorStyle`, `loadingDataErrorStyle`, `emptyStateStyle` могут показывать, лежащие в **Assets**. Для этого необходимо только называть картинки: _placeholder_error_icon_, _placeholder_loading_data_icon_, _placeholder_empty_state_icon_ соответственно. Если нужной картинки не будет, то она просто не отобразится в плейсхолдере. При таком изменении никаких дополнительных настроек делать не нужно.
|
||||
|
||||
Также при создании фабрики, можно передать в нее объект соответствующий протоколу `PlaceholderLocalizationProvider` для добавления необходимого текста.
|
||||
|
||||
При добавлении иных изменений нужно либо делать наследника `PlaceholderFactory`, либо передавать стиль через метод `createImageStylePlaceholder(_:)`. В обоих случаях изменить стандартный стиль можно через соответствующие методы фабрики:
|
||||
|
||||
- `errorStyle()`
|
||||
- `loadingDataErrorStyle()`
|
||||
- `emptyStateStyle()`
|
||||
*/
|
||||
class CustomViewController: BaseInitializableViewController {
|
||||
private var currentPlaceholder: UIView?
|
||||
|
||||
private var customErrorStyle: DefaultPlaceholderStyle {
|
||||
factoryWithCustomErrorStyle.errorStyle()
|
||||
.update { style in
|
||||
style.withButtons(1, axis: .vertical) { _, buttonStyle in
|
||||
buttonStyle.action = (target: nil, action: #selector(closePlaceholder), event: .touchUpInside)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let factoryWithCustomErrorStyle = PlaceholderFactory()
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// some activity with error result...
|
||||
|
||||
let currentPlaceholder = factoryWithCustomErrorStyle.createImageStylePlaceholder(customErrorStyle)
|
||||
|
||||
// custom presentation of the placeholder...
|
||||
|
||||
self.currentPlaceholder = currentPlaceholder
|
||||
}
|
||||
|
||||
@objc private func closePlaceholder() {
|
||||
currentPlaceholder?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
/*:
|
||||
## Использование плейсхолдеров без фабрики
|
||||
|
||||
Если необходимо использовать заглушки без фабрики, то их конфигурацию можно доверить методу `apply(style:)` у каждого `DefaultPlaceholderView`
|
||||
*/
|
||||
class PlaceholderHolderViewController: BaseInitializableViewController, ConfigurableView {
|
||||
|
||||
private let placeholder = DefaultPlaceholderView()
|
||||
|
||||
override func addViews() {
|
||||
super.addViews()
|
||||
|
||||
view.addSubview(placeholder)
|
||||
}
|
||||
|
||||
override func configureLayout() {
|
||||
super.configureLayout()
|
||||
|
||||
placeholder.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
placeholder.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
placeholder.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
placeholder.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
placeholder.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
override func configureAppearance() {
|
||||
super.configureAppearance()
|
||||
|
||||
placeholder.isHidden = true
|
||||
}
|
||||
|
||||
func configure(with error: ErrorType) {
|
||||
switch error {
|
||||
case .internetConnection:
|
||||
placeholder.apply(style: Self.internetConnectionErrorStyle)
|
||||
|
||||
case .unknown:
|
||||
placeholder.apply(style: Self.unknownErrorStyle)
|
||||
}
|
||||
|
||||
placeholder.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
extension PlaceholderHolderViewController {
|
||||
static var internetConnectionErrorStyle: DefaultPlaceholderStyle {
|
||||
factory.errorStyle().update { _ in
|
||||
// some configurations
|
||||
}
|
||||
}
|
||||
|
||||
static var unknownErrorStyle: DefaultPlaceholderStyle {
|
||||
factory.errorStyle().update { _ in
|
||||
// some configurations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ErrorType {
|
||||
case internetConnection
|
||||
case unknown
|
||||
}
|
||||
|
||||
import PlaygroundSupport
|
||||
|
||||
let placeholder = PlaceholderHolderViewController()
|
||||
|
||||
PlaygroundPage.current.liveView = placeholder
|
||||
|
||||
placeholder.configure(with: .internetConnection)
|
||||
|
||||
/*:
|
||||
## Создание кастомных заглушек
|
||||
|
||||
Если необходимо показывать что-то кроме `UIImageView`, можно создать наследника `BasePlaceholderView`.
|
||||
|
||||
В качестве примера показан заглушка с lottie анимацией:
|
||||
|
||||
```swift
|
||||
import Lottie
|
||||
|
||||
public final class LottiePlaceholderStyle: BasePlaceholderStyle<LottiePlaceholderView.Appearance>, PlaceholderStyle {
|
||||
|
||||
public static var defaultStyle: LottiePlaceholderStyle {
|
||||
.init()
|
||||
}
|
||||
|
||||
public var animationName: String
|
||||
public var animationSpeed: CGFloat
|
||||
public var loopMode: LottieLoopMode
|
||||
|
||||
public init(titleSubtitle: DefaultTitleSubtitleViewModel = .init(),
|
||||
appearance: LottiePlaceholderView.Appearance = .defaultAppearance,
|
||||
controlsViewAxis: NSLayoutConstraint.Axis = .vertical,
|
||||
buttonsStyles: [PlaceholderButtonStyle] = [],
|
||||
animationName: String = "",
|
||||
animationSpeed: CGFloat = 1,
|
||||
loopMode: LottieLoopMode = .loop) {
|
||||
|
||||
self.animationName = animationName
|
||||
self.animationSpeed = animationSpeed
|
||||
self.loopMode = loopMode
|
||||
|
||||
super.init(titleSubtitle: titleSubtitle,
|
||||
appearance: appearance,
|
||||
controlsViewAxis: controlsViewAxis,
|
||||
buttonsStyles: buttonsStyles)
|
||||
}
|
||||
}
|
||||
|
||||
public final class LottiePlaceholderView: BasePlaceholderView<LottieAnimationView> {
|
||||
public override var isImageViewHidden: Bool {
|
||||
super.isImageViewHidden || imageView.animation == nil
|
||||
}
|
||||
|
||||
public func apply(style: LottiePlaceholderStyle) {
|
||||
imageView.animation = LottieAnimation.named(style.animationName)
|
||||
imageView.animationSpeed = style.animationSpeed
|
||||
imageView.loopMode = style.loopMode
|
||||
imageView.play()
|
||||
|
||||
super.applyBaseStyle(style: style)
|
||||
|
||||
configureImageSizeConstraints(size: imageView.animation?.size ?? .zero)
|
||||
}
|
||||
|
||||
public func configure(appearance: Appearance) {
|
||||
configureAppearance(appearance: appearance)
|
||||
}
|
||||
|
||||
private func configureImageSizeConstraints(size: CGSize) {
|
||||
guard size != .zero else {
|
||||
return
|
||||
}
|
||||
|
||||
if size.height.isFinite, size.height > .zero {
|
||||
imageViewConstraints?.widthConstraint?.constant = size.height
|
||||
}
|
||||
|
||||
if size.width.isFinite, size.width > .zero {
|
||||
imageViewConstraints?.widthConstraint?.constant = size.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LottiePlaceholderView {
|
||||
public final class Appearance: BaseAppearance<UIView.DefaultWrappedAppearance>, ViewAppearance {
|
||||
public static var defaultAppearance: Self {
|
||||
.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LottieAnimationViewController: BaseViewController<UIView, Void> {
|
||||
let placeholderFactory = PlaceholderFactory()
|
||||
|
||||
static var lottieStyle: LottiePlaceholderStyle {
|
||||
.make {
|
||||
$0.animationName = "cat"
|
||||
$0.titleSubtitle = .init(title: "Long time no see, Nyan cat")
|
||||
}
|
||||
.updateAppearance {
|
||||
$0.imageViewAppearance.layout {
|
||||
$0.size = .fixedHeight(250)
|
||||
}
|
||||
|
||||
$0.textViewAppearance {
|
||||
$0.titleAppearance {
|
||||
$0.textAttributes = .init(font: .boldSystemFont(ofSize: 25), color: .systemPink, alignment: .center, isMultiline: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
showPlaceholder()
|
||||
}
|
||||
|
||||
func showPlaceholder() {
|
||||
let placeholder = LottiePlaceholderView()
|
||||
placeholder.apply(style: Self.lottieStyle)
|
||||
|
||||
placeholder.frame = view.frame
|
||||
view.addSubview(placeholder)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Плейсхолдеры для UIImageView
|
||||
|
||||
Вместе с полноразмерными заглушками была добавлена новая `UIImageView`, способная отображать картинку-плейсхолдер пока не присвоена image
|
||||
*/
|
||||
let placeholderImage = UIImage(named: "placeholder-image")
|
||||
|
||||
//: Теперь при использовании данного imageView будет отображаться картинка, созданная выше
|
||||
let placeholderImageView = DefaultPlaceholderImageView(placeholderImage: placeholderImage)
|
||||
|
||||
//: Здесь все еще отображается `placeholderImage`
|
||||
placeholderImageView.image = nil
|
||||
|
||||
//: Здесь placeholderImage спрячится
|
||||
placeholderImageView.image = UIImage(named: "image")
|
||||
|
||||
//: При обнулении картинки `placeholderImage` покажется заново
|
||||
placeholderImageView.image = nil
|
||||
|
||||
//: > При этом необязательно создавать картинку плейсхолдера отдельно. Создайте картинку с именем _global_image_placeholder_icon_ в Assets каталоге и она сама подгрузится в `DefaultPlaceholderImageView` при использовании инициализатора `init(image:placeholderImage:)`
|
||||
|
||||
/*:
|
||||
### Пример контроллера
|
||||
|
||||

|
||||
|
||||
```swift
|
||||
import TITableKitUtils
|
||||
import TableKit
|
||||
|
||||
class WorkingCatView: BaseInitializableView, ConfigurableView, AppearanceConfigurable {
|
||||
|
||||
typealias ViewModel = (image: UIImage?, title: String?, subtitle: String?)
|
||||
|
||||
private let catImageView = DefaultPlaceholderImageView()
|
||||
private let catLabel = DefaultTitleSubtitleView()
|
||||
|
||||
override func addViews() {
|
||||
super.addViews()
|
||||
|
||||
addSubviews(catImageView, catLabel)
|
||||
}
|
||||
|
||||
override func configureLayout() {
|
||||
super.configureLayout()
|
||||
|
||||
[catImageView, catLabel]
|
||||
.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
catImageView.topAnchor.constraint(equalTo: topAnchor, constant: 20),
|
||||
catImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
catImageView.heightAnchor.constraint(equalToConstant: 90),
|
||||
catImageView.widthAnchor.constraint(equalToConstant: 90),
|
||||
|
||||
catLabel.topAnchor.constraint(equalTo: catImageView.bottomAnchor, constant: 20),
|
||||
catLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
|
||||
catLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
|
||||
catLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20),
|
||||
])
|
||||
}
|
||||
|
||||
override func configureAppearance() {
|
||||
super.configureAppearance()
|
||||
|
||||
catImageView.contentMode = .scaleAspectFit
|
||||
}
|
||||
|
||||
func configure(with viewModel: ViewModel) {
|
||||
catImageView.image = viewModel.image
|
||||
catLabel.configure(with: .init(title: viewModel.title, subtitle: viewModel.subtitle))
|
||||
catImageView.placeholderImage = UIImage.gifImageWithName("cat-loader")
|
||||
}
|
||||
|
||||
func configure(appearance: Appearance) {
|
||||
catImageView.configure(appearance: appearance.catImageAppearance)
|
||||
catLabel.configure(appearance: appearance.catLabelAppearance)
|
||||
configureUIView(appearance: appearance)
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkingCatView {
|
||||
final class Appearance: UIView.BaseWrappedAppearance<UIView.DefaultWrappedLayout>, WrappedViewAppearance {
|
||||
static var defaultAppearance: Self {
|
||||
.init()
|
||||
}
|
||||
|
||||
var catImageAppearance: DefaultPlaceholderImageView.Appearance
|
||||
var catLabelAppearance: DefaultTitleSubtitleView.Appearance
|
||||
|
||||
public init(layout: UIView.DefaultWrappedLayout = .defaultLayout,
|
||||
backgroundColor: UIColor = .clear,
|
||||
border: UIViewBorder = .init(),
|
||||
shadow: UIViewShadow? = nil,
|
||||
catImageAppearance: DefaultPlaceholderImageView.Appearance = .defaultAppearance,
|
||||
catLabelAppearance: DefaultTitleSubtitleView.Appearance = .defaultAppearance) {
|
||||
|
||||
self.catImageAppearance = catImageAppearance
|
||||
self.catLabelAppearance = catLabelAppearance
|
||||
|
||||
super.init(layout: layout,
|
||||
backgroundColor: backgroundColor,
|
||||
border: border,
|
||||
shadow: shadow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CatsViewController: BaseViewController<TableKitTableView, Void> {
|
||||
|
||||
typealias ImageRow = WorkingCatView.InTableRow
|
||||
|
||||
let viewModels: [WorkingCatView.ViewModel] = [
|
||||
(image: UIImage(named: "cat-worker"), title: "Pusic", subtitle: "C++ dev"),
|
||||
(image: nil, title: "Luke", subtitle: "Jedi"), // image can't be loaded
|
||||
(image: .petDog, title: "Marzia", subtitle: "HR"),
|
||||
(image: .petFox, title: "Fox", subtitle: "iOS Dev")
|
||||
]
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// I swear it's a network request, images are loading ;)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { [weak self] in
|
||||
|
||||
let rows = (self?.viewModels ?? []).compactMap {
|
||||
self?.createRow($0)
|
||||
}
|
||||
self?.tableDirector.replace(withRows: rows)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindViews() {
|
||||
super.bindViews()
|
||||
|
||||
tableDirector += viewModels.map {
|
||||
createRow((nil, $0.title, $0.subtitle))
|
||||
}
|
||||
}
|
||||
|
||||
private func createRow(_ viewModel: WorkingCatView.ViewModel) -> ImageRow {
|
||||
ImageRow(item: viewModel)
|
||||
.with(appearance: Self.rowAppearance)
|
||||
}
|
||||
}
|
||||
|
||||
extension CatsViewController {
|
||||
static var rowAppearance: ImageRow.Appearance {
|
||||
.make { row in
|
||||
row.subviewAppearance { container in
|
||||
container.layout.insets = .edges(16)
|
||||
container.backgroundColor = UIColor(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 0.3)
|
||||
container.border = .init(color: .black, width: 1, cornerRadius: 10, roundedCorners: .allCorners)
|
||||
|
||||
container.catImageAppearance = Self.imageAppearance
|
||||
container.catLabelAppearance = Self.textAppearance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var imageAppearance: DefaultPlaceholderImageView.Appearance {
|
||||
.make {
|
||||
$0.border = .init(cornerRadius: 12, roundedCorners: .allCorners)
|
||||
$0.subviewAppearance.update {
|
||||
$0.backgroundColor = UIColor(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var textAppearance: DefaultTitleSubtitleView.Appearance {
|
||||
.make {
|
||||
$0.titleAppearance.textAttributes = .init(font: .systemFont(ofSize: 25), color: .black, alignment: .center, isMultiline: false)
|
||||
$0.subtitleAppearance.textAttributes = .init(font: .italicSystemFont(ofSize: 18), color: .gray, alignment: .center, isMultiline: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
*/
|
||||
|
|
@ -144,7 +144,7 @@ class CustomConfigurableSkeletonableView: UIView, SkeletonsPresenter {
|
|||
- форма
|
||||
- отступы
|
||||
|
||||
При этом все view делятся на:
|
||||
При этом все view делятся на:
|
||||
- `UIView` с subviews (контейнеры)
|
||||
- `UIView` без subviews
|
||||
- `UILabel`
|
||||
|
|
@ -156,9 +156,9 @@ class CustomConfigurableSkeletonableView: UIView, SkeletonsPresenter {
|
|||
### Анимация
|
||||
|
||||
`SkeletonsConfiguration` для настройки анимации принимает тип `(SkeletonsLayer) -> CAAnimationGroup`. Это означает, что при необходимости вы можете создать любую необходимую анимацию. Анимация будет применена к каждому скелетону.
|
||||
|
||||
|
||||
Однако для удобства существует уже определенный класс `SkeletonsAnimationBuilder` со статическим методом `createDirectionalGradientAnimation(_:)` для создания анимаций в одну из сторон:
|
||||
|
||||
|
||||
```swift
|
||||
public enum SkeletonsAnimationDirection {
|
||||
case leftToRight
|
||||
|
|
@ -172,12 +172,12 @@ class CustomConfigurableSkeletonableView: UIView, SkeletonsPresenter {
|
|||
}
|
||||
```
|
||||
*/
|
||||
let confWithLeftToRightAnim = SkeletonsConfiguration(animation: { _ in
|
||||
let confWithLeftToRightAnim = SkeletonsConfiguration(animation: { _ in
|
||||
let animConfig = DirectionalSkeletonsAnimationConfiguration(direction: .leftToRight, duration: 1.5)
|
||||
return SkeletonsAnimationBuilder.createDirectionalGradientAnimation(animConfig)
|
||||
})
|
||||
|
||||
let confWithTopToBottomAnim = SkeletonsConfiguration(animation: { _ in
|
||||
let confWithTopToBottomAnim = SkeletonsConfiguration(animation: { _ in
|
||||
let animConfig = DirectionalSkeletonsAnimationConfiguration(direction: .topToBottom, duration: 1.5)
|
||||
return SkeletonsAnimationBuilder.createDirectionalGradientAnimation(animConfig)
|
||||
})
|
||||
|
|
@ -248,7 +248,7 @@ var confWithPadding: SkeletonsConfiguration {
|
|||
## Что если нужно больше?
|
||||
|
||||
Если стандартной настройки не хватает, то в конфигуратор можно передать объект, соответствующий протоколу `SkeletonsConfigurationDelegate` через который можно настроить слой скелетона для каждой вью отдельно
|
||||
|
||||
|
||||
```swift
|
||||
public protocol SkeletonsConfigurationDelegate: AnyObject {
|
||||
func layerDidConfigured(_ layer: SkeletonLayer, forViewType type: SkeletonLayer.ViewType)
|
||||
|
|
|
|||
|
|
@ -1,2 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='6.0' target-platform='ios' display-mode='raw' buildActiveScheme='true' executeOnSourceChanges='true'/>
|
||||
<playground version='6.0' target-platform='ios' display-mode='raw' buildActiveScheme='true' executeOnSourceChanges='true'>
|
||||
<pages>
|
||||
<page name='Skeletons'/>
|
||||
<page name='Placeholder'/>
|
||||
</pages>
|
||||
</playground>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIUIElements'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Bunch of useful protocols and views.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIUIKitCore'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Core UI elements: protocols, views and helpers.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIWebView'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Universal web view API'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIYandexMapUtils'
|
||||
s.version = '1.39.0'
|
||||
s.version = '1.40.0'
|
||||
s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.'
|
||||
s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,598 @@
|
|||
|
||||
# Placeholder API
|
||||
|
||||
_TIUIElements_ добавляет `DefaultPlaceholderView` - плейсхолдер. Он представляет собой 3 вертикально расположенные view: `UIImageView`, `DefaultTitleSubtitleView`, `UIStackView`.
|
||||
|
||||
> `UIStackView` используется для добавления кнопок в различном количестве и с необходимым расположением (горизонтальным/вертикальным)
|
||||
|
||||
## Принцип работы
|
||||
|
||||
Для создания и конфигурации плейсхолдера существует фабрика `PlaceholderFactory`. Для этого следует воспользоваться методом `createImageStylePlaceholder(_:)`.
|
||||
|
||||
```swift
|
||||
import TIUIElements
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
let factory = PlaceholderFactory()
|
||||
let defaultPlaceholderView = factory.createLoadingDataErrorPlaceholder()
|
||||
```
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Метод `createImageStylePlaceholder(_:)` принимает в себя аргумент типа `DefaultPlaceholderStyle`, позволяющий полностью сконфигурировать создаваемую view
|
||||
|
||||
```swift
|
||||
let styleWithText = DefaultPlaceholderStyle(titleSubtitle: .init(title: "Server error has occured!"))
|
||||
let placeholderWithErrorText = factory.createImageStylePlaceholder(styleWithText)
|
||||
```
|
||||
|
||||
Возможные опции для конфигурации:
|
||||
|
||||
- картинка
|
||||
- текст (_title_, _subtitle_)
|
||||
- внешний вид плейсхолдера в целом и каждой отдельной view
|
||||
- расположение view внутри плейсхолдера относительно друг друга
|
||||
- расоложение кнопок внутри *stackView* (горизонтальное/вертикальное)
|
||||
- внешний вид кнопок
|
||||
- текст и картинка кнопок
|
||||
- действия кнопок
|
||||
|
||||
> Стоит учитывать, что если картинка плейсхолдера не указывается, то `UIImageView` будет скрыт. Тот же принцип касается и кнопок - если вы не добавили стилей кнопок, то `UIStackView` скрывается.
|
||||
|
||||
```swift
|
||||
let customStyle = DefaultPlaceholderStyle(
|
||||
image: UIImage(named: "proj-placeholder-image"),
|
||||
titleSubtitle: .init(title: "An error has occured", subtitle: "Please, reload the page"),
|
||||
appearance: .make { placeholder in
|
||||
placeholder.backgroundColor = .blue
|
||||
|
||||
placeholder.textViewAppearance { textView in
|
||||
textView.titleAppearance { title in
|
||||
title.textAttributes = .init(font: .systemFont(ofSize: 20),
|
||||
color: .black,
|
||||
alignment: .natural,
|
||||
isMultiline: false)
|
||||
}
|
||||
textView.subtitleAppearance = textView.titleAppearance
|
||||
}
|
||||
|
||||
}, buttonsStyles: [
|
||||
.init(titles: [.normal: "Reload"],
|
||||
appearance: [
|
||||
.normal: UIButton.DefaultAppearance(textAttributes: .init(font: .systemFont(ofSize: 20),
|
||||
color: .black,
|
||||
alignment: .natural,
|
||||
isMultiline: false))
|
||||
])
|
||||
])
|
||||
|
||||
let placeholderView = factory.createImageStylePlaceholder(customStyle)
|
||||
```
|
||||
|
||||
### Методы конфигурации
|
||||
|
||||
Как видно из прошлого примера даже самая простая настройка может выглядеть очень громоздко. Для улучшения читаемости и простоты переиспользования, `DefaultPlaceholderStyle` соответствует протоколу `PlaceholderStyle`, который добавляет следующие методы для конструирования стилей плейсхолдеров:
|
||||
|
||||
- `make(_:)`: статический метод для создания стиля. Принимает в себя функцию с переменной типа создоваемого стиля
|
||||
- `update(_:)`: метод для изменения уже существующего стиля. Принимает в себя функцию с переменной типа создаваемого стиля
|
||||
- `updateAppearance(_:)`: метод для изменения внешнего вида плейсхолдера. Принимает в себя функцию с переменной типа `ViewAppearance`. В случае `DefaultPlaceholderStyle` это `DefaultPlaceholderView.Appearance`
|
||||
- `withButton(_:)`: метод для добавления новой кнопки. Принимает в себя фунцию с переменной типа `PlaceholderButtonStyle`.
|
||||
- `withButtons(_:axis:_:)`: метод для добавления/изменения срузу нескольких кнопок. Первым агрументом указывается количество кнопок, вторым их расположение, третьим - функция двух переменных, где первая переменная - это номер (индекс) кнопки, вторая - `PlaceholderButtonStyle`
|
||||
|
||||
```swift
|
||||
let styleFromMake = DefaultPlaceholderStyle.make { style in
|
||||
style.image = UIImage(named: "proj-placeholder-image")
|
||||
style.titleSubtitle = .init(title: "An error has occured", subtitle: "Please, reload the page")
|
||||
}
|
||||
|
||||
let styleWithUpdatedImage = styleFromMake.update { style in
|
||||
style.image = UIImage(named: "proj-other-placeholder-image")
|
||||
}
|
||||
|
||||
let styleWithAppearance = styleFromMake.updateAppearance { placeholder in
|
||||
placeholder.backgroundColor = .blue
|
||||
|
||||
placeholder.textViewAppearance { textView in
|
||||
textView.titleAppearance { title in
|
||||
title.textAttributes = .init(font: .systemFont(ofSize: 20),
|
||||
color: .black,
|
||||
alignment: .natural,
|
||||
isMultiline: false)
|
||||
}
|
||||
|
||||
textView.subtitleAppearance = textView.titleAppearance
|
||||
}
|
||||
}
|
||||
|
||||
let styleWithButton = styleWithAppearance.withButton { buttonStyle in
|
||||
buttonStyle.titles = [.normal: "Reload"]
|
||||
buttonStyle.appearance = [.normal: UIButton.DefaultAppearance.make { button in
|
||||
button.textAttributes = .init(font: .systemFont(ofSize: 20),
|
||||
color: .black,
|
||||
alignment: .natural,
|
||||
isMultiline: false)
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
Стоит обратить внимание на то, что если бы метод использовался на `styleWithButton`, то у уже добавленной кнопки (она была бы с индексом 0) сохранился стиль, так что его можно было либо дополнить, либо переопределить
|
||||
|
||||
```swift
|
||||
let styleWithTwoButtons = styleWithAppearance.withButtons(2, axis: .vertical) { index, buttonStyle in
|
||||
if index == .zero {
|
||||
buttonStyle.titles = [.normal: "Reload"]
|
||||
buttonStyle.appearance = [.normal: UIButton.DefaultAppearance.make { button in
|
||||
button.textAttributes = .init(font: .systemFont(ofSize: 20),
|
||||
color: .black,
|
||||
alignment: .natural,
|
||||
isMultiline: false)
|
||||
}]
|
||||
}
|
||||
|
||||
if index == 1 {
|
||||
buttonStyle.titles = [.normal: "Wait"]
|
||||
buttonStyle.appearance = [.normal: UIButton.DefaultAppearance.make { button in
|
||||
button.textAttributes = .init(font: .systemFont(ofSize: 20),
|
||||
color: .black,
|
||||
alignment: .natural,
|
||||
isMultiline: false)
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Стандартные стили
|
||||
|
||||
Для удобства у `PlaceholderFactory` есть несколько стандартных стилей, которые можно использовать для ускоренной разработки и изменять под свои нужды при необходимости:
|
||||
|
||||
- `errorStyle`
|
||||
- `loadingDataErrorStyle`
|
||||
- `emptyStateStyle`
|
||||
|
||||
У `PlaceholderFactory` есть готовые методы для создания плейсхолдера с такими стилями
|
||||
|
||||

|
||||
|
||||
```swift
|
||||
let errorStylePlaceholder = factory.createErrorPlaceholder()
|
||||
```
|
||||
|
||||

|
||||
|
||||
```swift
|
||||
let loadingDataErrorStylePlaceholder = factory.createLoadingDataErrorPlaceholder()
|
||||
```
|
||||
|
||||

|
||||
|
||||
```swift
|
||||
let emptyStateStylePlaceholder = factory.createEmptyStatePlaceholder()
|
||||
```
|
||||
|
||||
### Кастомизация стандартных стилей
|
||||
|
||||
Стили `errorStyle`, `loadingDataErrorStyle`, `emptyStateStyle` могут показывать, лежащие в **Assets**. Для этого необходимо только называть картинки: _placeholder_error_icon_, _placeholder_loading_data_icon_, _placeholder_empty_state_icon_ соответственно. Если нужной картинки не будет, то она просто не отобразится в плейсхолдере. При таком изменении никаких дополнительных настроек делать не нужно.
|
||||
|
||||
Также при создании фабрики, можно передать в нее объект соответствующий протоколу `PlaceholderLocalizationProvider` для добавления необходимого текста.
|
||||
|
||||
При добавлении иных изменений нужно либо делать наследника `PlaceholderFactory`, либо передавать стиль через метод `createImageStylePlaceholder(_:)`. В обоих случаях изменить стандартный стиль можно через соответствующие методы фабрики:
|
||||
|
||||
- `errorStyle()`
|
||||
- `loadingDataErrorStyle()`
|
||||
- `emptyStateStyle()`
|
||||
|
||||
```swift
|
||||
class CustomViewController: BaseInitializableViewController {
|
||||
private var currentPlaceholder: UIView?
|
||||
|
||||
private var customErrorStyle: DefaultPlaceholderStyle {
|
||||
factoryWithCustomErrorStyle.errorStyle()
|
||||
.update { style in
|
||||
style.withButtons(1, axis: .vertical) { _, buttonStyle in
|
||||
buttonStyle.action = (target: nil, action: #selector(closePlaceholder), event: .touchUpInside)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let factoryWithCustomErrorStyle = PlaceholderFactory()
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// some activity with error result...
|
||||
|
||||
let currentPlaceholder = factoryWithCustomErrorStyle.createImageStylePlaceholder(customErrorStyle)
|
||||
|
||||
// custom presentation of the placeholder...
|
||||
|
||||
self.currentPlaceholder = currentPlaceholder
|
||||
}
|
||||
|
||||
@objc private func closePlaceholder() {
|
||||
currentPlaceholder?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Использование плейсхолдеров без фабрики
|
||||
|
||||
Если необходимо использовать заглушки без фабрики, то их конфигурацию можно доверить методу `apply(style:)` у каждого `DefaultPlaceholderView`
|
||||
|
||||
```swift
|
||||
class PlaceholderHolderViewController: BaseInitializableViewController, ConfigurableView {
|
||||
|
||||
private let placeholder = DefaultPlaceholderView()
|
||||
|
||||
override func addViews() {
|
||||
super.addViews()
|
||||
|
||||
view.addSubview(placeholder)
|
||||
}
|
||||
|
||||
override func configureLayout() {
|
||||
super.configureLayout()
|
||||
|
||||
placeholder.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
placeholder.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
placeholder.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
placeholder.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
placeholder.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
override func configureAppearance() {
|
||||
super.configureAppearance()
|
||||
|
||||
placeholder.isHidden = true
|
||||
}
|
||||
|
||||
func configure(with error: ErrorType) {
|
||||
switch error {
|
||||
case .internetConnection:
|
||||
placeholder.apply(style: Self.internetConnectionErrorStyle)
|
||||
|
||||
case .unknown:
|
||||
placeholder.apply(style: Self.unknownErrorStyle)
|
||||
}
|
||||
|
||||
placeholder.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
extension PlaceholderHolderViewController {
|
||||
static var internetConnectionErrorStyle: DefaultPlaceholderStyle {
|
||||
factory.errorStyle().update { _ in
|
||||
// some configurations
|
||||
}
|
||||
}
|
||||
|
||||
static var unknownErrorStyle: DefaultPlaceholderStyle {
|
||||
factory.errorStyle().update { _ in
|
||||
// some configurations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ErrorType {
|
||||
case internetConnection
|
||||
case unknown
|
||||
}
|
||||
|
||||
import PlaygroundSupport
|
||||
|
||||
let placeholder = PlaceholderHolderViewController()
|
||||
|
||||
PlaygroundPage.current.liveView = placeholder
|
||||
|
||||
placeholder.configure(with: .internetConnection)
|
||||
```
|
||||
|
||||
## Создание кастомных заглушек
|
||||
|
||||
Если необходимо показывать что-то кроме `UIImageView`, можно создать наследника `BasePlaceholderView`.
|
||||
|
||||
В качестве примера показан заглушка с lottie анимацией:
|
||||
|
||||
```swift
|
||||
import Lottie
|
||||
|
||||
public final class LottiePlaceholderStyle: BasePlaceholderStyle<LottiePlaceholderView.Appearance>, PlaceholderStyle {
|
||||
|
||||
public static var defaultStyle: LottiePlaceholderStyle {
|
||||
.init()
|
||||
}
|
||||
|
||||
public var animationName: String
|
||||
public var animationSpeed: CGFloat
|
||||
public var loopMode: LottieLoopMode
|
||||
|
||||
public init(titleSubtitle: DefaultTitleSubtitleViewModel = .init(),
|
||||
appearance: LottiePlaceholderView.Appearance = .defaultAppearance,
|
||||
controlsViewAxis: NSLayoutConstraint.Axis = .vertical,
|
||||
buttonsStyles: [PlaceholderButtonStyle] = [],
|
||||
animationName: String = "",
|
||||
animationSpeed: CGFloat = 1,
|
||||
loopMode: LottieLoopMode = .loop) {
|
||||
|
||||
self.animationName = animationName
|
||||
self.animationSpeed = animationSpeed
|
||||
self.loopMode = loopMode
|
||||
|
||||
super.init(titleSubtitle: titleSubtitle,
|
||||
appearance: appearance,
|
||||
controlsViewAxis: controlsViewAxis,
|
||||
buttonsStyles: buttonsStyles)
|
||||
}
|
||||
}
|
||||
|
||||
public final class LottiePlaceholderView: BasePlaceholderView<LottieAnimationView> {
|
||||
public override var isImageViewHidden: Bool {
|
||||
super.isImageViewHidden || imageView.animation == nil
|
||||
}
|
||||
|
||||
public func apply(style: LottiePlaceholderStyle) {
|
||||
imageView.animation = LottieAnimation.named(style.animationName)
|
||||
imageView.animationSpeed = style.animationSpeed
|
||||
imageView.loopMode = style.loopMode
|
||||
imageView.play()
|
||||
|
||||
super.applyBaseStyle(style: style)
|
||||
|
||||
configureImageSizeConstraints(size: imageView.animation?.size ?? .zero)
|
||||
}
|
||||
|
||||
public func configure(appearance: Appearance) {
|
||||
configureAppearance(appearance: appearance)
|
||||
}
|
||||
|
||||
private func configureImageSizeConstraints(size: CGSize) {
|
||||
guard size != .zero else {
|
||||
return
|
||||
}
|
||||
|
||||
if size.height.isFinite, size.height > .zero {
|
||||
imageViewConstraints?.widthConstraint?.constant = size.height
|
||||
}
|
||||
|
||||
if size.width.isFinite, size.width > .zero {
|
||||
imageViewConstraints?.widthConstraint?.constant = size.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LottiePlaceholderView {
|
||||
public final class Appearance: BaseAppearance<UIView.DefaultWrappedAppearance>, ViewAppearance {
|
||||
public static var defaultAppearance: Self {
|
||||
.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LottieAnimationViewController: BaseViewController<UIView, Void> {
|
||||
let placeholderFactory = PlaceholderFactory()
|
||||
|
||||
static var lottieStyle: LottiePlaceholderStyle {
|
||||
.make {
|
||||
$0.animationName = "cat"
|
||||
$0.titleSubtitle = .init(title: "Long time no see, Nyan cat")
|
||||
}
|
||||
.updateAppearance {
|
||||
$0.imageViewAppearance.layout {
|
||||
$0.size = .fixedHeight(250)
|
||||
}
|
||||
|
||||
$0.textViewAppearance {
|
||||
$0.titleAppearance {
|
||||
$0.textAttributes = .init(font: .boldSystemFont(ofSize: 25), color: .systemPink, alignment: .center, isMultiline: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
showPlaceholder()
|
||||
}
|
||||
|
||||
func showPlaceholder() {
|
||||
let placeholder = LottiePlaceholderView()
|
||||
placeholder.apply(style: Self.lottieStyle)
|
||||
|
||||
placeholder.frame = view.frame
|
||||
view.addSubview(placeholder)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Плейсхолдеры для UIImageView
|
||||
|
||||
Вместе с полноразмерными заглушками была добавлена новая `UIImageView`, способная отображать картинку-плейсхолдер пока не присвоена image
|
||||
|
||||
```swift
|
||||
let placeholderImage = UIImage(named: "placeholder-image")
|
||||
```
|
||||
|
||||
Теперь при использовании данного imageView будет отображаться картинка, созданная выше
|
||||
|
||||
```swift
|
||||
let placeholderImageView = DefaultPlaceholderImageView(placeholderImage: placeholderImage)
|
||||
```
|
||||
|
||||
Здесь все еще отображается `placeholderImage`
|
||||
|
||||
```swift
|
||||
placeholderImageView.image = nil
|
||||
```
|
||||
|
||||
Здесь placeholderImage спрячится
|
||||
|
||||
```swift
|
||||
placeholderImageView.image = UIImage(named: "image")
|
||||
```
|
||||
|
||||
При обнулении картинки `placeholderImage` покажется заново
|
||||
|
||||
```swift
|
||||
placeholderImageView.image = nil
|
||||
```
|
||||
|
||||
> При этом необязательно создавать картинку плейсхолдера отдельно. Создайте картинку с именем _global_image_placeholder_icon_ в Assets каталоге и она сама подгрузится в `DefaultPlaceholderImageView` при использовании инициализатора `init(image:placeholderImage:)`
|
||||
### Пример контроллера
|
||||
|
||||

|
||||
|
||||
```swift
|
||||
import TITableKitUtils
|
||||
import TableKit
|
||||
|
||||
class WorkingCatView: BaseInitializableView, ConfigurableView, AppearanceConfigurable {
|
||||
|
||||
typealias ViewModel = (image: UIImage?, title: String?, subtitle: String?)
|
||||
|
||||
private let catImageView = DefaultPlaceholderImageView()
|
||||
private let catLabel = DefaultTitleSubtitleView()
|
||||
|
||||
override func addViews() {
|
||||
super.addViews()
|
||||
|
||||
addSubviews(catImageView, catLabel)
|
||||
}
|
||||
|
||||
override func configureLayout() {
|
||||
super.configureLayout()
|
||||
|
||||
[catImageView, catLabel]
|
||||
.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
catImageView.topAnchor.constraint(equalTo: topAnchor, constant: 20),
|
||||
catImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
catImageView.heightAnchor.constraint(equalToConstant: 90),
|
||||
catImageView.widthAnchor.constraint(equalToConstant: 90),
|
||||
|
||||
catLabel.topAnchor.constraint(equalTo: catImageView.bottomAnchor, constant: 20),
|
||||
catLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
|
||||
catLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
|
||||
catLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20),
|
||||
])
|
||||
}
|
||||
|
||||
override func configureAppearance() {
|
||||
super.configureAppearance()
|
||||
|
||||
catImageView.contentMode = .scaleAspectFit
|
||||
}
|
||||
|
||||
func configure(with viewModel: ViewModel) {
|
||||
catImageView.image = viewModel.image
|
||||
catLabel.configure(with: .init(title: viewModel.title, subtitle: viewModel.subtitle))
|
||||
catImageView.placeholderImage = UIImage.gifImageWithName("cat-loader")
|
||||
}
|
||||
|
||||
func configure(appearance: Appearance) {
|
||||
catImageView.configure(appearance: appearance.catImageAppearance)
|
||||
catLabel.configure(appearance: appearance.catLabelAppearance)
|
||||
configureUIView(appearance: appearance)
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkingCatView {
|
||||
final class Appearance: UIView.BaseWrappedAppearance<UIView.DefaultWrappedLayout>, WrappedViewAppearance {
|
||||
static var defaultAppearance: Self {
|
||||
.init()
|
||||
}
|
||||
|
||||
var catImageAppearance: DefaultPlaceholderImageView.Appearance
|
||||
var catLabelAppearance: DefaultTitleSubtitleView.Appearance
|
||||
|
||||
public init(layout: UIView.DefaultWrappedLayout = .defaultLayout,
|
||||
backgroundColor: UIColor = .clear,
|
||||
border: UIViewBorder = .init(),
|
||||
shadow: UIViewShadow? = nil,
|
||||
catImageAppearance: DefaultPlaceholderImageView.Appearance = .defaultAppearance,
|
||||
catLabelAppearance: DefaultTitleSubtitleView.Appearance = .defaultAppearance) {
|
||||
|
||||
self.catImageAppearance = catImageAppearance
|
||||
self.catLabelAppearance = catLabelAppearance
|
||||
|
||||
super.init(layout: layout,
|
||||
backgroundColor: backgroundColor,
|
||||
border: border,
|
||||
shadow: shadow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CatsViewController: BaseViewController<TableKitTableView, Void> {
|
||||
|
||||
typealias ImageRow = WorkingCatView.InTableRow
|
||||
|
||||
let viewModels: [WorkingCatView.ViewModel] = [
|
||||
(image: UIImage(named: "cat-worker"), title: "Pusic", subtitle: "C++ dev"),
|
||||
(image: nil, title: "Luke", subtitle: "Jedi"), // image can't be loaded
|
||||
(image: .petDog, title: "Marzia", subtitle: "HR"),
|
||||
(image: .petFox, title: "Fox", subtitle: "iOS Dev")
|
||||
]
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// I swear it's a network request, images are loading ;)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { [weak self] in
|
||||
|
||||
let rows = (self?.viewModels ?? []).compactMap {
|
||||
self?.createRow($0)
|
||||
}
|
||||
self?.tableDirector.replace(withRows: rows)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindViews() {
|
||||
super.bindViews()
|
||||
|
||||
tableDirector += viewModels.map {
|
||||
createRow((nil, $0.title, $0.subtitle))
|
||||
}
|
||||
}
|
||||
|
||||
private func createRow(_ viewModel: WorkingCatView.ViewModel) -> ImageRow {
|
||||
ImageRow(item: viewModel)
|
||||
.with(appearance: Self.rowAppearance)
|
||||
}
|
||||
}
|
||||
|
||||
extension CatsViewController {
|
||||
static var rowAppearance: ImageRow.Appearance {
|
||||
.make { row in
|
||||
row.subviewAppearance { container in
|
||||
container.layout.insets = .edges(16)
|
||||
container.backgroundColor = UIColor(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 0.3)
|
||||
container.border = .init(color: .black, width: 1, cornerRadius: 10, roundedCorners: .allCorners)
|
||||
|
||||
container.catImageAppearance = Self.imageAppearance
|
||||
container.catLabelAppearance = Self.textAppearance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var imageAppearance: DefaultPlaceholderImageView.Appearance {
|
||||
.make {
|
||||
$0.border = .init(cornerRadius: 12, roundedCorners: .allCorners)
|
||||
$0.subviewAppearance.update {
|
||||
$0.backgroundColor = UIColor(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var textAppearance: DefaultTitleSubtitleView.Appearance {
|
||||
.make {
|
||||
$0.titleAppearance.textAttributes = .init(font: .systemFont(ofSize: 25), color: .black, alignment: .center, isMultiline: false)
|
||||
$0.subtitleAppearance.textAttributes = .init(font: .italicSystemFont(ofSize: 18), color: .gray, alignment: .center, isMultiline: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 779 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 730 KiB |
|
|
@ -150,7 +150,7 @@ class CustomConfigurableSkeletonableView: UIView, SkeletonsPresenter {
|
|||
- форма
|
||||
- отступы
|
||||
|
||||
При этом все view делятся на:
|
||||
При этом все view делятся на:
|
||||
- `UIView` с subviews (контейнеры)
|
||||
- `UIView` без subviews
|
||||
- `UILabel`
|
||||
|
|
@ -162,9 +162,9 @@ class CustomConfigurableSkeletonableView: UIView, SkeletonsPresenter {
|
|||
### Анимация
|
||||
|
||||
`SkeletonsConfiguration` для настройки анимации принимает тип `(SkeletonsLayer) -> CAAnimationGroup`. Это означает, что при необходимости вы можете создать любую необходимую анимацию. Анимация будет применена к каждому скелетону.
|
||||
|
||||
|
||||
Однако для удобства существует уже определенный класс `SkeletonsAnimationBuilder` со статическим методом `createDirectionalGradientAnimation(_:)` для создания анимаций в одну из сторон:
|
||||
|
||||
|
||||
```swift
|
||||
public enum SkeletonsAnimationDirection {
|
||||
case leftToRight
|
||||
|
|
@ -179,12 +179,12 @@ class CustomConfigurableSkeletonableView: UIView, SkeletonsPresenter {
|
|||
```
|
||||
|
||||
```swift
|
||||
let confWithLeftToRightAnim = SkeletonsConfiguration(animation: { _ in
|
||||
let confWithLeftToRightAnim = SkeletonsConfiguration(animation: { _ in
|
||||
let animConfig = DirectionalSkeletonsAnimationConfiguration(direction: .leftToRight, duration: 1.5)
|
||||
return SkeletonsAnimationBuilder.createDirectionalGradientAnimation(animConfig)
|
||||
})
|
||||
|
||||
let confWithTopToBottomAnim = SkeletonsConfiguration(animation: { _ in
|
||||
let confWithTopToBottomAnim = SkeletonsConfiguration(animation: { _ in
|
||||
let animConfig = DirectionalSkeletonsAnimationConfiguration(direction: .topToBottom, duration: 1.5)
|
||||
return SkeletonsAnimationBuilder.createDirectionalGradientAnimation(animConfig)
|
||||
})
|
||||
|
|
@ -267,7 +267,7 @@ var confWithPadding: SkeletonsConfiguration {
|
|||
## Что если нужно больше?
|
||||
|
||||
Если стандартной настройки не хватает, то в конфигуратор можно передать объект, соответствующий протоколу `SkeletonsConfigurationDelegate` через который можно настроить слой скелетона для каждой вью отдельно
|
||||
|
||||
|
||||
```swift
|
||||
public protocol SkeletonsConfigurationDelegate: AnyObject {
|
||||
func layerDidConfigured(_ layer: SkeletonLayer, forViewType type: SkeletonLayer.ViewType)
|
||||
|
|
|
|||
Loading…
Reference in New Issue