diff --git a/CHANGELOG.md b/CHANGELOG.md index 550d063b..818957b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index 3db0176f..ffa12cef 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -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' } diff --git a/TIApplication/Sources/BundleIdentifier.swift b/TIApplication/Sources/BundleIdentifier.swift new file mode 100644 index 00000000..a56d9d4d --- /dev/null +++ b/TIApplication/Sources/BundleIdentifier.swift @@ -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 + } +} diff --git a/TIApplication/Sources/CoreDependencies.swift b/TIApplication/Sources/CoreDependencies.swift index bfadd75c..df0b1c21 100644 --- a/TIApplication/Sources/CoreDependencies.swift +++ b/TIApplication/Sources/CoreDependencies.swift @@ -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) } } diff --git a/TIApplication/TIApplication.podspec b/TIApplication/TIApplication.podspec index 2a313988..bf420d69 100644 --- a/TIApplication/TIApplication.podspec +++ b/TIApplication/TIApplication.podspec @@ -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' } diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec index 9d9a9172..e759bcf0 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -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' } diff --git a/TIBottomSheet/Sources/BottomSheet/BaseModalViewController+Appearance.swift b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController+Appearance.swift index 53a4002e..7bf68fb7 100644 --- a/TIBottomSheet/Sources/BottomSheet/BaseModalViewController+Appearance.swift +++ b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController+Appearance.swift @@ -34,8 +34,8 @@ extension BaseModalViewController { public var footerViewState: ModalFooterView.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) } diff --git a/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift index 8f340340..9196397b 100644 --- a/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift +++ b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift @@ -153,7 +153,7 @@ open class BaseModalViewController { } /*: ## Обертка вокруг существующего контроллера - Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `BaseModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер + Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `DefaultModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер */ import TIUIKitCore @@ -24,13 +24,13 @@ final class OldMassiveViewController: BaseInitializableViewController { // some implementation } -typealias ModalOldMassiveViewController = BaseModalWrapperViewController +typealias ModalOldMassiveViewController = DefaultModalWrapperViewController 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() 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() 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 diff --git a/TIBottomSheet/TIBottomSheet.podspec b/TIBottomSheet/TIBottomSheet.podspec index 7b071f12..28b24659 100644 --- a/TIBottomSheet/TIBottomSheet.podspec +++ b/TIBottomSheet/TIBottomSheet.podspec @@ -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' } diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.podspec b/TICoreGraphicsUtils/TICoreGraphicsUtils.podspec index 79ba72cd..a1fd5618 100644 --- a/TICoreGraphicsUtils/TICoreGraphicsUtils.podspec +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.podspec @@ -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' } diff --git a/TIDeeplink/TIDeeplink.podspec b/TIDeeplink/TIDeeplink.podspec index 825e29ec..5890e810 100644 --- a/TIDeeplink/TIDeeplink.podspec +++ b/TIDeeplink/TIDeeplink.podspec @@ -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' } diff --git a/TIDeveloperUtils/TIDeveloperUtils.podspec b/TIDeveloperUtils/TIDeveloperUtils.podspec index c70ba1fb..9e21416f 100644 --- a/TIDeveloperUtils/TIDeveloperUtils.podspec +++ b/TIDeveloperUtils/TIDeveloperUtils.podspec @@ -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' } diff --git a/TIEcommerce/TIEcommerce.podspec b/TIEcommerce/TIEcommerce.podspec index 2108e49a..94adc4f9 100644 --- a/TIEcommerce/TIEcommerce.podspec +++ b/TIEcommerce/TIEcommerce.podspec @@ -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' } diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index 61495197..80e40ae6 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -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' } diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec index fcf470ec..9294f593 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -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' } diff --git a/TIKeychainUtils/Sources/SingleValueKeychainStorage/CodableSingleValueKeychainStorage.swift b/TIKeychainUtils/Sources/SingleValueKeychainStorage/CodableSingleValueKeychainStorage.swift new file mode 100644 index 00000000..4141288f --- /dev/null +++ b/TIKeychainUtils/Sources/SingleValueKeychainStorage/CodableSingleValueKeychainStorage.swift @@ -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: BaseSingleValueKeychainStorage { + public init(keychain: Keychain, + storageKey: StorageKey, + 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) + } +} diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift index af651031..dd0288a0 100644 --- a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift @@ -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 diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index 66842ac8..1b892461 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -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' } diff --git a/TILogging/TILogging.podspec b/TILogging/TILogging.podspec index 7780280d..d39d9a1c 100644 --- a/TILogging/TILogging.podspec +++ b/TILogging/TILogging.podspec @@ -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' } diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index 74c3ff47..d6a957fc 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -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' } diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index 1de69f72..690e0b98 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -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' } diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index bc32133c..4afc89f2 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -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' } diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index 49928a9d..df85df41 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -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' } diff --git a/TIPagination/TIPagination.podspec b/TIPagination/TIPagination.podspec index 4b5dffee..65e34a80 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -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" diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec index c527a695..bc8dc2e9 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -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' } diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index 0ba317d3..7600727f 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -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' } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index fd0e1cd3..769b3648 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -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' } diff --git a/TITextProcessing/TITextProcessing.podspec b/TITextProcessing/TITextProcessing.podspec index 939f0d8c..1cc2f590 100644 --- a/TITextProcessing/TITextProcessing.podspec +++ b/TITextProcessing/TITextProcessing.podspec @@ -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' } diff --git a/TIUIElements/Sources/Appearance/UIButton+Appearance.swift b/TIUIElements/Sources/Appearance/UIButton+Appearance.swift index f30cd0eb..dcc923fe 100644 --- a/TIUIElements/Sources/Appearance/UIButton+Appearance.swift +++ b/TIUIElements/Sources/Appearance/UIButton+Appearance.swift @@ -45,14 +45,15 @@ extension UIButton { } } - open class BaseAppearance: UIView.BaseAppearance { + open class BaseAppearance: + UIView.BaseAppearance { 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) } diff --git a/TIUIElements/Sources/Appearance/UILabel+Appearance.swift b/TIUIElements/Sources/Appearance/UILabel+Appearance.swift index 176c2389..8e917a5f 100644 --- a/TIUIElements/Sources/Appearance/UILabel+Appearance.swift +++ b/TIUIElements/Sources/Appearance/UILabel+Appearance.swift @@ -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) } diff --git a/TIUIElements/Sources/Appearance/UIVIew+AppearanceConfigurable.swift b/TIUIElements/Sources/Appearance/UIVIew+AppearanceConfigurable.swift index 21cab868..f8db83b5 100644 --- a/TIUIElements/Sources/Appearance/UIVIew+AppearanceConfigurable.swift +++ b/TIUIElements/Sources/Appearance/UIVIew+AppearanceConfigurable.swift @@ -25,12 +25,9 @@ import UIKit public extension UIView { func configureUIView(appearance: BaseAppearance) { - 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 diff --git a/TIUIElements/Sources/Appearance/UIView+Appearance.swift b/TIUIElements/Sources/Appearance/UIView+Appearance.swift index 440c1e66..2a1508f0 100644 --- a/TIUIElements/Sources/Appearance/UIView+Appearance.swift +++ b/TIUIElements/Sources/Appearance/UIView+Appearance.swift @@ -29,17 +29,17 @@ extension UIView { open class BaseAppearance { 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: BaseAppearance {} - public final class DefaultWrappedViewHolderAppearance: BaseWrappedViewHolderAppearance, - WrappedViewHolderAppearance { + public final class DefaultWrappedViewHolderAppearance: + BaseWrappedViewHolderAppearance, + 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) + } + } +} diff --git a/TIUIElements/Sources/Appearance/UIViewBackground/GradientValues.swift b/TIUIElements/Sources/Appearance/UIViewBackground/GradientValues.swift new file mode 100644 index 00000000..28314e77 --- /dev/null +++ b/TIUIElements/Sources/Appearance/UIViewBackground/GradientValues.swift @@ -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) + } +} diff --git a/TIUIElements/Sources/Appearance/UIViewBackground/UIViewColorBackground.swift b/TIUIElements/Sources/Appearance/UIViewBackground/UIViewColorBackground.swift new file mode 100644 index 00000000..b3cf1aff --- /dev/null +++ b/TIUIElements/Sources/Appearance/UIViewBackground/UIViewColorBackground.swift @@ -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 + } +} diff --git a/TIUIElements/Sources/Appearance/UIViewBackground/UIViewGradientBackground.swift b/TIUIElements/Sources/Appearance/UIViewBackground/UIViewGradientBackground.swift new file mode 100644 index 00000000..e9a1821d --- /dev/null +++ b/TIUIElements/Sources/Appearance/UIViewBackground/UIViewGradientBackground.swift @@ -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 + } +} diff --git a/TIUIElements/Sources/Appearance/UIViewBorder/BaseUIViewBorder.swift b/TIUIElements/Sources/Appearance/UIViewBorder/BaseUIViewBorder.swift new file mode 100644 index 00000000..a9ea1959 --- /dev/null +++ b/TIUIElements/Sources/Appearance/UIViewBorder/BaseUIViewBorder.swift @@ -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 + } +} diff --git a/TIUIElements/Sources/Appearance/UIViewBorder/UIViewRoundedBorder.swift b/TIUIElements/Sources/Appearance/UIViewBorder/UIViewRoundedBorder.swift new file mode 100644 index 00000000..d5f9fa18 --- /dev/null +++ b/TIUIElements/Sources/Appearance/UIViewBorder/UIViewRoundedBorder.swift @@ -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 + } +} diff --git a/TIUIElements/Sources/Separators/SeparatorAppearance.swift b/TIUIElements/Sources/Separators/SeparatorAppearance.swift index ee4914f0..78885e2d 100644 --- a/TIUIElements/Sources/Separators/SeparatorAppearance.swift +++ b/TIUIElements/Sources/Separators/SeparatorAppearance.swift @@ -39,6 +39,6 @@ public final class SeparatorLayout: UIView.BaseWrappedLayout, WrappedViewLayout public final class SeparatorAppearance: UIView.BaseAppearance, ViewAppearance { public static var defaultAppearance: Self { - Self(backgroundColor: .lightGray) + Self(background: UIViewColorBackground(color: .lightGray)) } } diff --git a/TIUIElements/Sources/Views/BaseStackView/BaseStackView.swift b/TIUIElements/Sources/Views/BaseStackView/BaseStackView.swift index f0158699..76e00419 100644 --- a/TIUIElements/Sources/Views/BaseStackView/BaseStackView.swift +++ b/TIUIElements/Sources/Views/BaseStackView/BaseStackView.swift @@ -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) } diff --git a/TIUIElements/Sources/Views/ListItemView/BaseListItemAppearance.swift b/TIUIElements/Sources/Views/ListItemView/BaseListItemAppearance.swift index 5e4b843f..b4e6e1e1 100644 --- a/TIUIElements/Sources/Views/ListItemView/BaseListItemAppearance.swift +++ b/TIUIElements/Sources/Views/ListItemView/BaseListItemAppearance.swift @@ -23,28 +23,29 @@ import TIUIKitCore import UIKit -open class BaseListItemAppearance: UIView.BaseAppearance { +open class BaseListItemAppearance: + UIView.BaseAppearance { - 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) } diff --git a/TIUIElements/Sources/Views/Placeholder/PlaceholderFactory.swift b/TIUIElements/Sources/Views/Placeholder/PlaceholderFactory.swift index 7dbfe0cd..d8e8600f 100644 --- a/TIUIElements/Sources/Views/Placeholder/PlaceholderFactory.swift +++ b/TIUIElements/Sources/Views/Placeholder/PlaceholderFactory.swift @@ -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, diff --git a/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift b/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift index 772d4538..e558a295 100644 --- a/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift +++ b/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift @@ -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) } diff --git a/TIUIElements/Sources/Views/Skeletons/Protocols/SkeletonsPresenter.swift b/TIUIElements/Sources/Views/Skeletons/Protocols/SkeletonsPresenter.swift index b9ca217d..198186bc 100644 --- a/TIUIElements/Sources/Views/Skeletons/Protocols/SkeletonsPresenter.swift +++ b/TIUIElements/Sources/Views/Skeletons/Protocols/SkeletonsPresenter.swift @@ -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 } } diff --git a/TIUIElements/Sources/Views/Skeletons/UIView+PresentingSkeletons.swift b/TIUIElements/Sources/Views/Skeletons/UIView+PresentingSkeletons.swift index 62684133..3ed92d51 100644 --- a/TIUIElements/Sources/Views/Skeletons/UIView+PresentingSkeletons.swift +++ b/TIUIElements/Sources/Views/Skeletons/UIView+PresentingSkeletons.swift @@ -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) } diff --git a/TIUIElements/Sources/Views/Skeletons/UIViewController+PresentingSkeletons.swift b/TIUIElements/Sources/Views/Skeletons/UIViewController+PresentingSkeletons.swift index 5da4d039..5c42810a 100644 --- a/TIUIElements/Sources/Views/Skeletons/UIViewController+PresentingSkeletons.swift +++ b/TIUIElements/Sources/Views/Skeletons/UIViewController+PresentingSkeletons.swift @@ -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() } } diff --git a/TIUIElements/Sources/Views/StatefulButton/DefaultConfigurableStatefulButton/DefaultConfigurableStatefulButton.swift b/TIUIElements/Sources/Views/StatefulButton/DefaultConfigurableStatefulButton/DefaultConfigurableStatefulButton.swift index 60c4b597..ecb82a12 100644 --- a/TIUIElements/Sources/Views/StatefulButton/DefaultConfigurableStatefulButton/DefaultConfigurableStatefulButton.swift +++ b/TIUIElements/Sources/Views/StatefulButton/DefaultConfigurableStatefulButton/DefaultConfigurableStatefulButton.swift @@ -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 { - 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?) { diff --git a/TIUIElements/Sources/Views/StatefulButton/StatefulButton+Appearance.swift b/TIUIElements/Sources/Views/StatefulButton/StatefulButton+Appearance.swift index 8d19521f..ea34aa8b 100644 --- a/TIUIElements/Sources/Views/StatefulButton/StatefulButton+Appearance.swift +++ b/TIUIElements/Sources/Views/StatefulButton/StatefulButton+Appearance.swift @@ -25,7 +25,7 @@ import UIKit extension StatefulButton { public typealias StateAppearance = UIButton.BaseAppearance - public typealias StateAppearances = [State: StateAppearance] + public typealias StateAppearances = [StateKey: StateAppearance] open class BaseAppearance: UIView.BaseAppearance { @@ -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) { 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) diff --git a/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift b/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift index eb354409..dcafee80 100644 --- a/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift +++ b/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift @@ -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? @@ -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 diff --git a/TIUIElements/Sources/Views/TitleSubtitleView/DefaultTitleSubtitleView+Appearance.swift b/TIUIElements/Sources/Views/TitleSubtitleView/DefaultTitleSubtitleView+Appearance.swift index dc6906a2..bdc0ea6c 100644 --- a/TIUIElements/Sources/Views/TitleSubtitleView/DefaultTitleSubtitleView+Appearance.swift +++ b/TIUIElements/Sources/Views/TitleSubtitleView/DefaultTitleSubtitleView+Appearance.swift @@ -23,9 +23,9 @@ import TIUIKitCore import UIKit -extension DefaultTitleSubtitleView { +public extension DefaultTitleSubtitleView { - public final class Appearance: UIView.BaseAppearance, WrappedViewAppearance { + final class Appearance: UIView.BaseAppearance, 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) } diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift index d00f8b0e..cf1cd29d 100644 --- a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift @@ -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 diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/ViewBackground.xcplaygroundpage/Contents.swift b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/ViewBackground.xcplaygroundpage/Contents.swift new file mode 100644 index 00000000..7e354944 --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/ViewBackground.xcplaygroundpage/Contents.swift @@ -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) diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/contents.xcplayground b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/contents.xcplayground index 6e3097f4..6bd78ea9 100644 --- a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/contents.xcplayground +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/contents.xcplayground @@ -3,5 +3,6 @@ + \ No newline at end of file diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index db7a7ee6..aee14016 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -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' } diff --git a/TIUIKitCore/Sources/Appearance/UIViewBackground.swift b/TIUIKitCore/Sources/Appearance/UIViewBackground.swift new file mode 100644 index 00000000..899b711d --- /dev/null +++ b/TIUIKitCore/Sources/Appearance/UIViewBackground.swift @@ -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) +} diff --git a/TIUIKitCore/Sources/Appearance/UIViewBorder.swift b/TIUIKitCore/Sources/Appearance/UIViewBorder.swift index 00389343..97e53698 100644 --- a/TIUIKitCore/Sources/Appearance/UIViewBorder.swift +++ b/TIUIKitCore/Sources/Appearance/UIViewBorder.swift @@ -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) } diff --git a/TIUIKitCore/Sources/Appearance/ViewAppearance.swift b/TIUIKitCore/Sources/Appearance/ViewAppearance.swift index 322d3641..d37834b6 100644 --- a/TIUIKitCore/Sources/Appearance/ViewAppearance.swift +++ b/TIUIKitCore/Sources/Appearance/ViewAppearance.swift @@ -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 } } diff --git a/TIUIKitCore/Sources/Extensions/UIButton/UIButton+StateConfiguration.swift b/TIUIKitCore/Sources/Extensions/UIButton/UIButton+StateConfiguration.swift index 45a14cd4..9856472e 100644 --- a/TIUIKitCore/Sources/Extensions/UIButton/UIButton+StateConfiguration.swift +++ b/TIUIKitCore/Sources/Extensions/UIButton/UIButton+StateConfiguration.swift @@ -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) } } } diff --git a/TIUIKitCore/Sources/Extensions/UIControlState/UIControlState+Dictionary.swift b/TIUIKitCore/Sources/Extensions/UIControlState/UIControlState+Dictionary.swift index c83b00fe..8731d9d8 100644 --- a/TIUIKitCore/Sources/Extensions/UIControlState/UIControlState+Dictionary.swift +++ b/TIUIKitCore/Sources/Extensions/UIControlState/UIControlState+Dictionary.swift @@ -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?] } diff --git a/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift b/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift index ff0db7eb..01317407 100644 --- a/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift +++ b/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift @@ -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) { + customAttributes: [NSAttributedString.Key: Any] = [:], + paragraphStyleConfiguration: ParameterClosure? = 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 diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index 186f3483..1937a07e 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -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' } diff --git a/TIWebView/TIWebView.podspec b/TIWebView/TIWebView.podspec index 2e4837e1..b40abf2b 100644 --- a/TIWebView/TIWebView.podspec +++ b/TIWebView/TIWebView.podspec @@ -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 diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec index 1c5facd3..6ed989a1 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -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' } diff --git a/docs/tibottomsheet/tibottomsheet.md b/docs/tibottomsheet/tibottomsheet.md index 2e7e2173..d7425a68 100644 --- a/docs/tibottomsheet/tibottomsheet.md +++ b/docs/tibottomsheet/tibottomsheet.md @@ -16,7 +16,7 @@ class EmptyViewController: BaseModalViewController { } ## Обертка вокруг существующего контроллера - Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `BaseModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер + Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `DefaultModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер ```swift import TIUIKitCore @@ -25,13 +25,13 @@ final class OldMassiveViewController: BaseInitializableViewController { // some implementation } -typealias ModalOldMassiveViewController = BaseModalWrapperViewController +typealias ModalOldMassiveViewController = DefaultModalWrapperViewController 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() 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() 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 ``` diff --git a/docs/tikeychainutils/singlevaluestorage.md b/docs/tikeychainutils/singlevaluestorage.md index 07d0518f..cf92ac47 100644 --- a/docs/tikeychainutils/singlevaluestorage.md +++ b/docs/tikeychainutils/singlevaluestorage.md @@ -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 diff --git a/docs/tiuielements/skeletons.md b/docs/tiuielements/skeletons.md index a31fd600..efd24bf4 100644 --- a/docs/tiuielements/skeletons.md +++ b/docs/tiuielements/skeletons.md @@ -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 diff --git a/docs/tiuielements/viewbackground.md b/docs/tiuielements/viewbackground.md new file mode 100644 index 00000000..d1f6fa9c --- /dev/null +++ b/docs/tiuielements/viewbackground.md @@ -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) +```