feat: TIApplication module and other fixes and improvements

This commit is contained in:
Ivan Smolin 2023-07-24 10:32:33 +03:00
parent b8611321fb
commit eaea4abd75
66 changed files with 935 additions and 320 deletions

View File

@ -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

View File

@ -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

View File

@ -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"),

View File

@ -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 }

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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 }

View File

@ -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'}

View File

@ -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 }

View File

@ -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' }

View File

@ -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' }

View File

@ -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 }

View File

@ -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)) })
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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 }

View File

@ -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 }

View File

@ -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 }

View File

@ -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 }

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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 }

View File

@ -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 }

View File

@ -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)

View File

@ -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 }

View File

@ -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)

View File

@ -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 }

View File

@ -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 }

View File

@ -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 }

View File

@ -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 }

View File

@ -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 }

View File

@ -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 }

View File

@ -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,

View File

@ -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
}
}

View File

@ -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,

View File

@ -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))
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -52,7 +52,7 @@ open class BasePlaceholderImageView<Placeholder: UIView>: UIImageView,
}
public lazy var placeholderConstraints: SubviewConstraints = {
configureWrappedViewLayout()
createSubviewConstraints()
}()
open override var image: UIImage? {

View File

@ -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?()
}
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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?()
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -51,7 +51,7 @@ open class ReusableCollectionContainerView<View: UIView>: UICollectionReusableVi
}
private lazy var subviewContraints: SubviewConstraints = {
configureWrappedViewLayout()
createSubviewConstraints()
}()
// MARK: - Initialization

View File

@ -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())
}

View File

@ -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' }

View File

@ -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)

View File

@ -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' }

View File

@ -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' }

View File

@ -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 }

View File

@ -2,6 +2,7 @@ TILogging
TISwiftUtils
TIPagination
TIFoundationUtils
TIApplication
TICoreGraphicsUtils
TIKeychainUtils
TIUIKitCore

View File

@ -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