feat: TIApplication module and other fixes and improvements
This commit is contained in:
parent
b8611321fb
commit
eaea4abd75
|
|
@ -1,5 +1,14 @@
|
|||
# Changelog
|
||||
|
||||
### 1.52.0
|
||||
|
||||
- **Added**: `TIApplication` module with core dependencies of main application and its extension targets
|
||||
- **Added**: `DefaultHomogeneousItemsCollectionView` default collection view implementation with configurable identical-type cells
|
||||
- **Update**: Changed implementation of `AppInstallLifetimeSingleValueStorage`. Now it uses `SingleValueStorage<Bool>` to be able to migrate stored UserDefaults values
|
||||
- **Added**: `UserLocationFetcher.OnLocationFetchFailureCallback` and `ItemDistanceTo` in `TIMapUtils`
|
||||
- **Added**: Tap handler closure to `DefaultConfigurableStatefulButton.ViewModel`
|
||||
|
||||
|
||||
### 1.51.0
|
||||
|
||||
- **Added**: `BaseModalViewController` implementing `PanModalPresentable` with additional functionality
|
||||
|
|
|
|||
8
Makefile
8
Makefile
|
|
@ -1,13 +1,13 @@
|
|||
export SRCROOT := $(shell pwd)
|
||||
|
||||
push_to_podspecs: TISwiftUtils.target TIFoundationUtils.target TICoreGraphicsUtils.target TIKeychainUtils.target TIUIKitCore.target TIUIElements.target TIWebView.target TIBottomSheet.target TISwiftUICore.target TITableKitUtils.target TIDeeplink.target TIDeveloperUtils.target TILogging.target TINetworking.target TIMoyaNetworking.target TINetworkingCache.target TIMapUtils.target TIAppleMapUtils.target TIGoogleMapUtils.target TIPagination.target TIAuth.target TIEcommerce.target TITextProcessing.target
|
||||
push_to_podspecs: TISwiftUtils.target TIFoundationUtils.target TICoreGraphicsUtils.target TIKeychainUtils.target TIUIKitCore.target TIUIElements.target TIWebView.target TIBottomSheet.target TISwiftUICore.target TITableKitUtils.target TIDeeplink.target TIDeveloperUtils.target TILogging.target TINetworking.target TIMoyaNetworking.target TINetworkingCache.target TIMapUtils.target TIAppleMapUtils.target TIGoogleMapUtils.target TIPagination.target TIAuth.target TIEcommerce.target TITextProcessing.target TIApplication.target
|
||||
$(call clean)
|
||||
|
||||
TISwiftUtils.target:
|
||||
MODULE_NAME="TISwiftUtils" ./project-scripts/push_to_podspecs.sh
|
||||
touch TISwiftUtils.target
|
||||
|
||||
TIFoundationUtils.target: TISwiftUtils.target
|
||||
TIFoundationUtils.target: TISwiftUtils.target TILogging.target
|
||||
MODULE_NAME="TIFoundationUtils" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIFoundationUtils.target
|
||||
|
||||
|
|
@ -99,5 +99,9 @@ TITextProcessing.target:
|
|||
MODULE_NAME="TITextProcessing" ./project-scripts/push_to_podspecs.sh
|
||||
touch TITextProcessing.target
|
||||
|
||||
TIApplication.target: TIFoundationUtils.target TILogging.target
|
||||
MODULE_NAME="TIApplication" ./project-scripts/push_to_podspecs.sh
|
||||
touch TIApplication.target
|
||||
|
||||
clean:
|
||||
rm *.target
|
||||
|
|
|
|||
|
|
@ -11,13 +11,19 @@ let package = Package(
|
|||
],
|
||||
products: [
|
||||
|
||||
// MARK: - Application
|
||||
|
||||
.library(name: "TIApplication", targets: ["TIApplication"]),
|
||||
|
||||
// MARK: - UIKit
|
||||
|
||||
.library(name: "TIUIKitCore", targets: ["TIUIKitCore"]),
|
||||
.library(name: "TIUIElements", targets: ["TIUIElements"]),
|
||||
.library(name: "TIWebView", targets: ["TIWebView"]),
|
||||
.library(name: "TIBottomSheet", targets: ["TIBottomSheet"]),
|
||||
|
||||
// MARK: - SwiftUI
|
||||
|
||||
.library(name: "TISwiftUICore", targets: ["TISwiftUICore"]),
|
||||
|
||||
// MARK: - Utils
|
||||
|
|
@ -41,6 +47,7 @@ let package = Package(
|
|||
.library(name: "TIAppleMapUtils", targets: ["TIAppleMapUtils"]),
|
||||
|
||||
// MARK: - Elements
|
||||
|
||||
.library(name: "OTPSwiftView", targets: ["OTPSwiftView"]),
|
||||
.library(name: "TITransitions", targets: ["TITransitions"]),
|
||||
.library(name: "TIPagination", targets: ["TIPagination"]),
|
||||
|
|
@ -60,7 +67,15 @@ let package = Package(
|
|||
],
|
||||
targets: [
|
||||
|
||||
// MARK: - Application architecture
|
||||
|
||||
.target(name: "TIApplication",
|
||||
dependencies: ["TILogging", "TIFoundationUtils", "KeychainAccess"],
|
||||
path: "TIApplication/Sources",
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
// MARK: - UIKit
|
||||
|
||||
.target(name: "TIUIKitCore", dependencies: ["TISwiftUtils"], path: "TIUIKitCore/Sources"),
|
||||
|
||||
.target(name: "TIUIElements",
|
||||
|
|
@ -77,6 +92,7 @@ let package = Package(
|
|||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
// MARK: - SwiftUI
|
||||
|
||||
.target(name: "TISwiftUICore",
|
||||
dependencies: ["TIUIKitCore", "TISwiftUtils"],
|
||||
path: "TISwiftUICore/Sources"),
|
||||
|
|
@ -140,6 +156,7 @@ let package = Package(
|
|||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
// MARK: - Elements
|
||||
|
||||
.target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"),
|
||||
.target(name: "TITransitions", path: "TITransitions/Sources"),
|
||||
.target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/Sources"),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIAppleMapUtils'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import TIFoundationUtils
|
||||
import KeychainAccess
|
||||
import TILogging
|
||||
import UIKit
|
||||
|
||||
public struct CoreDependencies {
|
||||
public var dateFormattersResusePool = DateFormattersReusePool()
|
||||
public var iso8601DateFormattersReusePool = ISO8601DateFormattersReusePool()
|
||||
public var jsonCodingConfigurator: JsonCodingConfigurator
|
||||
public var jsonKeyValueDecoder: JSONKeyValueDecoder
|
||||
public var jsonKeyValueEncoder: JSONKeyValueEncoder
|
||||
|
||||
public var device: UIDevice = .current
|
||||
|
||||
public var bundle: Bundle = .main
|
||||
public var fileManager: FileManager = .default
|
||||
|
||||
public var logger: DefaultOSLogErrorLogger
|
||||
|
||||
public var keychain: Keychain
|
||||
public var defaults: UserDefaults
|
||||
|
||||
public var appGroupDefaults: UserDefaults?
|
||||
public var appGroupKeychain: Keychain?
|
||||
|
||||
public var appGroupCacheDirectory: URL?
|
||||
|
||||
public var networkCallbackQueue: DispatchQueue
|
||||
|
||||
public init(bundleIdentifierPrefix: String,
|
||||
appIdentifier: String,
|
||||
customAppGroupIdentifier: String? = nil) {
|
||||
|
||||
jsonCodingConfigurator = JsonCodingConfigurator(dateFormattersReusePool: dateFormattersResusePool,
|
||||
iso8601DateFormattersReusePool: iso8601DateFormattersReusePool)
|
||||
|
||||
jsonKeyValueDecoder = JSONKeyValueDecoder(jsonDecoder: jsonCodingConfigurator.jsonDecoder)
|
||||
jsonKeyValueEncoder = JSONKeyValueEncoder(jsonEncoder: jsonCodingConfigurator.jsonEncoder)
|
||||
|
||||
let bundleIdentifier = bundleIdentifierPrefix + "." + appIdentifier
|
||||
|
||||
logger = DefaultOSLogErrorLogger(subsystem: bundleIdentifier, category: "general")
|
||||
|
||||
keychain = Keychain(service: bundleIdentifier)
|
||||
defaults = .standard
|
||||
|
||||
let appGroupIdentifier = customAppGroupIdentifier ?? "group." + bundleIdentifierPrefix
|
||||
|
||||
if let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) {
|
||||
var appGroupCacheURL: URL
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
appGroupCacheURL = containerURL.appending(path: "Caches", directoryHint: .isDirectory)
|
||||
} else {
|
||||
appGroupCacheURL = containerURL.appendingPathComponent("Caches", isDirectory: true)
|
||||
}
|
||||
|
||||
var resourceValues = URLResourceValues()
|
||||
resourceValues.isExcludedFromBackup = true
|
||||
|
||||
do {
|
||||
try fileManager.createDirectory(at: appGroupCacheURL,
|
||||
withIntermediateDirectories: true)
|
||||
|
||||
try appGroupCacheURL.setResourceValues(resourceValues)
|
||||
|
||||
appGroupCacheDirectory = appGroupCacheURL
|
||||
} catch {
|
||||
logger.log(error: error, file: #file, line: #line)
|
||||
}
|
||||
|
||||
appGroupDefaults = UserDefaults(suiteName: appGroupIdentifier)
|
||||
appGroupKeychain = Keychain(service: bundleIdentifierPrefix, accessGroup: appGroupIdentifier)
|
||||
} else {
|
||||
appGroupCacheDirectory = nil
|
||||
appGroupDefaults = nil
|
||||
appGroupKeychain = nil
|
||||
}
|
||||
|
||||
networkCallbackQueue = DispatchQueue(label: bundleIdentifier + ".network-callback-queue", attributes: .concurrent)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2022 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,9 +20,7 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol AuthSettingsStorage: AnyObject {
|
||||
/// Should be true by default (on app first run)
|
||||
var shouldResetStoredAuthData: Bool { get set }
|
||||
public protocol TargetDependencies {
|
||||
static func assemble() -> Self
|
||||
static func assembleForPreview() -> Self
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIApplication'
|
||||
s.version = '1.52.0'
|
||||
s.summary = 'Application architecture.'
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
||||
s.ios.deployment_target = '11.0'
|
||||
s.swift_versions = ['5.7']
|
||||
|
||||
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 'TIFoundationUtils', s.version.to_s
|
||||
s.dependency 'TILogging', s.version.to_s
|
||||
s.dependency 'KeychainAccess', "~> 4.2"
|
||||
end
|
||||
|
|
@ -40,8 +40,8 @@ open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage<Data>
|
|||
|
||||
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
|
||||
localAuthContext: LAContext = Defaults.reusableLAContext,
|
||||
settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(),
|
||||
encryptedTokenKeyStorageKey: StorageKey<Data> = Defaults.encryptedTokenKeyStorageKey) {
|
||||
encryptedTokenKeyStorageKey: StorageKey<Data> = Defaults.encryptedTokenKeyStorageKey,
|
||||
appFirstRunCheckStorage: BoolValueDefaultsStorage = DefaultResetAuthSettingsStorage()) {
|
||||
|
||||
let getValueClosure: GetValueClosure = { keychain, storageKey in
|
||||
do {
|
||||
|
|
@ -64,9 +64,9 @@ open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage<Data>
|
|||
}
|
||||
|
||||
super.init(keychain: keychain.authenticationContext(localAuthContext),
|
||||
settingsStorage: settingsStorage,
|
||||
storageKey: encryptedTokenKeyStorageKey,
|
||||
getValueClosure: getValueClosure,
|
||||
storeValueClosure: storeValueClosure)
|
||||
storeValueClosure: storeValueClosure,
|
||||
appFirstRunCheckStorage: appFirstRunCheckStorage)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ open class DefaultEncryptedTokenStorage: SingleValueAuthKeychainStorage<StringEn
|
|||
}
|
||||
|
||||
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
|
||||
settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(),
|
||||
encryptedTokenStorageKey: StorageKey<StringEncryptionResult> = Defaults.encryptedTokenStorageKey) {
|
||||
encryptedTokenStorageKey: StorageKey<StringEncryptionResult> = Defaults.encryptedTokenStorageKey,
|
||||
appFirstRunCheckStorage: BoolValueDefaultsStorage = DefaultResetAuthSettingsStorage()) {
|
||||
|
||||
let getValueClosure: GetValueClosure = { keychain, storageKey in
|
||||
do {
|
||||
|
|
@ -60,9 +60,9 @@ open class DefaultEncryptedTokenStorage: SingleValueAuthKeychainStorage<StringEn
|
|||
}
|
||||
|
||||
super.init(keychain: keychain,
|
||||
settingsStorage: settingsStorage,
|
||||
storageKey: encryptedTokenStorageKey,
|
||||
getValueClosure: getValueClosure,
|
||||
storeValueClosure: storeValueClosure)
|
||||
storeValueClosure: storeValueClosure,
|
||||
appFirstRunCheckStorage: appFirstRunCheckStorage)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,16 +23,16 @@
|
|||
import TIFoundationUtils
|
||||
import Foundation
|
||||
|
||||
open class FingerprintsReinstallChecker: AppReinstallChecker {
|
||||
open class DefaultResetAuthSettingsStorage: DefaultAppFirstRunCheckStorage {
|
||||
public enum Defaults {
|
||||
public static var shouldResetFingerprintsKey: StorageKey<Bool> {
|
||||
.init(rawValue: "shouldResetFingerprints")
|
||||
public static var shouldResetAuthDataKey: StorageKey<Bool> {
|
||||
.init(rawValue: "shouldResetAuthData")
|
||||
}
|
||||
}
|
||||
|
||||
public override init(defaultsStorage: UserDefaults = .standard,
|
||||
storageKey: StorageKey<Bool> = Defaults.shouldResetFingerprintsKey) {
|
||||
public override init(defaults: UserDefaults = .standard,
|
||||
storageKey: StorageKey<Bool> = Defaults.shouldResetAuthDataKey) {
|
||||
|
||||
super.init(defaultsStorage: defaultsStorage, storageKey: storageKey)
|
||||
super.init(defaults: defaults, storageKey: storageKey)
|
||||
}
|
||||
}
|
||||
|
|
@ -25,51 +25,29 @@ import KeychainAccess
|
|||
import Foundation
|
||||
import TIKeychainUtils
|
||||
|
||||
open class SingleValueAuthKeychainStorage<ValueType>: BaseSingleValueKeychainStorage<ValueType> {
|
||||
open class SingleValueAuthKeychainStorage<ValueType>: AppInstallLifetimeSingleValueStorage<
|
||||
BaseSingleValueKeychainStorage<ValueType>,
|
||||
BoolValueDefaultsStorage> {
|
||||
public typealias GetValueClosure = BaseSingleValueKeychainStorage<ValueType>.GetValueClosure
|
||||
public typealias StoreValueClosure = BaseSingleValueKeychainStorage<ValueType>.StoreValueClosure
|
||||
|
||||
open class Defaults {
|
||||
public static var keychainServiceIdentifier: String {
|
||||
Bundle.main.bundleIdentifier ?? "ru.touchin.TIAuth"
|
||||
}
|
||||
}
|
||||
|
||||
public let settingsStorage: AuthSettingsStorage
|
||||
|
||||
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
|
||||
settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(),
|
||||
storageKey: StorageKey<ValueType>,
|
||||
getValueClosure: @escaping GetValueClosure,
|
||||
storeValueClosure: @escaping StoreValueClosure) {
|
||||
storeValueClosure: @escaping StoreValueClosure,
|
||||
appFirstRunCheckStorage: BoolValueDefaultsStorage = DefaultResetAuthSettingsStorage()) {
|
||||
|
||||
self.settingsStorage = settingsStorage
|
||||
let keychainStorage = BaseSingleValueKeychainStorage(keychain: keychain,
|
||||
storageKey: storageKey,
|
||||
getValueClosure: getValueClosure,
|
||||
storeValueClosure: storeValueClosure)
|
||||
|
||||
super.init(keychain: keychain,
|
||||
storageKey: storageKey,
|
||||
getValueClosure: getValueClosure,
|
||||
storeValueClosure: storeValueClosure)
|
||||
}
|
||||
|
||||
// MARK: - SingleValueStorage
|
||||
|
||||
open override func hasStoredValue() -> Bool {
|
||||
!settingsStorage.shouldResetStoredAuthData && super.hasStoredValue()
|
||||
}
|
||||
|
||||
open override func getValue() -> Result<ValueType, StorageError> {
|
||||
guard !settingsStorage.shouldResetStoredAuthData else {
|
||||
let result: Result<ValueType, StorageError>
|
||||
|
||||
do {
|
||||
try storage.remove(storageKey.rawValue)
|
||||
settingsStorage.shouldResetStoredAuthData = false
|
||||
|
||||
result = .failure(.valueNotFound)
|
||||
} catch {
|
||||
result = .failure(.unableToWriteData(underlyingError: error))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return super.getValue()
|
||||
super.init(storage: keychainStorage, appFirstRunCheckStorage: appFirstRunCheckStorage)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIAuth'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIBottomSheet'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.0'
|
||||
s.summary = 'Base models for creating bottom sheet view controllers'
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'castlele' => 'nikita.semenov@touchin.ru',
|
||||
'petropavel13' => 'ivan.smolin@touchin.ru'}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TICoreGraphicsUtils'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.0'
|
||||
s.summary = 'CoreGraphics drawing helpers'
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIDeeplink'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.0'
|
||||
s.summary = 'Deeplink service API'
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru',
|
||||
'castlele' => 'nikita.semenov@touchin.ru' }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIDeveloperUtils'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.0'
|
||||
s.summary = 'Universal web view API'
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru',
|
||||
'castlele' => 'nikita.semenov@touchin.ru' }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIEcommerce'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.0'
|
||||
s.summary = 'Cart, products, promocodes, bonuses and other related actions'
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class BoolValueDefaultsStorage: BaseSingleValueDefaultsStorage<Bool> {
|
||||
public init(defaults: UserDefaults, storageKey: StorageKey<Bool>) {
|
||||
super.init(defaults: defaults,
|
||||
storageKey: storageKey,
|
||||
getValueClosure: { .success($0.bool(forKey: $1.rawValue)) },
|
||||
storeValueClosure: { .success($0.set($1, forKey: $2.rawValue)) })
|
||||
}
|
||||
}
|
||||
|
|
@ -52,9 +52,7 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType,
|
|||
public func store(value: ValueType) -> Result<Void, StorageError> {
|
||||
targetStorage.store(value: value)
|
||||
.flatMap {
|
||||
if case let .failure(error) = sourceStorage.deleteValue() {
|
||||
logErrorIfNeeded(error, file: #file, line: #line)
|
||||
}
|
||||
deleteSourceValue()
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
|
@ -63,9 +61,7 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType,
|
|||
public func getValue() -> Result<ValueType, StorageError> {
|
||||
targetStorage.getValue()
|
||||
.flatMap {
|
||||
if case let .failure(error) = sourceStorage.deleteValue() {
|
||||
logErrorIfNeeded(error, file: #file, line: #line)
|
||||
}
|
||||
deleteSourceValue()
|
||||
|
||||
return .success($0)
|
||||
}
|
||||
|
|
@ -73,7 +69,11 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType,
|
|||
sourceStorage.getValue()
|
||||
.flatMap { value in
|
||||
store(value: value)
|
||||
.map { _ in value }
|
||||
.map { _ in
|
||||
deleteSourceValue()
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +107,12 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType,
|
|||
|
||||
errorLogger.log(error: error, file: file, line: line)
|
||||
}
|
||||
|
||||
private func deleteSourceValue() {
|
||||
if case let .failure(error) = sourceStorage.deleteValue() {
|
||||
logErrorIfNeeded(error, file: #file, line: #line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Factory methods
|
||||
|
|
|
|||
|
|
@ -20,51 +20,52 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
open class AppInstallLifetimeSingleValueStorage<Storage: SingleValueStorage>: SingleValueStorage
|
||||
where Storage.ErrorType == StorageError {
|
||||
public typealias DefaultAppFirstRunCheckStorage = BoolValueDefaultsStorage
|
||||
|
||||
public let appReinstallChecker: AppReinstallChecker
|
||||
open class AppInstallLifetimeSingleValueStorage<Storage: SingleValueStorage, AppFirstRunCheckStorage: SingleValueStorage>: SingleValueStorage
|
||||
where Storage.ErrorType == StorageError,
|
||||
AppFirstRunCheckStorage.ErrorType == Storage.ErrorType,
|
||||
AppFirstRunCheckStorage.ValueType == Bool {
|
||||
|
||||
public let appFirstRunCheckStorage: AppFirstRunCheckStorage
|
||||
public let wrappedStorage: Storage
|
||||
|
||||
public init(storage: Storage,
|
||||
appReinstallChecker: AppReinstallChecker) {
|
||||
appFirstRunCheckStorage: AppFirstRunCheckStorage) {
|
||||
|
||||
self.wrappedStorage = storage
|
||||
self.appReinstallChecker = appReinstallChecker
|
||||
self.appFirstRunCheckStorage = appFirstRunCheckStorage
|
||||
}
|
||||
|
||||
// MARK: - SingleValueStorage
|
||||
|
||||
open func hasStoredValue() -> Bool {
|
||||
if appReinstallChecker.isAppFirstRun {
|
||||
return false
|
||||
}
|
||||
let hasStoredValueResult = appFirstRunCheckStorage.getValue()
|
||||
.flatMap { .success($0 ? false : wrappedStorage.hasStoredValue()) }
|
||||
|
||||
return wrappedStorage.hasStoredValue()
|
||||
return (try? hasStoredValueResult.get()) ?? false
|
||||
}
|
||||
|
||||
open func getValue() -> Result<Storage.ValueType, Storage.ErrorType> {
|
||||
guard appReinstallChecker.isAppFirstRun else {
|
||||
return wrappedStorage.getValue()
|
||||
}
|
||||
appFirstRunCheckStorage.getValue()
|
||||
.flatMap {
|
||||
guard $0 else {
|
||||
return wrappedStorage.getValue()
|
||||
}
|
||||
|
||||
let result = wrappedStorage.deleteValue()
|
||||
|
||||
if case .success = result {
|
||||
appReinstallChecker.isAppFirstRun = false
|
||||
}
|
||||
|
||||
return result.flatMap { .failure(.valueNotFound) }
|
||||
return wrappedStorage.deleteValue()
|
||||
.flatMap {
|
||||
appFirstRunCheckStorage.store(value: false)
|
||||
}
|
||||
.flatMap { .failure(.valueNotFound) }
|
||||
}
|
||||
}
|
||||
|
||||
open func store(value: Storage.ValueType) -> Result<Void, Storage.ErrorType> {
|
||||
let result = wrappedStorage.store(value: value)
|
||||
|
||||
if case .success = result {
|
||||
appReinstallChecker.isAppFirstRun = false
|
||||
}
|
||||
|
||||
return result
|
||||
wrappedStorage.store(value: value)
|
||||
.flatMap {
|
||||
appFirstRunCheckStorage.store(value: false)
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteValue() -> Result<Void, Storage.ErrorType> {
|
||||
|
|
@ -73,10 +74,12 @@ where Storage.ErrorType == StorageError {
|
|||
}
|
||||
|
||||
public extension SingleValueStorage {
|
||||
func appInstallLifetimeStorage(reinstallChecker: AppReinstallChecker) -> AppInstallLifetimeSingleValueStorage<Self>
|
||||
where Self.ErrorType == ErrorType {
|
||||
func appInstallLifetimeStorage<FirstRunCheckStorage: SingleValueStorage>(appFirstRunCheckStorage: FirstRunCheckStorage) -> AppInstallLifetimeSingleValueStorage<Self, FirstRunCheckStorage>
|
||||
where Self.ErrorType == ErrorType,
|
||||
FirstRunCheckStorage.ErrorType == ErrorType,
|
||||
FirstRunCheckStorage.ValueType == Bool {
|
||||
|
||||
AppInstallLifetimeSingleValueStorage(storage: self,
|
||||
appReinstallChecker: reinstallChecker)
|
||||
appFirstRunCheckStorage: appFirstRunCheckStorage)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIFoundationUtils'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIGoogleMapUtils'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIKeychainUtils'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TILogging'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.0'
|
||||
s.summary = 'Logging for TI libraries.'
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// 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 TILogging
|
||||
import os
|
||||
|
||||
public final class TIMapLogger: DefaultOSLogErrorLogger {
|
||||
public init(category: String) {
|
||||
super.init(log: OSLog(subsystem: "TIMapUtils", category: category))
|
||||
}
|
||||
}
|
||||
|
|
@ -35,10 +35,12 @@ open class UserLocationFetcher: NSObject, CLLocationManagerDelegate {
|
|||
case fullAccuracyDenied
|
||||
}
|
||||
|
||||
public typealias LocationCallback = (CLLocation) -> Void
|
||||
public typealias OnAuthSuccessCallback = (CLLocationManager) -> Void
|
||||
public typealias OnAuthFailureCallback = (Failure) -> Void
|
||||
|
||||
public typealias OnLocationCallback = (CLLocation) -> Void
|
||||
public typealias OnLocationFetchFailureCallback = (Error) -> Void
|
||||
|
||||
public var locationManager: CLLocationManager
|
||||
public var accuracyRequest: AccuracyRequest
|
||||
|
||||
|
|
@ -81,21 +83,26 @@ open class UserLocationFetcher: NSObject, CLLocationManagerDelegate {
|
|||
public var authSuccessCallback: OnAuthSuccessCallback
|
||||
public var authFailureCallback: OnAuthFailureCallback?
|
||||
|
||||
public var locationCallback: LocationCallback?
|
||||
public var onLocationCallback: OnLocationCallback?
|
||||
public var onLocationFetchFailureCallback: OnLocationFetchFailureCallback?
|
||||
|
||||
public var logger: ErrorLogger = DefaultOSLogErrorLogger(subsystem: "TIMapUtils", category: "UserLocationFetcher")
|
||||
public var logger: ErrorLogger = TIMapLogger(category: "UserLocationFetcher")
|
||||
|
||||
public init(locationManager: CLLocationManager = CLLocationManager(),
|
||||
accuracyRequest: AccuracyRequest = .default,
|
||||
onSuccess: @escaping OnAuthSuccessCallback = { $0.requestLocation() },
|
||||
onFailure: OnAuthFailureCallback? = nil,
|
||||
locationCallback: LocationCallback? = nil) {
|
||||
onLocationFetchFailureCallback: OnLocationFetchFailureCallback? = nil,
|
||||
onLocationCallback: OnLocationCallback? = nil) {
|
||||
|
||||
self.locationManager = locationManager
|
||||
self.accuracyRequest = accuracyRequest
|
||||
|
||||
self.authSuccessCallback = onSuccess
|
||||
self.authFailureCallback = onFailure
|
||||
self.locationCallback = locationCallback
|
||||
|
||||
self.onLocationCallback = onLocationCallback
|
||||
self.onLocationFetchFailureCallback = onLocationFetchFailureCallback
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
|
@ -174,14 +181,16 @@ open class UserLocationFetcher: NSObject, CLLocationManagerDelegate {
|
|||
}
|
||||
|
||||
open func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let locationCallback else {
|
||||
guard let onLocationCallback else {
|
||||
return
|
||||
}
|
||||
|
||||
locations.forEach(locationCallback)
|
||||
locations.forEach(onLocationCallback)
|
||||
}
|
||||
|
||||
open func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
onLocationFetchFailureCallback?(error)
|
||||
|
||||
logger.log(error: error, file: #file, line: #line)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2022 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,33 +20,27 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIFoundationUtils
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
open class DefaultAuthSettingsStorage: AuthSettingsStorage {
|
||||
public enum Defaults {
|
||||
public static var shouldResetAuthDataKey: StorageKey<Bool> {
|
||||
.init(rawValue: "shouldResetAuthData")
|
||||
}
|
||||
public struct ItemDistanceTo<T> {
|
||||
public let item: T
|
||||
public let distance: CLLocationDistance
|
||||
|
||||
public init(item: T, distance: CLLocationDistance) {
|
||||
self.item = item
|
||||
self.distance = distance
|
||||
}
|
||||
|
||||
private let reinstallChecker: AppReinstallChecker
|
||||
public init(item: T,
|
||||
distanceTo location: CLLocation) where T: MapLocatable, T.Position == CLLocationCoordinate2D {
|
||||
|
||||
// MARK: - PinCodeSettingsStorage
|
||||
if let itemPosition = item.position {
|
||||
let itemLocation = CLLocation(latitude: itemPosition.latitude,
|
||||
longitude: itemPosition.longitude)
|
||||
|
||||
open var shouldResetStoredAuthData: Bool {
|
||||
get {
|
||||
reinstallChecker.isAppFirstRun
|
||||
self.init(item: item, distance: itemLocation.distance(from: location))
|
||||
} else {
|
||||
self.init(item: item, distance: .infinity)
|
||||
}
|
||||
set {
|
||||
reinstallChecker.isAppFirstRun = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public init(defaultsStorage: UserDefaults = .standard,
|
||||
storageKey: StorageKey<Bool> = Defaults.shouldResetAuthDataKey) {
|
||||
|
||||
self.reinstallChecker = AppReinstallChecker(defaultsStorage: defaultsStorage,
|
||||
storageKey: storageKey)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIMapUtils'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIMoyaNetworking'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.0'
|
||||
s.summary = 'Moya + Swagger network service.'
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@ open class DefaultEndpointSecurityPreprocessor: EndpointRequestPreprocessor {
|
|||
return Cancellables.nonCancellable()
|
||||
}
|
||||
|
||||
let remainingSchemes = schemes.dropFirst()
|
||||
|
||||
let preprocessorsGroup: [KeyValueTuple<SecurityScheme, SecuritySchemePreprocessor>] = schemeGroup.compactMap {
|
||||
guard let preprocessor = schemePreprocessors[$0.key] else {
|
||||
return nil
|
||||
|
|
@ -98,7 +100,7 @@ open class DefaultEndpointSecurityPreprocessor: EndpointRequestPreprocessor {
|
|||
// unable to satisfy group requirement
|
||||
// try next scheme group
|
||||
return preprocess(request: request,
|
||||
using: schemes.dropFirst(),
|
||||
using: remainingSchemes,
|
||||
schemePreprocessors: schemePreprocessors,
|
||||
completion: completion)
|
||||
}
|
||||
|
|
@ -112,13 +114,13 @@ open class DefaultEndpointSecurityPreprocessor: EndpointRequestPreprocessor {
|
|||
completion(.success(modifiedRequest))
|
||||
|
||||
case let .failure(error):
|
||||
guard !schemes.isEmpty else {
|
||||
guard !remainingSchemes.isEmpty else {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
preprocess(request: request,
|
||||
using: schemes.dropFirst(),
|
||||
using: remainingSchemes,
|
||||
schemePreprocessors: schemePreprocessors,
|
||||
completion: completion)
|
||||
.add(to: scope)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TINetworking'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ public struct EndpointCacheService<Content: Codable>: SingleValueStorage {
|
|||
|
||||
public init(serializedRequest: SerializedRequest,
|
||||
cacheLifetime: Expiry,
|
||||
jsonCodingConfigurator: JsonCodingConfigurator) throws {
|
||||
jsonCodingConfigurator: JsonCodingConfigurator,
|
||||
appGroupDirectory: URL? = nil) throws {
|
||||
|
||||
self.serializedRequest = serializedRequest
|
||||
|
||||
|
|
@ -47,7 +48,8 @@ public struct EndpointCacheService<Content: Codable>: SingleValueStorage {
|
|||
}
|
||||
|
||||
let diskConfig = DiskConfig(name: nameWithoutLeadingSlash,
|
||||
expiry: cacheLifetime)
|
||||
expiry: cacheLifetime,
|
||||
directory: appGroupDirectory)
|
||||
let memoryConfig = MemoryConfig(expiry: cacheLifetime,
|
||||
countLimit: 0,
|
||||
totalCostLimit: 0)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TINetworkingCache'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.0'
|
||||
s.summary = 'Caching results of EndpointRequests.'
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIPagination'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.0'
|
||||
s.summary = 'Generic pagination component.'
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TISwiftUICore'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TISwiftUtils'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TITableKitUtils'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TITextProcessing'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ extension UIView {
|
|||
public var centerOffset: UIOffset
|
||||
public var insets: UIEdgeInsets
|
||||
|
||||
public init(insets: UIEdgeInsets = .nan,
|
||||
public init(insets: UIEdgeInsets = .zero,
|
||||
size: CGSize = .infinity,
|
||||
centerOffset: UIOffset = .nan) {
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ extension UIView {
|
|||
open class BaseSpecedWrappedLayout: BaseWrappedLayout {
|
||||
public var spacing: CGFloat
|
||||
|
||||
public init(insets: UIEdgeInsets = .nan,
|
||||
public init(insets: UIEdgeInsets = .zero,
|
||||
size: CGSize = .infinity,
|
||||
centerOffset: UIOffset = .nan,
|
||||
spacing: CGFloat = .zero) {
|
||||
|
|
@ -98,7 +98,7 @@ extension UIView {
|
|||
public var distribution: UIStackView.Distribution
|
||||
public var alignment: UIStackView.Alignment
|
||||
|
||||
public init(insets: UIEdgeInsets = .nan,
|
||||
public init(insets: UIEdgeInsets = .zero,
|
||||
size: CGSize = .infinity,
|
||||
centerOffset: UIOffset = .nan,
|
||||
spacing: CGFloat = .zero,
|
||||
|
|
|
|||
|
|
@ -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 TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
open class BaseInitializeableStackView: UIStackView, InitializableViewProtocol {
|
||||
public var callbacks: [ViewCallbacks] = []
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
initializeView()
|
||||
}
|
||||
|
||||
public required init(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
|
||||
initializeView()
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
for callback in callbacks {
|
||||
callback.onDidLayoutSubviews()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - InitializableViewProtocol
|
||||
|
||||
open func addViews() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func configureLayout() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func bindViews() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func configureAppearance() {
|
||||
// override in subclass
|
||||
}
|
||||
|
||||
open func localize() {
|
||||
// override in subclass
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
import UIKit
|
||||
import TIUIKitCore
|
||||
|
||||
open class BaseStackView<View: UIView>: UIStackView {
|
||||
open class BaseStackView<View: UIView>: BaseInitializeableStackView {
|
||||
public var customArrangedSubviews: [View] = []
|
||||
|
||||
@available(*, unavailable, message: "Use strongly-typed version of this function")
|
||||
|
|
@ -56,6 +56,8 @@ open class BaseStackView<View: UIView>: UIStackView {
|
|||
customArrangedSubviews.remove(at: indexOfView)
|
||||
|
||||
super.removeArrangedSubview(view)
|
||||
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
|
||||
open func replaceArrangedSubviews(_ newViews: [View]) {
|
||||
|
|
@ -73,8 +75,10 @@ open class BaseStackView<View: UIView>: UIStackView {
|
|||
|
||||
super.insertArrangedSubview(view, at: stackIndex)
|
||||
}
|
||||
}
|
||||
|
||||
open func configureUIStackView(appearance: BaseWrappedAppearance<some StackLayout>) {
|
||||
public extension UIStackView {
|
||||
func configureUIStackView(appearance: BaseWrappedAppearance<some StackLayout>) {
|
||||
configureUIView(appearance: appearance)
|
||||
|
||||
axis = appearance.layout.axis
|
||||
|
|
@ -91,7 +95,7 @@ extension BaseStackView: ConfigurableView where View: ConfigurableView {
|
|||
}
|
||||
|
||||
extension BaseStackView: AppearanceConfigurable where View: AppearanceConfigurable {
|
||||
public final class Appearance: UIView.BaseWrappedAppearance<UIView.DefaultStackLayout>, WrappedViewAppearance {
|
||||
public final class Appearance: BaseWrappedAppearance<DefaultStackLayout>, WrappedViewAppearance {
|
||||
|
||||
public static var defaultAppearance: Self {
|
||||
Self()
|
||||
|
|
@ -99,7 +103,7 @@ extension BaseStackView: AppearanceConfigurable where View: AppearanceConfigurab
|
|||
|
||||
public var arrangedSubviewsAppearance: View.Appearance
|
||||
|
||||
public init(layout: UIView.DefaultStackLayout = .defaultLayout,
|
||||
public init(layout: DefaultStackLayout = .defaultLayout,
|
||||
backgroundColor: UIColor = .clear,
|
||||
border: UIViewBorder = .init(),
|
||||
shadow: UIViewShadow? = nil,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import TISwiftUtils
|
||||
import Foundation
|
||||
|
||||
open class DefaultCollectionViewModel<ItemViewModel> {
|
||||
public typealias SelectItemHandlerPayload = (itemViewModel: ItemViewModel, indexPath: IndexPath)
|
||||
|
||||
public var items: [ItemViewModel]
|
||||
public var selectItemHandler: ParameterClosure<SelectItemHandlerPayload>?
|
||||
|
||||
public init(items: [ItemViewModel],
|
||||
selectItemHandler: ParameterClosure<SelectItemHandlerPayload>? = nil) {
|
||||
|
||||
self.items = items
|
||||
self.selectItemHandler = selectItemHandler
|
||||
}
|
||||
|
||||
open func numberOfSections() -> Int {
|
||||
1
|
||||
}
|
||||
|
||||
open func numberOfItems(inSection section: Int) -> Int {
|
||||
items.count
|
||||
}
|
||||
|
||||
open func itemViewModel(at indexPath: IndexPath) -> ItemViewModel {
|
||||
items[indexPath.row]
|
||||
}
|
||||
|
||||
open func didSelectItem(at indexPath: IndexPath) {
|
||||
selectItemHandler?(SelectItemHandlerPayload(itemViewModel(at: indexPath), indexPath))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
//
|
||||
// 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 DefaultHomogeneousItemsCollectionView<CellContentView: UIView & ConfigurableView & AppearanceConfigurable>:
|
||||
BaseInitializeableCollectionView,
|
||||
UICollectionViewDataSource,
|
||||
ConfigurableView,
|
||||
AppearanceConfigurable
|
||||
where CellContentView.Appearance: WrappedViewAppearance {
|
||||
|
||||
typealias CellType = CellContentView.InCollectionCell
|
||||
|
||||
public private(set) var viewModel: DefaultCollectionViewModel<CellContentView.ViewModelType>?
|
||||
|
||||
private var appearance: Appearance = .defaultAppearance
|
||||
|
||||
// MARK: - BaseInitializeableCollectionView
|
||||
|
||||
override open func bindViews() {
|
||||
super.bindViews()
|
||||
|
||||
register(CellType.self, forCellWithReuseIdentifier: String(describing: CellType.self))
|
||||
|
||||
dataSource = self
|
||||
}
|
||||
|
||||
override open func configureLayout() {
|
||||
super.configureLayout()
|
||||
|
||||
collectionViewLayout = createLayout()
|
||||
}
|
||||
|
||||
override open func configureAppearance() {
|
||||
super.configureAppearance()
|
||||
|
||||
isScrollEnabled = false
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDataSource
|
||||
|
||||
open func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||
viewModel?.numberOfSections() ?? .zero
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
viewModel?.numberOfItems(inSection: section) ?? .zero
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CellType.self), for: indexPath)
|
||||
|
||||
guard let viewModel else {
|
||||
return cell
|
||||
}
|
||||
|
||||
switch cell {
|
||||
case let configureableCell as CellType:
|
||||
configureableCell.configure(with: viewModel.itemViewModel(at: indexPath))
|
||||
configureableCell.configure(appearance: appearance)
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: - ConfigurableView
|
||||
|
||||
open func configure(with viewModel: DefaultCollectionViewModel<CellContentView.ViewModelType>) {
|
||||
self.viewModel = viewModel
|
||||
|
||||
reloadData()
|
||||
}
|
||||
|
||||
// MARK: - AppearanceConfigurable
|
||||
|
||||
open func configure(appearance: DefaultWrappedViewHolderAppearance<CellContentView.Appearance, DefaultWrappedLayout>) {
|
||||
configureUIView(appearance: appearance)
|
||||
|
||||
self.appearance = appearance
|
||||
}
|
||||
|
||||
// MARK: - Subclass override
|
||||
|
||||
open func createLayout() -> UICollectionViewLayout {
|
||||
collectionViewLayout
|
||||
}
|
||||
}
|
||||
|
|
@ -33,13 +33,17 @@ open class BaseListItemView<LeadingView: UIView,
|
|||
|
||||
// MARK: - Constraints
|
||||
|
||||
public private(set) lazy var middleViewLeadingConstraint: NSLayoutConstraint = {
|
||||
middleView.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor)
|
||||
public private(set) lazy var leadingViewTrailingToSuperviewConstraint: NSLayoutConstraint = {
|
||||
leadingView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||
}()
|
||||
|
||||
public private(set) lazy var leadingToTrailingViewConstraint: NSLayoutConstraint = {
|
||||
leadingView.trailingAnchor.constraint(equalTo: trailingView.leadingAnchor)
|
||||
}()
|
||||
|
||||
public private(set) lazy var leadingViewConstraints: SubviewConstraints = {
|
||||
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
trailingConstraint: middleViewLeadingConstraint,
|
||||
trailingConstraint: middleViewLeadingToLeadingViewConstraint,
|
||||
topConstraint: leadingView.topAnchor.constraint(equalTo: topAnchor),
|
||||
bottomConstraint: leadingView.bottomAnchor.constraint(equalTo: bottomAnchor))
|
||||
|
||||
|
|
@ -57,13 +61,25 @@ open class BaseListItemView<LeadingView: UIView,
|
|||
sizeConstraints: sizeConstraints)
|
||||
}()
|
||||
|
||||
public private(set) lazy var trailingViewLeadingToMiddleConstraint: NSLayoutConstraint = {
|
||||
trailingView.leadingAnchor.constraint(equalTo: middleView.trailingAnchor)
|
||||
public private(set) lazy var middleViewLeadingToLeadingViewConstraint: NSLayoutConstraint = {
|
||||
middleView.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor)
|
||||
}()
|
||||
|
||||
public private(set) lazy var middleToTrailingViewConstraint: NSLayoutConstraint = {
|
||||
middleView.trailingAnchor.constraint(equalTo: trailingView.leadingAnchor)
|
||||
}()
|
||||
|
||||
public private(set) lazy var middleViewLeadingToSuperViewConstraint: NSLayoutConstraint = {
|
||||
middleView.leadingAnchor.constraint(equalTo: leadingAnchor)
|
||||
}()
|
||||
|
||||
public private(set) lazy var middleViewTrailingToSuperViewConstraint: NSLayoutConstraint = {
|
||||
middleView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||
}()
|
||||
|
||||
public private(set) lazy var middleViewConstraints: SubviewConstraints = {
|
||||
let edgeConstraints = EdgeConstraints(leadingConstraint: middleViewLeadingConstraint,
|
||||
trailingConstraint: trailingViewLeadingToMiddleConstraint,
|
||||
let edgeConstraints = EdgeConstraints(leadingConstraint: middleViewLeadingToLeadingViewConstraint,
|
||||
trailingConstraint: middleToTrailingViewConstraint,
|
||||
topConstraint: middleView.topAnchor.constraint(equalTo: topAnchor),
|
||||
bottomConstraint: middleView.bottomAnchor.constraint(equalTo: bottomAnchor))
|
||||
|
||||
|
|
@ -81,18 +97,14 @@ open class BaseListItemView<LeadingView: UIView,
|
|||
sizeConstraints: sizeConstraints)
|
||||
}()
|
||||
|
||||
public private(set) lazy var middleViewLeadingToSuperViewConstraint: NSLayoutConstraint = {
|
||||
middleView.leadingAnchor.constraint(equalTo: leadingAnchor)
|
||||
}()
|
||||
|
||||
public private(set) lazy var middleViewTrailingToSuperViewConstraint: NSLayoutConstraint = {
|
||||
middleView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||
public private(set) lazy var trailingViewLeadingToSuperviewConstraint: NSLayoutConstraint = {
|
||||
trailingView.leadingAnchor.constraint(equalTo: leadingAnchor)
|
||||
}()
|
||||
|
||||
public private(set) lazy var trailingViewConstraints: SubviewConstraints = {
|
||||
let trailingConstraint = trailingView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||
|
||||
let edgeConstraints = EdgeConstraints(leadingConstraint: trailingViewLeadingToMiddleConstraint,
|
||||
let edgeConstraints = EdgeConstraints(leadingConstraint: middleToTrailingViewConstraint,
|
||||
trailingConstraint: trailingConstraint,
|
||||
topConstraint: trailingView.topAnchor.constraint(equalTo: topAnchor),
|
||||
bottomConstraint: trailingView.bottomAnchor.constraint(equalTo: bottomAnchor))
|
||||
|
|
@ -117,6 +129,10 @@ open class BaseListItemView<LeadingView: UIView,
|
|||
leadingView.isHidden
|
||||
}
|
||||
|
||||
open var isMiddleViewHidded: Bool {
|
||||
middleView.isHidden
|
||||
}
|
||||
|
||||
open var isTrailingViewHidden: Bool {
|
||||
trailingView.isHidden
|
||||
}
|
||||
|
|
@ -144,83 +160,144 @@ open class BaseListItemView<LeadingView: UIView,
|
|||
|
||||
configureUIView(appearance: appearance)
|
||||
|
||||
update(leadingViewLayout: appearance.leadingViewAppearance.layout,
|
||||
middleViewLayout: appearance.middleViewAppearance.layout)
|
||||
let leadingViewTrailingSpacing = updateLeadingConstaints(leadingViewLayout: appearance.leadingViewAppearance.layout,
|
||||
middleViewLayout: appearance.middleViewAppearance.layout,
|
||||
trailingViewLayout: appearance.trailingAppearance.layout)
|
||||
|
||||
update(middleViewLayout: appearance.middleViewAppearance.layout)
|
||||
let trailingViewLeadingSpacing = updateTrailingConstraints(trailingViewLayout: appearance.trailingAppearance.layout,
|
||||
middleViewLayout: appearance.middleViewAppearance.layout,
|
||||
leadingViewLayout: appearance.leadingViewAppearance.layout)
|
||||
|
||||
update(trailingViewLayout: appearance.trailingAppearance.layout,
|
||||
middleViewLayout: appearance.middleViewAppearance.layout)
|
||||
updateMiddleConstraints(middleViewLayout: appearance.middleViewAppearance.layout,
|
||||
leadingViewTrailingSpacing: leadingViewTrailingSpacing,
|
||||
trailingViewLeadingSpacing: trailingViewLeadingSpacing)
|
||||
}
|
||||
|
||||
// MARK: - Private methdos
|
||||
|
||||
private func update(leadingViewLayout: WrappedViewLayout,
|
||||
middleViewLayout: WrappedViewLayout) {
|
||||
private func updateLeadingConstaints(leadingViewLayout: WrappedViewLayout,
|
||||
middleViewLayout: WrappedViewLayout,
|
||||
trailingViewLayout: WrappedViewLayout) -> CGFloat {
|
||||
|
||||
let leadingToSuperviewContraint: NSLayoutConstraint
|
||||
let leadingViewLeftInset: CGFloat
|
||||
|
||||
if isLeadingViewHidden {
|
||||
switch (isLeadingViewHidden, isMiddleViewHidded) {
|
||||
case (true, true):
|
||||
leadingViewConstraints.deactivate()
|
||||
middleViewLeadingToSuperViewConstraint.isActive = true
|
||||
|
||||
leadingToSuperviewContraint = middleViewLeadingToSuperViewConstraint
|
||||
leadingViewLeftInset = middleViewLayout.insets.left
|
||||
} else {
|
||||
let middleViewLeadingConstant = leadingViewLayout.insets.add(\.left,
|
||||
to: \.right,
|
||||
of: middleViewLayout.insets,
|
||||
onNan: .zero)
|
||||
return UIEdgeInsets.nan.right
|
||||
|
||||
middleViewLeadingConstraint.setActiveConstantOrDeactivate(constant: middleViewLeadingConstant)
|
||||
leadingViewConstraints.edgeConstraints.leadingConstraint.isActive = true
|
||||
middleViewConstraints.edgeConstraints.leadingConstraint.isActive = true
|
||||
middleViewLeadingToSuperViewConstraint.isActive = false
|
||||
case (false, false):
|
||||
leadingViewConstraints.edgeConstraints.trailingConstraint.isActive = false
|
||||
leadingViewConstraints.edgeConstraints.trailingConstraint = middleViewLeadingToLeadingViewConstraint
|
||||
|
||||
leadingViewConstraints.update(from: leadingViewLayout)
|
||||
|
||||
leadingToSuperviewContraint = leadingViewConstraints.edgeConstraints.leadingConstraint
|
||||
leadingViewLeftInset = leadingViewLayout.insets.left
|
||||
return middleViewLayout.insets.add(\.left,
|
||||
to: \.right,
|
||||
of: leadingViewLayout.insets)
|
||||
|
||||
case (true, false):
|
||||
leadingViewConstraints.deactivate()
|
||||
|
||||
return middleViewLayout.insets.left
|
||||
|
||||
case (false, true):
|
||||
leadingViewConstraints.edgeConstraints.trailingConstraint.isActive = false
|
||||
|
||||
defer {
|
||||
leadingViewConstraints.update(from: leadingViewLayout)
|
||||
}
|
||||
|
||||
if isTrailingViewHidden {
|
||||
leadingViewConstraints.edgeConstraints.trailingConstraint = leadingViewTrailingToSuperviewConstraint
|
||||
|
||||
return leadingViewLayout.insets.right
|
||||
} else {
|
||||
leadingViewConstraints.edgeConstraints.trailingConstraint = leadingToTrailingViewConstraint
|
||||
|
||||
return leadingViewLayout.insets.add(\.right,
|
||||
to: \.right,
|
||||
of: trailingViewLayout.insets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateMiddleConstraints(middleViewLayout: WrappedViewLayout,
|
||||
leadingViewTrailingSpacing: CGFloat,
|
||||
trailingViewLeadingSpacing: CGFloat) {
|
||||
|
||||
guard !isMiddleViewHidded else {
|
||||
middleViewConstraints.deactivate()
|
||||
return
|
||||
}
|
||||
|
||||
leadingToSuperviewContraint.constant = leadingViewLeftInset.isFinite ? leadingViewLeftInset : .zero
|
||||
leadingToSuperviewContraint.isActive = true
|
||||
}
|
||||
middleViewConstraints.edgeConstraints.leadingConstraint.isActive = false
|
||||
|
||||
private func update(middleViewLayout: WrappedViewLayout) {
|
||||
middleViewConstraints.update(from: middleViewLayout)
|
||||
}
|
||||
if isLeadingViewHidden {
|
||||
middleViewConstraints.edgeConstraints.leadingConstraint = middleViewLeadingToSuperViewConstraint
|
||||
} else {
|
||||
middleViewConstraints.edgeConstraints.leadingConstraint = middleViewLeadingToLeadingViewConstraint
|
||||
}
|
||||
|
||||
private func update(trailingViewLayout: WrappedViewLayout,
|
||||
middleViewLayout: WrappedViewLayout) {
|
||||
|
||||
let trailingToSuperviewConstraint: NSLayoutConstraint
|
||||
let trailingViewRightInset: CGFloat
|
||||
middleViewConstraints.edgeConstraints.trailingConstraint.isActive = false
|
||||
|
||||
if isTrailingViewHidden {
|
||||
trailingViewConstraints.deactivate()
|
||||
middleViewTrailingToSuperViewConstraint.isActive = true
|
||||
|
||||
trailingToSuperviewConstraint = middleViewTrailingToSuperViewConstraint
|
||||
trailingViewRightInset = middleViewLayout.insets.right
|
||||
middleViewConstraints.edgeConstraints.trailingConstraint = middleViewTrailingToSuperViewConstraint
|
||||
} else {
|
||||
let trailingViewLeadingToMiddleConstant = middleViewLayout.insets.add(\.right,
|
||||
to: \.left,
|
||||
of: trailingViewLayout.insets,
|
||||
onNan: .zero)
|
||||
middleViewConstraints.edgeConstraints.trailingConstraint = middleToTrailingViewConstraint
|
||||
}
|
||||
|
||||
trailingViewLeadingToMiddleConstraint.setActiveConstantOrDeactivate(constant: trailingViewLeadingToMiddleConstant)
|
||||
trailingViewLeadingToMiddleConstraint.isActive = true
|
||||
middleViewTrailingToSuperViewConstraint.isActive = false
|
||||
middleViewConstraints.update(from: middleViewLayout)
|
||||
|
||||
middleViewConstraints.edgeConstraints.leadingConstraint
|
||||
.setActiveConstantOrDeactivate(constant: leadingViewTrailingSpacing)
|
||||
|
||||
middleViewConstraints.edgeConstraints.trailingConstraint
|
||||
.setActiveConstantOrDeactivate(constant: trailingViewLeadingSpacing)
|
||||
}
|
||||
|
||||
private func updateTrailingConstraints(trailingViewLayout: WrappedViewLayout,
|
||||
middleViewLayout: WrappedViewLayout,
|
||||
leadingViewLayout: WrappedViewLayout) -> CGFloat {
|
||||
|
||||
switch (isMiddleViewHidded, isTrailingViewHidden) {
|
||||
case (true, true):
|
||||
trailingViewConstraints.deactivate()
|
||||
|
||||
return UIEdgeInsets.nan.left
|
||||
|
||||
case (true, false):
|
||||
trailingViewConstraints.edgeConstraints.leadingConstraint.isActive = false
|
||||
|
||||
defer {
|
||||
trailingViewConstraints.update(from: trailingViewLayout)
|
||||
}
|
||||
|
||||
if isLeadingViewHidden {
|
||||
trailingViewConstraints.edgeConstraints.leadingConstraint = trailingViewLeadingToSuperviewConstraint
|
||||
|
||||
return trailingViewLayout.insets.left
|
||||
} else {
|
||||
trailingViewConstraints.edgeConstraints.leadingConstraint = middleToTrailingViewConstraint
|
||||
|
||||
return trailingViewLayout.insets.add(\.left,
|
||||
to: \.right,
|
||||
of: middleViewLayout.insets)
|
||||
}
|
||||
|
||||
case (false, true):
|
||||
trailingViewConstraints.deactivate()
|
||||
|
||||
return middleViewLayout.insets.right
|
||||
|
||||
case (false, false):
|
||||
trailingViewConstraints.edgeConstraints.leadingConstraint.isActive = false
|
||||
trailingViewConstraints.edgeConstraints.leadingConstraint = middleToTrailingViewConstraint
|
||||
|
||||
trailingViewConstraints.update(from: trailingViewLayout)
|
||||
|
||||
trailingToSuperviewConstraint = trailingViewConstraints.edgeConstraints.trailingConstraint
|
||||
trailingViewRightInset = trailingViewLayout.insets.right
|
||||
return trailingViewLayout.insets.add(\.right,
|
||||
to: \.left,
|
||||
of: middleViewLayout.insets)
|
||||
}
|
||||
|
||||
trailingToSuperviewConstraint.constant = trailingViewRightInset.isFinite ? -trailingViewRightInset : .zero
|
||||
trailingToSuperviewConstraint.isActive = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ open class BasePlaceholderImageView<Placeholder: UIView>: UIImageView,
|
|||
}
|
||||
|
||||
public lazy var placeholderConstraints: SubviewConstraints = {
|
||||
configureWrappedViewLayout()
|
||||
createSubviewConstraints()
|
||||
}()
|
||||
|
||||
open override var image: UIImage? {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2022 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
|
||||
|
|
@ -21,28 +21,17 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import TISwiftUtils
|
||||
|
||||
open class AppReinstallChecker {
|
||||
private let defaultsStorage: UserDefaults
|
||||
private let storageKey: String
|
||||
@MainActor
|
||||
public final class ClosureTarget: NSObject {
|
||||
public var handler: UIVoidClosure?
|
||||
|
||||
open var isAppFirstRun: Bool {
|
||||
get {
|
||||
guard defaultsStorage.object(forKey: storageKey) != nil else {
|
||||
return true
|
||||
}
|
||||
|
||||
return defaultsStorage.bool(forKey: storageKey)
|
||||
}
|
||||
set {
|
||||
defaultsStorage.set(newValue, forKey: storageKey)
|
||||
}
|
||||
public init(handler: UIVoidClosure?) {
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
public init(defaultsStorage: UserDefaults = .standard,
|
||||
storageKey: StorageKey<Bool>) {
|
||||
|
||||
self.defaultsStorage = defaultsStorage
|
||||
self.storageKey = storageKey.rawValue
|
||||
@objc public func onAction() {
|
||||
self.handler?()
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
import TISwiftUtils
|
||||
|
||||
public final class DefaultConfigurableStatefulButton: StatefulButton, ConfigurableView, AppearanceConfigurable {
|
||||
|
||||
|
|
@ -32,6 +33,12 @@ public final class DefaultConfigurableStatefulButton: StatefulButton, Configurab
|
|||
public func configure(with viewModel: ViewModel) {
|
||||
viewModel.willReuse(view: self)
|
||||
|
||||
let previousTarget = target(forAction: #selector(ClosureTarget.onAction), withSender: nil)
|
||||
|
||||
removeTarget(previousTarget,
|
||||
action: #selector(ClosureTarget.onAction),
|
||||
for: .touchUpInside)
|
||||
|
||||
stateViewModelMap = viewModel.stateViewModelMap
|
||||
|
||||
for (state, viewModel) in viewModel.stateViewModelMap {
|
||||
|
|
@ -44,6 +51,10 @@ public final class DefaultConfigurableStatefulButton: StatefulButton, Configurab
|
|||
|
||||
configureStatefulButton(appearance: appearance)
|
||||
|
||||
addTarget(viewModel.closureTarget,
|
||||
action: #selector(ClosureTarget.onAction),
|
||||
for: .touchUpInside)
|
||||
|
||||
viewModel.didCompleteConfiguration(of: self)
|
||||
}
|
||||
|
||||
|
|
@ -55,8 +66,8 @@ public final class DefaultConfigurableStatefulButton: StatefulButton, Configurab
|
|||
}
|
||||
}
|
||||
|
||||
extension DefaultConfigurableStatefulButton {
|
||||
public final class ViewModel: DefaultUIViewPresenter<DefaultConfigurableStatefulButton> {
|
||||
public extension DefaultConfigurableStatefulButton {
|
||||
final class ViewModel: DefaultUIViewPresenter<DefaultConfigurableStatefulButton> {
|
||||
public var stateViewModelMap: [State: BaseButtonViewModel]
|
||||
public var currentState: State {
|
||||
didSet {
|
||||
|
|
@ -64,11 +75,22 @@ extension DefaultConfigurableStatefulButton {
|
|||
}
|
||||
}
|
||||
|
||||
var closureTarget: ClosureTarget
|
||||
|
||||
public var tapHandler: UIVoidClosure? {
|
||||
didSet {
|
||||
closureTarget.handler = tapHandler
|
||||
}
|
||||
}
|
||||
|
||||
public init(stateViewModelMap: [State: BaseButtonViewModel],
|
||||
currentState: State) {
|
||||
currentState: State,
|
||||
tapHander: UIVoidClosure?) {
|
||||
|
||||
self.stateViewModelMap = stateViewModelMap
|
||||
self.currentState = currentState
|
||||
self.tapHandler = tapHander
|
||||
self.closureTarget = ClosureTarget(handler: tapHander)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,16 +30,14 @@ extension StatefulButton {
|
|||
open class BaseAppearance<Layout: ViewLayout, ContentLayout: BaseContentLayout & ViewLayout>:
|
||||
UIView.BaseAppearance<Layout> {
|
||||
|
||||
public enum Defaults {
|
||||
public static var stateAppearances: StateAppearances {
|
||||
[
|
||||
.normal: UIButton.DefaultStateAppearance.defaultAppearance,
|
||||
.highlighted: UIButton.DefaultStateAppearance.defaultAppearance,
|
||||
.selected: UIButton.DefaultStateAppearance.defaultAppearance,
|
||||
.disabled: UIButton.DefaultStateAppearance.defaultAppearance,
|
||||
.loading: UIButton.DefaultStateAppearance.defaultAppearance
|
||||
]
|
||||
}
|
||||
public static var defaultStateAppearances: StateAppearances {
|
||||
[
|
||||
.normal: UIButton.DefaultStateAppearance.defaultAppearance,
|
||||
.highlighted: UIButton.DefaultStateAppearance.defaultAppearance,
|
||||
.selected: UIButton.DefaultStateAppearance.defaultAppearance,
|
||||
.disabled: UIButton.DefaultStateAppearance.defaultAppearance,
|
||||
.loading: UIButton.DefaultStateAppearance.defaultAppearance
|
||||
]
|
||||
}
|
||||
|
||||
public var stateAppearances: StateAppearances
|
||||
|
|
@ -48,7 +46,7 @@ extension StatefulButton {
|
|||
backgroundColor: UIColor = .clear,
|
||||
border: UIViewBorder = .init(),
|
||||
shadow: UIViewShadow? = nil,
|
||||
stateAppearances: StateAppearances = Defaults.stateAppearances) {
|
||||
stateAppearances: StateAppearances = defaultStateAppearances) {
|
||||
|
||||
self.stateAppearances = stateAppearances
|
||||
|
||||
|
|
@ -58,7 +56,6 @@ extension StatefulButton {
|
|||
shadow: shadow)
|
||||
}
|
||||
|
||||
|
||||
public func set(appearanceBuilder: (StateAppearance) -> Void, for states: [State]) {
|
||||
for state in states {
|
||||
stateAppearances[state] = DefaultStateAppearance.make(builder: appearanceBuilder)
|
||||
|
|
@ -66,14 +63,16 @@ extension StatefulButton {
|
|||
}
|
||||
}
|
||||
|
||||
open class BasePositionAppearance<Layout: ViewLayout, ContentLayout: BaseContentLayout & ViewLayout>: BaseAppearance<Layout, ContentLayout> {
|
||||
open class BasePositionAppearance<Layout: ViewLayout, ContentLayout: BaseContentLayout & ViewLayout>:
|
||||
BaseAppearance<Layout, ContentLayout> {
|
||||
|
||||
public var activityIndicatorPosition: ActivityIndicatorPosition
|
||||
|
||||
public init(layout: Layout = .defaultLayout,
|
||||
backgroundColor: UIColor = .clear,
|
||||
border: UIViewBorder = .init(),
|
||||
shadow: UIViewShadow? = nil,
|
||||
stateAppearances: StateAppearances = Defaults.stateAppearances,
|
||||
stateAppearances: StateAppearances = defaultStateAppearances,
|
||||
activityIndicatorPosition: ActivityIndicatorPosition = .center) {
|
||||
|
||||
self.activityIndicatorPosition = activityIndicatorPosition
|
||||
|
|
@ -86,7 +85,8 @@ extension StatefulButton {
|
|||
}
|
||||
}
|
||||
|
||||
public final class DefaultPositionAppearance: BasePositionAppearance<DefaultWrappedLayout, DefaultContentLayout>, WrappedViewAppearance {
|
||||
public final class DefaultPositionAppearance: BasePositionAppearance<DefaultWrappedLayout, DefaultContentLayout>,
|
||||
WrappedViewAppearance {
|
||||
public static var defaultAppearance: Self {
|
||||
Self()
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@ extension StatefulButton {
|
|||
backgroundColor: UIColor = .clear,
|
||||
border: UIViewBorder = .init(),
|
||||
shadow: UIViewShadow? = nil,
|
||||
stateAppearances: StateAppearances = Defaults.stateAppearances,
|
||||
stateAppearances: StateAppearances = defaultStateAppearances,
|
||||
activityIndicatorPlacement: ActivityIndicatorPlacement = .center) {
|
||||
|
||||
self.activityIndicatorPlacement = activityIndicatorPlacement
|
||||
|
|
@ -116,7 +116,8 @@ extension StatefulButton {
|
|||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
public final class DefaultPlacementAppearance: BasePlacementAppearance<DefaultWrappedLayout, DefaultContentLayout>, WrappedViewAppearance {
|
||||
public final class DefaultPlacementAppearance: BasePlacementAppearance<DefaultWrappedLayout, DefaultContentLayout>,
|
||||
WrappedViewAppearance {
|
||||
public static var defaultAppearance: Self {
|
||||
Self()
|
||||
}
|
||||
|
|
@ -139,6 +140,19 @@ extension StatefulButton {
|
|||
public func configureStatefulButton(appearance: BasePositionAppearance<some ViewLayout, some BaseContentLayout>) {
|
||||
configureBaseStatefulButton(appearance: appearance)
|
||||
|
||||
let baseOnStateChanged = onStateChanged
|
||||
|
||||
onStateChanged = { [weak self] in
|
||||
baseOnStateChanged?($0)
|
||||
|
||||
self?.configureStatefulButtonActivityIndicator(appearance: appearance)
|
||||
}
|
||||
|
||||
onStateChanged?(state)
|
||||
}
|
||||
|
||||
private func configureStatefulButtonActivityIndicator(appearance: BasePositionAppearance<some ViewLayout,
|
||||
some BaseContentLayout>) {
|
||||
switch appearance.activityIndicatorPosition {
|
||||
case .beforeTitle:
|
||||
activityIndicatorShouldCenterInView = false
|
||||
|
|
@ -150,7 +164,7 @@ extension StatefulButton {
|
|||
|
||||
case .center:
|
||||
activityIndicatorShouldCenterInView = true
|
||||
|
||||
|
||||
super.setImage(.init(), for: .loading)
|
||||
}
|
||||
|
||||
|
|
@ -174,6 +188,19 @@ extension StatefulButton {
|
|||
public func configureStatefulButton(appearance: BasePlacementAppearance<some ViewLayout, some BaseContentLayout>) {
|
||||
configureBaseStatefulButton(appearance: appearance)
|
||||
|
||||
let baseOnStateChanged = onStateChanged
|
||||
|
||||
onStateChanged = { [weak self] in
|
||||
baseOnStateChanged?($0)
|
||||
|
||||
self?.configureActivityIndicator(appearance: appearance)
|
||||
}
|
||||
|
||||
onStateChanged?(state)
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
private func configureActivityIndicator(appearance: BasePlacementAppearance<some ViewLayout, some BaseContentLayout>) {
|
||||
var config = configuration ?? .plain()
|
||||
|
||||
switch appearance.activityIndicatorPlacement {
|
||||
|
|
@ -187,6 +214,7 @@ extension StatefulButton {
|
|||
|
||||
config.imagePlacement = imagePlacement
|
||||
config.imagePadding = padding
|
||||
|
||||
case .center:
|
||||
activityIndicatorShouldCenterInView = true
|
||||
|
||||
|
|
|
|||
|
|
@ -48,8 +48,19 @@ open class StatefulButton: BaseInitializableButton {
|
|||
|
||||
public typealias StateEventPropagations = [State: Bool]
|
||||
|
||||
public var isLoading = false {
|
||||
didSet {
|
||||
private var backedIsLoading = false
|
||||
|
||||
public var isLoading: Bool {
|
||||
get {
|
||||
backedIsLoading
|
||||
}
|
||||
set {
|
||||
guard backedIsLoading != newValue else {
|
||||
return
|
||||
}
|
||||
|
||||
backedIsLoading = newValue
|
||||
|
||||
if isLoading {
|
||||
if activityIndicator == nil {
|
||||
configureDefaultActivityIndicator()
|
||||
|
|
@ -75,7 +86,7 @@ open class StatefulButton: BaseInitializableButton {
|
|||
activityIndicator?.removeFromSuperview()
|
||||
}
|
||||
didSet {
|
||||
if let activityIndicator = activityIndicator {
|
||||
if let activityIndicator {
|
||||
addSubview(activityIndicator)
|
||||
}
|
||||
}
|
||||
|
|
@ -128,19 +139,46 @@ open class StatefulButton: BaseInitializableButton {
|
|||
}
|
||||
|
||||
override open var isEnabled: Bool {
|
||||
didSet {
|
||||
get {
|
||||
super.isEnabled
|
||||
}
|
||||
set {
|
||||
guard isEnabled != newValue else {
|
||||
return
|
||||
}
|
||||
|
||||
super.isEnabled = newValue
|
||||
|
||||
updateAppearance()
|
||||
}
|
||||
}
|
||||
|
||||
override open var isHighlighted: Bool {
|
||||
didSet {
|
||||
get {
|
||||
super.isHighlighted
|
||||
}
|
||||
set {
|
||||
guard isHighlighted != newValue else {
|
||||
return
|
||||
}
|
||||
|
||||
super.isHighlighted = newValue
|
||||
|
||||
updateAppearance()
|
||||
}
|
||||
}
|
||||
|
||||
open override var isSelected: Bool {
|
||||
didSet {
|
||||
get {
|
||||
super.isSelected
|
||||
}
|
||||
set {
|
||||
guard isSelected != newValue else {
|
||||
return
|
||||
}
|
||||
|
||||
super.isSelected = newValue
|
||||
|
||||
updateAppearance()
|
||||
}
|
||||
}
|
||||
|
|
@ -178,7 +216,7 @@ open class StatefulButton: BaseInitializableButton {
|
|||
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let pointInsideView = self.point(inside: point, with: event)
|
||||
|
||||
if !isEnabled && pointInsideView, event?.allTouches?.isEmpty == false {
|
||||
if !isEnabled && pointInsideView, !(event?.allTouches?.isEmpty ?? true) {
|
||||
onDisabledStateTapHandler?()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ public final class DefaultTitleSubtitleView: BaseInitializableView,
|
|||
private let titleLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
|
||||
private var appearance: Appearance = .defaultAppearance
|
||||
|
||||
public var titleLableBottomConstraint: NSLayoutConstraint?
|
||||
public var spacingConstraint: NSLayoutConstraint?
|
||||
|
||||
|
|
@ -58,7 +60,7 @@ public final class DefaultTitleSubtitleView: BaseInitializableView,
|
|||
subtitleLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
spacingConstraint,
|
||||
subtitleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
subtitleLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
subtitleLabel.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
].compactMap { $0 })
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +68,10 @@ public final class DefaultTitleSubtitleView: BaseInitializableView,
|
|||
|
||||
public func configure(with viewModel: DefaultTitleSubtitleViewModel) {
|
||||
titleLabel.text = viewModel.title
|
||||
titleLabel.configureUILabel(appearance: appearance.titleAppearance)
|
||||
|
||||
subtitleLabel.text = viewModel.subtitle
|
||||
subtitleLabel.configureUILabel(appearance: appearance.subtitleAppearance)
|
||||
|
||||
setSubtitle(hidden: viewModel.isSubtitleHidden)
|
||||
}
|
||||
|
|
@ -82,6 +87,8 @@ public final class DefaultTitleSubtitleView: BaseInitializableView,
|
|||
// MARK: - AppearanceConfigurable
|
||||
|
||||
public func configure(appearance: Appearance) {
|
||||
self.appearance = appearance
|
||||
|
||||
configureUIView(appearance: appearance)
|
||||
titleLabel.configureUILabel(appearance: appearance.titleAppearance)
|
||||
subtitleLabel.configureUILabel(appearance: appearance.subtitleAppearance)
|
||||
|
|
|
|||
|
|
@ -42,8 +42,11 @@ public struct CenterConstraints: ConstraintsSet {
|
|||
self.centerYConstraint = centerYConstraint
|
||||
}
|
||||
|
||||
public func update(offset: UIOffset) {
|
||||
@discardableResult
|
||||
public func update(offset: UIOffset) -> Self {
|
||||
centerXConstraint.setActiveConstantOrDeactivate(constant: offset.horizontal)
|
||||
centerYConstraint.setActiveConstantOrDeactivate(constant: offset.vertical)
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,10 +64,13 @@ public struct EdgeConstraints: ConstraintsSet {
|
|||
self.bottomConstraint = bottomConstraint
|
||||
}
|
||||
|
||||
public func update(from insets: UIEdgeInsets) {
|
||||
@discardableResult
|
||||
public func update(from insets: UIEdgeInsets) -> Self {
|
||||
leadingConstraint.setActiveConstantOrDeactivate(constant: insets.left)
|
||||
trailingConstraint.setActiveConstantOrDeactivate(constant: -insets.right)
|
||||
topConstraint.setActiveConstantOrDeactivate(constant: insets.top)
|
||||
bottomConstraint.setActiveConstantOrDeactivate(constant: -insets.bottom)
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,11 @@ public struct SizeConstraints: ConstraintsSet {
|
|||
self.heightConstraint = heightConstraint
|
||||
}
|
||||
|
||||
public func update(from size: CGSize) {
|
||||
@discardableResult
|
||||
public func update(from size: CGSize) -> Self {
|
||||
widthConstraint.setActiveConstantOrDeactivate(constant: size.width)
|
||||
heightConstraint.setActiveConstantOrDeactivate(constant: size.height)
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ open class ContainerCollectionViewCell<View: UIView>: UICollectionViewCell, Init
|
|||
}
|
||||
|
||||
private lazy var subviewContraints: SubviewConstraints = {
|
||||
configureWrappedViewLayout()
|
||||
createSubviewConstraints()
|
||||
}()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
|
@ -86,7 +86,7 @@ open class ContainerCollectionViewCell<View: UIView>: UICollectionViewCell, Init
|
|||
}
|
||||
|
||||
open func configureLayout() {
|
||||
subviewContraints.edgeConstraints.activate()
|
||||
update(subviewConstraints: subviewContraints)
|
||||
}
|
||||
|
||||
open func configureAppearance() {
|
||||
|
|
@ -101,3 +101,13 @@ open class ContainerCollectionViewCell<View: UIView>: UICollectionViewCell, Init
|
|||
View()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AppearanceConfigurable
|
||||
|
||||
extension ContainerCollectionViewCell: AppearanceConfigurable where View: AppearanceConfigurable,
|
||||
View.Appearance: WrappedViewAppearance {
|
||||
|
||||
public func configure(appearance: DefaultWrappedViewHolderAppearance<View.Appearance, DefaultWrappedLayout>) {
|
||||
configureWrappedViewHolder(appearance: appearance)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ open class ContainerScrollView<ContentView: UIView>: BaseInitializeableScrollVie
|
|||
}
|
||||
|
||||
private lazy var subviewContraints: SubviewConstraints = {
|
||||
configureWrappedViewLayout()
|
||||
createSubviewConstraints()
|
||||
}()
|
||||
|
||||
open func createView() -> ContentView {
|
||||
|
|
@ -62,6 +62,12 @@ open class ContainerScrollView<ContentView: UIView>: BaseInitializeableScrollVie
|
|||
|
||||
addSubview(wrappedView)
|
||||
}
|
||||
|
||||
open override func configureLayout() {
|
||||
super.configureLayout()
|
||||
|
||||
update(subviewConstraints: subviewContraints)
|
||||
}
|
||||
}
|
||||
|
||||
extension ContainerScrollView: AppearanceConfigurable where View: AppearanceConfigurable,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ open class ContainerTableViewCell<View: UIView>: BaseInitializableCell, WrappedV
|
|||
}
|
||||
|
||||
private lazy var subviewContraints: SubviewConstraints = {
|
||||
configureWrappedViewLayout()
|
||||
createSubviewConstraints()
|
||||
}()
|
||||
|
||||
// MARK: - InitializableView
|
||||
|
|
@ -62,7 +62,7 @@ open class ContainerTableViewCell<View: UIView>: BaseInitializableCell, WrappedV
|
|||
open override func configureLayout() {
|
||||
super.configureLayout()
|
||||
|
||||
subviewContraints.edgeConstraints.activate()
|
||||
update(subviewConstraints: subviewContraints)
|
||||
}
|
||||
|
||||
open func createView() -> View {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ open class ContainerView<View: UIView>: BaseInitializableView, WrappedViewHolder
|
|||
}
|
||||
|
||||
private lazy var subviewContraints: SubviewConstraints = {
|
||||
configureWrappedViewLayout()
|
||||
createSubviewConstraints()
|
||||
}()
|
||||
|
||||
// MARK: - InitializableView
|
||||
|
|
@ -60,7 +60,7 @@ open class ContainerView<View: UIView>: BaseInitializableView, WrappedViewHolder
|
|||
public override func configureLayout() {
|
||||
super.configureLayout()
|
||||
|
||||
subviewContraints.edgeConstraints.activate()
|
||||
update(subviewConstraints: subviewContraints)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ open class ReusableCollectionContainerView<View: UIView>: UICollectionReusableVi
|
|||
}
|
||||
|
||||
private lazy var subviewContraints: SubviewConstraints = {
|
||||
configureWrappedViewLayout()
|
||||
createSubviewConstraints()
|
||||
}()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
|
|
|||
|
|
@ -52,13 +52,10 @@ public extension WrappedViewHolder {
|
|||
centerYConstraint: wrappedView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor))
|
||||
}
|
||||
|
||||
func configureWrappedViewLayout() -> SubviewConstraints {
|
||||
func createSubviewConstraints() -> SubviewConstraints {
|
||||
wrappedView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let contentEdgeConstraints = wrappedViewEdgeConstraints()
|
||||
contentEdgeConstraints.activate()
|
||||
|
||||
return SubviewConstraints(edgeConstraints: contentEdgeConstraints,
|
||||
return SubviewConstraints(edgeConstraints: wrappedViewEdgeConstraints(),
|
||||
centerConstraints: wrappedViewCenterConstraints(),
|
||||
sizeConstraints: wrappedViewSizeConstraints())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIUIElements'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru',
|
||||
'castlele' => 'nikita.semenov@touchin.ru' }
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ open class BaseTextAttributes {
|
|||
|
||||
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()
|
||||
var configuration = button.configuration ?? .plain()
|
||||
|
||||
if let title = string {
|
||||
button.setTitle(nil, for: state)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIUIKitCore'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru',
|
||||
'castlele' => 'nikita.semenov@touchin.ru' }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIWebView'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.0'
|
||||
s.summary = 'Universal web view API'
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru',
|
||||
'castlele' => 'nikita.semenov@touchin.ru' }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIYandexMapUtils'
|
||||
s.version = '1.51.0'
|
||||
s.version = '1.52.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.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ TILogging
|
|||
TISwiftUtils
|
||||
TIPagination
|
||||
TIFoundationUtils
|
||||
TIApplication
|
||||
TICoreGraphicsUtils
|
||||
TIKeychainUtils
|
||||
TIUIKitCore
|
||||
|
|
|
|||
|
|
@ -16,16 +16,16 @@
|
|||
# SRCROOT=`pwd` ./project-scripts/push_to_podspecs.sh
|
||||
#
|
||||
|
||||
GIT_REPO_PATH="git.svc.touchin.ru/TouchInstinct/Podspecs"
|
||||
GIT_REPO_PATH="https://git.svc.touchin.ru/TouchInstinct/Podspecs"
|
||||
|
||||
if [ -z "${MODULE_NAME}" ]; then
|
||||
for module_name in $(cat ${SRCROOT}/project-scripts/ordered_modules_list.txt); do
|
||||
bundle exec pod repo push "git@${GIT_REPO_PATH}.git" ${SRCROOT}/${module_name}/${module_name}.podspec "$@" --allow-warnings
|
||||
bundle exec pod repo push "${GIT_REPO_PATH}" ${SRCROOT}/${module_name}/${module_name}.podspec "$@" --allow-warnings
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
exit $?
|
||||
fi
|
||||
done
|
||||
else
|
||||
bundle exec pod repo push "git@${GIT_REPO_PATH}.git" ${SRCROOT}/${MODULE_NAME}/${MODULE_NAME}.podspec "$@" --allow-warnings
|
||||
bundle exec pod repo push "${GIT_REPO_PATH}" ${SRCROOT}/${MODULE_NAME}/${MODULE_NAME}.podspec "$@" --allow-warnings
|
||||
fi
|
||||
|
|
|
|||
Loading…
Reference in New Issue