Merge pull request 'feat: Custom string attributes to `BaseTextAttributes`' (#15) from feature/uiviewbackground into master

Reviewed-on: #15
This commit is contained in:
Ivan Smolin 2023-10-09 23:52:09 +03:00
commit 767c19d17b
69 changed files with 973 additions and 222 deletions

View File

@ -1,5 +1,12 @@
# Changelog
### 1.53.0
- **Added**: Custom string attributes to `BaseTextAttributes`
- **Added**: Customizeable `UIViewBackground` and `UIViewBorder` for `UIView.Appearance`
- **Added**: Keychain single value storage for codable models -`CodableSingleValueKeychainStorage`
- **Update**: Renamed methods `startAnimation` and `stopAnimation` of `SkeletonPresenter`, so it won't conflict with `Animatable` protocol anymore
### 1.52.0
- **Added**: `TIApplication` module with core dependencies of main application and its extension targets

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIAppleMapUtils'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -0,0 +1,54 @@
//
// 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
public struct BundleIdentifier {
public let appPrefix: String
public let appIdentifier: String
public let fullIdentifier: String
public let defaultAppGroupIdenfier: String
static let bundleIdentifierSeparator = "."
public init(appPrefix: String, appIdentifier: String) {
self.appPrefix = appPrefix
self.appIdentifier = appIdentifier
self.fullIdentifier = appPrefix + Self.bundleIdentifierSeparator + appIdentifier
self.defaultAppGroupIdenfier = "group." + fullIdentifier
}
public init?(bundle: Bundle = .main) {
guard let fullIdenfifier = bundle.bundleIdentifier,
var components = bundle.bundleIdentifier?
.components(separatedBy: Self.bundleIdentifierSeparator),
let lastComponent = components.popLast() else {
return nil
}
self.fullIdentifier = fullIdenfifier
self.appIdentifier = lastComponent
self.appPrefix = components.joined(separator: Self.bundleIdentifierSeparator)
self.defaultAppGroupIdenfier = "group." + fullIdentifier
}
}

View File

@ -50,8 +50,7 @@ public struct CoreDependencies {
public var networkCallbackQueue: DispatchQueue
public init(bundleIdentifierPrefix: String,
appIdentifier: String,
public init(bundleIdentifier: BundleIdentifier,
customAppGroupIdentifier: String? = nil) {
jsonCodingConfigurator = JsonCodingConfigurator(dateFormattersReusePool: dateFormattersResusePool,
@ -60,14 +59,12 @@ public struct CoreDependencies {
jsonKeyValueDecoder = JSONKeyValueDecoder(jsonDecoder: jsonCodingConfigurator.jsonDecoder)
jsonKeyValueEncoder = JSONKeyValueEncoder(jsonEncoder: jsonCodingConfigurator.jsonEncoder)
let bundleIdentifier = bundleIdentifierPrefix + "." + appIdentifier
logger = DefaultOSLogErrorLogger(subsystem: bundleIdentifier.fullIdentifier, category: "general")
logger = DefaultOSLogErrorLogger(subsystem: bundleIdentifier, category: "general")
keychain = Keychain(service: bundleIdentifier)
keychain = Keychain(service: bundleIdentifier.fullIdentifier).accessibility(.whenUnlockedThisDeviceOnly)
defaults = .standard
let appGroupIdentifier = customAppGroupIdentifier ?? "group." + bundleIdentifierPrefix
let appGroupIdentifier = customAppGroupIdentifier ?? bundleIdentifier.defaultAppGroupIdenfier
if let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) {
var appGroupCacheURL: URL
@ -93,13 +90,27 @@ public struct CoreDependencies {
}
appGroupDefaults = UserDefaults(suiteName: appGroupIdentifier)
appGroupKeychain = Keychain(service: bundleIdentifierPrefix, accessGroup: appGroupIdentifier)
appGroupKeychain = Keychain(service: bundleIdentifier.appPrefix,
accessGroup: appGroupIdentifier)
.accessibility(.whenUnlockedThisDeviceOnly)
} else {
appGroupCacheDirectory = nil
appGroupDefaults = nil
appGroupKeychain = nil
}
networkCallbackQueue = DispatchQueue(label: bundleIdentifier + ".network-callback-queue", attributes: .concurrent)
networkCallbackQueue = DispatchQueue(label: bundleIdentifier.fullIdentifier + ".network-callback-queue",
attributes: .concurrent)
}
public init?(bundle: Bundle = .main,
customAppGroupIdentifier: String? = nil) {
guard let bundleIdentifier = BundleIdentifier(bundle: bundle) else {
return nil
}
self.init(bundleIdentifier: bundleIdentifier,
customAppGroupIdentifier: customAppGroupIdentifier)
}
}

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIApplication'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Application architecture.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIAuth'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Login, registration, confirmation and other related actions'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -34,8 +34,8 @@ extension BaseModalViewController {
public var footerViewState: ModalFooterView<FooterContentView>.State
public init(layout: UIView.DefaultWrappedLayout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
presentationDetents: [ModalViewPresentationDetent] = [.maxHeight],
dragViewState: DragView.State = .hidden,
@ -48,7 +48,7 @@ extension BaseModalViewController {
self.footerViewState = footerViewState
super.init(layout: layout,
backgroundColor: backgroundColor,
background: background,
border: border,
shadow: shadow)
}

View File

@ -153,7 +153,7 @@ open class BaseModalViewController<ContentView: UIView,
// MARK: - Modal View Controller Configuration
public var viewControllerAppearance: BaseAppearance = .init(backgroundColor: .white)
public var viewControllerAppearance: BaseAppearance = .init(background: UIViewColorBackground(color: .white))
open var panScrollable: UIScrollView? {
contentView as? UIScrollView

View File

@ -34,11 +34,11 @@ public final class DragView: BaseInitializableView, AppearanceConfigurable {
}
static var dragViewBorder: UIViewBorder {
UIViewBorder(cornerRadius: dragViewSize.height / 2, roundedCorners: .allCorners)
UIViewRoundedBorder(cornerRadius: dragViewSize.height / 2)
}
static var dragViewBackgroundColor: UIColor {
.lightGray
static var dragViewBackground: UIViewColorBackground {
UIViewColorBackground(color: .lightGray)
}
static var dragViewSize: CGSize {
@ -57,7 +57,7 @@ public final class DragView: BaseInitializableView, AppearanceConfigurable {
Self(layout: DefaultWrappedLayout(insets: .vertical(top: Constants.dragViewTopInset),
size: Constants.dragViewSize,
centerOffset: .centerHorizontal()),
backgroundColor: Constants.dragViewBackgroundColor,
background: Constants.dragViewBackground,
border: Constants.dragViewBorder)
}
}

View File

@ -219,14 +219,17 @@ public extension ModalHeaderView {
public var contentViewState: ContentViewState
public init(layout: DefaultWrappedLayout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
contentViewState: ContentViewState = .leadingButton(.init())) {
self.contentViewState = contentViewState
super.init(layout: layout, backgroundColor: backgroundColor, border: border, shadow: shadow)
super.init(layout: layout,
background: background,
border: border,
shadow: shadow)
}
}
}

View File

@ -15,7 +15,7 @@ class EmptyViewController: BaseModalViewController<UIView, UIView> { }
/*:
## Обертка вокруг существующего контроллера
Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `BaseModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер
Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `DefaultModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер
*/
import TIUIKitCore
@ -24,13 +24,13 @@ final class OldMassiveViewController: BaseInitializableViewController {
// some implementation
}
typealias ModalOldMassiveViewController = BaseModalWrapperViewController<OldMassiveViewController>
typealias ModalOldMassiveViewController = DefaultModalWrapperViewController<OldMassiveViewController>
class PresentingViewController: BaseInitializableViewController {
// some implementation
@objc private func onButtonTapped() {
presentPanModal(ModalOldMassiveViewController())
presentPanModal(ModalOldMassiveViewController(contentViewController: OldMassiveViewController()))
}
}
@ -48,16 +48,18 @@ class PresentingViewController: BaseInitializableViewController {
Вот пример настройки внешнего вида так, чтобы был видет dragView и headerView с левой кнопкой:
*/
import TIUIElements
let customViewController = BaseModalViewController<UIView, UIView>()
customViewController.viewControllerAppearance = BaseModalViewController.DefaultAppearance.make {
$0.dragViewState = .presented(.defaultAppearance)
$0.headerViewState = .presented(.make {
$0.layout.size = .fixedHeight(52)
$0.backgroundColor = .white
$0.contentViewState = .buttonLeft(.init(titles: [.normal: "Close"],
appearance: .init(stateAppearances: [
.normal: .init(backgroundColor: .blue)
])))
$0.contentViewState = .leadingButton(.init(titles: [.normal: "Close"],
appearance: .init(stateAppearances: [
.normal: .init(background: UIViewColorBackground(color: .blue))
])))
})
}
@ -84,10 +86,10 @@ detentsViewController.viewControllerAppearance.presentationDetents = [.headerOnl
let shadowViewController = BaseModalViewController<UIView, UIView>()
let dimmedView = PassthroughDimmedView()
dimmedView.hitTestHandlerView = view
dimmedView.configureUIView(appearance: .init(shadow: UIViewShadow(radius: 8,
color: .black,
opacity: 0.3)))
dimmedView.hitTestHandlerView = shadowViewController.view
dimmedView.configureUIView(appearance: DefaultAppearance(shadow: UIViewShadow(radius: 8,
color: .black,
opacity: 0.3)))
shadowViewController.dimmedView = dimmedView

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIBottomSheet'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Base models for creating bottom sheet view controllers'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TICoreGraphicsUtils'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'CoreGraphics drawing helpers'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIDeeplink'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Deeplink service API'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIDeveloperUtils'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Universal web view API'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIEcommerce'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Cart, products, promocodes, bonuses and other related actions'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIFoundationUtils'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Set of helpers for Foundation framework classes.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIGoogleMapUtils'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -0,0 +1,45 @@
//
// 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 TIFoundationUtils
import KeychainAccess
open class CodableSingleValueKeychainStorage<ValueType: Codable>: BaseSingleValueKeychainStorage<ValueType> {
public init(keychain: Keychain,
storageKey: StorageKey<ValueType>,
encoder: CodableKeyValueEncoder = JSONKeyValueEncoder(),
decoder: CodableKeyValueDecoder = JSONKeyValueDecoder()) {
let getValueClosure: GetValueClosure = {
$0.codableObject(forKey: $1, decoder: decoder)
}
let storeValueClosure: StoreValueClosure = {
$0.set(encodableObject: $1, forKey: $2, encoder: encoder)
}
super.init(keychain: keychain,
storageKey: storageKey,
getValueClosure: getValueClosure,
storeValueClosure: storeValueClosure)
}
}

View File

@ -60,10 +60,10 @@ import Foundation
let defaults = UserDefaults.standard // or AppGroup defaults
let appReinstallChecker = AppReinstallChecker(defaultsStorage: defaults,
storageKey: .deleteApiToken)
let appReinstallChecker = DefaultAppFirstRunCheckStorage(defaults: defaults,
storageKey: .deleteApiToken)
let appInstallAwareTokenStorage = apiTokenKeychainStorage.appInstallLifetimeStorage(reinstallChecker: appReinstallChecker)
let appInstallAwareTokenStorage = apiTokenKeychainStorage.appInstallLifetimeStorage(appFirstRunCheckStorage: appReinstallChecker)
if appInstallAwareTokenStorage.hasStoredValue() {
// app wasn't reinstalled, token is exist

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIKeychainUtils'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Set of helpers for Keychain classes.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TILogging'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Logging for TI libraries.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIMapUtils'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Set of helpers for map objects clustering and interacting.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIMoyaNetworking'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Moya + Swagger network service.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TINetworking'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Swagger-frendly networking layer helpers.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TINetworkingCache'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Caching results of EndpointRequests.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIPagination'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Generic pagination component.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }
@ -10,7 +10,14 @@ Pod::Spec.new do |s|
s.ios.deployment_target = '11.0'
s.swift_versions = ['5.7']
s.source_files = s.name + '/Sources/**/*'
sources = 'Sources/**/*.swift'
if ENV["DEVELOPMENT_INSTALL"] # installing using :path =>
s.source_files = sources
s.exclude_files = s.name + '.app'
else
s.source_files = s.name + '/' + sources
s.exclude_files = s.name + '/*.app'
end
s.dependency 'TISwiftUtils', s.version.to_s
s.dependency 'Cursors', "~> 0.6.0"

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TISwiftUICore'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Core UI elements: protocols, views and helpers.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TISwiftUtils'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Bunch of useful helpers for Swift development.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITableKitUtils'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Set of helpers for TableKit classes.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITextProcessing'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'A text processing service helping to get a text mask and a placeholder from incoming regex.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -45,14 +45,15 @@ extension UIButton {
}
}
open class BaseAppearance<Layout: ViewLayout, ContentLayout: BaseContentLayout & ViewLayout>: UIView.BaseAppearance<Layout> {
open class BaseAppearance<Layout: ViewLayout, ContentLayout: BaseContentLayout & ViewLayout>:
UIView.BaseAppearance<Layout> {
public var textAttributes: BaseTextAttributes?
public var contentLayout: ContentLayout
public init(layout: Layout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
textAttributes: BaseTextAttributes? = nil,
contentLayout: ContentLayout = .defaultLayout) {
@ -61,7 +62,7 @@ extension UIButton {
self.contentLayout = contentLayout
super.init(layout: layout,
backgroundColor: backgroundColor,
background: background,
border: border,
shadow: shadow)
}

View File

@ -29,15 +29,15 @@ extension UILabel {
public var textAttributes: BaseTextAttributes?
public init(layout: Layout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
textAttributes: BaseTextAttributes? = nil) {
self.textAttributes = textAttributes
super.init(layout: layout,
backgroundColor: backgroundColor,
background: background,
border: border,
shadow: shadow)
}

View File

@ -25,12 +25,9 @@ import UIKit
public extension UIView {
func configureUIView(appearance: BaseAppearance<some ViewLayout>) {
backgroundColor = appearance.backgroundColor
appearance.background.apply(to: self)
layer.masksToBounds = true
layer.maskedCorners = appearance.border.roundedCorners
layer.cornerRadius = appearance.border.cornerRadius
layer.borderWidth = appearance.border.width
layer.borderColor = appearance.border.color.cgColor
appearance.border.apply(to: self)
guard let shadow = appearance.shadow else {
return

View File

@ -29,17 +29,17 @@ extension UIView {
open class BaseAppearance<Layout: ViewLayout> {
public var layout: Layout
public var backgroundColor: UIColor
public var background: UIViewBackground
public var border: UIViewBorder
public var shadow: UIViewShadow?
public init(layout: Layout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil) {
self.layout = layout
self.backgroundColor = backgroundColor
self.background = background
self.border = border
self.shadow = shadow
}
@ -59,15 +59,15 @@ extension UIView {
public var subviewAppearance: SubviewAppearance
public init(layout: Layout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
subviewAppearance: SubviewAppearance = .defaultAppearance) {
self.subviewAppearance = subviewAppearance
super.init(layout: layout,
backgroundColor: backgroundColor,
background: background,
border: border,
shadow: shadow)
}
@ -75,9 +75,9 @@ extension UIView {
open class BaseWrappedAppearance<Layout: WrappedViewLayout>: BaseAppearance<Layout> {}
public final class DefaultWrappedViewHolderAppearance<SubviewAppearance: WrappedViewAppearance,
Layout: ViewLayout>: BaseWrappedViewHolderAppearance<SubviewAppearance, Layout>,
WrappedViewHolderAppearance {
public final class DefaultWrappedViewHolderAppearance<SubviewAppearance: WrappedViewAppearance, Layout: ViewLayout>:
BaseWrappedViewHolderAppearance<SubviewAppearance, Layout>,
WrappedViewHolderAppearance {
public static var defaultAppearance: Self {
Self()
}
@ -97,3 +97,14 @@ extension UIView {
}
extension UIView.DefaultWrappedViewHolderAppearance: WrappedViewAppearance where Layout: WrappedViewLayout {}
public extension UIView.BaseAppearance {
var backgroundColor: UIColor? {
get {
(background as? UIViewColorBackground)?.color
}
set {
background = UIViewColorBackground(color: newValue)
}
}
}

View File

@ -0,0 +1,83 @@
//
// 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 QuartzCore
public struct GradientValues {
public enum Defaults {
public static var colors: [CGColor]? {
nil
}
public static var locations: [NSNumber]? {
nil
}
public static var startPoint: CGPoint {
CGPoint(x: 0.5, y: 0)
}
public static var endPoint: CGPoint {
CGPoint(x: 0.5, y: 1)
}
public static var type: CAGradientLayerType {
.axial
}
}
public var colors: [CGColor]?
public var locations: [NSNumber]?
public var startPoint: CGPoint
public var endPoint: CGPoint
public var type: CAGradientLayerType
public init(colors: [CGColor]? = Defaults.colors,
locations: [NSNumber]? = Defaults.locations,
startPoint: CGPoint = Defaults.startPoint,
endPoint: CGPoint = Defaults.endPoint,
type: CAGradientLayerType = Defaults.type) {
self.colors = colors
self.locations = locations
self.startPoint = startPoint
self.endPoint = endPoint
self.type = type
}
}
public extension GradientValues {
typealias Stop = (color: CGColor, location: CGFloat)
static func elliptical(stops: [Stop],
center: CGPoint,
outerEdge: CGPoint) -> Self {
GradientValues(colors: stops.map { $0.color },
locations: stops.map { $0.location as NSNumber },
startPoint: center,
endPoint: outerEdge,
type: .radial)
}
}

View File

@ -0,0 +1,42 @@
//
// 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 UIViewColorBackground: UIViewBackground {
public var color: UIColor?
public init(color: UIColor?) {
self.color = color
}
// MARK: - UIViewBackground
open func apply(to view: UIView) {
view.backgroundColor = color
}
open func remove(from view: UIView) {
view.backgroundColor = nil
}
}

View File

@ -0,0 +1,117 @@
//
// 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 UIViewGradientBackground: UIViewBackground {
public var gradientLayer = CAGradientLayer()
public var gradientValues: GradientValues {
didSet {
update(gradientLayer: gradientLayer, with: gradientValues)
}
}
public private(set) weak var hostingView: UIView?
public var observeViewBoundsChange: Bool {
didSet {
if let hostingView, observeViewBoundsChange {
subscribeToBoundsChange(of: hostingView)
} else if !observeViewBoundsChange {
stopViewBoundsChangeObservation()
}
}
}
public var viewBoundsObservation: NSKeyValueObservation?
public init(values: GradientValues = GradientValues(),
observeViewBoundsChange: Bool = true) {
self.gradientValues = values
self.observeViewBoundsChange = observeViewBoundsChange
gradientLayer.name = "UIViewGradientBackground_" + UUID().uuidString
update(gradientLayer: gradientLayer, with: values)
}
// MARK: - UIViewBackground
open func apply(to view: UIView) {
guard indexOf(gradientSublayer: gradientLayer, in: view) == nil else {
return
}
hostingView = view
view.layer.addSublayer(gradientLayer)
updateGradientLayer(bounds: view.bounds, in: view)
if observeViewBoundsChange {
subscribeToBoundsChange(of: view)
}
}
open func remove(from view: UIView) {
stopViewBoundsChangeObservation()
gradientLayer.removeFromSuperlayer()
hostingView = nil
}
open func update(gradientLayer: CAGradientLayer, with values: GradientValues) {
gradientLayer.colors = values.colors
gradientLayer.locations = values.locations
gradientLayer.startPoint = values.startPoint
gradientLayer.endPoint = values.endPoint
gradientLayer.type = values.type
}
open func subscribeToBoundsChange(of view: UIView) {
viewBoundsObservation = view.observe(\.bounds,
options: [.new]) { [weak self] view, change in
guard let newvalue = change.newValue else {
return
}
self?.updateGradientLayer(bounds: newvalue, in: view)
}
}
open func stopViewBoundsChangeObservation() {
viewBoundsObservation = nil
}
open func indexOf(gradientSublayer: CAGradientLayer, in view: UIView) -> Int? {
view.layer.sublayers?.firstIndex(of: gradientSublayer)
}
open func updateGradientLayer(bounds: CGRect, in view: UIView) {
gradientLayer.frame = bounds
}
}

View File

@ -0,0 +1,58 @@
//
// 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 BaseUIViewBorder: UIViewBorder {
open class Defaults { // swiftlint:disable:this convenience_type
open class var color: UIColor {
.black
}
open class var width: CGFloat {
.zero
}
}
public var color: UIColor
public var width: CGFloat
public init(color: UIColor = Defaults.color,
width: CGFloat = Defaults.width) {
self.color = color
self.width = width
}
// MARK: - UIViewBorder
open func apply(to view: UIView) {
view.layer.borderWidth = width
view.layer.borderColor = color.cgColor
}
open func remove(from view: UIView) {
view.layer.borderWidth = Defaults.width
view.layer.borderColor = Defaults.color.cgColor
}
}

View File

@ -0,0 +1,66 @@
//
// 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 UIViewRoundedBorder: BaseUIViewBorder {
open class Defaults: BaseUIViewBorder.Defaults {
open class var cornerRadius: CGFloat {
8
}
open class var roundedCorners: CACornerMask {
.allCorners
}
}
public var cornerRadius: CGFloat
public var roundedCorners: CACornerMask
public init(color: UIColor = Defaults.color,
width: CGFloat = Defaults.width,
cornerRadius: CGFloat = Defaults.cornerRadius,
roundedCorners: CACornerMask = Defaults.roundedCorners) {
self.cornerRadius = cornerRadius
self.roundedCorners = roundedCorners
super.init(color: color, width: width)
}
// MARK: - UIViewBorder
override open func apply(to view: UIView) {
super.apply(to: view)
view.layer.maskedCorners = roundedCorners
view.layer.cornerRadius = cornerRadius
}
override open func remove(from view: UIView) {
super.remove(from: view)
view.layer.maskedCorners = Defaults.roundedCorners
view.layer.cornerRadius = Defaults.cornerRadius
}
}

View File

@ -39,6 +39,6 @@ public final class SeparatorLayout: UIView.BaseWrappedLayout, WrappedViewLayout
public final class SeparatorAppearance: UIView.BaseAppearance<SeparatorLayout>, ViewAppearance {
public static var defaultAppearance: Self {
Self(backgroundColor: .lightGray)
Self(background: UIViewColorBackground(color: .lightGray))
}
}

View File

@ -104,15 +104,15 @@ extension BaseStackView: AppearanceConfigurable where View: AppearanceConfigurab
public var arrangedSubviewsAppearance: View.Appearance
public init(layout: DefaultStackLayout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
arrangedSubviewsAppearance: View.Appearance = .defaultAppearance) {
self.arrangedSubviewsAppearance = arrangedSubviewsAppearance
super.init(layout: layout,
backgroundColor: backgroundColor,
background: background,
border: border,
shadow: shadow)
}

View File

@ -23,28 +23,29 @@
import TIUIKitCore
import UIKit
open class BaseListItemAppearance<LeadingViewAppearance: WrappedViewAppearance,
MiddleViewAppearance: WrappedViewAppearance,
TrailingViewAppearance: WrappedViewAppearance>: UIView.BaseAppearance<UIView.DefaultWrappedLayout> {
open class BaseListItemAppearance<LeadingAppearance: WrappedViewAppearance,
MiddleAppearance: WrappedViewAppearance,
TrailingAppearance: WrappedViewAppearance>:
UIView.BaseAppearance<UIView.DefaultWrappedLayout> {
public var leadingViewAppearance: LeadingViewAppearance
public var middleViewAppearance: MiddleViewAppearance
public var trailingAppearance: TrailingViewAppearance
public var leadingViewAppearance: LeadingAppearance
public var middleViewAppearance: MiddleAppearance
public var trailingAppearance: TrailingAppearance
public init(layout: UIView.DefaultWrappedLayout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
leadingViewAppearance: LeadingViewAppearance = .defaultAppearance,
middleViewAppearance: MiddleViewAppearance = .defaultAppearance,
trailingViewAppearance: TrailingViewAppearance = .defaultAppearance) {
leadingViewAppearance: LeadingAppearance = .defaultAppearance,
middleViewAppearance: MiddleAppearance = .defaultAppearance,
trailingViewAppearance: TrailingAppearance = .defaultAppearance) {
self.leadingViewAppearance = leadingViewAppearance
self.middleViewAppearance = middleViewAppearance
self.trailingAppearance = trailingViewAppearance
super.init(layout: layout,
backgroundColor: backgroundColor,
background: background,
border: border,
shadow: shadow)
}

View File

@ -92,7 +92,6 @@ open class PlaceholderFactory {
}
}
// MARK: - Placeholder creation
open func createEmptyStatePlaceholder() -> DefaultPlaceholderView {
@ -123,9 +122,8 @@ private extension PlaceholderFactory {
static var defaultButtonAppearance: StatefulButton.DefaultPositionAppearance {
.make {
if let normalAppearance = $0.stateAppearances[.normal] {
normalAppearance.border.cornerRadius = 25
normalAppearance.border.roundedCorners = .allCorners
normalAppearance.backgroundColor = UIColor(red: 0.892, green: 0.906, blue: 0.92, alpha: 0.5)
normalAppearance.border = UIViewRoundedBorder(cornerRadius: 25)
normalAppearance.backgroundColor = #colorLiteral(red: 0.892, green: 0.906, blue: 0.92, alpha: 0.5)
normalAppearance.textAttributes = .init(font: .systemFont(ofSize: 20, weight: .bold),
color: .black,
alignment: .natural,

View File

@ -269,8 +269,8 @@ extension BasePlaceholderView {
public var controlsViewAppearance: UIView.DefaultSpacedWrappedAppearance
public init(layout: UIView.DefaultWrappedLayout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
imageViewAppearance: ImageViewAppearance = .defaultAppearance,
textViewAppearance: DefaultTitleSubtitleView.Appearance = .defaultAppearance,
@ -281,7 +281,7 @@ extension BasePlaceholderView {
self.controlsViewAppearance = controlsViewAppearance
super.init(layout: layout,
backgroundColor: backgroundColor,
background: background,
border: border,
shadow: shadow)
}

View File

@ -30,8 +30,8 @@ public protocol SkeletonsPresenter {
func showSkeletons()
func hideSkeletons()
func startAnimation()
func stopAnimation()
func startSkeletonAnimation()
func stopSkeletonAnimation()
}
// MARK: - SkeletonsPresenter + Default implemetation
@ -62,16 +62,16 @@ extension SkeletonsPresenter {
// MARK: - UIView + SkeletonsPresenter
extension SkeletonsPresenter where Self: UIView {
public var skeletonsHolder: UIView {
public extension SkeletonsPresenter where Self: UIView {
var skeletonsHolder: UIView {
self
}
}
// MARK: - UIViewController + SkeletonsPresenter
extension SkeletonsPresenter where Self: UIViewController {
public var skeletonsHolder: UIView {
public extension SkeletonsPresenter where Self: UIViewController {
var skeletonsHolder: UIView {
view
}
}

View File

@ -23,17 +23,18 @@
import TISwiftUtils
import UIKit
extension UIView {
public extension UIView {
// MARK: - Public methods
/// Shows skeletons on the view
///
/// - Parameters:
/// - viewsToSkeletons: views that will be converted to skeletons. If nil was passed subviews will be converted to skeletons
/// - viewsToSkeletons: views that will be converted to skeletons.
/// If nil was passed subviews will be converted to skeletons
/// - config: configuration of the skeletons' layers
public func showSkeletons(viewsToSkeletons: [UIView]?,
_ config: SkeletonsConfiguration) {
func showSkeletons(viewsToSkeletons: [UIView]?,
_ config: SkeletonsConfiguration) {
let viewsToSkeletons = viewsToSkeletons ?? skeletonableViews
isUserInteractionEnabled = false
@ -53,19 +54,19 @@ extension UIView {
.skeletonsShown()
}
public func hideSkeletons() {
func hideSkeletons() {
isUserInteractionEnabled = true
layer.skeletonLayers
.forEach { $0.remove(from: self) }
}
public func startAnimation() {
func startSkeletonAnimation() {
layer.skeletonLayers
.forEach { $0.startAnimation() }
}
public func stopAnimation() {
func stopSkeletonAnimation() {
layer.skeletonLayers
.forEach { $0.stopAnimation() }
}
@ -87,7 +88,6 @@ extension UIView {
subviewSkeletonLayers = view.skeletonableViews
.map { getSkeletonLayer(forView: $0, withConfiguration: conf, forceNoContainers: true) }
.flatMap { $0 }
} else {
skeletonLayer.bind(to: view.viewType)
}
@ -105,9 +105,9 @@ extension UIView {
// MARK: - Helper extension
extension Array where Element: SkeletonLayer {
public extension Array where Element: SkeletonLayer {
@discardableResult
public func skeletonsShown() -> Self {
func skeletonsShown() -> Self {
self.forEach { subLayer in
subLayer.skeletonsChangedState(.shown)
}
@ -116,7 +116,7 @@ extension Array where Element: SkeletonLayer {
}
@discardableResult
public func insert(onto view: UIView, at index: UInt32 = .max) -> Self {
func insert(onto view: UIView, at index: UInt32 = .max) -> Self {
self.forEach { subLayer in
view.layer.insertSublayer(subLayer, at: index)
}

View File

@ -22,28 +22,29 @@
import UIKit
extension UIViewController {
public extension UIViewController {
/// Shows skeletons
///
/// - Parameters:
/// - viewsToSkeletons: views that will be converted to skeletons. If nil was passed subviews of the view will be converted to skeletons
/// - viewsToSkeletons: views that will be converted to skeletons.
/// If nil was passed subviews of the view will be converted to skeletons
/// - config: configuration of the skeletons' layers
public func showSkeletons(viewsToSkeletons: [UIView]?,
_ config: SkeletonsConfiguration) {
func showSkeletons(viewsToSkeletons: [UIView]?,
_ config: SkeletonsConfiguration) {
view.showSkeletons(viewsToSkeletons: viewsToSkeletons, config)
}
public func hideSkeletons() {
func hideSkeletons() {
view.hideSkeletons()
}
public func startAnimation() {
view.startAnimation()
func startSkeletonAnimation() {
view.startSkeletonAnimation()
}
public func stopAnimation() {
view.stopAnimation()
func stopSkeletonAnimation() {
view.stopSkeletonAnimation()
}
}

View File

@ -42,9 +42,9 @@ public final class DefaultConfigurableStatefulButton: StatefulButton, Configurab
stateViewModelMap = viewModel.stateViewModelMap
for (state, viewModel) in viewModel.stateViewModelMap {
setTitle(viewModel.title, for: state)
setImage(viewModel.image, for: state)
setBackgroundImage(viewModel.backgroundImage, for: state)
setTitle(viewModel.title, for: state.controlState)
setImage(viewModel.image, for: state.controlState)
setBackgroundImage(viewModel.backgroundImage, for: state.controlState)
}
apply(state: viewModel.currentState)
@ -68,7 +68,7 @@ public final class DefaultConfigurableStatefulButton: StatefulButton, Configurab
public extension DefaultConfigurableStatefulButton {
final class ViewModel: DefaultUIViewPresenter<DefaultConfigurableStatefulButton> {
public var stateViewModelMap: [State: BaseButtonViewModel]
public var stateViewModelMap: [StateKey: BaseButtonViewModel]
public var currentState: State {
didSet {
view?.apply(state: currentState)
@ -83,7 +83,7 @@ public extension DefaultConfigurableStatefulButton {
}
}
public init(stateViewModelMap: [State: BaseButtonViewModel],
public init(stateViewModelMap: [StateKey: BaseButtonViewModel],
currentState: State,
tapHander: UIVoidClosure?) {

View File

@ -25,7 +25,7 @@ import UIKit
extension StatefulButton {
public typealias StateAppearance = UIButton.BaseAppearance<UIView.NoLayout, DefaultContentLayout>
public typealias StateAppearances = [State: StateAppearance]
public typealias StateAppearances = [StateKey: StateAppearance]
open class BaseAppearance<Layout: ViewLayout, ContentLayout: BaseContentLayout & ViewLayout>:
UIView.BaseAppearance<Layout> {
@ -43,22 +43,22 @@ extension StatefulButton {
public var stateAppearances: StateAppearances
public init(layout: Layout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
stateAppearances: StateAppearances = defaultStateAppearances) {
self.stateAppearances = stateAppearances
super.init(layout: layout,
backgroundColor: backgroundColor,
background: background,
border: border,
shadow: shadow)
}
public func set(appearanceBuilder: (StateAppearance) -> Void, for states: [State]) {
for state in states {
stateAppearances[state] = DefaultStateAppearance.make(builder: appearanceBuilder)
stateAppearances[.init(controlState: state)] = DefaultStateAppearance.make(builder: appearanceBuilder)
}
}
}
@ -69,8 +69,8 @@ extension StatefulButton {
public var activityIndicatorPosition: ActivityIndicatorPosition
public init(layout: Layout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
stateAppearances: StateAppearances = defaultStateAppearances,
activityIndicatorPosition: ActivityIndicatorPosition = .center) {
@ -78,7 +78,7 @@ extension StatefulButton {
self.activityIndicatorPosition = activityIndicatorPosition
super.init(layout: layout,
backgroundColor: backgroundColor,
background: background,
border: border,
shadow: shadow,
stateAppearances: stateAppearances)
@ -99,8 +99,8 @@ extension StatefulButton {
public var activityIndicatorPlacement: ActivityIndicatorPlacement
public init(layout: Layout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
stateAppearances: StateAppearances = defaultStateAppearances,
activityIndicatorPlacement: ActivityIndicatorPlacement = .center) {
@ -108,7 +108,7 @@ extension StatefulButton {
self.activityIndicatorPlacement = activityIndicatorPlacement
super.init(layout: layout,
backgroundColor: backgroundColor,
background: background,
border: border,
shadow: shadow,
stateAppearances: stateAppearances)
@ -127,7 +127,7 @@ extension StatefulButton {
extension StatefulButton {
public func configureBaseStatefulButton(appearance: BaseAppearance<some ViewLayout, some BaseContentLayout>) {
onStateChanged = { [weak self] in
if let stateAppearance = appearance.stateAppearances[$0] {
if let stateAppearance = appearance.stateAppearances[.init(controlState: $0)] {
self?.configureUIButton(appearance: stateAppearance, for: $0)
} else if $0 != .normal, let stateAppearance = appearance.stateAppearances[.normal] {
self?.configureUIButton(appearance: stateAppearance, for: .normal)

View File

@ -33,8 +33,13 @@ public extension UIControl.State {
static var loading = Self(rawValue: 1 << 16 | Self.disabled.rawValue) // includes disabled state
}
open class StatefulButton: BaseInitializableButton {
public extension UIControl.StateKey {
static var loading: Self {
.init(controlState: .loading)
}
}
open class StatefulButton: BaseInitializableButton {
public enum ActivityIndicatorPosition {
case beforeTitle(padding: CGFloat)
case center
@ -46,7 +51,7 @@ open class StatefulButton: BaseInitializableButton {
case center
}
public typealias StateEventPropagations = [State: Bool]
public typealias StateEventPropagations = [StateKey: Bool]
private var backedIsLoading = false
@ -94,7 +99,7 @@ open class StatefulButton: BaseInitializableButton {
var activityIndicatorShouldCenterInView = false
var stateViewModelMap: [State: BaseButtonViewModel] = [:]
var stateViewModelMap: [StateKey: BaseButtonViewModel] = [:]
var onStateChanged: ParameterClosure<State>?
@ -103,12 +108,12 @@ open class StatefulButton: BaseInitializableButton {
private var eventPropagations: StateEventPropagations = [:]
public func setEventPropagation(_ eventPropagation: Bool, for state: State) {
eventPropagations[state] = eventPropagation
eventPropagations[.init(controlState: state)] = eventPropagation
}
// MARK: - UIButton override
open override func setImage(_ image: UIImage?, for state: UIControl.State) {
open override func setImage(_ image: UIImage?, for state: State) {
guard state != .loading else {
return
}
@ -116,21 +121,21 @@ open class StatefulButton: BaseInitializableButton {
super.setImage(image, for: state)
}
open override func title(for state: UIControl.State) -> String? {
stateViewModelMap[state]?.title ?? super.title(for: state)
open override func title(for state: State) -> String? {
stateViewModelMap[.init(controlState: state)]?.title ?? super.title(for: state)
}
open override func image(for state: UIControl.State) -> UIImage? {
stateViewModelMap[state]?.image ?? super.image(for: state)
open override func image(for state: State) -> UIImage? {
stateViewModelMap[.init(controlState: state)]?.image ?? super.image(for: state)
}
open override func backgroundImage(for state: UIControl.State) -> UIImage? {
stateViewModelMap[state]?.backgroundImage ?? super.backgroundImage(for: state)
open override func backgroundImage(for state: State) -> UIImage? {
stateViewModelMap[.init(controlState: state)]?.backgroundImage ?? super.backgroundImage(for: state)
}
// MARK: - UIControl override
open override var state: UIControl.State {
open override var state: State {
if isLoading {
return super.state.union(.loading)
} else {
@ -222,7 +227,7 @@ open class StatefulButton: BaseInitializableButton {
let touchEventReceiver = super.hitTest(point, with: event)
let shouldPropagateEvent = (eventPropagations[state] ?? true) || isHidden
let shouldPropagateEvent = (eventPropagations[.init(controlState: state)] ?? true) || isHidden
if pointInsideView && touchEventReceiver == nil && !shouldPropagateEvent {
return self // disable propagation

View File

@ -23,9 +23,9 @@
import TIUIKitCore
import UIKit
extension DefaultTitleSubtitleView {
public extension DefaultTitleSubtitleView {
public final class Appearance: UIView.BaseAppearance<UIView.DefaultSpacedWrappedLayout>, WrappedViewAppearance {
final class Appearance: UIView.BaseAppearance<UIView.DefaultSpacedWrappedLayout>, WrappedViewAppearance {
public static var defaultAppearance: Appearance {
Self()
@ -35,8 +35,8 @@ extension DefaultTitleSubtitleView {
public var subtitleAppearance: UILabel.DefaultAppearance
public init(layout: Layout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
titleAppearance: UILabel.DefaultAppearance = .defaultAppearance,
subtitleAppearance: UILabel.DefaultAppearance = .defaultAppearance) {
@ -45,7 +45,7 @@ extension DefaultTitleSubtitleView {
self.subtitleAppearance = subtitleAppearance
super.init(layout: layout,
backgroundColor: backgroundColor,
background: background,
border: border,
shadow: shadow)
}

View File

@ -17,8 +17,8 @@
Базовая настройка для показа скелетонов не требуется. `UIView` и `UIViewController` уже имеют все необходимые методы для работы:
- `showSkeletons(viewsToSkeletons:_:)` : используется для показа скелетонов, если была передана конфигурация анимации, то она запустится автоматически. `viewsToSkeletons` - опциональный массив `UIView`, определяющий, какие вью будут конвертироваться в скелетоны. Если nil, то проход будет осуществляться по списку subview
- `hideSkeletons()` : используется для скрытия скелетонов
- `startAnimation()` : используется для старта анимации на скелетонах (если анимания не была сконфигурирована в методе `showSkeletons(viewsToSkeletons:_:)` то ничего не произойдет)
- `stopAnimation()` : используется для остановки анимации на скелетонах
- `startSkeletonAnimation()` : используется для старта анимации на скелетонах (если анимания не была сконфигурирована в методе `showSkeletons(viewsToSkeletons:_:)` то ничего не произойдет)
- `stopSkeletonAnimation()` : используется для остановки анимации на скелетонах
*/
import TIUIKitCore
import TIUIElements
@ -74,7 +74,7 @@ class CanShowAndHideSkeletons: BaseInitializableViewController {
let textAttributes = BaseTextAttributes(font: .systemFont(ofSize: 25), color: .black, alignment: .natural, isMultiline: false)
view.configureUIView(appearance: UIView.DefaultAppearance(backgroundColor: .white))
view.configureUIView(appearance: UIView.DefaultAppearance(background: UIViewColorBackground(color: .white)))
label.configureUILabel(appearance: UILabel.DefaultAppearance.make {
$0.textAttributes = textAttributes

View File

@ -0,0 +1,91 @@
/*:
# UIViewBackground
Для задания фона UIView можно использовать реализации протокола UIViewBackground:
## UIViewColorBackground - сплошной фон одного цвета
*/
import TIUIElements
import UIKit
let viewFrame = CGRect(origin: .zero,
size: CGSize(width: 164, height: 192))
let solidFillBackground = UIViewColorBackground(color: .green)
let genericView = UIView(frame: viewFrame)
solidFillBackground.apply(to: genericView)
Nef.Playground.liveView(genericView)
/*:
## UIViewGradientBackground - градиентный фон
Для задания градиентного фона необходимо определить GradientValues и применить фон в UIView:
*/
let gradientView = UIView(frame: viewFrame)
let gradientColorStart = UIColor(red: 0.8, green: 0.74, blue: 1, alpha: 1).cgColor
let gradientColorEnd = UIColor(red: 1, green: 0.82, blue: 0.84, alpha: 1).cgColor
let centerPoint = CGPoint(x: 0.98, y: 0.95)
let outerPoint = CGPoint(x: 0.02, y: .zero)
let gradientValues: GradientValues = .elliptical(stops: [
(gradientColorStart, 0),
(gradientColorEnd, 1)
],
center: centerPoint,
outerEdge: outerPoint)
let gradientBackground = UIViewGradientBackground(values: gradientValues)
gradientBackground.apply(to: gradientView)
gradientView.layer.round(corners: .allCorners, radius: 20)
Nef.Playground.liveView(gradientView)
/*:
### Использование внутри кастомной view
Также возможно использование градиентного фона внутри кастомной view с более точным контролем над обновлением состояния
*/
final class GradientView: BaseInitializableView {
var gradientBackground = UIViewGradientBackground(observeViewBoundsChange: false) {
willSet {
gradientBackground.remove(from: self)
}
didSet {
gradientBackground.gradientValues = gradientValues
}
}
var gradientValues: GradientValues = GradientValues() {
didSet {
gradientBackground.gradientValues = gradientValues
}
}
override func configureAppearance() {
super.configureAppearance()
gradientBackground.apply(to: self)
}
override func layoutSubviews() {
super.layoutSubviews()
gradientBackground.updateGradientLayer(bounds: bounds, in: self)
}
}
let customGradientView = GradientView(frame: viewFrame)
customGradientView.gradientValues = gradientValues
customGradientView.layer.round(corners: .allCorners, radius: 20)
Nef.Playground.liveView(customGradientView)

View File

@ -3,5 +3,6 @@
<pages>
<page name='Skeletons'/>
<page name='Placeholder'/>
<page name='ViewBackground'/>
</pages>
</playground>

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIElements'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Bunch of useful protocols and views.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -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.
//
import UIKit.UIView
public protocol UIViewBackground {
func apply(to view: UIView)
func remove(from view: UIView)
}

View File

@ -22,20 +22,7 @@
import UIKit
public struct UIViewBorder {
public var color: UIColor
public var width: CGFloat
public var cornerRadius: CGFloat
public var roundedCorners: CACornerMask
public init(color: UIColor = .clear,
width: CGFloat = .zero,
cornerRadius: CGFloat = .zero,
roundedCorners: CACornerMask = []) {
self.color = color
self.width = width
self.cornerRadius = cornerRadius
self.roundedCorners = roundedCorners
}
public protocol UIViewBorder {
func apply(to view: UIView)
func remove(from view: UIView)
}

View File

@ -28,7 +28,7 @@ public protocol ViewAppearance {
static var defaultAppearance: Self { get }
var layout: Layout { get }
var backgroundColor: UIColor { get }
var background: UIViewBackground { get }
var border: UIViewBorder { get }
var shadow: UIViewShadow? { get }
}

View File

@ -24,24 +24,24 @@ import UIKit.UIButton
public extension UIButton {
func set(titleColors: StateColors) {
titleColors.forEach { setTitleColor($1, for: $0) }
titleColors.forEach { setTitleColor($1, for: $0.controlState) }
}
func set(titles: StateTitles) {
titles.forEach { setTitle($1, for: $0) }
titles.forEach { setTitle($1, for: $0.controlState) }
}
func set(attributtedTitles: StateAttributedTitles) {
attributtedTitles.forEach { setAttributedTitle($1, for: $0) }
attributtedTitles.forEach { setAttributedTitle($1, for: $0.controlState) }
}
// MARK: - Images
func set(images: StateImages) {
images.forEach { setImage($1, for: $0) }
images.forEach { setImage($1, for: $0.controlState) }
}
func set(backgroundImages: StateImages) {
backgroundImages.forEach { setBackgroundImage($1, for: $0) }
backgroundImages.forEach { setBackgroundImage($1, for: $0.controlState) }
}
}

View File

@ -22,15 +22,43 @@
import UIKit.UIControl
extension UIControl.State: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(Int(rawValue))
}
}
public extension UIControl {
typealias StateColors = [UIControl.State: UIColor?]
typealias StateImages = [UIControl.State: UIImage?]
typealias StateTitles = [UIControl.State: String?]
typealias StateAttributedTitles = [UIControl.State: NSAttributedString?]
struct StateKey: Hashable {
public static var normal: StateKey {
.init(controlState: .normal)
}
public static var highlighted: StateKey {
.init(controlState: .highlighted)
}
public static var disabled: StateKey {
.init(controlState: .disabled)
}
public static var selected: StateKey {
.init(controlState: .selected)
}
public let controlState: State
public init(controlState: State) {
self.controlState = controlState
}
public init(rawValue: State.RawValue) {
self.controlState = State(rawValue: rawValue)
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(controlState.rawValue)
}
}
typealias StateColors = [StateKey: UIColor?]
typealias StateImages = [StateKey: UIImage?]
typealias StateTitles = [StateKey: String?]
typealias StateAttributedTitles = [StateKey: NSAttributedString?]
}

View File

@ -30,6 +30,7 @@ open class BaseTextAttributes {
public let color: UIColor
public let paragraphStyle: NSParagraphStyle
public let numberOfLines: Int
public let customAttributes: [NSAttributedString.Key: Any]
private let forceAttributedStringUsage: Bool
@ -39,18 +40,23 @@ open class BaseTextAttributes {
.foregroundColor: color,
.paragraphStyle: paragraphStyle
]
.merging(customAttributes) { _, new in
new
}
}
public init(font: UIFont,
color: UIColor,
numberOfLines: Int,
paragraphStyleConfiguration: ParameterClosure<NSMutableParagraphStyle>) {
customAttributes: [NSAttributedString.Key: Any] = [:],
paragraphStyleConfiguration: ParameterClosure<NSMutableParagraphStyle>? = nil) {
self.font = font
self.color = color
self.customAttributes = customAttributes
let mutableParagraphStyle = NSMutableParagraphStyle()
paragraphStyleConfiguration(mutableParagraphStyle)
paragraphStyleConfiguration?(mutableParagraphStyle)
let equator = KeyPathEquatable(rhs: mutableParagraphStyle, lhs: NSParagraphStyle.default)
@ -71,7 +77,7 @@ open class BaseTextAttributes {
equator(\.lineBreakStrategy)
]
forceAttributedStringUsage = !equalityResults.allSatisfy { $0 }
forceAttributedStringUsage = !equalityResults.allSatisfy { $0 } || !customAttributes.isEmpty
self.paragraphStyle = mutableParagraphStyle
self.numberOfLines = numberOfLines

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIKitCore'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Core UI elements: protocols, views and helpers.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIWebView'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Universal web view API'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }
@ -11,7 +11,14 @@ Pod::Spec.new do |s|
s.ios.deployment_target = '11.0'
s.swift_versions = ['5.7']
s.source_files = s.name + '/Sources/**/*'
sources = 'Sources/**/*.swift'
if ENV["DEVELOPMENT_INSTALL"] # installing using :path =>
s.source_files = sources
s.exclude_files = s.name + '.app'
else
s.source_files = s.name + '/' + sources
s.exclude_files = s.name + '/*.app'
end
s.dependency 'TIUIKitCore', s.version.to_s
s.dependency 'TISwiftUtils', s.version.to_s

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIYandexMapUtils'
s.version = '1.52.0'
s.version = '1.53.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -16,7 +16,7 @@ class EmptyViewController: BaseModalViewController<UIView, UIView> { }
## Обертка вокруг существующего контроллера
Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `BaseModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер
Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `DefaultModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер
```swift
import TIUIKitCore
@ -25,13 +25,13 @@ final class OldMassiveViewController: BaseInitializableViewController {
// some implementation
}
typealias ModalOldMassiveViewController = BaseModalWrapperViewController<OldMassiveViewController>
typealias ModalOldMassiveViewController = DefaultModalWrapperViewController<OldMassiveViewController>
class PresentingViewController: BaseInitializableViewController {
// some implementation
@objc private func onButtonTapped() {
presentPanModal(ModalOldMassiveViewController())
presentPanModal(ModalOldMassiveViewController(contentViewController: OldMassiveViewController()))
}
}
```
@ -49,16 +49,18 @@ class PresentingViewController: BaseInitializableViewController {
Вот пример настройки внешнего вида так, чтобы был видет dragView и headerView с левой кнопкой:
```swift
import TIUIElements
let customViewController = BaseModalViewController<UIView, UIView>()
customViewController.viewControllerAppearance = BaseModalViewController.DefaultAppearance.make {
$0.dragViewState = .presented(.defaultAppearance)
$0.headerViewState = .presented(.make {
$0.layout.size = .fixedHeight(52)
$0.backgroundColor = .white
$0.contentViewState = .buttonLeft(.init(titles: [.normal: "Close"],
appearance: .init(stateAppearances: [
.normal: .init(backgroundColor: .blue)
])))
$0.contentViewState = .leadingButton(.init(titles: [.normal: "Close"],
appearance: .init(stateAppearances: [
.normal: .init(background: UIViewColorBackground(color: .blue))
])))
})
}
```
@ -85,10 +87,10 @@ detentsViewController.viewControllerAppearance.presentationDetents = [.headerOnl
```swift
let shadowViewController = BaseModalViewController<UIView, UIView>()
let dimmedView = PassthroughDimmedView()
dimmedView.hitTestHandlerView = view
dimmedView.configureUIView(appearance: .init(shadow: UIViewShadow(radius: 8,
color: .black,
opacity: 0.3)))
dimmedView.hitTestHandlerView = shadowViewController.view
dimmedView.configureUIView(appearance: DefaultAppearance(shadow: UIViewShadow(radius: 8,
color: .black,
opacity: 0.3)))
shadowViewController.dimmedView = dimmedView
```

View File

@ -58,10 +58,10 @@ import Foundation
let defaults = UserDefaults.standard // or AppGroup defaults
let appReinstallChecker = AppReinstallChecker(defaultsStorage: defaults,
storageKey: .deleteApiToken)
let appReinstallChecker = DefaultAppFirstRunCheckStorage(defaults: defaults,
storageKey: .deleteApiToken)
let appInstallAwareTokenStorage = apiTokenKeychainStorage.appInstallLifetimeStorage(reinstallChecker: appReinstallChecker)
let appInstallAwareTokenStorage = apiTokenKeychainStorage.appInstallLifetimeStorage(appFirstRunCheckStorage: appReinstallChecker)
if appInstallAwareTokenStorage.hasStoredValue() {
// app wasn't reinstalled, token is exist

View File

@ -17,8 +17,8 @@
Базовая настройка для показа скелетонов не требуется. `UIView` и `UIViewController` уже имеют все необходимые методы для работы:
- `showSkeletons(viewsToSkeletons:_:)` : используется для показа скелетонов, если была передана конфигурация анимации, то она запустится автоматически. `viewsToSkeletons` - опциональный массив `UIView`, определяющий, какие вью будут конвертироваться в скелетоны. Если nil, то проход будет осуществляться по списку subview
- `hideSkeletons()` : используется для скрытия скелетонов
- `startAnimation()` : используется для старта анимации на скелетонах (если анимания не была сконфигурирована в методе `showSkeletons(viewsToSkeletons:_:)` то ничего не произойдет)
- `stopAnimation()` : используется для остановки анимации на скелетонах
- `startSkeletonAnimation()` : используется для старта анимации на скелетонах (если анимания не была сконфигурирована в методе `showSkeletons(viewsToSkeletons:_:)` то ничего не произойдет)
- `stopSkeletonAnimation()` : используется для остановки анимации на скелетонах
```swift
import TIUIKitCore
@ -75,7 +75,7 @@ class CanShowAndHideSkeletons: BaseInitializableViewController {
let textAttributes = BaseTextAttributes(font: .systemFont(ofSize: 25), color: .black, alignment: .natural, isMultiline: false)
view.configureUIView(appearance: UIView.DefaultAppearance(backgroundColor: .white))
view.configureUIView(appearance: UIView.DefaultAppearance(background: UIViewColorBackground(color: .white)))
label.configureUILabel(appearance: UILabel.DefaultAppearance.make {
$0.textAttributes = textAttributes

View File

@ -0,0 +1,92 @@
# UIViewBackground
Для задания фона UIView можно использовать реализации протокола UIViewBackground:
## UIViewColorBackground - сплошной фон одного цвета
```swift
import TIUIElements
import UIKit
let viewFrame = CGRect(origin: .zero,
size: CGSize(width: 164, height: 192))
let solidFillBackground = UIViewColorBackground(color: .green)
let genericView = UIView(frame: viewFrame)
solidFillBackground.apply(to: genericView)
Nef.Playground.liveView(genericView)
```
## UIViewGradientBackground - градиентный фон
Для задания градиентного фона необходимо определить GradientValues и применить фон в UIView:
```swift
let gradientView = UIView(frame: viewFrame)
let gradientColorStart = UIColor(red: 0.8, green: 0.74, blue: 1, alpha: 1).cgColor
let gradientColorEnd = UIColor(red: 1, green: 0.82, blue: 0.84, alpha: 1).cgColor
let centerPoint = CGPoint(x: 0.98, y: 0.95)
let outerPoint = CGPoint(x: 0.02, y: .zero)
let gradientValues: GradientValues = .elliptical(stops: [
(gradientColorStart, 0),
(gradientColorEnd, 1)
],
center: centerPoint,
outerEdge: outerPoint)
let gradientBackground = UIViewGradientBackground(values: gradientValues)
gradientBackground.apply(to: gradientView)
gradientView.layer.round(corners: .allCorners, radius: 20)
Nef.Playground.liveView(gradientView)
```
### Использование внутри кастомной view
Также возможно использование градиентного фона внутри кастомной view с более точным контролем над обновлением состояния
```swift
final class GradientView: BaseInitializableView {
var gradientBackground = UIViewGradientBackground(observeViewBoundsChange: false) {
willSet {
gradientBackground.remove(from: self)
}
didSet {
gradientBackground.gradientValues = gradientValues
}
}
var gradientValues: GradientValues = GradientValues() {
didSet {
gradientBackground.gradientValues = gradientValues
}
}
override func configureAppearance() {
super.configureAppearance()
gradientBackground.apply(to: self)
}
override func layoutSubviews() {
super.layoutSubviews()
gradientBackground.updateGradientLayer(bounds: bounds, in: self)
}
}
let customGradientView = GradientView(frame: viewFrame)
customGradientView.gradientValues = gradientValues
customGradientView.layer.round(corners: .allCorners, radius: 20)
Nef.Playground.liveView(customGradientView)
```