diff --git a/CHANGELOG.md b/CHANGELOG.md index eaa39c1d..99b9b311 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### 1.48.0 + +- **Added**: `BaseStackView` with configurable items appearance +- **Fixed**: `CollectionTableViewCell` self-sizing +- **Added**: `ViewAppearance.WrappedViewLayout` support for all `WrappedViewHolders` +- **Added**: `ViewCallbacks` support for all `BaseInitializeableViews` + ### 1.47.0 - **Added**: `flatMap` operator for `AsyncOperation` diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index 37f275b2..c240a0ed 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAppleMapUtils' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec index 3a4416da..6506a712 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAuth' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Login, registration, confirmation and other related actions' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIDeeplink/TIDeeplink.podspec b/TIDeeplink/TIDeeplink.podspec index b78e4656..a9aee6ff 100644 --- a/TIDeeplink/TIDeeplink.podspec +++ b/TIDeeplink/TIDeeplink.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeeplink' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Deeplink service API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIDeveloperUtils/Sources/DashedBoundsLayer/DashedBoundLayer.swift b/TIDeveloperUtils/Sources/DashedBoundsLayer/DashedBoundLayer.swift index a631060f..cbc46486 100644 --- a/TIDeveloperUtils/Sources/DashedBoundsLayer/DashedBoundLayer.swift +++ b/TIDeveloperUtils/Sources/DashedBoundsLayer/DashedBoundLayer.swift @@ -59,6 +59,20 @@ open class DashedBoundsLayer: CAShapeLayer { public extension UIView { + @discardableResult + func debugBoundsVisually(debugSubviews: Bool = true, after delay: DispatchTimeInterval? = nil) -> UIView { + guard let delay else { + debugBoundsVisually(debugSubviews: debugSubviews) + return self + } + + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + self.debugBoundsVisually(debugSubviews: debugSubviews) + } + + return self + } + @discardableResult func debugBoundsVisually(debugSubviews: Bool = true) -> UIView { disableBoundsVisuallyDebug() @@ -93,8 +107,10 @@ public extension UIView { public extension UIViewController { @discardableResult - func debugBoundsVisually(debugSubviews: Bool = true) -> UIViewController { - view.debugBoundsVisually(debugSubviews: debugSubviews) + func debugBoundsVisually(debugSubviews: Bool = true, + after delay: DispatchTimeInterval? = nil) -> UIViewController { + + view.debugBoundsVisually(debugSubviews: debugSubviews, after: delay) return self } } diff --git a/TIDeveloperUtils/TIDeveloperUtils.podspec b/TIDeveloperUtils/TIDeveloperUtils.podspec index 1fd97b1f..efe45d85 100644 --- a/TIDeveloperUtils/TIDeveloperUtils.podspec +++ b/TIDeveloperUtils/TIDeveloperUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeveloperUtils' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Universal web view API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIEcommerce/Sources/Filters/FiltersViews/ListFilters/FiltersTableView/BaseFiltersTableView.swift b/TIEcommerce/Sources/Filters/FiltersViews/ListFilters/FiltersTableView/BaseFiltersTableView.swift index 84d28809..157ac359 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/ListFilters/FiltersTableView/BaseFiltersTableView.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/ListFilters/FiltersTableView/BaseFiltersTableView.swift @@ -28,8 +28,7 @@ import UIKit @available(iOS 13.0, *) open class BaseFiltersTableView: - UITableView, - InitializableViewProtocol, + BaseInitializeableTableView, Updatable, UITableViewDelegate where CellType.ViewModelType: FilterCellViewModelProtocol & Hashable { @@ -67,24 +66,16 @@ open class BaseFiltersTableView CGFloat { diff --git a/TIEcommerce/Sources/Filters/FiltersViews/TagsFilters/FiltersCollectionView/Views/BaseFiltersCollectionView.swift b/TIEcommerce/Sources/Filters/FiltersViews/TagsFilters/FiltersCollectionView/Views/BaseFiltersCollectionView.swift index 9fa9e155..24941042 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/TagsFilters/FiltersCollectionView/Views/BaseFiltersCollectionView.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/TagsFilters/FiltersCollectionView/Views/BaseFiltersCollectionView.swift @@ -22,13 +22,13 @@ import TISwiftUtils import TIUIKitCore +import TIUIElements import UIKit @available(iOS 13.0, *) open class BaseFiltersCollectionView: - UICollectionView, - InitializableViewProtocol, + BaseInitializeableCollectionView, Updatable, UICollectionViewDelegate where CellType.ViewModelType: FilterCellViewModelProtocol & Hashable { @@ -53,7 +53,6 @@ open class BaseFiltersCollectionView 'MIT', :file => 'LICENSE' } diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index 1881b9d8..2aebc4ac 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Set of helpers for Foundation framework classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec index b5ffdfd9..abcaa73a 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIGoogleMapUtils' - s.version = '1.47.0' + s.version = '1.48.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/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } 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 e1a757c0..d689e954 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 @@ -106,8 +106,8 @@ switch expirationCheckStorage.getValue() { case let .success(token): // use token break -case let .failure(storageError) - if .valueNotFound = storageError { +case let .failure(storageError): + if .valueNotFound == storageError { // token is missing or expired, request new token } else { // handle storage error diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index 69183289..be7b8c61 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Set of helpers for Keychain classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TILogging/TILogging.podspec b/TILogging/TILogging.podspec index 87d0c220..4462442f 100644 --- a/TILogging/TILogging.podspec +++ b/TILogging/TILogging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TILogging' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Logging for TI libraries.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index 7475e4b8..9eebc217 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMapUtils' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Set of helpers for map objects clustering and interacting.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index f2f8afe1..9dc1e7e2 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Moya + Swagger network service.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index ffdf6bec..5d13194b 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Swagger-frendly networking layer helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index 6d715d3e..e4b34e38 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Caching results of EndpointRequests.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIPagination/TIPagination.podspec b/TIPagination/TIPagination.podspec index c566dc32..a5003aaf 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Generic pagination component.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec index 1dcada42..67e85faf 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUICore' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index ef389fb6..ae5386da 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Bunch of useful helpers for Swift development.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index 01cdfed1..9f5fab98 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Set of helpers for TableKit classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + 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/**/*' + 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 'TIUIElements', s.version.to_s s.dependency 'TISwiftUtils', s.version.to_s diff --git a/TITextProcessing/TITextProcessing.podspec b/TITextProcessing/TITextProcessing.podspec index 752fba5c..e5d52087 100644 --- a/TITextProcessing/TITextProcessing.podspec +++ b/TITextProcessing/TITextProcessing.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITextProcessing' - s.version = '1.47.0' + s.version = '1.48.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/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIElements/Sources/Appearance/UIView+Appearance.swift b/TIUIElements/Sources/Appearance/UIView+Appearance.swift index eafab957..440c1e66 100644 --- a/TIUIElements/Sources/Appearance/UIView+Appearance.swift +++ b/TIUIElements/Sources/Appearance/UIView+Appearance.swift @@ -24,71 +24,6 @@ import TIUIKitCore import UIKit extension UIView { - - // MARK: - Layout Variations - - public struct NoLayout: ViewLayout { - public static var defaultLayout: Self { - Self() - } - } - - open class BaseSizeLayout { - public var size: CGSize - - public init(size: CGSize = .infinity) { - self.size = size - } - } - - public final class DefaultLayout: BaseSizeLayout, SizeViewLayout { - public static var defaultLayout: Self { - Self() - } - } - - // MARK: - WrappedView Layout - - open class BaseWrappedLayout: BaseSizeLayout { - public var centerOffset: UIOffset - public var insets: UIEdgeInsets - - public init(insets: UIEdgeInsets = .zero, - size: CGSize = .infinity, - centerOffset: UIOffset = .nan) { - - self.centerOffset = centerOffset - self.insets = insets - - super.init(size: size) - } - } - - open class BaseSpecedWrappedLayout: BaseWrappedLayout { - public var spacing: CGFloat - - public init(insets: UIEdgeInsets = .zero, - size: CGSize = .infinity, - centerOffset: UIOffset = .nan, - spacing: CGFloat = .zero) { - self.spacing = spacing - - super.init(insets: insets, size: size, centerOffset: centerOffset) - } - } - - public final class DefaultWrappedLayout: BaseWrappedLayout, WrappedViewLayout { - public static var defaultLayout: Self { - Self() - } - } - - public final class DefaultSpacedWrappedLayout: BaseSpecedWrappedLayout, SpacedWrappedViewLayout { - public static var defaultLayout: Self { - Self() - } - } - // MARK: - Appearance Variations open class BaseAppearance { @@ -138,9 +73,7 @@ extension UIView { } } - open class BaseWrappedAppearance: BaseAppearance { - - } + open class BaseWrappedAppearance: BaseAppearance {} public final class DefaultWrappedViewHolderAppearance: BaseWrappedViewHolderAppearance, @@ -163,6 +96,4 @@ extension UIView { } } -extension UIView.DefaultWrappedViewHolderAppearance: WrappedViewAppearance where Layout: WrappedViewLayout { - -} +extension UIView.DefaultWrappedViewHolderAppearance: WrappedViewAppearance where Layout: WrappedViewLayout {} diff --git a/TIUIElements/Sources/Appearance/UIView+ViewLayout.swift b/TIUIElements/Sources/Appearance/UIView+ViewLayout.swift new file mode 100644 index 00000000..94904d82 --- /dev/null +++ b/TIUIElements/Sources/Appearance/UIView+ViewLayout.swift @@ -0,0 +1,125 @@ +// +// 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 +import TIUIKitCore + +extension UIView { + + // MARK: - Layout Variations + + public struct NoLayout: ViewLayout { + public static var defaultLayout: Self { + Self() + } + } + + open class BaseSizeLayout { + public var size: CGSize + + public init(size: CGSize = .infinity) { + self.size = size + } + } + + public final class DefaultLayout: BaseSizeLayout, SizeViewLayout { + public static var defaultLayout: Self { + Self() + } + } + + // MARK: - WrappedView Layout + + open class BaseWrappedLayout: BaseSizeLayout { + public var centerOffset: UIOffset + public var insets: UIEdgeInsets + + public init(insets: UIEdgeInsets = .zero, + size: CGSize = .infinity, + centerOffset: UIOffset = .nan) { + + self.centerOffset = centerOffset + self.insets = insets + + super.init(size: size) + } + } + + public final class DefaultWrappedLayout: BaseWrappedLayout, WrappedViewLayout { + public static var defaultLayout: Self { + Self() + } + } + + open class BaseSpecedWrappedLayout: BaseWrappedLayout { + public var spacing: CGFloat + + public init(insets: UIEdgeInsets = .zero, + size: CGSize = .infinity, + centerOffset: UIOffset = .nan, + spacing: CGFloat = .zero) { + self.spacing = spacing + + super.init(insets: insets, size: size, centerOffset: centerOffset) + } + } + + // MARK: - SpacedWrappedLayout + + public final class DefaultSpacedWrappedLayout: BaseSpecedWrappedLayout, SpacedWrappedViewLayout { + public static var defaultLayout: Self { + Self() + } + } + + // MARK: - StackLayout + + open class BaseStackLayout: UIView.BaseSpecedWrappedLayout { + public var axis: NSLayoutConstraint.Axis + public var distribution: UIStackView.Distribution + public var alignment: UIStackView.Alignment + + public init(insets: UIEdgeInsets = .zero, + size: CGSize = .infinity, + centerOffset: UIOffset = .nan, + spacing: CGFloat = .zero, + axis: NSLayoutConstraint.Axis = .vertical, + distribution: UIStackView.Distribution = .fill, + alignment: UIStackView.Alignment = .fill) { + + self.axis = axis + self.distribution = distribution + self.alignment = alignment + + super.init(insets: insets, + size: size, + centerOffset: centerOffset, + spacing: spacing) + } + } + + public final class DefaultStackLayout: BaseStackLayout, StackLayout { + public static var defaultLayout: Self { + Self() + } + } +} diff --git a/TIUIElements/Sources/ViewCallbacks/UIView+Callbacks.swift b/TIUIElements/Sources/ViewCallbacks/UIView+Callbacks.swift new file mode 100644 index 00000000..0a2a3e99 --- /dev/null +++ b/TIUIElements/Sources/ViewCallbacks/UIView+Callbacks.swift @@ -0,0 +1,46 @@ +// +// 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 +import TIUIKitCore + +extension UIView { + open class RoundCornersCallback: BaseViewCallbacks { + public var corners: CACornerMask + + public init(view: View, corners: CACornerMask = .allCorners) { + self.corners = corners + + super.init(view: view) + } + + override open func onDidLayoutSubviews() { + withStrongView { + $0.layer.round(corners: corners, radius: calculateCornerRadius(for: $0.bounds)) + } + } + + open func calculateCornerRadius(for bounds: CGRect) -> CGFloat { + min(bounds.width, bounds.height) / 2 + } + } +} diff --git a/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableButton.swift b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableButton.swift new file mode 100644 index 00000000..701e51bb --- /dev/null +++ b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableButton.swift @@ -0,0 +1,70 @@ +// +// 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 +import TIUIKitCore + +open class BaseInitializableButton: UIButton, InitializableViewProtocol { + public var callbacks: [ViewCallbacks] = [] + + override public init(frame: CGRect) { + super.init(frame: frame) + + initializeView() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + initializeView() + } + + override open func layoutSubviews() { + super.layoutSubviews() + + for callback in callbacks { + callback.onDidLayoutSubviews() + } + } + + // MARK: - InitializableView + + open func addViews() { + // empty for subclasses overriding + } + + open func bindViews() { + // empty for subclasses overriding + } + + open func configureLayout() { + // empty for subclasses overriding + } + + open func configureAppearance() { + // empty for subclasses overriding + } + + open func localize() { + // empty for subclasses overriding + } +} diff --git a/TIUIElements/Sources/Views/Cells/BaseInitializableCell.swift b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableCell.swift similarity index 89% rename from TIUIElements/Sources/Views/Cells/BaseInitializableCell.swift rename to TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableCell.swift index 7f45b55f..35d7b240 100644 --- a/TIUIElements/Sources/Views/Cells/BaseInitializableCell.swift +++ b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableCell.swift @@ -24,6 +24,8 @@ import TIUIKitCore import UIKit open class BaseInitializableCell: UITableViewCell, InitializableViewProtocol { + public var callbacks: [ViewCallbacks] = [] + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: .default, reuseIdentifier: reuseIdentifier) @@ -33,6 +35,16 @@ open class BaseInitializableCell: UITableViewCell, InitializableViewProtocol { @available(*, unavailable) required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) + + initializeView() + } + + override open func layoutSubviews() { + super.layoutSubviews() + + for callback in callbacks { + callback.onDidLayoutSubviews() + } } // MARK: - InitializableView diff --git a/TIUIElements/Sources/Views/BaseInitializableControl.swift b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableControl.swift similarity index 89% rename from TIUIElements/Sources/Views/BaseInitializableControl.swift rename to TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableControl.swift index aef5ec83..d70783ac 100644 --- a/TIUIElements/Sources/Views/BaseInitializableControl.swift +++ b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableControl.swift @@ -24,6 +24,8 @@ import UIKit import TIUIKitCore open class BaseInitializableControl: UIControl, InitializableViewProtocol { + public var callbacks: [ViewCallbacks] = [] + override public init(frame: CGRect) { super.init(frame: frame) @@ -36,6 +38,14 @@ open class BaseInitializableControl: UIControl, InitializableViewProtocol { initializeView() } + override open func layoutSubviews() { + super.layoutSubviews() + + for callback in callbacks { + callback.onDidLayoutSubviews() + } + } + // MARK: - InitializableView open func addViews() { diff --git a/TIUIElements/Sources/Views/BaseInitializableTextView.swift b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableTextView.swift similarity index 89% rename from TIUIElements/Sources/Views/BaseInitializableTextView.swift rename to TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableTextView.swift index c0aa13db..69c7ec28 100644 --- a/TIUIElements/Sources/Views/BaseInitializableTextView.swift +++ b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableTextView.swift @@ -24,6 +24,8 @@ import UIKit.UITextView import TIUIKitCore open class BaseInitializableTextView: UITextView, InitializableViewProtocol { + public var callbacks: [ViewCallbacks] = [] + public override init(frame: CGRect, textContainer: NSTextContainer?) { super.init(frame: frame, textContainer: textContainer) @@ -36,6 +38,14 @@ open class BaseInitializableTextView: UITextView, InitializableViewProtocol { initializeView() } + override open func layoutSubviews() { + super.layoutSubviews() + + for callback in callbacks { + callback.onDidLayoutSubviews() + } + } + // MARK: - InitializableView open func addViews() { diff --git a/TIUIElements/Sources/Views/BaseInitializableView.swift b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableView.swift similarity index 89% rename from TIUIElements/Sources/Views/BaseInitializableView.swift rename to TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableView.swift index d7192498..cf27eb00 100644 --- a/TIUIElements/Sources/Views/BaseInitializableView.swift +++ b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializableView.swift @@ -24,6 +24,8 @@ import UIKit import TIUIKitCore open class BaseInitializableView: UIView, InitializableViewProtocol { + public var callbacks: [ViewCallbacks] = [] + override public init(frame: CGRect) { super.init(frame: frame) @@ -36,6 +38,14 @@ open class BaseInitializableView: UIView, InitializableViewProtocol { initializeView() } + override open func layoutSubviews() { + super.layoutSubviews() + + for callback in callbacks { + callback.onDidLayoutSubviews() + } + } + // MARK: - InitializableView open func addViews() { diff --git a/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializeableCollectionView.swift b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializeableCollectionView.swift new file mode 100644 index 00000000..76665c52 --- /dev/null +++ b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializeableCollectionView.swift @@ -0,0 +1,70 @@ +// +// 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 +import TIUIKitCore + +open class BaseInitializeableCollectionView: UICollectionView, InitializableViewProtocol { + public var callbacks: [ViewCallbacks] = [] + + public override init(frame: CGRect, collectionViewLayout: UICollectionViewLayout) { + super.init(frame: frame, collectionViewLayout: collectionViewLayout) + + initializeView() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + initializeView() + } + + override open func layoutSubviews() { + super.layoutSubviews() + + for callback in callbacks { + callback.onDidLayoutSubviews() + } + } + + // MARK: - InitializableView + + open func addViews() { + // empty for subclasses overriding + } + + open func bindViews() { + // empty for subclasses overriding + } + + open func configureLayout() { + // empty for subclasses overriding + } + + open func configureAppearance() { + backgroundColor = .clear + } + + open func localize() { + // empty for subclasses overriding + } +} diff --git a/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializeableTableView.swift b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializeableTableView.swift new file mode 100644 index 00000000..638cb195 --- /dev/null +++ b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializeableTableView.swift @@ -0,0 +1,70 @@ +// +// 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 +import TIUIKitCore + +open class BaseInitializeableTableView: UITableView, InitializableViewProtocol { + public var callbacks: [ViewCallbacks] = [] + + public override init(frame: CGRect, style: UITableView.Style) { + super.init(frame: frame, style: style) + + initializeView() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + initializeView() + } + + override open func layoutSubviews() { + super.layoutSubviews() + + for callback in callbacks { + callback.onDidLayoutSubviews() + } + } + + // MARK: - InitializableView + + open func addViews() { + // empty for subclasses overriding + } + + open func bindViews() { + // empty for subclasses overriding + } + + open func configureLayout() { + // empty for subclasses overriding + } + + open func configureAppearance() { + backgroundColor = .clear + } + + open func localize() { + // empty for subclasses overriding + } +} diff --git a/TIUIElements/Sources/Views/BaseStackView/BaseStackView.swift b/TIUIElements/Sources/Views/BaseStackView/BaseStackView.swift new file mode 100644 index 00000000..36c51f2d --- /dev/null +++ b/TIUIElements/Sources/Views/BaseStackView/BaseStackView.swift @@ -0,0 +1,124 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit +import TIUIKitCore + +open class BaseStackView: UIStackView { + public var customArrangedSubviews: [View] = [] + + @available(*, unavailable, message: "Use strongly-typed version of this function") + open override func addArrangedSubview(_ view: UIView) { + super.addArrangedSubview(view) + } + + @available(*, unavailable, message: "Use strongly-typed version of this function") + open override func removeArrangedSubview(_ view: UIView) { + super.removeArrangedSubview(view) + } + + @available(*, unavailable, message: "Use strongly-typed version of this function") + open override func insertArrangedSubview(_ view: UIView, at stackIndex: Int) { + super.insertArrangedSubview(view, at: stackIndex) + } + + open func addArrangedSubview(_ view: View) { + customArrangedSubviews.append(view) + + super.addArrangedSubview(view) + } + + open func removeArrangedSubview(_ view: View) { + guard let indexOfView = customArrangedSubviews.firstIndex(where: { $0 === view }) else { + assertionFailure("Unable to find \(view) in \(customArrangedSubviews)") + return + } + + customArrangedSubviews.remove(at: indexOfView) + + super.removeArrangedSubview(view) + } + + open func replaceArrangedSubviews(_ newViews: [View]) { + for existingSubview in customArrangedSubviews { + removeArrangedSubview(existingSubview) + } + + for view in newViews { + addArrangedSubview(view) + } + } + + open func insertArrangedSubview(_ view: View, at stackIndex: Int) { + customArrangedSubviews.insert(view, at: stackIndex) + + super.insertArrangedSubview(view, at: stackIndex) + } + + open func configureUIStackView(appearance: BaseWrappedAppearance) { + configureUIView(appearance: appearance) + + axis = appearance.layout.axis + distribution = appearance.layout.distribution + alignment = appearance.layout.alignment + spacing = appearance.layout.spacing + } +} + +extension BaseStackView: ConfigurableView where View: ConfigurableView { + public func configure(with viewModels: [View.ViewModelType]) { + replaceArrangedSubviews(viewModels.map { View(viewModel: $0) }) + } +} + +extension BaseStackView: AppearanceConfigurable where View: AppearanceConfigurable { + public final class Appearance: UIView.BaseWrappedAppearance, WrappedViewAppearance { + + public static var defaultAppearance: Self { + Self() + } + + public var arrangedSubviewsAppearance: View.Appearance + + public init(layout: UIView.DefaultStackLayout = .defaultLayout, + backgroundColor: UIColor = .clear, + border: UIViewBorder = .init(), + shadow: UIViewShadow? = nil, + arrangedSubviewsAppearance: View.Appearance = .defaultAppearance) { + + self.arrangedSubviewsAppearance = arrangedSubviewsAppearance + + super.init(layout: layout, + backgroundColor: backgroundColor, + border: border, + shadow: shadow) + } + } + + public func configure(appearance: Appearance) { + configureUIStackView(appearance: appearance) + + for customSubview in customArrangedSubviews { + customSubview.configure(appearance: appearance.arrangedSubviewsAppearance) + } + } +} diff --git a/TIUIElements/Sources/Views/StatefulButton/RoundedStatefulButton.swift b/TIUIElements/Sources/Views/DefaultConfigurableLabel/DefaultConfigurableLabel.swift similarity index 61% rename from TIUIElements/Sources/Views/StatefulButton/RoundedStatefulButton.swift rename to TIUIElements/Sources/Views/DefaultConfigurableLabel/DefaultConfigurableLabel.swift index 893995eb..987f4446 100644 --- a/TIUIElements/Sources/Views/StatefulButton/RoundedStatefulButton.swift +++ b/TIUIElements/Sources/Views/DefaultConfigurableLabel/DefaultConfigurableLabel.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Touch Instinct +// 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 @@ -20,38 +20,19 @@ // THE SOFTWARE. // -import TIUIKitCore import UIKit +import TIUIKitCore -open class RoundedStatefulButton: StatefulButton { +public final class DefaultConfigurableLabel: UILabel, ConfigurableView, AppearanceConfigurable { + // MARK: - ConfigurableView - // UIView override - - override public init(frame: CGRect) { - super.init(frame: frame) - - configureAppearance() + public func configure(with text: String) { + self.text = text } - required public init?(coder: NSCoder) { - super.init(coder: coder) + // MARK: - AppearanceConfigurable - configureAppearance() - } - - open override func layoutSubviews() { - super.layoutSubviews() - - layer.cornerRadius = calculateCornerRadius(for: bounds) - } - - // MARK: - Open methods - - open func configureAppearance() { - layer.round(corners: .allCorners) - } - - open func calculateCornerRadius(for bounds: CGRect) -> CGFloat { - min(bounds.width, bounds.height) / 2 + public func configure(appearance: UILabel.DefaultAppearance) { + configureUILabel(appearance: appearance) } } diff --git a/TIUIElements/Sources/Views/Helpers/WrappedViewLayout+Helpers.swift b/TIUIElements/Sources/Views/Helpers/WrappedViewLayout+Helpers.swift deleted file mode 100644 index e9881d1b..00000000 --- a/TIUIElements/Sources/Views/Helpers/WrappedViewLayout+Helpers.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// 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 - -extension WrappedViewLayout { - func setupSize(widthConstraint: NSLayoutConstraint?, - heightConstraint: NSLayoutConstraint?) { - - if size.width.isFinite { - widthConstraint?.constant = size.width - widthConstraint?.isActive = true - } else { - widthConstraint?.isActive = false - } - - if size.height.isFinite { - heightConstraint?.constant = size.height - heightConstraint?.isActive = true - } else { - heightConstraint?.isActive = false - } - } - - func setupCenterYOffset(centerYConstraint: NSLayoutConstraint?, - topConstraint: NSLayoutConstraint?, - bottomConstraint: NSLayoutConstraint?) { - - let centerYOffset = centerOffset.vertical - - if centerYOffset.isFinite { - centerYConstraint?.constant = centerYOffset - centerYConstraint?.isActive = true - topConstraint?.isActive = false - bottomConstraint?.isActive = false - - } else { - topConstraint?.constant = insets.top - bottomConstraint?.constant = -insets.bottom - centerYConstraint?.isActive = false - topConstraint?.isActive = true - bottomConstraint?.isActive = true - } - } - - func setupCenterXOffset(centerXConstraint: NSLayoutConstraint?, - leadingConstraint: NSLayoutConstraint?, - trailingConstraint: NSLayoutConstraint?) { - - let centerXOffset = centerOffset.horizontal - - if centerXOffset.isFinite { - centerXConstraint?.constant = centerXOffset - centerXConstraint?.isActive = true - leadingConstraint?.isActive = false - trailingConstraint?.isActive = false - - } else { - leadingConstraint?.constant = insets.left - trailingConstraint?.constant = -insets.right - centerXConstraint?.isActive = false - leadingConstraint?.isActive = true - trailingConstraint?.isActive = true - } - } -} diff --git a/TIUIElements/Sources/Views/ListItemView/BaseListItemView.swift b/TIUIElements/Sources/Views/ListItemView/BaseListItemView.swift index 4d2003b3..e5a9aa5e 100644 --- a/TIUIElements/Sources/Views/ListItemView/BaseListItemView.swift +++ b/TIUIElements/Sources/Views/ListItemView/BaseListItemView.swift @@ -33,55 +33,57 @@ open class BaseListItemView: UIImageView, - InitializableViewProtocol { + InitializableViewProtocol, + WrappedViewHolder { - public let placeholderView = Placeholder() + public var callbacks: [ViewCallbacks] = [] - public var placeholderConstraints: SubviewConstraints? + // MARK: - WrappedViewHolder + + public private(set) lazy var wrappedView = Placeholder() + + public var contentInsets: UIEdgeInsets = .zero { + didSet { + update(subviewConstraints: placeholderConstraints) + } + } + + public var contentSize: CGSize = .infinity { + didSet { + update(subviewConstraints: placeholderConstraints) + } + } + + public var contentCenterOffset: UIOffset = .nan { + didSet { + update(subviewConstraints: placeholderConstraints) + } + } + + public lazy var placeholderConstraints: SubviewConstraints = { + configureWrappedViewLayout() + }() open override var image: UIImage? { get { super.image } set { - placeholderView.isHidden = newValue != nil + wrappedView.isHidden = newValue != nil super.image = newValue } @@ -61,29 +86,22 @@ open class BasePlaceholderImageView: UIImageView, initializeView() } + override open func layoutSubviews() { + super.layoutSubviews() + + for callback in callbacks { + callback.onDidLayoutSubviews() + } + } + // MARK: - InitializableViewProtocol open func addViews() { - addSubview(placeholderView) + addSubview(wrappedView) } open func configureLayout() { - placeholderView.translatesAutoresizingMaskIntoConstraints = false - - let placeholderConstraints = SubviewConstraints( - centerXConstraint: placeholderView.centerXAnchor.constraint(equalTo: centerXAnchor), - centerYConstraint: placeholderView.centerYAnchor.constraint(equalTo: centerYAnchor), - leadingConstraint: placeholderView.leadingAnchor.constraint(equalTo: leadingAnchor), - topConstraint: placeholderView.topAnchor.constraint(equalTo: topAnchor), - trailingConstraint: placeholderView.trailingAnchor.constraint(equalTo: trailingAnchor), - bottomConstraint: placeholderView.bottomAnchor.constraint(equalTo: bottomAnchor), - widthConstraint: placeholderView.widthAnchor.constraint(equalToConstant: .zero), - heightConstraint: placeholderView.heightAnchor.constraint(equalToConstant: .zero)) - - NSLayoutConstraint.activate(placeholderConstraints.constraints) - NSLayoutConstraint.deactivate(placeholderConstraints.sizeConstraints) - - self.placeholderConstraints = placeholderConstraints + // override in subclass } open func bindViews() { @@ -103,22 +121,7 @@ open class BasePlaceholderImageView: UIImageView, open func configureBasePlaceholder(appearance: BaseWrappedViewHolderAppearance) { configureUIView(appearance: appearance) - placeholderView.configureUIView(appearance: appearance.subviewAppearance) - configurePlaceholderLayout(layout: appearance.subviewAppearance.layout) - } - - // MARK: - Private methods - - private func configurePlaceholderLayout(layout: some WrappedViewLayout) { - layout.setupSize(widthConstraint: placeholderConstraints?.widthConstraint, - heightConstraint: placeholderConstraints?.heightConstraint) - - layout.setupCenterYOffset(centerYConstraint: placeholderConstraints?.centerYConstraint, - topConstraint: placeholderConstraints?.topConstraint, - bottomConstraint: placeholderConstraints?.bottomConstraint) - - layout.setupCenterXOffset(centerXConstraint: placeholderConstraints?.centerXConstraint, - leadingConstraint: placeholderConstraints?.leadingConstraint, - trailingConstraint: placeholderConstraints?.trailingConstraint) + wrappedView.configureUIView(appearance: appearance.subviewAppearance) + updateContentLayout(from: appearance.subviewAppearance.layout) } } diff --git a/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift b/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift index 55663664..7167c1ce 100644 --- a/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift +++ b/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift @@ -30,9 +30,46 @@ open class BasePlaceholderView: BaseInitializableView { public let textView = DefaultTitleSubtitleView() public let controlsStackView = UIStackView() - public var imageViewConstraints: SubviewConstraints? - public var textViewConstraints: SubviewConstraints? - public var controlsViewConstraints: SubviewConstraints? + public private(set) lazy var imageViewConstraints: SubviewConstraints = { + SubviewConstraints(edgeConstraints: EdgeConstraints(leadingConstraint: imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingConstraint: imageView.trailingAnchor.constraint(equalTo: trailingAnchor), + topConstraint: imageView.topAnchor.constraint(equalTo: topAnchor), + bottomConstraint: imageView.bottomAnchor.constraint(equalTo: textView.topAnchor)), + centerConstraints: CenterConstraints(centerXConstraint: imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + centerYConstraint: imageView.centerYAnchor.constraint(equalTo: centerYAnchor)), + sizeConstraints: SizeConstraints(widthConstraint: imageView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: imageView.heightAnchor.constraint(equalToConstant: .zero))) + }() + + public private(set) lazy var textViewTopToSuperviewTopConstraint: NSLayoutConstraint = { + textView.topAnchor.constraint(equalTo: topAnchor) + }() + + public private(set) lazy var textViewBottomToSuperviewBottomConstraint: NSLayoutConstraint = { + textView.bottomAnchor.constraint(equalTo: bottomAnchor) + }() + + public private(set) lazy var textViewConstraints: SubviewConstraints = { + SubviewConstraints(edgeConstraints: EdgeConstraints(leadingConstraint: textView.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingConstraint: textView.trailingAnchor.constraint(equalTo: trailingAnchor), + topConstraint: textView.topAnchor.constraint(equalTo: imageView.bottomAnchor), + bottomConstraint: textView.bottomAnchor.constraint(lessThanOrEqualTo: controlsStackView.topAnchor)), + centerConstraints: CenterConstraints(centerXConstraint: textView.centerXAnchor.constraint(equalTo: centerXAnchor), + centerYConstraint: textView.centerYAnchor.constraint(equalTo: centerYAnchor)), + sizeConstraints: SizeConstraints(widthConstraint: textView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: textView.heightAnchor.constraint(equalToConstant: .zero))) + }() + + public private(set) lazy var controlsViewConstraints: SubviewConstraints = { + SubviewConstraints(edgeConstraints: EdgeConstraints(leadingConstraint: controlsStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingConstraint: controlsStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + topConstraint: controlsStackView.topAnchor.constraint(equalTo: textView.bottomAnchor), + bottomConstraint: controlsStackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor)), + centerConstraints: CenterConstraints(centerXConstraint: controlsStackView.centerXAnchor.constraint(equalTo: centerXAnchor), + centerYConstraint: controlsStackView.centerYAnchor.constraint(equalTo: centerYAnchor)), + sizeConstraints: SizeConstraints(widthConstraint: controlsStackView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: controlsStackView.heightAnchor.constraint(equalToConstant: .zero))) + }() public var keyboardDidShownObserver: NSObjectProtocol? public var keyboardDidHiddenObserver: NSObjectProtocol? @@ -71,42 +108,15 @@ open class BasePlaceholderView: BaseInitializableView { [imageView, textView, controlsStackView] .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } - imageViewConstraints = .init( - centerXConstraint: imageView.centerXAnchor.constraint(equalTo: centerXAnchor), - centerYConstraint: imageView.centerYAnchor.constraint(equalTo: centerYAnchor), - leadingConstraint: imageView.leadingAnchor.constraint(equalTo: leadingAnchor), - topConstraint: imageView.topAnchor.constraint(equalTo: topAnchor), - trailingConstraint: imageView.trailingAnchor.constraint(equalTo: trailingAnchor), - bottomConstraint: imageView.bottomAnchor.constraint(equalTo: textView.topAnchor), - widthConstraint: imageView.widthAnchor.constraint(equalToConstant: .zero), - heightConstraint: imageView.heightAnchor.constraint(equalToConstant: .zero)) - - textViewConstraints = .init( - centerXConstraint: textView.centerXAnchor.constraint(equalTo: centerXAnchor), - centerYConstraint: textView.centerYAnchor.constraint(equalTo: centerYAnchor), - leadingConstraint: textView.leadingAnchor.constraint(equalTo: leadingAnchor), - topConstraint: textView.topAnchor.constraint(equalTo: imageView.bottomAnchor), - trailingConstraint: textView.trailingAnchor.constraint(equalTo: trailingAnchor), - bottomConstraint: textView.bottomAnchor.constraint(lessThanOrEqualTo: controlsStackView.topAnchor)) - - controlsViewConstraints = .init( - centerXConstraint: controlsStackView.centerXAnchor.constraint(equalTo: centerXAnchor), - leadingConstraint: controlsStackView.leadingAnchor.constraint(equalTo: leadingAnchor), - topConstraint: controlsStackView.topAnchor.constraint(equalTo: textView.bottomAnchor), - trailingConstraint: controlsStackView.trailingAnchor.constraint(equalTo: trailingAnchor), - bottomConstraint: controlsStackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor), - widthConstraint: controlsStackView.widthAnchor.constraint(equalToConstant: .zero), - heightConstraint: controlsStackView.heightAnchor.constraint(equalToConstant: .zero)) - NSLayoutConstraint.activate( - (imageViewConstraints?.constraints ?? []) - + (textViewConstraints?.constraints ?? []) - + (controlsViewConstraints?.constraints ?? []) + imageViewConstraints.allConstraints + + textViewConstraints.allConstraints + + controlsViewConstraints.allConstraints ) NSLayoutConstraint.deactivate( - (imageViewConstraints?.sizeConstraints ?? []) - + (controlsViewConstraints?.sizeConstraints ?? []) + imageViewConstraints.sizeConstraints.allConstraints + + controlsViewConstraints.sizeConstraints.allConstraints ) } @@ -179,59 +189,42 @@ open class BasePlaceholderView: BaseInitializableView { let multiplier = isKeyboardHidden ? 1.0 : -1.0 if let height = getKeyboardHeight(notification) { - controlsViewConstraints?.bottomConstraint?.constant = multiplier * height / 2 + controlsViewConstraints.edgeConstraints.bottomConstraint.constant = multiplier * height / 2 } } // MARK: - Private methods private func configureImageViewLayout(layout: WrappedViewLayout) { - guard !isImageViewHidden, let imageViewConstraints = imageViewConstraints else { - NSLayoutConstraint.deactivate(imageViewConstraints?.constraints ?? []) + guard !isImageViewHidden else { + imageViewConstraints.deactivate() return } - configureLayout(layout: layout, constraints: imageViewConstraints) + imageViewConstraints.update(from: layout) } private func configureTextViewLayout(layout: WrappedViewLayout) { - if isImageViewHidden { - self.textViewConstraints?.topConstraint?.isActive = false - self.textViewConstraints?.topConstraint = textView.topAnchor.constraint(equalTo: topAnchor) - } + textViewConstraints.edgeConstraints.topConstraint.isActive = !isImageViewHidden + textViewTopToSuperviewTopConstraint.isActive = isImageViewHidden - if isControlsViewHidden { - self.textViewConstraints?.bottomConstraint?.isActive = false - self.textViewConstraints?.bottomConstraint = textView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor) - } + textViewConstraints.edgeConstraints.bottomConstraint.isActive = !isControlsViewHidden + textViewBottomToSuperviewBottomConstraint.isActive = isControlsViewHidden - if let textViewConstraints = textViewConstraints { - configureLayout(layout: layout, constraints: textViewConstraints) - } + textViewConstraints.update(from: layout) } private func configureControlsViewLayout(layout: SpacedWrappedViewLayout) { - guard !isControlsViewHidden, let controlsViewConstraints = controlsViewConstraints else { - NSLayoutConstraint.deactivate(controlsViewConstraints?.constraints ?? []) + guard !isControlsViewHidden else { + controlsViewConstraints.deactivate() return } - configureLayout(layout: layout, constraints: controlsViewConstraints) + controlsViewConstraints.update(from: layout) + controlsStackView.spacing = layout.spacing } - private func configureLayout(layout: WrappedViewLayout, constraints: SubviewConstraints) { - layout.setupSize(widthConstraint: constraints.widthConstraint, heightConstraint: constraints.heightConstraint) - - layout.setupCenterYOffset(centerYConstraint: constraints.centerYConstraint, - topConstraint: constraints.topConstraint, - bottomConstraint: constraints.bottomConstraint) - - layout.setupCenterXOffset(centerXConstraint: constraints.centerXConstraint, - leadingConstraint: constraints.leadingConstraint, - trailingConstraint: constraints.trailingConstraint) - } - private func getKeyboardHeight(_ notification: Notification) -> CGFloat? { guard let userInfo = notification.userInfo else { return nil diff --git a/TIUIElements/Sources/Views/Placeholder/Views/DefaultPlaceholderImageView.swift b/TIUIElements/Sources/Views/Placeholder/Views/DefaultPlaceholderImageView.swift index f73677dd..61f5b787 100644 --- a/TIUIElements/Sources/Views/Placeholder/Views/DefaultPlaceholderImageView.swift +++ b/TIUIElements/Sources/Views/Placeholder/Views/DefaultPlaceholderImageView.swift @@ -35,10 +35,10 @@ final public class DefaultPlaceholderImageView: BasePlaceholderImageView, App super.applyBaseStyle(style: style) - configureImageSizeConstraints(size: imageView.image?.size ?? .zero) + guard let image = imageView.image else { + return + } + + imageViewConstraints.sizeConstraints.update(from: image.size) } // MARK: - AppearanceConfigurable @@ -42,22 +46,6 @@ public final class DefaultPlaceholderView: BasePlaceholderView, App public func configure(appearance: Appearance) { configureAppearance(appearance: appearance) } - - // MARK: - Private methods - - private func configureImageSizeConstraints(size: CGSize) { - guard size != .zero else { - return - } - - if size.height.isFinite, size.height > .zero { - imageViewConstraints?.heightConstraint?.constant = size.height - } - - if size.width.isFinite, size.width > .zero { - imageViewConstraints?.widthConstraint?.constant = size.width - } - } } // MARK: - Default appearance model diff --git a/TIUIElements/Sources/Views/StatefulButton/BaseButtonViewModel.swift b/TIUIElements/Sources/Views/StatefulButton/BaseButtonViewModel.swift new file mode 100644 index 00000000..cebe9fb7 --- /dev/null +++ b/TIUIElements/Sources/Views/StatefulButton/BaseButtonViewModel.swift @@ -0,0 +1,38 @@ +// +// 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 + +open class BaseButtonViewModel { + public var title: String + public var image: UIImage? + public var backgroundImage: UIImage? + + public init(title: String, + image: UIImage? = nil, + backgroundImage: UIImage? = nil) { + + self.title = title + self.image = image + self.backgroundImage = backgroundImage + } +} diff --git a/TIUIElements/Sources/Views/StatefulButton/DefaultConfigurableStatefulButton.swift b/TIUIElements/Sources/Views/StatefulButton/DefaultConfigurableStatefulButton.swift new file mode 100644 index 00000000..dfdbc5fb --- /dev/null +++ b/TIUIElements/Sources/Views/StatefulButton/DefaultConfigurableStatefulButton.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 TIUIKitCore +import UIKit + +public final class DefaultConfigurableStatefulButton: StatefulButton, ConfigurableView, AppearanceConfigurable { + + // MARK: - ConfigurableView + + public func configure(with stateViewModelMap: [State: BaseButtonViewModel]) { + for (state, viewModel) in stateViewModelMap { + setTitle(viewModel.title, for: state) + setImage(viewModel.image, for: state) + setBackgroundImage(viewModel.backgroundImage, for: state) + } + } + + // MARK: - AppearanceConfigurable + + public func configure(appearance: Appearance) { + configureUIButton(appearance: appearance) + + stateAppearance = appearance.stateAppearance + } +} + +extension DefaultConfigurableStatefulButton { + + public final class Appearance: UIButton.BaseAppearance, WrappedViewAppearance { + + public enum Defaults { + public static var stateAppearance: StateAppearance { + [ + .normal: DefaultAppearance.defaultAppearance, + .highlighted: DefaultAppearance.defaultAppearance, + .selected: DefaultAppearance.defaultAppearance, + .disabled: DefaultAppearance.defaultAppearance + ] + } + } + + public static var defaultAppearance: Self { + Self() + } + + public var stateAppearance: StateAppearance + + public init(stateAppearance: StateAppearance = Defaults.stateAppearance) { + self.stateAppearance = stateAppearance + + let defaultAppearance = stateAppearance[.normal] + + super.init(layout: defaultAppearance?.layout ?? .defaultLayout, + backgroundColor: defaultAppearance?.backgroundColor ?? .clear, + border: defaultAppearance?.border ?? .init(), + shadow: defaultAppearance?.shadow, + textAttributes: defaultAppearance?.textAttributes, + contentInsets: defaultAppearance?.contentInsets ?? .zero, + titleInsets: defaultAppearance?.titleInsets ?? .zero, + imageInsets: defaultAppearance?.imageInsets ?? .zero) + } + } +} diff --git a/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift b/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift index 2c3abe2d..4a491203 100644 --- a/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift +++ b/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift @@ -24,7 +24,7 @@ import TISwiftUtils import TIUIKitCore import UIKit -open class StatefulButton: UIButton { +open class StatefulButton: BaseInitializableButton { public enum ActivityIndicatorPosition { case center @@ -75,7 +75,7 @@ open class StatefulButton: UIButton { // MARK: - Background - private var stateAppearance: StateAppearance = [:] { + var stateAppearance: StateAppearance = [:] { didSet { updateAppearance() } diff --git a/TIUIElements/Sources/Helpers/SubviewConstraints.swift b/TIUIElements/Sources/Wrappers/Constraints/CenterConstraints.swift similarity index 57% rename from TIUIElements/Sources/Helpers/SubviewConstraints.swift rename to TIUIElements/Sources/Wrappers/Constraints/CenterConstraints.swift index b02f1e4f..6aace408 100644 --- a/TIUIElements/Sources/Helpers/SubviewConstraints.swift +++ b/TIUIElements/Sources/Wrappers/Constraints/CenterConstraints.swift @@ -20,33 +20,30 @@ // THE SOFTWARE. // -import class UIKit.NSLayoutConstraint +import UIKit -public struct SubviewConstraints { - public var centerXConstraint: NSLayoutConstraint? - public var centerYConstraint: NSLayoutConstraint? - public var leadingConstraint: NSLayoutConstraint? - public var topConstraint: NSLayoutConstraint? - public var trailingConstraint: NSLayoutConstraint? - public var bottomConstraint: NSLayoutConstraint? - public var widthConstraint: NSLayoutConstraint? - public var heightConstraint: NSLayoutConstraint? +public struct CenterConstraints: ConstraintsSet { + public let centerXConstraint: NSLayoutConstraint + public let centerYConstraint: NSLayoutConstraint - public var constraints: [NSLayoutConstraint] { + // MARK: - ConstraintsSet + + public var allConstraints: [NSLayoutConstraint] { [ centerXConstraint, - leadingConstraint, - topConstraint, - trailingConstraint, - bottomConstraint, - widthConstraint, - heightConstraint + centerYConstraint ] - .compactMap { $0 } } - public var sizeConstraints: [NSLayoutConstraint] { - [widthConstraint, heightConstraint] - .compactMap { $0 } + public init(centerXConstraint: NSLayoutConstraint, + centerYConstraint: NSLayoutConstraint) { + + self.centerXConstraint = centerXConstraint + self.centerYConstraint = centerYConstraint + } + + public func update(offset: UIOffset) { + centerXConstraint.setActiveConstantOrDeactivate(constant: offset.horizontal) + centerYConstraint.setActiveConstantOrDeactivate(constant: offset.vertical) } } diff --git a/TIUIElements/Sources/Wrappers/Constraints/ConstraintsSet.swift b/TIUIElements/Sources/Wrappers/Constraints/ConstraintsSet.swift new file mode 100644 index 00000000..d1aeec12 --- /dev/null +++ b/TIUIElements/Sources/Wrappers/Constraints/ConstraintsSet.swift @@ -0,0 +1,37 @@ +// +// 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.NSLayoutConstraint + +public protocol ConstraintsSet { + var allConstraints: [NSLayoutConstraint] { get } +} + +public extension ConstraintsSet { + func activate() { + NSLayoutConstraint.activate(allConstraints) + } + + func deactivate() { + NSLayoutConstraint.deactivate(allConstraints) + } +} diff --git a/TIUIElements/Sources/Wrappers/EdgeConstraints.swift b/TIUIElements/Sources/Wrappers/Constraints/EdgeConstraints.swift similarity index 75% rename from TIUIElements/Sources/Wrappers/EdgeConstraints.swift rename to TIUIElements/Sources/Wrappers/Constraints/EdgeConstraints.swift index cccfdd8f..d6278aa6 100644 --- a/TIUIElements/Sources/Wrappers/EdgeConstraints.swift +++ b/TIUIElements/Sources/Wrappers/Constraints/EdgeConstraints.swift @@ -22,12 +22,14 @@ import UIKit -public struct EdgeConstraints { +public struct EdgeConstraints: ConstraintsSet { public let leadingConstraint: NSLayoutConstraint public let trailingConstraint: NSLayoutConstraint public let topConstraint: NSLayoutConstraint public let bottomConstraint: NSLayoutConstraint + // MARK: - ConstraintsSet + public var allConstraints: [NSLayoutConstraint] { [ leadingConstraint, @@ -37,6 +39,20 @@ public struct EdgeConstraints { ] } + public var horizontal: [NSLayoutConstraint] { + [ + leadingConstraint, + trailingConstraint + ] + } + + public var vertical: [NSLayoutConstraint] { + [ + topConstraint, + bottomConstraint + ] + } + public init(leadingConstraint: NSLayoutConstraint, trailingConstraint: NSLayoutConstraint, topConstraint: NSLayoutConstraint, @@ -48,18 +64,10 @@ public struct EdgeConstraints { self.bottomConstraint = bottomConstraint } - public func activate() { - NSLayoutConstraint.activate(allConstraints) - } - - public func deactivate() { - NSLayoutConstraint.deactivate(allConstraints) - } - public func update(from insets: UIEdgeInsets) { - leadingConstraint.constant = insets.left - trailingConstraint.constant = -insets.right - topConstraint.constant = insets.top - bottomConstraint.constant = -insets.bottom + leadingConstraint.setActiveConstantOrDeactivate(constant: insets.left) + trailingConstraint.setActiveConstantOrDeactivate(constant: -insets.right) + topConstraint.setActiveConstantOrDeactivate(constant: insets.top) + bottomConstraint.setActiveConstantOrDeactivate(constant: -insets.bottom) } } diff --git a/TIUIElements/Sources/Wrappers/Constraints/NSLayoutConstraint+SetActiveOrDeactivate.swift b/TIUIElements/Sources/Wrappers/Constraints/NSLayoutConstraint+SetActiveOrDeactivate.swift new file mode 100644 index 00000000..4929b2c3 --- /dev/null +++ b/TIUIElements/Sources/Wrappers/Constraints/NSLayoutConstraint+SetActiveOrDeactivate.swift @@ -0,0 +1,34 @@ +// +// 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.NSLayoutConstraint + +extension NSLayoutConstraint { + func setActiveConstantOrDeactivate(constant: CGFloat) { + if constant.isFinite { + self.constant = constant + isActive = true + } else { + isActive = false + } + } +} diff --git a/TIUIElements/Sources/Wrappers/Constraints/SizeConstraints.swift b/TIUIElements/Sources/Wrappers/Constraints/SizeConstraints.swift new file mode 100644 index 00000000..d65caae7 --- /dev/null +++ b/TIUIElements/Sources/Wrappers/Constraints/SizeConstraints.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) 2022 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 + +public struct SizeConstraints: ConstraintsSet { + public let widthConstraint: NSLayoutConstraint + public let heightConstraint: NSLayoutConstraint + + // MARK: - ConstraintsSet + + public var allConstraints: [NSLayoutConstraint] { + [ + widthConstraint, + heightConstraint + ] + } + + public init(widthConstraint: NSLayoutConstraint, + heightConstraint: NSLayoutConstraint) { + + self.widthConstraint = widthConstraint + self.heightConstraint = heightConstraint + } + + public func update(from size: CGSize) { + widthConstraint.setActiveConstantOrDeactivate(constant: size.width) + heightConstraint.setActiveConstantOrDeactivate(constant: size.height) + } +} diff --git a/TIUIElements/Sources/Wrappers/Constraints/SubviewConstraints.swift b/TIUIElements/Sources/Wrappers/Constraints/SubviewConstraints.swift new file mode 100644 index 00000000..64372a50 --- /dev/null +++ b/TIUIElements/Sources/Wrappers/Constraints/SubviewConstraints.swift @@ -0,0 +1,67 @@ +// +// 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 +import TIUIKitCore + +public struct SubviewConstraints: ConstraintsSet { + public var edgeConstraints: EdgeConstraints + public var centerConstraints: CenterConstraints + public var sizeConstraints: SizeConstraints + + // MARK: - ConstraintsSet + + public var allConstraints: [NSLayoutConstraint] { + edgeConstraints.allConstraints + centerConstraints.allConstraints + sizeConstraints.allConstraints + } + + public init(edgeConstraints: EdgeConstraints, + centerConstraints: CenterConstraints, + sizeConstraints: SizeConstraints) { + + self.edgeConstraints = edgeConstraints + self.centerConstraints = centerConstraints + self.sizeConstraints = sizeConstraints + } + + func update(insets: UIEdgeInsets, size: CGSize, centerOffset: UIOffset) { + centerConstraints.update(offset: centerOffset) + sizeConstraints.update(from: size) + edgeConstraints.update(from: insets) + + for verticalConstraint in edgeConstraints.vertical { + verticalConstraint.isActive = !centerOffset.vertical.isFinite + } + + for horizontalConstraint in edgeConstraints.horizontal { + horizontalConstraint.isActive = !centerOffset.horizontal.isFinite + } + } + + // MARK: - WrappedViewLayout shortcut + + func update(from layout: some WrappedViewLayout) { + update(insets: layout.insets, + size: layout.size, + centerOffset: layout.centerOffset) + } +} diff --git a/TIUIElements/Sources/Wrappers/Containers/CollectionTableViewCell.swift b/TIUIElements/Sources/Wrappers/Containers/CollectionTableViewCell.swift index abf70b41..43c2aed1 100644 --- a/TIUIElements/Sources/Wrappers/Containers/CollectionTableViewCell.swift +++ b/TIUIElements/Sources/Wrappers/Containers/CollectionTableViewCell.swift @@ -24,16 +24,38 @@ import UIKit open class CollectionTableViewCell: ContainerTableViewCell { + private var contentSizeObservation: NSKeyValueObservation? + + open override func bindViews() { + super.bindViews() + + if #available(iOS 16, *) { + contentSizeObservation = wrappedView.observe(\.contentSize, + options: [.new]) { collectionView, change in + + if let contentSize = change.newValue, !contentSize.equalTo(collectionView.bounds.size) { + self.invalidateIntrinsicContentSize() + } + } + } + } + // MARK: - UIView Overrides open override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize { + if #unavailable(iOS 16.0.0) { + wrappedView.setNeedsLayout() + wrappedView.layoutIfNeeded() + } + let cachedCollectionFrame = wrappedView.frame wrappedView.frame.size.width = targetSize.width - contentInsets.left - contentInsets.right let collectionContentHeight = wrappedView.collectionViewLayout.collectionViewContentSize.height wrappedView.frame = cachedCollectionFrame + return CGSize(width: targetSize.width, height: collectionContentHeight + contentInsets.top + contentInsets.bottom) } diff --git a/TIUIElements/Sources/Wrappers/Containers/ContainerCollectionViewCell.swift b/TIUIElements/Sources/Wrappers/Containers/ContainerCollectionViewCell.swift index e071af05..983a88da 100644 --- a/TIUIElements/Sources/Wrappers/Containers/ContainerCollectionViewCell.swift +++ b/TIUIElements/Sources/Wrappers/Containers/ContainerCollectionViewCell.swift @@ -24,17 +24,34 @@ import UIKit import TIUIKitCore open class ContainerCollectionViewCell: UICollectionViewCell, InitializableViewProtocol, WrappedViewHolder { + + public var callbacks: [ViewCallbacks] = [] + // MARK: - WrappedViewHolder public private(set) lazy var wrappedView = createView() public var contentInsets: UIEdgeInsets = .zero { didSet { - contentEdgeConstraints?.update(from: contentInsets) + update(subviewConstraints: subviewContraints) } } - private var contentEdgeConstraints: EdgeConstraints? + public var contentSize: CGSize = .infinity { + didSet { + update(subviewConstraints: subviewContraints) + } + } + + public var contentCenterOffset: UIOffset = .nan { + didSet { + update(subviewConstraints: subviewContraints) + } + } + + private lazy var subviewContraints: SubviewConstraints = { + configureWrappedViewLayout() + }() // MARK: - Initialization @@ -50,6 +67,14 @@ open class ContainerCollectionViewCell: UICollectionViewCell, Init initializeView() } + override open func layoutSubviews() { + super.layoutSubviews() + + for callback in callbacks { + callback.onDidLayoutSubviews() + } + } + // MARK: - InitializableView open func addViews() { @@ -61,7 +86,7 @@ open class ContainerCollectionViewCell: UICollectionViewCell, Init } open func configureLayout() { - contentEdgeConstraints = configureWrappedViewLayout() + subviewContraints.edgeConstraints.activate() } open func configureAppearance() { diff --git a/TIUIElements/Sources/Wrappers/Containers/ContainerTableViewCell.swift b/TIUIElements/Sources/Wrappers/Containers/ContainerTableViewCell.swift index 585ed55c..fc2a93f9 100644 --- a/TIUIElements/Sources/Wrappers/Containers/ContainerTableViewCell.swift +++ b/TIUIElements/Sources/Wrappers/Containers/ContainerTableViewCell.swift @@ -24,17 +24,32 @@ import UIKit import TIUIKitCore open class ContainerTableViewCell: BaseInitializableCell, WrappedViewHolder { + // MARK: - WrappedViewHolder public private(set) lazy var wrappedView = createView() public var contentInsets: UIEdgeInsets = .zero { didSet { - contentEdgeConstraints?.update(from: contentInsets) + update(subviewConstraints: subviewContraints) } } - private var contentEdgeConstraints: EdgeConstraints? + public var contentSize: CGSize = .infinity { + didSet { + update(subviewConstraints: subviewContraints) + } + } + + public var contentCenterOffset: UIOffset = .nan { + didSet { + update(subviewConstraints: subviewContraints) + } + } + + private lazy var subviewContraints: SubviewConstraints = { + configureWrappedViewLayout() + }() // MARK: - InitializableView @@ -44,10 +59,10 @@ open class ContainerTableViewCell: BaseInitializableCell, WrappedV contentView.addSubview(wrappedView) } - override open func configureLayout() { + open override func configureLayout() { super.configureLayout() - contentEdgeConstraints = configureWrappedViewLayout() + subviewContraints.edgeConstraints.activate() } open func createView() -> View { @@ -57,7 +72,7 @@ open class ContainerTableViewCell: BaseInitializableCell, WrappedV // MARK: - Open methods public func configureContainerTableViewCell(appearance: BaseWrappedViewHolderAppearance) { - contentInsets = appearance.subviewAppearance.layout.insets + updateContentLayout(from: appearance.subviewAppearance.layout) configureUIView(appearance: appearance) } } diff --git a/TIUIElements/Sources/Wrappers/Containers/ContainerView.swift b/TIUIElements/Sources/Wrappers/Containers/ContainerView.swift index 77e07dd8..e3366378 100644 --- a/TIUIElements/Sources/Wrappers/Containers/ContainerView.swift +++ b/TIUIElements/Sources/Wrappers/Containers/ContainerView.swift @@ -25,15 +25,29 @@ import UIKit public final class ContainerView: BaseInitializableView, WrappedViewHolder { - public var wrappedView = View() + public private(set) lazy var wrappedView = View() public var contentInsets: UIEdgeInsets = .zero { didSet { - contentEdgeConstraints?.update(from: contentInsets) + update(subviewConstraints: subviewContraints) } } - private var contentEdgeConstraints: EdgeConstraints? + public var contentSize: CGSize = .infinity { + didSet { + update(subviewConstraints: subviewContraints) + } + } + + public var contentCenterOffset: UIOffset = .nan { + didSet { + update(subviewConstraints: subviewContraints) + } + } + + private lazy var subviewContraints: SubviewConstraints = { + configureWrappedViewLayout() + }() // MARK: - InitializableView @@ -46,14 +60,13 @@ public final class ContainerView: BaseInitializableView, WrappedVi public override func configureLayout() { super.configureLayout() - contentEdgeConstraints = configureWrappedViewLayout() + subviewContraints.edgeConstraints.activate() } } // MARK: - ConfigurableView extension ContainerView: ConfigurableView where View: ConfigurableView { - public func configure(with viewModel: View.ViewModelType) { wrappedView.configure(with: viewModel) } @@ -69,6 +82,6 @@ extension ContainerView: AppearanceConfigurable where View: AppearanceConfigurab public func configure(appearance: Appearance) { wrappedView.configure(appearance: appearance.subviewAppearance) configureUIView(appearance: appearance) - contentInsets = appearance.subviewAppearance.layout.insets + updateContentLayout(from: appearance.subviewAppearance.layout) } } diff --git a/TIUIElements/Sources/Wrappers/Containers/ReusableCollectionContainerView.swift b/TIUIElements/Sources/Wrappers/Containers/ReusableCollectionContainerView.swift index c73cf449..9f62832c 100644 --- a/TIUIElements/Sources/Wrappers/Containers/ReusableCollectionContainerView.swift +++ b/TIUIElements/Sources/Wrappers/Containers/ReusableCollectionContainerView.swift @@ -24,17 +24,33 @@ import UIKit import TIUIKitCore open class ReusableCollectionContainerView: UICollectionReusableView, InitializableViewProtocol, WrappedViewHolder { + public var callbacks: [ViewCallbacks] = [] + // MARK: - WrappedViewHolder public private(set) lazy var wrappedView = createView() public var contentInsets: UIEdgeInsets = .zero { didSet { - contentEdgeConstraints?.update(from: contentInsets) + update(subviewConstraints: subviewContraints) } } - private var contentEdgeConstraints: EdgeConstraints? + public var contentSize: CGSize = .infinity { + didSet { + update(subviewConstraints: subviewContraints) + } + } + + public var contentCenterOffset: UIOffset = .nan { + didSet { + update(subviewConstraints: subviewContraints) + } + } + + private lazy var subviewContraints: SubviewConstraints = { + configureWrappedViewLayout() + }() // MARK: - Initialization @@ -50,6 +66,14 @@ open class ReusableCollectionContainerView: UICollectionReusableVi initializeView() } + override open func layoutSubviews() { + super.layoutSubviews() + + for callback in callbacks { + callback.onDidLayoutSubviews() + } + } + // MARK: - InitializableView open func addViews() { @@ -61,7 +85,7 @@ open class ReusableCollectionContainerView: UICollectionReusableVi } open func configureLayout() { - contentEdgeConstraints = configureWrappedViewLayout() + subviewContraints.edgeConstraints.activate() } open func configureAppearance() { diff --git a/TIUIElements/Sources/Wrappers/Extensions/WrappableView+Containers.swift b/TIUIElements/Sources/Wrappers/Extensions/WrappableView+Containers.swift index 67dffcdc..73d32fc2 100644 --- a/TIUIElements/Sources/Wrappers/Extensions/WrappableView+Containers.swift +++ b/TIUIElements/Sources/Wrappers/Extensions/WrappableView+Containers.swift @@ -20,8 +20,16 @@ // THE SOFTWARE. // +import UIKit.UICollectionView + public extension WrappableView { typealias InContainerView = ContainerView typealias InTableCell = ContainerTableViewCell typealias InSeparatableTableCell = ContainerSeparatorTableViewCell + + typealias InCollectionCell = ContainerCollectionViewCell +} + +public extension WrappableView where Self: UICollectionView { + typealias InCollectionTableCell = CollectionTableViewCell } diff --git a/TIUIElements/Sources/Wrappers/Protocols/WrappedViewHolder.swift b/TIUIElements/Sources/Wrappers/Protocols/WrappedViewHolder.swift index 720a2cc0..54b6dd50 100644 --- a/TIUIElements/Sources/Wrappers/Protocols/WrappedViewHolder.swift +++ b/TIUIElements/Sources/Wrappers/Protocols/WrappedViewHolder.swift @@ -21,30 +21,61 @@ // import UIKit +import TIUIKitCore -public protocol WrappedViewHolder { +public protocol WrappedViewHolder: AnyObject { associatedtype View: UIView var wrappedView: View { get } var contentView: UIView { get } var contentInsets: UIEdgeInsets { get set } + var contentSize: CGSize { get set } + var contentCenterOffset: UIOffset { get set } } public extension WrappedViewHolder { - func wrappedViewConstraints() -> EdgeConstraints { - .init(leadingConstraint: wrappedView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - trailingConstraint: wrappedView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - topConstraint: wrappedView.topAnchor.constraint(equalTo: contentView.topAnchor), - bottomConstraint: wrappedView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)) + func wrappedViewEdgeConstraints() -> EdgeConstraints { + EdgeConstraints(leadingConstraint: wrappedView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + trailingConstraint: wrappedView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + topConstraint: wrappedView.topAnchor.constraint(equalTo: contentView.topAnchor), + bottomConstraint: wrappedView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)) } - func configureWrappedViewLayout() -> EdgeConstraints { + func wrappedViewSizeConstraints() -> SizeConstraints { + SizeConstraints(widthConstraint: wrappedView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: wrappedView.heightAnchor.constraint(equalToConstant: .zero)) + } + + func wrappedViewCenterConstraints() -> CenterConstraints { + CenterConstraints(centerXConstraint: wrappedView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + centerYConstraint: wrappedView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)) + } + + func configureWrappedViewLayout() -> SubviewConstraints { wrappedView.translatesAutoresizingMaskIntoConstraints = false - let contentEdgeConstraints = wrappedViewConstraints() + let contentEdgeConstraints = wrappedViewEdgeConstraints() contentEdgeConstraints.activate() - return contentEdgeConstraints + return SubviewConstraints(edgeConstraints: contentEdgeConstraints, + centerConstraints: wrappedViewCenterConstraints(), + sizeConstraints: wrappedViewSizeConstraints()) + } + + // MARK: - SubviewConstraints shortcut + + func update(subviewConstraints: SubviewConstraints) { + subviewConstraints.update(insets: contentInsets, + size: contentSize, + centerOffset: contentCenterOffset) + } + + // MARK: - WrappedViewLayout shortcut + + func updateContentLayout(from layout: some WrappedViewLayout) { + contentInsets = layout.insets + contentSize = layout.size + contentCenterOffset = layout.centerOffset } } 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 1d0c53ab..d00f8b0e 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 @@ -307,10 +307,10 @@ extension DefaultPlaceholderImageView: Skeletonable { public func skeletonsChangedState(_ state: SkeletonsState) { switch state { case .shown: - placeholderView.isHidden = false + wrappedView.isHidden = false case .hidden: - placeholderView.isHidden = true + wrappedView.isHidden = true } } } diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index 30990585..4e9d4d0f 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Bunch of useful protocols and views.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIKitCore/Sources/Appearance/ViewLayout.swift b/TIUIKitCore/Sources/Appearance/ViewLayout.swift index 24c51307..5f3566ed 100644 --- a/TIUIKitCore/Sources/Appearance/ViewLayout.swift +++ b/TIUIKitCore/Sources/Appearance/ViewLayout.swift @@ -42,6 +42,12 @@ public protocol SpacedWrappedViewLayout: WrappedViewLayout { var spacing: CGFloat { get } } +public protocol StackLayout: SpacedWrappedViewLayout { + var axis: NSLayoutConstraint.Axis { get } + var distribution: UIStackView.Distribution { get } + var alignment: UIStackView.Alignment { get } +} + // MARK: - Creation methods extension ViewLayout { diff --git a/TIUIKitCore/Sources/ConfigurableView/ConfigurableView.swift b/TIUIKitCore/Sources/ConfigurableView/ConfigurableView.swift index cd5fc1ab..c7b505cf 100644 --- a/TIUIKitCore/Sources/ConfigurableView/ConfigurableView.swift +++ b/TIUIKitCore/Sources/ConfigurableView/ConfigurableView.swift @@ -20,8 +20,18 @@ // THE SOFTWARE. // +import UIKit.UIView + public protocol ConfigurableView { associatedtype ViewModelType func configure(with _: ViewModelType) } + +public extension ConfigurableView where Self: UIView { + init(viewModel: ViewModelType) { + self.init() + + self.configure(with: viewModel) + } +} diff --git a/TIUIKitCore/Sources/Extensions/InitializableView/InitializableView+Extensions.swift b/TIUIKitCore/Sources/Extensions/InitializableView/InitializableView+Extensions.swift index 2c10e7c9..a78f581d 100644 --- a/TIUIKitCore/Sources/Extensions/InitializableView/InitializableView+Extensions.swift +++ b/TIUIKitCore/Sources/Extensions/InitializableView/InitializableView+Extensions.swift @@ -28,5 +28,9 @@ public extension InitializableViewProtocol { bindViews() configureAppearance() localize() + + for callback in callbacks { + callback.onDidInitialize() + } } } diff --git a/TIUIKitCore/Sources/InitializableView/InitializableViewController.swift b/TIUIKitCore/Sources/InitializableView/InitializableViewController.swift index e4f7bb5c..6e7e5fae 100644 --- a/TIUIKitCore/Sources/InitializableView/InitializableViewController.swift +++ b/TIUIKitCore/Sources/InitializableView/InitializableViewController.swift @@ -21,11 +21,12 @@ // public protocol InitializableViewController: InitializableViewProtocol { - func configureBarButtons() } public extension InitializableViewController { + + @available(*, unavailable, message: "Use initializeController for UIViewController instead!") func initializeView() { assertionFailure("Use \(String(describing: initializeController)) for UIViewController instead!") } @@ -38,5 +39,9 @@ public extension InitializableViewController { configureBarButtons() localize() bindViews() + + for callback in callbacks { + callback.onDidInitialize() + } } } diff --git a/TIUIKitCore/Sources/InitializableView/InitializableViewProtocol.swift b/TIUIKitCore/Sources/InitializableView/InitializableViewProtocol.swift index 766256fb..5edf5874 100644 --- a/TIUIKitCore/Sources/InitializableView/InitializableViewProtocol.swift +++ b/TIUIKitCore/Sources/InitializableView/InitializableViewProtocol.swift @@ -23,8 +23,7 @@ /// Protocol with methods that should be called in constructor methods of view. public protocol InitializableViewProtocol { - /// Main method that should call other methods in particular order. - func initializeView() + var callbacks: [ViewCallbacks] { get set } /// Method for adding views to current view. func addViews() diff --git a/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift b/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift index 719578fb..d2737a5e 100644 --- a/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift +++ b/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift @@ -152,20 +152,20 @@ open class BaseTextAttributes { attributedTextConfiguration: { textView.attributedText = $0 }) } - open func configure(button: UIButton, with string: String? = nil) { + open func configure(button: UIButton, with string: String? = nil, for state: UIControl.State = .normal) { if #available(iOS 15, *) { var configuration = button.configuration ?? UIButton.Configuration.plain() if let title = string { - button.setTitle(nil, for: .normal) - button.setAttributedTitle(nil, for: .normal) + button.setTitle(nil, for: state) + button.setAttributedTitle(nil, for: state) configuration.attributedTitle = attributedString(for: title) button.configuration = configuration } } else { - button.setTitle(string, for: .normal) - button.setTitleColor(color, for: .normal) + button.setTitle(string, for: state) + button.setTitleColor(color, for: state) if let label = button.titleLabel { configure(label: label) diff --git a/TIUIKitCore/Sources/ViewCallbacks/BaseViewCallbacks.swift b/TIUIKitCore/Sources/ViewCallbacks/BaseViewCallbacks.swift new file mode 100644 index 00000000..ace9fca4 --- /dev/null +++ b/TIUIKitCore/Sources/ViewCallbacks/BaseViewCallbacks.swift @@ -0,0 +1,40 @@ +// +// 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. +// + +open class BaseViewCallbacks: ViewCallbacks { + public weak var view: View? + + public init(view: View) { + self.view = view + } + + open func onDidInitialize() {} + open func onDidLayoutSubviews() {} + + open func withStrongView(_ unwrappedViewClosure: (View) -> Void) { + guard let view else { + return + } + + unwrappedViewClosure(view) + } +} diff --git a/TIUIKitCore/Sources/ViewCallbacks/ViewCallbacks.swift b/TIUIKitCore/Sources/ViewCallbacks/ViewCallbacks.swift new file mode 100644 index 00000000..fc9d5836 --- /dev/null +++ b/TIUIKitCore/Sources/ViewCallbacks/ViewCallbacks.swift @@ -0,0 +1,26 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public protocol ViewCallbacks { + func onDidInitialize() + func onDidLayoutSubviews() +} diff --git a/TIUIKitCore/Sources/ViewControllers/BaseInitializableViewController.swift b/TIUIKitCore/Sources/ViewControllers/BaseInitializableViewController.swift index 75b98be0..f5e614e7 100644 --- a/TIUIKitCore/Sources/ViewControllers/BaseInitializableViewController.swift +++ b/TIUIKitCore/Sources/ViewControllers/BaseInitializableViewController.swift @@ -24,6 +24,8 @@ import UIKit.UIViewController open class BaseInitializableViewController: UIViewController, InitializableViewController { + public var callbacks: [ViewCallbacks] = [] + override open func viewDidLoad() { super.viewDidLoad() @@ -34,6 +36,14 @@ open class BaseInitializableViewController: UIViewController, InitializableViewC initializeController() } + override open func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + for callback in callbacks { + callback.onDidLayoutSubviews() + } + } + // MARK: - InitializableController open func addViews() { diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index 1349a3d2..cba9cadc 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIWebView/Sources/Views/BaseInitializableWebView.swift b/TIWebView/Sources/Views/BaseInitializableWebView.swift index 4782cd1b..358294d8 100644 --- a/TIWebView/Sources/Views/BaseInitializableWebView.swift +++ b/TIWebView/Sources/Views/BaseInitializableWebView.swift @@ -28,6 +28,8 @@ open class BaseInitializableWebView: WKWebView, InitializableViewProtocol, ConfigurableView { + public var callbacks: [ViewCallbacks] = [] + public var stateHandler: WebViewStateHandler public var viewModel: WebViewModel? { didSet { @@ -50,6 +52,14 @@ open class BaseInitializableWebView: WKWebView, fatalError("init(coder:) has not been implemented") } + override open func layoutSubviews() { + super.layoutSubviews() + + for callback in callbacks { + callback.onDidLayoutSubviews() + } + } + // MARK: - InitializableViewProtocol public func addViews() { diff --git a/TIWebView/TIWebView.podspec b/TIWebView/TIWebView.podspec index ac10df39..f8d00281 100644 --- a/TIWebView/TIWebView.podspec +++ b/TIWebView/TIWebView.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIWebView' - s.version = '1.47.0' + s.version = '1.48.0' s.summary = 'Universal web view API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec index 6dfd6024..8be745c3 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIYandexMapUtils' - s.version = '1.47.0' + s.version = '1.48.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/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/docs/tikeychainutils/singlevaluestorage.md b/docs/tikeychainutils/singlevaluestorage.md index ac420aca..d3dac300 100644 --- a/docs/tikeychainutils/singlevaluestorage.md +++ b/docs/tikeychainutils/singlevaluestorage.md @@ -104,8 +104,8 @@ switch expirationCheckStorage.getValue() { case let .success(token): // use token break -case let .failure(storageError) - if .valueNotFound = storageError { +case let .failure(storageError): + if .valueNotFound == storageError { // token is missing or expired, request new token } else { // handle storage error diff --git a/docs/tiuielements/skeletons.md b/docs/tiuielements/skeletons.md index 39705b71..a31fd600 100644 --- a/docs/tiuielements/skeletons.md +++ b/docs/tiuielements/skeletons.md @@ -331,10 +331,10 @@ extension DefaultPlaceholderImageView: Skeletonable { public func skeletonsChangedState(_ state: SkeletonsState) { switch state { case .shown: - placeholderView.isHidden = false + wrappedView.isHidden = false case .hidden: - placeholderView.isHidden = true + wrappedView.isHidden = true } } }