diff --git a/CHANGELOG.md b/CHANGELOG.md index 47f188e6..9b9eb707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### 1.50.0 + +- **Updated**: Fix activity indicator positioning for `StatefulButton` on iOS 15+ and disabled state touch handling +- **Added**: iOS 15+ activity indicator placement support in `StatefulButton` +- **Added**: `TICoreGraphicsUtils` module for drawing operations and other CoreGraphics related functionality +- **Update**: `MarkerIconFactory` can now return optional `UIImage`. In this case MapManagers will show the default marker icon. + ### 1.49.0 - **Added**: `BaseMigratingSingleValueKeychainStorage` and `BaseMigratingSingleValueDefaultsStorage` implementations for migrating keys from one storage to another. diff --git a/Package.swift b/Package.swift index fbaee8c0..b61b97ef 100644 --- a/Package.swift +++ b/Package.swift @@ -22,6 +22,7 @@ let package = Package( // MARK: - Utils .library(name: "TISwiftUtils", targets: ["TISwiftUtils"]), .library(name: "TIFoundationUtils", targets: ["TIFoundationUtils"]), + .library(name: "TICoreGraphicsUtils", targets: ["TICoreGraphicsUtils"]), .library(name: "TIKeychainUtils", targets: ["TIKeychainUtils"]), .library(name: "TITableKitUtils", targets: ["TITableKitUtils"]), .library(name: "TIDeeplink", targets: ["TIDeeplink"]), @@ -59,7 +60,13 @@ let package = Package( // MARK: - UIKit .target(name: "TIUIKitCore", dependencies: ["TISwiftUtils"], path: "TIUIKitCore/Sources"), - .target(name: "TIUIElements", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIUIElements/Sources"), + + .target(name: "TIUIElements", + dependencies: ["TIUIKitCore", "TISwiftUtils"], + path: "TIUIElements/Sources", + exclude: ["../TIUIElements.app"], + plugins: [.plugin(name: "TISwiftLintPlugin")]), + .target(name: "TIWebView", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIWebView/Sources"), // MARK: - SwiftUI @@ -77,6 +84,12 @@ let package = Package( exclude: ["TIFoundationUtils.app"], plugins: [.plugin(name: "TISwiftLintPlugin")]), + .target(name: "TICoreGraphicsUtils", + dependencies: [], + path: "TICoreGraphicsUtils/Sources", + exclude: ["../TICoreGraphicsUtils.app"], + plugins: [.plugin(name: "TISwiftLintPlugin")]), + .target(name: "TIKeychainUtils", dependencies: ["TIFoundationUtils", "KeychainAccess"], path: "TIKeychainUtils/Sources", @@ -108,7 +121,7 @@ let package = Package( // MARK: - Maps .target(name: "TIMapUtils", - dependencies: ["TILogging"], + dependencies: ["TILogging", "TICoreGraphicsUtils"], path: "TIMapUtils/Sources", plugins: [.plugin(name: "TISwiftLintPlugin")]), diff --git a/README.md b/README.md index 85e3087c..72009c64 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,8 @@ LICENSE - [TIFoundationUtils](docs/tifoundationutils) * [AsyncOperation](docs/tifoundationutils/asyncoperation.md) +- [TICoreGraphicsUtils](docs/ticoregraphicsutils) + * [DrawingOperations](docs/ticoregraphicsutils/drawingoperations.md) - [TIKeychainUtils](docs/tikeychainutils) * [SingleValueStorage](docs/tikeychainutils/singlevaluestorage.md) - [TIUIElements](docs/tiuielements) diff --git a/TIAppleMapUtils/Sources/AppleMapManager.swift b/TIAppleMapUtils/Sources/AppleMapManager.swift index 2afd1c8b..28a53b66 100644 --- a/TIAppleMapUtils/Sources/AppleMapManager.swift +++ b/TIAppleMapUtils/Sources/AppleMapManager.swift @@ -67,27 +67,18 @@ open class AppleMapManager: BaseMapManager ClusteringIdentifier, + iconFactory: DefaultMarkerIconFactory? = nil, + clusterIconFactory: DefaultClusterMarkerIconFactory? = nil, mapViewDelegate: MKMapViewDelegate? = nil, - selectPlacemarkHandler: @escaping SelectPlacemarkHandler) { - - self.init(map: map, - positionGetter: positionGetter, - clusteringIdentifierGetter: clusteringIdentifierGetter, - iconFactory: nil as DefaultMarkerIconFactory?, - clusterIconFactory: nil as DefaultClusterMarkerIconFactory?, - mapViewDelegate: mapViewDelegate, - selectPlacemarkHandler: selectPlacemarkHandler) - } - - public convenience init(map: MKMapView, - mapViewDelegate: MKMapViewDelegate? = nil, - selectPlacemarkHandler: @escaping SelectPlacemarkHandler) where DataModel: MapLocatable & Clusterable, DataModel.ClusterIdentifier == ClusteringIdentifier, DataModel.Position == CLLocationCoordinate2D { + selectPlacemarkHandler: @escaping SelectPlacemarkHandler) + where DataModel: MapLocatable, DataModel.Position == CLLocationCoordinate2D, + DataModel: Clusterable, DataModel.ClusterIdentifier == ClusteringIdentifier { self.init(map: map, positionGetter: { $0.position }, clusteringIdentifierGetter: { $0.clusterIdentifier }, + iconFactory: iconFactory, + clusterIconFactory: clusterIconFactory, mapViewDelegate: mapViewDelegate, selectPlacemarkHandler: selectPlacemarkHandler) } diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index 59ef13c5..05398773 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAppleMapUtils' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec index 3125d1ec..6f585a37 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAuth' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Login, registration, confirmation and other related actions' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TICoreGraphicsUtils/PlaygroundPodfile b/TICoreGraphicsUtils/PlaygroundPodfile new file mode 100644 index 00000000..08d928c3 --- /dev/null +++ b/TICoreGraphicsUtils/PlaygroundPodfile @@ -0,0 +1,8 @@ +ENV["DEVELOPMENT_INSTALL"] = "true" + +target 'TICoreGraphicsUtils' do + platform :ios, 11.0 + use_frameworks! + + pod 'TICoreGraphicsUtils', :path => '../../../../TICoreGraphicsUtils/TICoreGraphicsUtils.podspec' +end diff --git a/TIMapUtils/Sources/Drawing/Helpers/CGGeometry+Extensions.swift b/TICoreGraphicsUtils/Sources/Drawing/Helpers/CGGeometry+Extensions.swift similarity index 89% rename from TIMapUtils/Sources/Drawing/Helpers/CGGeometry+Extensions.swift rename to TICoreGraphicsUtils/Sources/Drawing/Helpers/CGGeometry+Extensions.swift index 16e77135..36ea6bb5 100644 --- a/TIMapUtils/Sources/Drawing/Helpers/CGGeometry+Extensions.swift +++ b/TICoreGraphicsUtils/Sources/Drawing/Helpers/CGGeometry+Extensions.swift @@ -22,11 +22,9 @@ import CoreGraphics.CGGeometry -public typealias CGContextSize = (width: Int, height: Int) - public extension CGSize { - var ceiledContextSize: CGContextSize { - (width: Int(ceil(width)), height: Int(ceil(height))) + var ceiledContextSize: CGSize { + CGSize(width: ceil(width), height: ceil(height)) } } diff --git a/TIMapUtils/Sources/Drawing/Helpers/CGSize+Resize.swift b/TICoreGraphicsUtils/Sources/Drawing/Helpers/CGSize+Resize.swift similarity index 100% rename from TIMapUtils/Sources/Drawing/Helpers/CGSize+Resize.swift rename to TICoreGraphicsUtils/Sources/Drawing/Helpers/CGSize+Resize.swift diff --git a/TIMapUtils/Sources/Drawing/Helpers/ResizeMode.swift b/TICoreGraphicsUtils/Sources/Drawing/Helpers/ResizeMode.swift similarity index 100% rename from TIMapUtils/Sources/Drawing/Helpers/ResizeMode.swift rename to TICoreGraphicsUtils/Sources/Drawing/Helpers/ResizeMode.swift diff --git a/TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift b/TICoreGraphicsUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift similarity index 100% rename from TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift rename to TICoreGraphicsUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift diff --git a/TIMapUtils/Sources/Drawing/Operations/CALayerDrawingOperation.swift b/TICoreGraphicsUtils/Sources/Drawing/Operations/CALayerDrawingOperation.swift similarity index 100% rename from TIMapUtils/Sources/Drawing/Operations/CALayerDrawingOperation.swift rename to TICoreGraphicsUtils/Sources/Drawing/Operations/CALayerDrawingOperation.swift diff --git a/TIMapUtils/Sources/Drawing/Operations/DrawingOperation.swift b/TICoreGraphicsUtils/Sources/Drawing/Operations/DrawingOperation.swift similarity index 100% rename from TIMapUtils/Sources/Drawing/Operations/DrawingOperation.swift rename to TICoreGraphicsUtils/Sources/Drawing/Operations/DrawingOperation.swift diff --git a/TIMapUtils/Sources/Drawing/Operations/OrientationAwareDrawingOperation.swift b/TICoreGraphicsUtils/Sources/Drawing/Operations/OrientationAwareDrawingOperation.swift similarity index 100% rename from TIMapUtils/Sources/Drawing/Operations/OrientationAwareDrawingOperation.swift rename to TICoreGraphicsUtils/Sources/Drawing/Operations/OrientationAwareDrawingOperation.swift diff --git a/TIMapUtils/Sources/Drawing/Operations/SolidFillDrawingOperation.swift b/TICoreGraphicsUtils/Sources/Drawing/Operations/SolidFillDrawingOperation.swift similarity index 99% rename from TIMapUtils/Sources/Drawing/Operations/SolidFillDrawingOperation.swift rename to TICoreGraphicsUtils/Sources/Drawing/Operations/SolidFillDrawingOperation.swift index 9b92d5ce..b644dbe1 100644 --- a/TIMapUtils/Sources/Drawing/Operations/SolidFillDrawingOperation.swift +++ b/TICoreGraphicsUtils/Sources/Drawing/Operations/SolidFillDrawingOperation.swift @@ -55,8 +55,10 @@ public struct SolidFillDrawingOperation: DrawingOperation { switch shape { case let .rect(rect): return rect + case let .ellipse(rect): return rect + case let .path(path): return path.boundingBox } @@ -68,8 +70,10 @@ public struct SolidFillDrawingOperation: DrawingOperation { switch shape { case let .rect(rect): context.fill(rect) + case let .ellipse(rect): context.fillEllipse(in: rect) + case let .path(path): context.addPath(path) context.fillPath() diff --git a/TICoreGraphicsUtils/Sources/Drawing/Operations/TemplateDrawingOperation.swift b/TICoreGraphicsUtils/Sources/Drawing/Operations/TemplateDrawingOperation.swift new file mode 100644 index 00000000..ed7fe282 --- /dev/null +++ b/TICoreGraphicsUtils/Sources/Drawing/Operations/TemplateDrawingOperation.swift @@ -0,0 +1,57 @@ +// +// 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 CoreGraphics + +public struct TemplateDrawingOperation: OrientationAwareDrawingOperation { + + public var image: CGImage + public var imageSize: CGSize + public var color: CGColor + public var flipHorizontallyDuringDrawing: Bool + + public init(image: CGImage, + imageSize: CGSize, + color: CGColor, + flipHorizontallyDuringDrawing: Bool = true) { + + self.image = image + self.imageSize = imageSize + self.color = color + self.flipHorizontallyDuringDrawing = flipHorizontallyDuringDrawing + } + + public func affectedArea(in context: CGContext?) -> CGRect { + CGRect(origin: .zero, size: imageSize) + } + + public func apply(in context: CGContext) { + let imageRect = CGRect(origin: .zero, size: imageSize) + + apply(in: context) { + $0.setFillColor(color) + $0.clip(to: imageRect, mask: image) + $0.fill(imageRect) + $0.setBlendMode(.multiply) + } + } +} diff --git a/TIMapUtils/Sources/Drawing/Operations/TextDrawingOperation.swift b/TICoreGraphicsUtils/Sources/Drawing/Operations/TextDrawingOperation.swift similarity index 65% rename from TIMapUtils/Sources/Drawing/Operations/TextDrawingOperation.swift rename to TICoreGraphicsUtils/Sources/Drawing/Operations/TextDrawingOperation.swift index 5bd14f3a..426952d5 100644 --- a/TIMapUtils/Sources/Drawing/Operations/TextDrawingOperation.swift +++ b/TICoreGraphicsUtils/Sources/Drawing/Operations/TextDrawingOperation.swift @@ -26,48 +26,45 @@ import CoreText public struct TextDrawingOperation: OrientationAwareDrawingOperation { public var text: String - public var font: CTFont - public var textColor: CGColor + public var textAttributes: [NSAttributedString.Key: Any] public var flipHorizontallyDuringDrawing: Bool public var desiredOffset: CGPoint - private var line: CTLine { - let textAttributes: [NSAttributedString.Key : Any] = [ - .font: font, - .foregroundColor: textColor - ] + public var attributedString: NSAttributedString { + NSAttributedString(string: text, attributes: textAttributes) + } - let attributedString = NSAttributedString(string: text, attributes: textAttributes) + public var line: CTLine { + CTLineCreateWithAttributedString(attributedString) + } - return CTLineCreateWithAttributedString(attributedString) + public var desiredContextSize: CGSize { + let imageBounds = CTLineGetImageBounds(line, nil) + + return CGSize(width: max(imageBounds.width, imageBounds.maxX) + desiredOffset.x, + height: max(imageBounds.height, imageBounds.maxY) + desiredOffset.y) } public init(text: String, - font: CTFont, - textColor: CGColor, + textAttributes: [NSAttributedString.Key: Any], flipHorizontallyDuringDrawing: Bool = true, desiredOffset: CGPoint = .zero) { self.text = text - self.font = font - self.textColor = textColor + self.textAttributes = textAttributes self.flipHorizontallyDuringDrawing = flipHorizontallyDuringDrawing self.desiredOffset = desiredOffset } + // MARK: - DrawingOperation + public func affectedArea(in context: CGContext? = nil) -> CGRect { - CGRect(origin: desiredOffset, size: CTLineGetImageBounds(line, context).size) + CTLineGetImageBounds(line, context).offset(by: desiredOffset) } public func apply(in context: CGContext) { apply(in: context) { - let originForDrawing = offsetForDrawing(CTLineGetImageBounds(line, context).origin) - let desiredOffsetForDrawing = offsetForDrawing(desiredOffset) - let textPosition = CGPoint(x: desiredOffsetForDrawing.x - originForDrawing.x, - y: desiredOffsetForDrawing.y - originForDrawing.y) - - $0.textPosition = textPosition - + $0.textPosition = offsetForDrawing(affectedArea(in: context).origin) CTLineDraw(line, $0) } } diff --git a/TIMapUtils/Sources/Drawing/Operations/TransformDrawingOperation.swift b/TICoreGraphicsUtils/Sources/Drawing/Operations/TransformDrawingOperation.swift similarity index 100% rename from TIMapUtils/Sources/Drawing/Operations/TransformDrawingOperation.swift rename to TICoreGraphicsUtils/Sources/Drawing/Operations/TransformDrawingOperation.swift diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/.gitignore b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/.gitignore new file mode 100644 index 00000000..8b404f15 --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/.gitignore @@ -0,0 +1,5 @@ +# gitignore nef files +**/build/ +**/nef/ +LICENSE +end diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/Info.plist b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/Info.plist new file mode 100644 index 00000000..831ea97a --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + launcher + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon + CFBundleIdentifier + com.fortysevendeg.nef + CFBundleInfoDictionaryVersion + 6.0 + CFBundleSupportedPlatforms + + MacOSX + + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + 10.14 + NSHumanReadableCopyright + Copyright © 2019 The nef Authors. All rights reserved. + + diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/.gitignore b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/.gitignore new file mode 100644 index 00000000..18bd1f3b --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/.gitignore @@ -0,0 +1,26 @@ +## gitignore nef files +**/build/ +**/nef/ +LICENSE + +## User data +**/xcuserdata/ +podfile.lock +**.DS_Store + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## CocoaPods +**Pods** + +## Carthage +**Carthage** + +## SPM +.build +.swiftpm +swiftpm diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/Podfile b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/Podfile new file mode 100644 index 00000000..08d928c3 --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/Podfile @@ -0,0 +1,8 @@ +ENV["DEVELOPMENT_INSTALL"] = "true" + +target 'TICoreGraphicsUtils' do + platform :ios, 11.0 + use_frameworks! + + pod 'TICoreGraphicsUtils', :path => '../../../../TICoreGraphicsUtils/TICoreGraphicsUtils.podspec' +end diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.playground/Pages/DrawingOperations.xcplaygroundpage/Contents.swift b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.playground/Pages/DrawingOperations.xcplaygroundpage/Contents.swift new file mode 100644 index 00000000..c89d162e --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.playground/Pages/DrawingOperations.xcplaygroundpage/Contents.swift @@ -0,0 +1,128 @@ +/*: + # `DrawingOperation` - протокол для инкапсулирования низкоуровневых вызовов отрисовки CoreGraphicss. + +Позволяет: + +- сгруппировать низкоуровневые операции CoreGraphics в более высокоуровневые +- использовать композицию из высокоуровневых операций +- получить размер изображения необходимый для создания контекста + + ## Базовые операции + + "Из коробки" доступны самые часто испольуемые операции. + */ + +/*: + ### `SolidFillDrawingOperation` + + Операция для заполнения области рисования выбранным цветом и выбранной формой. + Например, можно нарисовать прямоугольник или круг на существующей области рисования + или просто создать изображение с однотонный фоном. +*/ + +import TICoreGraphicsUtils +import UIKit + +let solidFillSize = CGSize(width: 200, height: 200) + +let renderer = UIGraphicsImageRenderer(size: solidFillSize) +let solidFillDrawingOperation = SolidFillDrawingOperation(color: UIColor.purple.cgColor, + rect: CGRect(origin: .zero, + size: solidFillSize)) + +let solidFillImage = renderer.image { + solidFillDrawingOperation.apply(in: $0.cgContext) +} + +/*: + ### `BorderDrawingOperation` + + Операция создаёт рамку определённой формы и размера. +*/ + +let borderDrawingOperation = BorderDrawingOperation(frameableContentRect: CGRect(origin: .zero, + size: solidFillSize), + border: 2, + color: UIColor.red.cgColor, + radius: 4, + exteriorBorder: false) + +let borderImage = renderer.image { + borderDrawingOperation.apply(in: $0.cgContext) +} + +/*: + ### `CALayerDrawingOperation` + + Операция отрисовывает содержимое CALayer в изображение. Обычно используется для снапшота UIView. +*/ + +let button = UIButton(type: .custom) +button.setTitle("This is button", for: .normal) +button.setBackgroundImage(borderImage, for: .normal) + +button.sizeToFit() + +let layerDrawingOperation = CALayerDrawingOperation(layer: button.layer, offset: .zero) + +let layerRenderer = UIGraphicsImageRenderer(size: button.bounds.size) + +let buttonSnapshotImage = layerRenderer.image { + layerDrawingOperation.apply(in: $0.cgContext) +} + +/*: + ### `TextDrawingOperation` + + Операция отрисовывает текст с заданными атрибутами +*/ + +let textDrawingOperaton = TextDrawingOperation(text: "This is string", + textAttributes: [ + .font: UIFont.boldSystemFont(ofSize: 15), + .foregroundColor: UIColor.white + ]) + +let textRenderer = UIGraphicsImageRenderer(size: textDrawingOperaton.desiredContextSize) + +let textImage = textRenderer.image { + textDrawingOperaton.apply(in: $0.cgContext) +} + +/*: + ### `TemplateDrawingOperation` + + Операция заменяет все цвета кроме прозрачного на переданный цвет. + Используется для приданиям иконкам определённого цвета (аналог tintColor). +*/ + +if let cgImage = textImage.cgImage { + let templateDrawingOperation = TemplateDrawingOperation(image: cgImage, + imageSize: textImage.size, + color: UIColor.red.cgColor) + + let tintedImage = textRenderer.image { + templateDrawingOperation.apply(in: $0.cgContext) + } +} + +/*: + ### `TransformDrawingOperation` + + Операция производит изменение размера изображения учитывая пропорции + и другие настройки (по аналогии с contentMode у UIImage). +*/ + +if let cgImage = textImage.cgImage { + let newSize = CGSize(width: textImage.size.width / 1.5, height: textImage.size.height) + let resizeOperation = TransformDrawingOperation(image: cgImage, + imageSize: textImage.size, + maxNewSize: newSize, + resizeMode: .scaleAspectFill) + + let resizeRenderer = UIGraphicsImageRenderer(size: newSize) + + let resizedImage = resizeRenderer.image { + resizeOperation.apply(in: $0.cgContext) + } +} diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.playground/Sources/NefPlaygroundSupport.swift b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.playground/Sources/NefPlaygroundSupport.swift new file mode 100644 index 00000000..2ffea80a --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.playground/Sources/NefPlaygroundSupport.swift @@ -0,0 +1,30 @@ +import UIKit + +public protocol NefPlaygroundLiveViewable {} +extension UIView: NefPlaygroundLiveViewable {} +extension UIViewController: NefPlaygroundLiveViewable {} + +#if NOT_IN_PLAYGROUND +public enum Nef { + public enum Playground { + public static func liveView(_ view: NefPlaygroundLiveViewable) {} + public static func needsIndefiniteExecution(_ state: Bool) {} + } +} + +#else +import PlaygroundSupport + +public enum Nef { + public enum Playground { + public static func liveView(_ view: NefPlaygroundLiveViewable) { + PlaygroundPage.current.liveView = (view as! PlaygroundLiveViewable) + } + + public static func needsIndefiniteExecution(_ state: Bool) { + PlaygroundPage.current.needsIndefiniteExecution = state + } + } +} + +#endif diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.playground/contents.xcplayground b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.playground/contents.xcplayground new file mode 100644 index 00000000..f19b449d --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.playground/contents.xcplayground @@ -0,0 +1,2 @@ + + diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.xcodeproj/project.pbxproj b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.xcodeproj/project.pbxproj new file mode 100644 index 00000000..61ddb9c2 --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.xcodeproj/project.pbxproj @@ -0,0 +1,396 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A319933CF180B210C469FFB1 /* Pods_TICoreGraphicsUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE2E24C8921A6FA2CDB65242 /* Pods_TICoreGraphicsUtils.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 82AF4D8AFDF24586DD661012 /* Pods-TICoreGraphicsUtils.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TICoreGraphicsUtils.debug.xcconfig"; path = "Target Support Files/Pods-TICoreGraphicsUtils/Pods-TICoreGraphicsUtils.debug.xcconfig"; sourceTree = ""; }; + 8BACBE8322576CAD00266845 /* TICoreGraphicsUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TICoreGraphicsUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8BACBE8622576CAD00266845 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9238CAF7BE416E9ACF0C153E /* Pods-TICoreGraphicsUtils.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TICoreGraphicsUtils.release.xcconfig"; path = "Target Support Files/Pods-TICoreGraphicsUtils/Pods-TICoreGraphicsUtils.release.xcconfig"; sourceTree = ""; }; + AE2E24C8921A6FA2CDB65242 /* Pods_TICoreGraphicsUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TICoreGraphicsUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8BACBE8022576CAD00266845 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A319933CF180B210C469FFB1 /* Pods_TICoreGraphicsUtils.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2238D5BA030C14EBEF939AC4 /* Pods */ = { + isa = PBXGroup; + children = ( + 82AF4D8AFDF24586DD661012 /* Pods-TICoreGraphicsUtils.debug.xcconfig */, + 9238CAF7BE416E9ACF0C153E /* Pods-TICoreGraphicsUtils.release.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 8B39A26221D40F8700DE2643 = { + isa = PBXGroup; + children = ( + 8BACBE8422576CAD00266845 /* TICoreGraphicsUtils */, + 8B39A26C21D40F8700DE2643 /* Products */, + 2238D5BA030C14EBEF939AC4 /* Pods */, + B51B4BA44249652EDC4FBF61 /* Frameworks */, + ); + sourceTree = ""; + }; + 8B39A26C21D40F8700DE2643 /* Products */ = { + isa = PBXGroup; + children = ( + 8BACBE8322576CAD00266845 /* TICoreGraphicsUtils.framework */, + ); + name = Products; + sourceTree = ""; + }; + 8BACBE8422576CAD00266845 /* TICoreGraphicsUtils */ = { + isa = PBXGroup; + children = ( + 8BACBE8622576CAD00266845 /* Info.plist */, + ); + path = TICoreGraphicsUtils; + sourceTree = ""; + }; + B51B4BA44249652EDC4FBF61 /* Frameworks */ = { + isa = PBXGroup; + children = ( + AE2E24C8921A6FA2CDB65242 /* Pods_TICoreGraphicsUtils.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 8BACBE7E22576CAD00266845 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 8BACBE8222576CAD00266845 /* TICoreGraphicsUtils */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TICoreGraphicsUtils" */; + buildPhases = ( + 3A6585E20D9DE6C1873231DC /* [CP] Check Pods Manifest.lock */, + 8BACBE7E22576CAD00266845 /* Headers */, + 8BACBE7F22576CAD00266845 /* Sources */, + 8BACBE8022576CAD00266845 /* Frameworks */, + 8BACBE8122576CAD00266845 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TICoreGraphicsUtils; + productName = TICoreGraphicsUtils2; + productReference = 8BACBE8322576CAD00266845 /* TICoreGraphicsUtils.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8B39A26321D40F8700DE2643 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1010; + LastUpgradeCheck = 1200; + ORGANIZATIONNAME = "47 Degrees"; + TargetAttributes = { + 8BACBE8222576CAD00266845 = { + CreatedOnToolsVersion = 10.1; + }; + }; + }; + buildConfigurationList = 8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TICoreGraphicsUtils" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8B39A26221D40F8700DE2643; + productRefGroup = 8B39A26C21D40F8700DE2643 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8BACBE8222576CAD00266845 /* TICoreGraphicsUtils */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8BACBE8122576CAD00266845 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3A6585E20D9DE6C1873231DC /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-TICoreGraphicsUtils-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8BACBE7F22576CAD00266845 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 8B39A27721D40F8800DE2643 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8B39A27821D40F8800DE2643 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 8BACBE8822576CAD00266845 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 82AF4D8AFDF24586DD661012 /* Pods-TICoreGraphicsUtils.debug.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + CURRENT_TICoreGraphicsUtils_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/TICoreGraphicsUtils/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TICoreGraphicsUtils; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 8BACBE8922576CAD00266845 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9238CAF7BE416E9ACF0C153E /* Pods-TICoreGraphicsUtils.release.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + CURRENT_TICoreGraphicsUtils_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/TICoreGraphicsUtils/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TICoreGraphicsUtils; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TICoreGraphicsUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8B39A27721D40F8800DE2643 /* Debug */, + 8B39A27821D40F8800DE2643 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TICoreGraphicsUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8BACBE8822576CAD00266845 /* Debug */, + 8BACBE8922576CAD00266845 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8B39A26321D40F8700DE2643 /* Project object */; +} diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..fa52e350 --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.xcodeproj/xcshareddata/xcschemes/TICoreGraphicsUtils.xcscheme b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.xcodeproj/xcshareddata/xcschemes/TICoreGraphicsUtils.xcscheme new file mode 100644 index 00000000..072e00e6 --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.xcodeproj/xcshareddata/xcschemes/TICoreGraphicsUtils.xcscheme @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.xcworkspace/contents.xcworkspacedata b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..e707089c --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils/Info.plist b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils/Info.plist new file mode 100644 index 00000000..98d14f60 --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + NSHumanReadableCopyright + Copyright © 2019. The nef authors. + + diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/launcher b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/launcher new file mode 100755 index 00000000..77cab282 --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/MacOS/launcher @@ -0,0 +1,6 @@ +#!/bin/bash + +workspace="TICoreGraphicsUtils.xcworkspace" +workspacePath=$(echo "$0" | rev | cut -f2- -d '/' | rev) + +open "`pwd`/$workspacePath/$workspace" diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/Resources/AppIcon.icns b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/Resources/AppIcon.icns new file mode 100644 index 00000000..32814f1c Binary files /dev/null and b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/Resources/AppIcon.icns differ diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/Resources/Assets.car b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/Resources/Assets.car new file mode 100644 index 00000000..79d9ea89 Binary files /dev/null and b/TICoreGraphicsUtils/TICoreGraphicsUtils.app/Contents/Resources/Assets.car differ diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.playground b/TICoreGraphicsUtils/TICoreGraphicsUtils.playground new file mode 120000 index 00000000..ed91c693 --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.playground @@ -0,0 +1 @@ +TICoreGraphicsUtils.app/Contents/MacOS/TICoreGraphicsUtils.playground \ No newline at end of file diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.podspec b/TICoreGraphicsUtils/TICoreGraphicsUtils.podspec new file mode 100644 index 00000000..b2754d8d --- /dev/null +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'TICoreGraphicsUtils' + s.version = '1.50.0' + s.summary = 'CoreGraphics drawing helpers' + s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + 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.framework = 'CoreGraphics' +end diff --git a/TIDeepLink/PlaygroundPodfile b/TIDeepLink/PlaygroundPodfile index c2add1b9..5d3978ab 100644 --- a/TIDeepLink/PlaygroundPodfile +++ b/TIDeepLink/PlaygroundPodfile @@ -4,6 +4,7 @@ target 'TIDeeplink' do platform :ios, 11 use_frameworks! + pod 'TILogging', :path => '../../../../TILogging/TILogging.podspec' pod 'TIFoundationUtils', :path => '../../../../TIFoundationUtils/TIFoundationUtils.podspec' pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' pod 'TIDeeplink', :path => '../../../../TIDeeplink/TIDeeplink.podspec' diff --git a/TIDeepLink/TIDeeplink.app/Contents/MacOS/Podfile b/TIDeepLink/TIDeeplink.app/Contents/MacOS/Podfile index c2add1b9..5d3978ab 100644 --- a/TIDeepLink/TIDeeplink.app/Contents/MacOS/Podfile +++ b/TIDeepLink/TIDeeplink.app/Contents/MacOS/Podfile @@ -4,6 +4,7 @@ target 'TIDeeplink' do platform :ios, 11 use_frameworks! + pod 'TILogging', :path => '../../../../TILogging/TILogging.podspec' pod 'TIFoundationUtils', :path => '../../../../TIFoundationUtils/TIFoundationUtils.podspec' pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' pod 'TIDeeplink', :path => '../../../../TIDeeplink/TIDeeplink.podspec' diff --git a/TIDeeplink/TIDeeplink.podspec b/TIDeeplink/TIDeeplink.podspec index 04f4b5b0..49c419a0 100644 --- a/TIDeeplink/TIDeeplink.podspec +++ b/TIDeeplink/TIDeeplink.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeeplink' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Deeplink service API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIDeveloperUtils/TIDeveloperUtils.podspec b/TIDeveloperUtils/TIDeveloperUtils.podspec index 3f26677b..62bf96fc 100644 --- a/TIDeveloperUtils/TIDeveloperUtils.podspec +++ b/TIDeveloperUtils/TIDeveloperUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeveloperUtils' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Universal web view API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIEcommerce/TIEcommerce.podspec b/TIEcommerce/TIEcommerce.podspec index 988443f0..46af1443 100644 --- a/TIEcommerce/TIEcommerce.podspec +++ b/TIEcommerce/TIEcommerce.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIEcommerce' - s.version = '1.49.0' + s.version = '1.50.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.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/Podfile b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/Podfile index 95c2fd95..fb5c773a 100644 --- a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/Podfile +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/Podfile @@ -4,6 +4,7 @@ target 'TIFoundationUtils' do platform :ios, 11.0 use_frameworks! + pod 'TILogging', :path => '../../../../TILogging/TILogging.podspec' pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' pod 'TIFoundationUtils', :path => '../../../../TIFoundationUtils/TIFoundationUtils.podspec' end diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index fb0aa6ef..1ae0655b 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Set of helpers for Foundation framework classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIGoogleMapUtils/Sources/GoogleMapManager.swift b/TIGoogleMapUtils/Sources/GoogleMapManager.swift index f3427fed..16068aa1 100644 --- a/TIGoogleMapUtils/Sources/GoogleMapManager.swift +++ b/TIGoogleMapUtils/Sources/GoogleMapManager.swift @@ -62,24 +62,16 @@ open class GoogleMapManager: BaseMapManager? = nil, + clusterIconFactory: DefaultClusterMarkerIconFactory? = nil, mapViewDelegate: GMSMapViewDelegate? = nil, - selectPlacemarkHandler: @escaping SelectPlacemarkHandler) { - - self.init(map: map, - positionGetter: positionGetter, - iconFactory: nil as DefaultMarkerIconFactory?, - clusterIconFactory: nil as DefaultClusterMarkerIconFactory?, - mapViewDelegate: mapViewDelegate, - selectPlacemarkHandler: selectPlacemarkHandler) - } - - public convenience init(map: GMSMapView, - mapViewDelegate: GMSMapViewDelegate? = nil, - selectPlacemarkHandler: @escaping SelectPlacemarkHandler) where DataModel: MapLocatable, DataModel.Position == CLLocationCoordinate2D { + selectPlacemarkHandler: @escaping SelectPlacemarkHandler) + where DataModel: MapLocatable, DataModel.Position == CLLocationCoordinate2D { self.init(map: map, positionGetter: { $0.position }, + iconFactory: iconFactory, + clusterIconFactory: clusterIconFactory, mapViewDelegate: mapViewDelegate, selectPlacemarkHandler: selectPlacemarkHandler) } diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec index d371a268..7f8ee7dc 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIGoogleMapUtils' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIKeychainUtils/PlaygroundPodfile b/TIKeychainUtils/PlaygroundPodfile index a39cbfee..be1e59eb 100644 --- a/TIKeychainUtils/PlaygroundPodfile +++ b/TIKeychainUtils/PlaygroundPodfile @@ -4,6 +4,7 @@ target 'TIModuleName' do platform :ios, 11 use_frameworks! + pod 'TILogging', :path => '../../../../TILogging/TILogging.podspec' pod 'TIFoundationUtils', :path => '../../../../TIFoundationUtils/TIFoundationUtils.podspec' pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' pod 'TIKeychainUtils', :path => '../../../../TIKeychainUtils/TIKeychainUtils.podspec' diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/Podfile b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/Podfile index f0a332cf..1898c7df 100644 --- a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/Podfile +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/Podfile @@ -4,6 +4,7 @@ target 'TIKeychainUtils' do platform :ios, 11 use_frameworks! + pod 'TILogging', :path => '../../../../TILogging/TILogging.podspec' pod 'TIFoundationUtils', :path => '../../../../TIFoundationUtils/TIFoundationUtils.podspec' pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' pod 'TIKeychainUtils', :path => '../../../../TIKeychainUtils/TIKeychainUtils.podspec' diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index e1b95183..f0a30835 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Set of helpers for Keychain classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TILogging/TILogging.podspec b/TILogging/TILogging.podspec index 05211f1c..e12e9b9e 100644 --- a/TILogging/TILogging.podspec +++ b/TILogging/TILogging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TILogging' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Logging for TI libraries.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMapUtils/Sources/IconProviders/AnyMarkerIconFactory.swift b/TIMapUtils/Sources/IconProviders/AnyMarkerIconFactory.swift index ccebd849..df370558 100644 --- a/TIMapUtils/Sources/IconProviders/AnyMarkerIconFactory.swift +++ b/TIMapUtils/Sources/IconProviders/AnyMarkerIconFactory.swift @@ -23,7 +23,7 @@ import UIKit.UIImage public final class AnyMarkerIconFactory: MarkerIconFactory { - public typealias IconProviderClosure = (Model, MarkerState) -> UIImage + public typealias IconProviderClosure = (Model, MarkerState) -> UIImage? public var iconProviderClosure: IconProviderClosure @@ -35,7 +35,7 @@ public final class AnyMarkerIconFactory: MarkerIconFactory { self.iconProviderClosure = { iconFactory.markerIcon(for: transform($0), state: $1) } } - public func markerIcon(for model: Model, state: MarkerState) -> UIImage { + public func markerIcon(for model: Model, state: MarkerState) -> UIImage? { iconProviderClosure(model, state) } } diff --git a/TIMapUtils/Sources/Drawing/Operations/CurrentLocationDrawingOperation.swift b/TIMapUtils/Sources/IconProviders/CurrentLocationDrawingOperation.swift similarity index 99% rename from TIMapUtils/Sources/Drawing/Operations/CurrentLocationDrawingOperation.swift rename to TIMapUtils/Sources/IconProviders/CurrentLocationDrawingOperation.swift index 334bee2b..f71ce223 100644 --- a/TIMapUtils/Sources/Drawing/Operations/CurrentLocationDrawingOperation.swift +++ b/TIMapUtils/Sources/IconProviders/CurrentLocationDrawingOperation.swift @@ -20,6 +20,7 @@ // THE SOFTWARE. // +import TICoreGraphicsUtils import CoreGraphics public struct CurrentLocationDrawingOperation: DrawingOperation { diff --git a/TIMapUtils/Sources/IconProviders/DefaultCachableMarkerIconFactory.swift b/TIMapUtils/Sources/IconProviders/DefaultCachableMarkerIconFactory.swift index b29381d8..9d26d0d5 100644 --- a/TIMapUtils/Sources/IconProviders/DefaultCachableMarkerIconFactory.swift +++ b/TIMapUtils/Sources/IconProviders/DefaultCachableMarkerIconFactory.swift @@ -37,12 +37,15 @@ open class DefaultCachableMarkerIconFactory: DefaultMarkerIconF super.init(createIconClosure: createIconClosure) } - open override func markerIcon(for model: M, state: MarkerState) -> UIImage { + open override func markerIcon(for model: M, state: MarkerState) -> UIImage? { let cacheKey = cacheKeyProvider(model, state) guard let cachedIcon = cache.object(forKey: cacheKey) else { let icon = super.markerIcon(for: model, state: state) - cache.setObject(icon, forKey: cacheKey) + + if let icon { + cache.setObject(icon, forKey: cacheKey) + } return icon } diff --git a/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift b/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift index 65044aaf..4c355a8f 100644 --- a/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift +++ b/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift @@ -21,6 +21,7 @@ // import UIKit +import TICoreGraphicsUtils open class DefaultClusterIconRenderer { public struct TextAttributes { @@ -74,14 +75,11 @@ open class DefaultClusterIconRenderer { } open func textDrawingOperation(for text: String) -> TextDrawingOperation { - let ctFont = CTFontCreateWithFontDescriptorAndOptions(textAttributes.font.fontDescriptor, - textAttributes.font.pointSize, - nil, - []) - - return TextDrawingOperation(text: text, - font: ctFont, - textColor: textAttributes.color.cgColor) + TextDrawingOperation(text: text, + textAttributes: [ + .font: textAttributes.font, + .foregroundColor: textAttributes.color + ]) } open func backgroundDrawingOperation(iconSize: CGSize, @@ -98,11 +96,12 @@ open class DefaultClusterIconRenderer { return SolidFillDrawingOperation(color: color.cgColor, path: path) + case let .image(image): guard let cgImage = image.cgImage else { return nil } - + return TransformDrawingOperation(image: cgImage, imageSize: image.size, maxNewSize: iconSize, @@ -138,7 +137,7 @@ open class DefaultClusterIconRenderer { var textDrawingOperation = textDrawingOperation(for: text) - let textSize = textDrawingOperation.affectedArea().size + let textSize = textDrawingOperation.desiredContextSize let textRadius = sqrt(textSize.height * textSize.height + textSize.width * textSize.width) / 2 let internalRadius = textRadius + marginToText @@ -149,7 +148,7 @@ open class DefaultClusterIconRenderer { let radius = CGFloat(min(iconSizeWithBorder.width, iconSizeWithBorder.height) / 2) textDrawingOperation.desiredOffset = CGPoint(x: (iconSizeWithBorder.width - textSize.width) / 2, - y: (iconSizeWithBorder.height - textSize.height) / 2) + y: (iconSizeWithBorder.height - textSize.height) / 2) let backgroundDrawingOperation = backgroundDrawingOperation(iconSize: iconSize, iconSizeWithBorder: iconSizeWithBorder, @@ -158,9 +157,11 @@ open class DefaultClusterIconRenderer { let borderDrawindOperation = borderDrawingOperation(iconSize: iconSize, cornerRadius: radius) - let operations = [backgroundDrawingOperation, - textDrawingOperation, - borderDrawindOperation] + let operations = [ + backgroundDrawingOperation, + textDrawingOperation, + borderDrawindOperation + ] .compactMap { $0 } return execute(drawingOperations: operations, diff --git a/TIMapUtils/Sources/IconProviders/DefaultClusterMarkerIconFactory.swift b/TIMapUtils/Sources/IconProviders/DefaultClusterMarkerIconFactory.swift index 04e800dc..8023eab6 100644 --- a/TIMapUtils/Sources/IconProviders/DefaultClusterMarkerIconFactory.swift +++ b/TIMapUtils/Sources/IconProviders/DefaultClusterMarkerIconFactory.swift @@ -25,19 +25,26 @@ import Foundation.NSString public final class DefaultClusterMarkerIconFactory: DefaultCachableMarkerIconFactory<[Model], NSString> { public typealias RendererPreparationClosure = ([Model], DefaultClusterIconRenderer) -> Void - public var clusterIconRenderer: DefaultClusterIconRenderer - public var beforeRenderCallback: RendererPreparationClosure? - - public init(beforeRenderCallback: RendererPreparationClosure? = nil) { - self.beforeRenderCallback = beforeRenderCallback - self.clusterIconRenderer = DefaultClusterIconRenderer() - - super.init { [clusterIconRenderer] models, _ in - beforeRenderCallback?(models, clusterIconRenderer) - - return clusterIconRenderer.renderCluster(of: models.count) - } cacheKeyProvider: { models, _ in + public static var defaultCacheKeyProvider: CacheKeyProvider { + { models, _ in String(models.count) as NSString } } + + public var clusterIconRenderer: DefaultClusterIconRenderer + public var beforeRenderCallback: RendererPreparationClosure? + + public init(clusterIconRenderer: DefaultClusterIconRenderer = DefaultClusterIconRenderer(), + beforeRenderCallback: RendererPreparationClosure? = nil, + cacheKeyProvider: @escaping CacheKeyProvider = defaultCacheKeyProvider) { + + self.beforeRenderCallback = beforeRenderCallback + self.clusterIconRenderer = clusterIconRenderer + + super.init(createIconClosure: { [clusterIconRenderer] models, _ in + beforeRenderCallback?(models, clusterIconRenderer) + + return clusterIconRenderer.renderCluster(of: models.count) + }, cacheKeyProvider: cacheKeyProvider) + } } diff --git a/TIMapUtils/Sources/IconProviders/DefaultMarkerIconFactory.swift b/TIMapUtils/Sources/IconProviders/DefaultMarkerIconFactory.swift index 2fb403f5..e254add8 100644 --- a/TIMapUtils/Sources/IconProviders/DefaultMarkerIconFactory.swift +++ b/TIMapUtils/Sources/IconProviders/DefaultMarkerIconFactory.swift @@ -23,7 +23,7 @@ import UIKit.UIImage open class DefaultMarkerIconFactory: MarkerIconFactory { - public typealias CreateIconClosure = (M, MarkerState) -> UIImage + public typealias CreateIconClosure = (M, MarkerState) -> UIImage? private let createIconClosure: CreateIconClosure @@ -31,11 +31,11 @@ open class DefaultMarkerIconFactory: MarkerIconFactory { self.createIconClosure = createIconClosure } - open func markerIcon(for model: M, state: MarkerState) -> UIImage { + open func markerIcon(for model: M, state: MarkerState) -> UIImage? { postprocess(icon: createIconClosure(model, state)) } - open func postprocess(icon: UIImage) -> UIImage { + open func postprocess(icon: UIImage?) -> UIImage? { icon } } diff --git a/TIMapUtils/Sources/IconProviders/MarkerIconFactory.swift b/TIMapUtils/Sources/IconProviders/MarkerIconFactory.swift index 919e338c..dd8b99c5 100644 --- a/TIMapUtils/Sources/IconProviders/MarkerIconFactory.swift +++ b/TIMapUtils/Sources/IconProviders/MarkerIconFactory.swift @@ -25,5 +25,5 @@ import UIKit.UIImage public protocol MarkerIconFactory { associatedtype Model - func markerIcon(for model: Model, state: MarkerState) -> UIImage + func markerIcon(for model: Model, state: MarkerState) -> UIImage? } diff --git a/TIMapUtils/Sources/IconProviders/StaticImageIconFactory.swift b/TIMapUtils/Sources/IconProviders/StaticImageIconFactory.swift index 65fad0aa..91f0de4a 100644 --- a/TIMapUtils/Sources/IconProviders/StaticImageIconFactory.swift +++ b/TIMapUtils/Sources/IconProviders/StaticImageIconFactory.swift @@ -29,7 +29,7 @@ public final class StaticImageIconFactory: MarkerIconFactory { self.image = image } - public func markerIcon(for model: Model, state: MarkerState) -> UIImage { + public func markerIcon(for model: Model, state: MarkerState) -> UIImage? { image } } diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index cf2051ba..d7ed6cb3 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMapUtils' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Set of helpers for map objects clustering and interacting.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -19,6 +19,7 @@ Pod::Spec.new do |s| s.exclude_files = s.name + '/*.app' end + s.dependency 'TICoreGraphicsUtils', s.version.to_s s.dependency 'TILogging', s.version.to_s end diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index acc102be..2af06524 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Moya + Swagger network service.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index 23417288..bd4ecc20 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Swagger-frendly networking layer helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index 32416764..bfadd0f1 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Caching results of EndpointRequests.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIPagination/TIPagination.podspec b/TIPagination/TIPagination.podspec index ec5395d1..a99e7deb 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Generic pagination component.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec index 32d0f5f9..b1f66557 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUICore' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index 8be4e979..61a5f078 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Bunch of useful helpers for Swift development.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index ba01a447..15c646b7 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Set of helpers for TableKit classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITextProcessing/TITextProcessing.podspec b/TITextProcessing/TITextProcessing.podspec index f3e06b0d..404f29ff 100644 --- a/TITextProcessing/TITextProcessing.podspec +++ b/TITextProcessing/TITextProcessing.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITextProcessing' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'A text processing service helping to get a text mask and a placeholder from incoming regex.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIElements/Sources/Appearance/UIButton+Appearance.swift b/TIUIElements/Sources/Appearance/UIButton+Appearance.swift index 971606dc..f30cd0eb 100644 --- a/TIUIElements/Sources/Appearance/UIButton+Appearance.swift +++ b/TIUIElements/Sources/Appearance/UIButton+Appearance.swift @@ -24,26 +24,41 @@ import TIUIKitCore import UIKit extension UIButton { - open class BaseAppearance: UIView.BaseAppearance { - - public var textAttributes: BaseTextAttributes? + open class BaseContentLayout { public var contentInsets: UIEdgeInsets public var titleInsets: UIEdgeInsets public var imageInsets: UIEdgeInsets + public init(contentInsets: UIEdgeInsets = .zero, + titleInsets: UIEdgeInsets = .zero, + imageInsets: UIEdgeInsets = .zero) { + + self.contentInsets = contentInsets + self.titleInsets = titleInsets + self.imageInsets = imageInsets + } + } + + public final class DefaultContentLayout: BaseContentLayout, ViewLayout { + public static var defaultLayout: Self { + Self() + } + } + + open class BaseAppearance: UIView.BaseAppearance { + + public var textAttributes: BaseTextAttributes? + public var contentLayout: ContentLayout + public init(layout: Layout = .defaultLayout, backgroundColor: UIColor = .clear, border: UIViewBorder = .init(), shadow: UIViewShadow? = nil, textAttributes: BaseTextAttributes? = nil, - contentInsets: UIEdgeInsets = .zero, - titleInsets: UIEdgeInsets = .zero, - imageInsets: UIEdgeInsets = .zero) { + contentLayout: ContentLayout = .defaultLayout) { self.textAttributes = textAttributes - self.contentInsets = contentInsets - self.titleInsets = titleInsets - self.imageInsets = imageInsets + self.contentLayout = contentLayout super.init(layout: layout, backgroundColor: backgroundColor, @@ -52,7 +67,13 @@ extension UIButton { } } - public final class DefaultAppearance: BaseAppearance, WrappedViewAppearance { + public final class DefaultStateAppearance: BaseAppearance, ViewAppearance { + public static var defaultAppearance: Self { + Self() + } + } + + public final class DefaultAppearance: BaseAppearance, WrappedViewAppearance { public static var defaultAppearance: Self { Self() } diff --git a/TIUIElements/Sources/Appearance/UIButton+AppearanceConfigurable.swift b/TIUIElements/Sources/Appearance/UIButton+AppearanceConfigurable.swift index 667bf059..6c7b56a5 100644 --- a/TIUIElements/Sources/Appearance/UIButton+AppearanceConfigurable.swift +++ b/TIUIElements/Sources/Appearance/UIButton+AppearanceConfigurable.swift @@ -24,30 +24,84 @@ import TIUIKitCore import UIKit extension UIButton { - public func configureUIButton(appearance: UIButton.BaseAppearance) { + public func configureUIButton(appearance: BaseAppearance, + for state: UIControl.State = .normal) { + appearance.textAttributes? .configure(button: self, - with: titleLabel?.attributedText?.string ?? titleLabel?.text) + with: title(for: state), + for: state) if #available(iOS 15, *) { - configuration?.contentInsets = .init(insets: appearance.contentInsets) + var config = configuration ?? .plain() - if configuration?.imagePlacement == .leading { - let padding = appearance.titleInsets.left + appearance.imageInsets.right - configuration?.imagePadding = padding - } - - if configuration?.imagePlacement == .trailing { - let padding = appearance.titleInsets.right + appearance.imageInsets.left - configuration?.imagePadding = padding - } + configureInsets(in: &config, + contentInsets: appearance.contentLayout.contentInsets, + titleInsets: appearance.contentLayout.titleInsets, + imageInsets: appearance.contentLayout.imageInsets) + configuration = config } else { - contentEdgeInsets = appearance.contentInsets - titleEdgeInsets = appearance.titleInsets - imageEdgeInsets = appearance.imageInsets + contentEdgeInsets = appearance.contentLayout.contentInsets + titleEdgeInsets = appearance.contentLayout.titleInsets + imageEdgeInsets = appearance.contentLayout.imageInsets } super.configureUIView(appearance: appearance) } + + @available(iOS 15.0, *) + private func configureInsets(in config: inout UIButton.Configuration, + contentInsets: UIEdgeInsets, + titleInsets: UIEdgeInsets, + imageInsets: UIEdgeInsets) { + + let topContentInset = contentInsets.top + titleInsets.top + let bottomContentInset = contentInsets.bottom + titleInsets.bottom + + let fullLeadingContentInset = contentInsets.left + titleInsets.left + let fullTrailingContentInset = contentInsets.right + titleInsets.right + + let hasNonEmptyImage = [ + config.image, + currentImage + ] + .compactMap { $0 } + .first { !($0.size.width.isZero || $0.size.height.isZero) } != nil + + if hasNonEmptyImage { + let leadingContentInset: CGFloat + let trailingContentInset: CGFloat + let imagePadding: CGFloat + + switch config.imagePlacement { + case .leading: + leadingContentInset = contentInsets.left + trailingContentInset = fullTrailingContentInset + imagePadding = titleInsets.left + imageInsets.right + + case .trailing: + leadingContentInset = fullLeadingContentInset + trailingContentInset = contentInsets.right + imagePadding = titleInsets.right + imageInsets.left + + default: + leadingContentInset = fullLeadingContentInset + trailingContentInset = fullTrailingContentInset + imagePadding = .zero + } + + config.contentInsets = NSDirectionalEdgeInsets(top: topContentInset, + leading: leadingContentInset, + bottom: bottomContentInset, + trailing: trailingContentInset) + + config.imagePadding = imagePadding + } else { + config.contentInsets = NSDirectionalEdgeInsets(top: topContentInset, + leading: fullLeadingContentInset, + bottom: bottomContentInset, + trailing: fullTrailingContentInset) + } + } } diff --git a/TIUIElements/Sources/Appearance/UIVIew+AppearanceConfigurable.swift b/TIUIElements/Sources/Appearance/UIVIew+AppearanceConfigurable.swift index 7f016266..21cab868 100644 --- a/TIUIElements/Sources/Appearance/UIVIew+AppearanceConfigurable.swift +++ b/TIUIElements/Sources/Appearance/UIVIew+AppearanceConfigurable.swift @@ -23,8 +23,8 @@ import TIUIKitCore import UIKit -extension UIView { - public func configureUIView(appearance: BaseAppearance) { +public extension UIView { + func configureUIView(appearance: BaseAppearance) { backgroundColor = appearance.backgroundColor layer.masksToBounds = true layer.maskedCorners = appearance.border.roundedCorners diff --git a/TIUIElements/Sources/Helpers/Extensions/UIViewController+FixedTopOffet.swift b/TIUIElements/Sources/Helpers/Extensions/UIViewController+FixedTopOffet.swift index a370590b..bf1c3de1 100644 --- a/TIUIElements/Sources/Helpers/Extensions/UIViewController+FixedTopOffet.swift +++ b/TIUIElements/Sources/Helpers/Extensions/UIViewController+FixedTopOffet.swift @@ -11,7 +11,16 @@ public extension UIViewController { if #available(iOS 13.0, *) { statusBarHeight = window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0 } else { - statusBarHeight = UIApplication.shared.statusBarFrame.height + // prevent compile-time failure: 'shared' is unavailable in application extensions for iOS + + let sharedSelector = Selector(("shared")) + + if UIApplication.responds(to: sharedSelector), + let sharedApplication = UIApplication.perform(sharedSelector).takeRetainedValue() as? UIApplication { + statusBarHeight = sharedApplication.statusBarFrame.height + } else { // application extension, statusBarFrame is not applicable + statusBarHeight = .zero + } } let navigationBarHeight = navigationBar?.bounds.height ?? 0 diff --git a/TIUIElements/Sources/Views/Placeholder/PlaceholderFactory.swift b/TIUIElements/Sources/Views/Placeholder/PlaceholderFactory.swift index 3027b9ab..7dbfe0cd 100644 --- a/TIUIElements/Sources/Views/Placeholder/PlaceholderFactory.swift +++ b/TIUIElements/Sources/Views/Placeholder/PlaceholderFactory.swift @@ -73,7 +73,7 @@ open class PlaceholderFactory { } .withButton { buttonStyle in buttonStyle.titles = [.normal: localizationProvider.repeatButtonTitle] - buttonStyle.appearance = [.normal: Self.defaultButtonAppearance] + buttonStyle.appearance = Self.defaultButtonAppearance } } @@ -120,15 +120,17 @@ open class PlaceholderFactory { // MARK: - Private configurations private extension PlaceholderFactory { - static var defaultButtonAppearance: UIButton.DefaultAppearance { + static var defaultButtonAppearance: StatefulButton.DefaultPositionAppearance { .make { - $0.border.cornerRadius = 25 - $0.border.roundedCorners = .allCorners - $0.backgroundColor = UIColor(red: 0.892, green: 0.906, blue: 0.92, alpha: 0.5) - $0.textAttributes = .init(font: .systemFont(ofSize: 20, weight: .bold), - color: .black, - alignment: .natural, - isMultiline: false) + if let normalAppearance = $0.stateAppearances[.normal] { + normalAppearance.border.cornerRadius = 25 + normalAppearance.border.roundedCorners = .allCorners + normalAppearance.backgroundColor = UIColor(red: 0.892, green: 0.906, blue: 0.92, alpha: 0.5) + normalAppearance.textAttributes = .init(font: .systemFont(ofSize: 20, weight: .bold), + color: .black, + alignment: .natural, + isMultiline: false) + } } } diff --git a/TIUIElements/Sources/Views/Placeholder/Styles/PlaceholderButtonStyle.swift b/TIUIElements/Sources/Views/Placeholder/Styles/PlaceholderButtonStyle.swift index ea9e1fdb..fc1a0ca1 100644 --- a/TIUIElements/Sources/Views/Placeholder/Styles/PlaceholderButtonStyle.swift +++ b/TIUIElements/Sources/Views/Placeholder/Styles/PlaceholderButtonStyle.swift @@ -28,12 +28,12 @@ open class PlaceholderButtonStyle { public var titles: UIControl.StateTitles public var images: UIControl.StateImages - public var appearance: StatefulButton.StateAppearance + public var appearance: StatefulButton.DefaultPositionAppearance public var action: UIButton.Action? public init(titles: UIControl.StateTitles = [:], images: UIControl.StateImages = [:], - appearance: StatefulButton.StateAppearance = [:], + appearance: StatefulButton.DefaultPositionAppearance = .defaultAppearance, action: UIButton.Action? = nil) { self.titles = titles diff --git a/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift b/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift index 7167c1ce..982a9dbc 100644 --- a/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift +++ b/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift @@ -155,7 +155,7 @@ open class BasePlaceholderView: BaseInitializableView { button.set(titles: $0.titles) button.set(images: $0.images) - button.set(appearance: $0.appearance) + button.configureStatefulButton(appearance: $0.appearance) if let action = $0.action { button.addTarget(action.target, action: action.action, for: action.event) diff --git a/TIUIElements/Sources/Views/StatefulButton/DefaultConfigurableStatefulButton.swift b/TIUIElements/Sources/Views/StatefulButton/DefaultConfigurableStatefulButton.swift deleted file mode 100644 index dfdbc5fb..00000000 --- a/TIUIElements/Sources/Views/StatefulButton/DefaultConfigurableStatefulButton.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// Copyright (c) 2023 Touch Instinct -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the Software), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import TIUIKitCore -import UIKit - -public final class DefaultConfigurableStatefulButton: StatefulButton, ConfigurableView, AppearanceConfigurable { - - // MARK: - ConfigurableView - - public func configure(with stateViewModelMap: [State: BaseButtonViewModel]) { - for (state, viewModel) in stateViewModelMap { - setTitle(viewModel.title, for: state) - setImage(viewModel.image, for: state) - setBackgroundImage(viewModel.backgroundImage, for: state) - } - } - - // MARK: - AppearanceConfigurable - - public func configure(appearance: Appearance) { - configureUIButton(appearance: appearance) - - stateAppearance = appearance.stateAppearance - } -} - -extension DefaultConfigurableStatefulButton { - - public final class Appearance: UIButton.BaseAppearance, WrappedViewAppearance { - - public enum Defaults { - public static var stateAppearance: StateAppearance { - [ - .normal: DefaultAppearance.defaultAppearance, - .highlighted: DefaultAppearance.defaultAppearance, - .selected: DefaultAppearance.defaultAppearance, - .disabled: DefaultAppearance.defaultAppearance - ] - } - } - - public static var defaultAppearance: Self { - Self() - } - - public var stateAppearance: StateAppearance - - public init(stateAppearance: StateAppearance = Defaults.stateAppearance) { - self.stateAppearance = stateAppearance - - let defaultAppearance = stateAppearance[.normal] - - super.init(layout: defaultAppearance?.layout ?? .defaultLayout, - backgroundColor: defaultAppearance?.backgroundColor ?? .clear, - border: defaultAppearance?.border ?? .init(), - shadow: defaultAppearance?.shadow, - textAttributes: defaultAppearance?.textAttributes, - contentInsets: defaultAppearance?.contentInsets ?? .zero, - titleInsets: defaultAppearance?.titleInsets ?? .zero, - imageInsets: defaultAppearance?.imageInsets ?? .zero) - } - } -} diff --git a/TIUIElements/Sources/Views/StatefulButton/DefaultConfigurableStatefulButton/DefaultConfigurableStatefulButton.swift b/TIUIElements/Sources/Views/StatefulButton/DefaultConfigurableStatefulButton/DefaultConfigurableStatefulButton.swift new file mode 100644 index 00000000..96342b9e --- /dev/null +++ b/TIUIElements/Sources/Views/StatefulButton/DefaultConfigurableStatefulButton/DefaultConfigurableStatefulButton.swift @@ -0,0 +1,74 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIUIKitCore +import UIKit + +public final class DefaultConfigurableStatefulButton: StatefulButton, ConfigurableView, AppearanceConfigurable { + + private var appearance: Appearance = .defaultAppearance + + // MARK: - ConfigurableView + + public func configure(with viewModel: ViewModel) { + viewModel.willReuse(view: self) + + stateViewModelMap = viewModel.stateViewModelMap + + for (state, viewModel) in viewModel.stateViewModelMap { + setTitle(viewModel.title, for: state) + setImage(viewModel.image, for: state) + setBackgroundImage(viewModel.backgroundImage, for: state) + } + + apply(state: viewModel.currentState) + + configureStatefulButton(appearance: appearance) + + viewModel.didCompleteConfiguration(of: self) + } + + // MARK: - AppearanceConfigurable + + public func configure(appearance: DefaultPositionAppearance) { + self.appearance = appearance + configureStatefulButton(appearance: appearance) + } +} + +extension DefaultConfigurableStatefulButton { + public final class ViewModel: DefaultUIViewPresenter { + public var stateViewModelMap: [State: BaseButtonViewModel] + public var currentState: State { + didSet { + view?.apply(state: currentState) + } + } + + public init(stateViewModelMap: [State: BaseButtonViewModel], + currentState: State) { + + self.stateViewModelMap = stateViewModelMap + self.currentState = currentState + } + } +} diff --git a/TIUIElements/Sources/Views/StatefulButton/StatefulButton+Appearance.swift b/TIUIElements/Sources/Views/StatefulButton/StatefulButton+Appearance.swift new file mode 100644 index 00000000..d8c100f2 --- /dev/null +++ b/TIUIElements/Sources/Views/StatefulButton/StatefulButton+Appearance.swift @@ -0,0 +1,198 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIUIKitCore +import UIKit + +extension StatefulButton { + public typealias StateAppearance = UIButton.BaseAppearance + public typealias StateAppearances = [State: StateAppearance] + + open class BaseAppearance: + UIView.BaseAppearance { + + 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 var stateAppearances: StateAppearances + + public init(layout: Layout = .defaultLayout, + backgroundColor: UIColor = .clear, + border: UIViewBorder = .init(), + shadow: UIViewShadow? = nil, + stateAppearances: StateAppearances = Defaults.stateAppearances) { + + self.stateAppearances = stateAppearances + + super.init(layout: layout, + backgroundColor: backgroundColor, + border: border, + shadow: shadow) + } + + + public func set(appearanceBuilder: (StateAppearance) -> Void, for states: [State]) { + for state in states { + stateAppearances[state] = DefaultStateAppearance.make(builder: appearanceBuilder) + } + } + } + + open class BasePositionAppearance: BaseAppearance { + public var activityIndicatorPosition: ActivityIndicatorPosition + + public init(layout: Layout = .defaultLayout, + backgroundColor: UIColor = .clear, + border: UIViewBorder = .init(), + shadow: UIViewShadow? = nil, + stateAppearances: StateAppearances = Defaults.stateAppearances, + activityIndicatorPosition: ActivityIndicatorPosition = .center) { + + self.activityIndicatorPosition = activityIndicatorPosition + + super.init(layout: layout, + backgroundColor: backgroundColor, + border: border, + shadow: shadow, + stateAppearances: stateAppearances) + } + } + + public final class DefaultPositionAppearance: BasePositionAppearance, WrappedViewAppearance { + public static var defaultAppearance: Self { + Self() + } + } + + @available(iOS 15.0, *) + open class BasePlacementAppearance: + BaseAppearance { + + public var activityIndicatorPlacement: ActivityIndicatorPlacement + + public init(layout: Layout = .defaultLayout, + backgroundColor: UIColor = .clear, + border: UIViewBorder = .init(), + shadow: UIViewShadow? = nil, + stateAppearances: StateAppearances = Defaults.stateAppearances, + activityIndicatorPlacement: ActivityIndicatorPlacement = .center) { + + self.activityIndicatorPlacement = activityIndicatorPlacement + + super.init(layout: layout, + backgroundColor: backgroundColor, + border: border, + shadow: shadow, + stateAppearances: stateAppearances) + } + } + + @available(iOS 15.0, *) + public final class DefaultPlacementAppearance: BasePlacementAppearance, WrappedViewAppearance { + public static var defaultAppearance: Self { + Self() + } + } +} + +extension StatefulButton { + public func configureBaseStatefulButton(appearance: BaseAppearance) { + onStateChanged = { [weak self] in + if let stateAppearance = appearance.stateAppearances[$0] { + self?.configureUIButton(appearance: stateAppearance, for: $0) + } else if $0 != .normal, let stateAppearance = appearance.stateAppearances[.normal] { + self?.configureUIButton(appearance: stateAppearance, for: .normal) + } + } + + onStateChanged?(state) + } + + public func configureStatefulButton(appearance: BasePositionAppearance) { + configureBaseStatefulButton(appearance: appearance) + + switch appearance.activityIndicatorPosition { + case .beforeTitle: + activityIndicatorShouldCenterInView = false + + let transparentImage = UIGraphicsImageRenderer(size: activityIndicatorSize) + .image { _ in } + + super.setImage(transparentImage, for: .loading) + + case .center: + activityIndicatorShouldCenterInView = true + + super.setImage(.init(), for: .loading) + } + + if #available(iOS 15.0, *) { + var config = configuration ?? .plain() + + if case let .beforeTitle(padding) = appearance.activityIndicatorPosition { + config.imagePlacement = .leading + config.imagePadding = padding + } + + configuration = config + } else { + if case let .beforeTitle(padding) = appearance.activityIndicatorPosition { + imageEdgeInsets.right += padding + } + } + } + + @available(iOS 15.0, *) + public func configureStatefulButton(appearance: BasePlacementAppearance) { + configureBaseStatefulButton(appearance: appearance) + + var config = configuration ?? .plain() + + switch appearance.activityIndicatorPlacement { + case let .placement(imagePlacement, padding): + activityIndicatorShouldCenterInView = false + + let transparentImage = UIGraphicsImageRenderer(size: activityIndicatorSize) + .image { _ in } + + super.setImage(transparentImage, for: .loading) + + config.imagePlacement = imagePlacement + config.imagePadding = padding + case .center: + activityIndicatorShouldCenterInView = true + + super.setImage(.init(), for: .loading) + } + + configuration = config + } +} diff --git a/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift b/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift index 4a491203..1ab5eeb5 100644 --- a/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift +++ b/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift @@ -24,29 +24,53 @@ import TISwiftUtils import TIUIKitCore import UIKit +public extension UIControl.State { + // All the bits from 17 - 24 (from 1 << 16 until 1 << 23) are there for your application to use + // while 25 - 32 (from 1 << 24 until 1 << 31) are there for internal frameworks to use. + // https://developer.apple.com/documentation/uikit/uicontrolstate/uicontrolstateapplication?language=objc + // https://developer.apple.com/documentation/uikit/uicontrolstate/uicontrolstatereserved?language=objc + // https://stackoverflow.com/a/43760213 + static var loading = Self(rawValue: 1 << 16 | Self.disabled.rawValue) // includes disabled state +} + open class StatefulButton: BaseInitializableButton { public enum ActivityIndicatorPosition { + case beforeTitle(padding: CGFloat) case center - case before(view: UIView, offset: CGFloat) - case after(view: UIView, offset: CGFloat) + } - var offset: CGFloat { - switch self { - case .center: - return .zero + @available(iOS 15.0, *) + public enum ActivityIndicatorPlacement { + case placement(NSDirectionalRectEdge, padding: CGFloat) + case center + } - case let .before(_, offset), let .after(_, offset): - return offset + public typealias StateEventPropagations = [State: Bool] + + public var isLoading = false { + didSet { + if isLoading { + if activityIndicator == nil { + configureDefaultActivityIndicator() + } + + activityIndicator?.startAnimating() + } else { + activityIndicator?.stopAnimating() } + + updateAppearance() } } - public typealias Appearance = UIButton.BaseAppearance - public typealias StateAppearance = [State: Appearance] - public typealias StateEventPropagations = [State: Bool] + public var additionalHitTestMargins: UIEdgeInsets = .zero - private var activityIndicator: ActivityIndicator? { + public var onDisabledStateTapHandler: VoidClosure? + + // MARK: - Internal properties + + var activityIndicator: ActivityIndicator? { willSet { activityIndicator?.removeFromSuperview() } @@ -57,48 +81,52 @@ open class StatefulButton: BaseInitializableButton { } } - public var isLoading = false { - didSet { - isLoading - ? activityIndicator?.startAnimating() - : activityIndicator?.stopAnimating() + var activityIndicatorShouldCenterInView = false - isEnabled = !isLoading - } - } + var stateViewModelMap: [State: BaseButtonViewModel] = [:] - public var additionalHitTestMargins: UIEdgeInsets = .zero + var onStateChanged: ParameterClosure? - public var onDisabledStateTapHandler: VoidClosure? + // MARK: - Private properties private var eventPropagations: StateEventPropagations = [:] - // MARK: - Background - - var stateAppearance: StateAppearance = [:] { - didSet { - updateAppearance() - } - } - - public func set(appearance: StateAppearance) { - appearance.forEach { setAppearance($1, for: $0) } - } - - public func setAppearance(_ appearance: Appearance, for state: State) { - stateAppearance[state] = appearance - } - - public func appearance(for state: State) -> Appearance? { - stateAppearance[state] - } - public func setEventPropagation(_ eventPropagation: Bool, for state: State) { eventPropagations[state] = eventPropagation } + // MARK: - UIButton override + + open override func setImage(_ image: UIImage?, for state: UIControl.State) { + guard state != .loading else { + return + } + + super.setImage(image, for: state) + } + + open override func title(for state: UIControl.State) -> String? { + stateViewModelMap[state]?.title ?? super.title(for: state) + } + + open override func image(for state: UIControl.State) -> UIImage? { + stateViewModelMap[state]?.image ?? super.image(for: state) + } + + open override func backgroundImage(for state: UIControl.State) -> UIImage? { + stateViewModelMap[state]?.backgroundImage ?? super.backgroundImage(for: state) + } + // MARK: - UIControl override + open override var state: UIControl.State { + if isLoading { + return super.state.union(.loading) + } else { + return super.state + } + } + override open var isEnabled: Bool { didSet { updateAppearance() @@ -119,6 +147,24 @@ open class StatefulButton: BaseInitializableButton { // MARK: - UIView override + open override func layoutSubviews() { + super.layoutSubviews() + + guard let activityIndicator, isLoading else { + return + } + + if activityIndicatorShouldCenterInView { + let indicatorSize = activityIndicatorSize + + activityIndicator.frame = CGRect(origin: CGPoint(x: bounds.midX - indicatorSize.width / 2, + y: bounds.midY - indicatorSize.height / 2), + size: indicatorSize) + } else { + activityIndicator.frame = imageView?.frame ?? .zero + } + } + open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { let insetBounds = CGRect(x: bounds.minX - additionalHitTestMargins.left, y: bounds.minY - additionalHitTestMargins.top, @@ -126,16 +172,13 @@ open class StatefulButton: BaseInitializableButton { height: bounds.height + additionalHitTestMargins.bottom) return super.point(inside: point, with: event) - || insetBounds.contains(point) + || insetBounds.contains(point) } open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let pointInsideView = self.point(inside: point, with: event) - // hitTest called multiple times, but we need only one - // so the current solution is to pick a final call with nil event - - if !isEnabled && pointInsideView && event == nil { + if !isEnabled && pointInsideView, event?.allTouches?.isEmpty == false { onDisabledStateTapHandler?() } @@ -152,63 +195,44 @@ open class StatefulButton: BaseInitializableButton { // MARK: - Public - public func configure(activityIndicator: ActivityIndicator, - at position: ActivityIndicatorPosition) { - + open func configure(activityIndicator: ActivityIndicator) { self.activityIndicator = activityIndicator - - let titleInset = activityIndicator.intrinsicContentSize.width + position.offset - - switch position { - case .center: - titleEdgeInsets = .zero - - case .before: - titleEdgeInsets = UIEdgeInsets(top: .zero, - left: titleInset, - bottom: .zero, - right: .zero) - - case .after: - titleEdgeInsets = UIEdgeInsets(top: .zero, - left: .zero, - bottom: .zero, - right: titleInset) - } - activityIndicator.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate(constraints(for: activityIndicator, - at: position)) + setNeedsLayout() + } + + open func configureDefaultActivityIndicator() { + let systemIndicator: UIActivityIndicatorView + + if #available(iOS 13.0, *) { + systemIndicator = UIActivityIndicatorView(style: .medium) + } else { + systemIndicator = UIActivityIndicatorView(style: .gray) + } + + configure(activityIndicator: systemIndicator) + } + + open func apply(state: State) { + isSelected = state.isSelected + isEnabled = !state.isDisabled + isHighlighted = state.isHightlightted + isLoading = state.isLoading + } + + // MARK: - Internal + + var activityIndicatorSize: CGSize { + activityIndicator?.intrinsicContentSize ?? .zero } // MARK: - Private - private func constraints(for activityIndicator: ActivityIndicator, - at position: ActivityIndicatorPosition) -> [NSLayoutConstraint] { - switch position { - case .center: - return [ - activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor), - activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor) - ] - - case let .before(view, offset): - return [ - activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor), - activityIndicator.trailingAnchor.constraint(equalTo: view.leadingAnchor, constant: -offset) - ] - - case let .after(view, offset): - return [ - activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor), - activityIndicator.leadingAnchor.constraint(equalTo: view.trailingAnchor, constant: offset) - ] - } - } - private func updateAppearance() { - if isEnabled { + if isLoading { + updateAppearance(to: .loading) + } else if isEnabled { if isHighlighted { updateAppearance(to: .highlighted) } else { @@ -220,10 +244,24 @@ open class StatefulButton: BaseInitializableButton { } private func updateAppearance(to state: State) { - if let appearance = stateAppearance[state] { - configureUIButton(appearance: appearance) - } else if state != .normal, let appearance = stateAppearance[.normal] { - configureUIButton(appearance: appearance) - } + onStateChanged?(state) + } +} + +private extension UIControl.State { + var isHightlightted: Bool { + contains([.highlighted]) + } + + var isSelected: Bool { + contains([.selected]) + } + + var isDisabled: Bool { + contains([.disabled]) + } + + var isLoading: Bool { + contains([.loading]) } } diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Placeholder.xcplaygroundpage/Contents.swift b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Placeholder.xcplaygroundpage/Contents.swift index 1e2985d9..46138048 100644 --- a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Placeholder.xcplaygroundpage/Contents.swift +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Placeholder.xcplaygroundpage/Contents.swift @@ -56,12 +56,12 @@ let customStyle = DefaultPlaceholderStyle( }, buttonsStyles: [ .init(titles: [.normal: "Reload"], - appearance: [ + appearance: .init(stateAppearances: [ .normal: UIButton.DefaultAppearance(textAttributes: .init(font: .systemFont(ofSize: 20), color: .black, alignment: .natural, isMultiline: false)) - ]) + ])) ]) let placeholderView = factory.createImageStylePlaceholder(customStyle) @@ -103,34 +103,40 @@ let styleWithAppearance = styleFromMake.updateAppearance { placeholder in let styleWithButton = styleWithAppearance.withButton { buttonStyle in buttonStyle.titles = [.normal: "Reload"] - buttonStyle.appearance = [.normal: UIButton.DefaultAppearance.make { button in - button.textAttributes = .init(font: .systemFont(ofSize: 20), - color: .black, - alignment: .natural, - isMultiline: false) - }] + buttonStyle.appearance = .make { + if let normalAppearance = $0.stateAppearances[.normal] { + normalAppearance.textAttributes = .init(font: .systemFont(ofSize: 20), + color: .black, + alignment: .natural, + isMultiline: false) + } + } } //: Стоит обратить внимание на то, что если бы метод использовался на `styleWithButton`, то у уже добавленной кнопки (она была бы с индексом 0) сохранился стиль, так что его можно было либо дополнить, либо переопределить let styleWithTwoButtons = styleWithAppearance.withButtons(2, axis: .vertical) { index, buttonStyle in if index == .zero { buttonStyle.titles = [.normal: "Reload"] - buttonStyle.appearance = [.normal: UIButton.DefaultAppearance.make { button in - button.textAttributes = .init(font: .systemFont(ofSize: 20), - color: .black, - alignment: .natural, - isMultiline: false) - }] + buttonStyle.appearance = .make { + if let normalAppearance = $0.stateAppearances[.normal] { + normalAppearance.textAttributes = .init(font: .systemFont(ofSize: 20), + color: .black, + alignment: .natural, + isMultiline: false) + } + } } if index == 1 { buttonStyle.titles = [.normal: "Wait"] - buttonStyle.appearance = [.normal: UIButton.DefaultAppearance.make { button in - button.textAttributes = .init(font: .systemFont(ofSize: 20), - color: .black, - alignment: .natural, - isMultiline: false) - }] + buttonStyle.appearance = .make { + if let normalAppearance = $0.stateAppearances[.normal] { + normalAppearance.textAttributes = .init(font: .systemFont(ofSize: 20), + color: .black, + alignment: .natural, + isMultiline: false) + } + } } } diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index f401396b..103c020f 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Bunch of useful protocols and views.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index 64ce3b1e..098ae299 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIWebView/TIWebView.podspec b/TIWebView/TIWebView.podspec index 6b7eaefb..77cfa44a 100644 --- a/TIWebView/TIWebView.podspec +++ b/TIWebView/TIWebView.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIWebView' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Universal web view API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIYandexMapUtils/Sources/YandexMapManager.swift b/TIYandexMapUtils/Sources/YandexMapManager.swift index f9ae44e0..de3d6d3d 100644 --- a/TIYandexMapUtils/Sources/YandexMapManager.swift +++ b/TIYandexMapUtils/Sources/YandexMapManager.swift @@ -60,25 +60,15 @@ open class YandexMapManager: BaseMapManager, + clusterIconFactory: DefaultClusterMarkerIconFactory? = nil, selectPlacemarkHandler: @escaping SelectPlacemarkHandler) where DataModel: MapLocatable, DataModel.Position == YMKPoint { self.init(map: map, positionGetter: { $0.position }, - defaultMarkerIcon: defaultMarkerIcon, + iconFactory: iconFactory, + clusterIconFactory: clusterIconFactory, selectPlacemarkHandler: selectPlacemarkHandler) } diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec index f77de80f..0e3d3db0 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIYandexMapUtils' - s.version = '1.49.0' + s.version = '1.50.0' s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/docs/ticoregraphicsutils/drawingoperations.md b/docs/ticoregraphicsutils/drawingoperations.md new file mode 100644 index 00000000..936596c2 --- /dev/null +++ b/docs/ticoregraphicsutils/drawingoperations.md @@ -0,0 +1,127 @@ + +# `DrawingOperation` - протокол для инкапсулирования низкоуровневых вызовов отрисовки CoreGraphicss. + +Позволяет: + +- сгруппировать низкоуровневые операции CoreGraphics в более высокоуровневые +- использовать композицию из высокоуровневых операций +- получить размер изображения необходимый для создания контекста + +## Базовые операции + + "Из коробки" доступны самые часто испольуемые операции. + +### `SolidFillDrawingOperation` + + Операция для заполнения области рисования выбранным цветом и выбранной формой. + Например, можно нарисовать прямоугольник или круг на существующей области рисования + или просто создать изображение с однотонный фоном. + +```swift +import TICoreGraphicsUtils +import UIKit + +let solidFillSize = CGSize(width: 200, height: 200) + +let renderer = UIGraphicsImageRenderer(size: solidFillSize) +let solidFillDrawingOperation = SolidFillDrawingOperation(color: UIColor.purple.cgColor, + rect: CGRect(origin: .zero, + size: solidFillSize)) + +let solidFillImage = renderer.image { + solidFillDrawingOperation.apply(in: $0.cgContext) +} +``` + +### `BorderDrawingOperation` + + Операция создаёт рамку определённой формы и размера. + +```swift +let borderDrawingOperation = BorderDrawingOperation(frameableContentRect: CGRect(origin: .zero, + size: solidFillSize), + border: 2, + color: UIColor.red.cgColor, + radius: 4, + exteriorBorder: false) + +let borderImage = renderer.image { + borderDrawingOperation.apply(in: $0.cgContext) +} +``` + +### `CALayerDrawingOperation` + + Операция отрисовывает содержимое CALayer в изображение. Обычно используется для снапшота UIView. + +```swift +let button = UIButton(type: .custom) +button.setTitle("This is button", for: .normal) +button.setBackgroundImage(borderImage, for: .normal) + +button.sizeToFit() + +let layerDrawingOperation = CALayerDrawingOperation(layer: button.layer, offset: .zero) + +let layerRenderer = UIGraphicsImageRenderer(size: button.bounds.size) + +let buttonSnapshotImage = layerRenderer.image { + layerDrawingOperation.apply(in: $0.cgContext) +} +``` + +### `TextDrawingOperation` + + Операция отрисовывает текст с заданными атрибутами + +```swift +let textDrawingOperaton = TextDrawingOperation(text: "This is string", + textAttributes: [ + .font: UIFont.boldSystemFont(ofSize: 15), + .foregroundColor: UIColor.white + ]) + +let textRenderer = UIGraphicsImageRenderer(size: textDrawingOperaton.desiredContextSize) + +let textImage = textRenderer.image { + textDrawingOperaton.apply(in: $0.cgContext) +} +``` + +### `TemplateDrawingOperation` + + Операция заменяет все цвета кроме прозрачного на переданный цвет. + Используется для приданиям иконкам определённого цвета (аналог tintColor). + +```swift +if let cgImage = textImage.cgImage { + let templateDrawingOperation = TemplateDrawingOperation(image: cgImage, + imageSize: textImage.size, + color: UIColor.red.cgColor) + + let tintedImage = textRenderer.image { + templateDrawingOperation.apply(in: $0.cgContext) + } +} +``` + +### `TransformDrawingOperation` + + Операция производит изменение размера изображения учитывая пропорции + и другие настройки (по аналогии с contentMode у UIImage). + +```swift +if let cgImage = textImage.cgImage { + let newSize = CGSize(width: textImage.size.width / 1.5, height: textImage.size.height) + let resizeOperation = TransformDrawingOperation(image: cgImage, + imageSize: textImage.size, + maxNewSize: newSize, + resizeMode: .scaleAspectFill) + + let resizeRenderer = UIGraphicsImageRenderer(size: newSize) + + let resizedImage = resizeRenderer.image { + resizeOperation.apply(in: $0.cgContext) + } +} +``` diff --git a/docs/tiuielements/placeholder.md b/docs/tiuielements/placeholder.md index aa2b78bb..293fb70e 100644 --- a/docs/tiuielements/placeholder.md +++ b/docs/tiuielements/placeholder.md @@ -59,12 +59,12 @@ let customStyle = DefaultPlaceholderStyle( }, buttonsStyles: [ .init(titles: [.normal: "Reload"], - appearance: [ + appearance: .init(stateAppearances: [ .normal: UIButton.DefaultAppearance(textAttributes: .init(font: .systemFont(ofSize: 20), color: .black, alignment: .natural, isMultiline: false)) - ]) + ])) ]) let placeholderView = factory.createImageStylePlaceholder(customStyle) @@ -107,12 +107,14 @@ let styleWithAppearance = styleFromMake.updateAppearance { placeholder in let styleWithButton = styleWithAppearance.withButton { buttonStyle in buttonStyle.titles = [.normal: "Reload"] - buttonStyle.appearance = [.normal: UIButton.DefaultAppearance.make { button in - button.textAttributes = .init(font: .systemFont(ofSize: 20), - color: .black, - alignment: .natural, - isMultiline: false) - }] + buttonStyle.appearance = .make { + if let normalAppearance = $0.stateAppearances[.normal] { + normalAppearance.textAttributes = .init(font: .systemFont(ofSize: 20), + color: .black, + alignment: .natural, + isMultiline: false) + } + } } ``` @@ -122,22 +124,26 @@ let styleWithButton = styleWithAppearance.withButton { buttonStyle in let styleWithTwoButtons = styleWithAppearance.withButtons(2, axis: .vertical) { index, buttonStyle in if index == .zero { buttonStyle.titles = [.normal: "Reload"] - buttonStyle.appearance = [.normal: UIButton.DefaultAppearance.make { button in - button.textAttributes = .init(font: .systemFont(ofSize: 20), - color: .black, - alignment: .natural, - isMultiline: false) - }] + buttonStyle.appearance = .make { + if let normalAppearance = $0.stateAppearances[.normal] { + normalAppearance.textAttributes = .init(font: .systemFont(ofSize: 20), + color: .black, + alignment: .natural, + isMultiline: false) + } + } } if index == 1 { buttonStyle.titles = [.normal: "Wait"] - buttonStyle.appearance = [.normal: UIButton.DefaultAppearance.make { button in - button.textAttributes = .init(font: .systemFont(ofSize: 20), - color: .black, - alignment: .natural, - isMultiline: false) - }] + buttonStyle.appearance = .make { + if let normalAppearance = $0.stateAppearances[.normal] { + normalAppearance.textAttributes = .init(font: .systemFont(ofSize: 20), + color: .black, + alignment: .natural, + isMultiline: false) + } + } } } ``` diff --git a/project-scripts/ordered_modules_list.txt b/project-scripts/ordered_modules_list.txt index 49d3f59c..48d9aa9b 100644 --- a/project-scripts/ordered_modules_list.txt +++ b/project-scripts/ordered_modules_list.txt @@ -2,6 +2,7 @@ TILogging TISwiftUtils TIPagination TIFoundationUtils +TICoreGraphicsUtils TIKeychainUtils TIUIKitCore TISwiftUICore