From 10181a91eb012db72cd54894cdbc713cd4bd5741 Mon Sep 17 00:00:00 2001 From: Almaz Ibragimov Date: Thu, 6 Aug 2026 00:40:42 +0300 Subject: [PATCH 1/8] Caches improvements --- .../Fallback/Cache/FallbackComponentCache.swift | 11 +++++++++-- Sources/Text/Cache/TextCache.swift | 9 +++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Sources/Component/Fallback/Cache/FallbackComponentCache.swift b/Sources/Component/Fallback/Cache/FallbackComponentCache.swift index 9db90cc6..823ed46e 100644 --- a/Sources/Component/Fallback/Cache/FallbackComponentCache.swift +++ b/Sources/Component/Fallback/Cache/FallbackComponentCache.swift @@ -8,9 +8,12 @@ public final class FallbackComponentCache { public init() { } - internal func resetSize(for content: Content) { - sizes.removeValue(forKey: FallbackComponentCacheKey(content: content)) + public func reset() { + sizes.removeAll(keepingCapacity: true) } +} + +extension FallbackComponentCache { internal func restoreSize( for content: Content, @@ -65,6 +68,10 @@ public final class FallbackComponentCache { self.sizes[key] = sizes } + + internal func resetSize(for content: Content) { + sizes.removeValue(forKey: FallbackComponentCacheKey(content: content)) + } } extension FallbackComponentCache { diff --git a/Sources/Text/Cache/TextCache.swift b/Sources/Text/Cache/TextCache.swift index 93ae240c..9f5e23b4 100644 --- a/Sources/Text/Cache/TextCache.swift +++ b/Sources/Text/Cache/TextCache.swift @@ -16,6 +16,15 @@ public final class TextCache: @unchecked Sendable { ) } + public func reset() { + layoutsQueue.async(flags: .barrier) { + self.layouts.removeAll(keepingCapacity: true) + } + } +} + +extension TextCache { + internal func restoreLayout(for key: TextCacheKey) -> TextLayout? { layoutsQueue.sync { layouts[key] From 77b22603c98820d77f70b3985419371127adf047 Mon Sep 17 00:00:00 2001 From: Almaz Ibragimov Date: Thu, 6 Aug 2026 13:11:06 +0300 Subject: [PATCH 2/8] EnvironmentValues improvements --- Levitan.xcodeproj/project.pbxproj | 24 +++++++------------ ...ronmentValues+ComponentContainerSize.swift | 20 +++++++++++++--- ...ft => EnvironmentValues+ComponentID.swift} | 18 ++++++++++++++ ...ntValues+ComponentLayoutInvalidation.swift | 17 +++++++++++++ ...onmentValues+ComponentViewController.swift | 23 ++++++++++++++++++ ...ronmentValues+FallbackComponentCache.swift | 16 +++++++++++++ .../EnvironmentValues+TextAnimation.swift | 7 ++++++ .../EnvironmentValues+TextCache.swift | 7 ++++++ .../EnvironmentValues+TextDecoration.swift | 7 ++++++ .../EnvironmentValues+TextTypography.swift | 7 ++++++ 10 files changed, 127 insertions(+), 19 deletions(-) rename Sources/Component/Environment/{EnvironmentValues+ComponentIdentifier.swift => EnvironmentValues+ComponentID.swift} (50%) diff --git a/Levitan.xcodeproj/project.pbxproj b/Levitan.xcodeproj/project.pbxproj index 15cabe1d..06a19e23 100755 --- a/Levitan.xcodeproj/project.pbxproj +++ b/Levitan.xcodeproj/project.pbxproj @@ -34,7 +34,7 @@ C00508A52F2139F6006016DE /* ComponentPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508702F2139F6006016DE /* ComponentPadding.swift */; }; C00508A62F2139F6006016DE /* ComponentHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005086C2F2139F6006016DE /* ComponentHostingController.swift */; }; C00508A72F2139F6006016DE /* AnyComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005084E2F2139F6006016DE /* AnyComponentView.swift */; }; - C00508A82F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005085E2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift */; }; + C00508A82F2139F6006016DE /* EnvironmentValues+ComponentID.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005085E2F2139F6006016DE /* EnvironmentValues+ComponentID.swift */; }; C00508A92F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508812F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift */; }; C00508AA2F2139F6006016DE /* AnyComponentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005084D2F2139F6006016DE /* AnyComponentPresenter.swift */; }; C00508AB2F2139F6006016DE /* ShadowModifier+ComponentTokenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508822F2139F6006016DE /* ShadowModifier+ComponentTokenModifier.swift */; }; @@ -84,7 +84,7 @@ C00508D72F2139F6006016DE /* ComponentPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508702F2139F6006016DE /* ComponentPadding.swift */; }; C00508D82F2139F6006016DE /* ComponentHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005086C2F2139F6006016DE /* ComponentHostingController.swift */; }; C00508D92F2139F6006016DE /* AnyComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005084E2F2139F6006016DE /* AnyComponentView.swift */; }; - C00508DA2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005085E2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift */; }; + C00508DA2F2139F6006016DE /* EnvironmentValues+ComponentID.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005085E2F2139F6006016DE /* EnvironmentValues+ComponentID.swift */; }; C00508DB2F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508812F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift */; }; C00508DC2F2139F6006016DE /* AnyComponentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005084D2F2139F6006016DE /* AnyComponentPresenter.swift */; }; C00508DD2F2139F6006016DE /* ShadowModifier+ComponentTokenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508822F2139F6006016DE /* ShadowModifier+ComponentTokenModifier.swift */; }; @@ -134,7 +134,7 @@ C00509092F2139F6006016DE /* ComponentPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508702F2139F6006016DE /* ComponentPadding.swift */; }; C005090A2F2139F6006016DE /* ComponentHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005086C2F2139F6006016DE /* ComponentHostingController.swift */; }; C005090B2F2139F6006016DE /* AnyComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005084E2F2139F6006016DE /* AnyComponentView.swift */; }; - C005090C2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005085E2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift */; }; + C005090C2F2139F6006016DE /* EnvironmentValues+ComponentID.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005085E2F2139F6006016DE /* EnvironmentValues+ComponentID.swift */; }; C005090D2F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508812F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift */; }; C005090E2F2139F6006016DE /* AnyComponentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005084D2F2139F6006016DE /* AnyComponentPresenter.swift */; }; C005090F2F2139F6006016DE /* ShadowModifier+ComponentTokenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508822F2139F6006016DE /* ShadowModifier+ComponentTokenModifier.swift */; }; @@ -1127,7 +1127,7 @@ C005085B2F2139F6006016DE /* EnvironmentValues+ComponentAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+ComponentAppearance.swift"; sourceTree = ""; }; C005085C2F2139F6006016DE /* EnvironmentValues+ComponentContainerSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+ComponentContainerSize.swift"; sourceTree = ""; }; C005085D2F2139F6006016DE /* EnvironmentValues+ComponentContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+ComponentContext.swift"; sourceTree = ""; }; - C005085E2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+ComponentIdentifier.swift"; sourceTree = ""; }; + C005085E2F2139F6006016DE /* EnvironmentValues+ComponentID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+ComponentID.swift"; sourceTree = ""; }; C005085F2F2139F6006016DE /* EnvironmentValues+ComponentLayoutInvalidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+ComponentLayoutInvalidation.swift"; sourceTree = ""; }; C00508602F2139F6006016DE /* EnvironmentValues+ComponentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+ComponentViewController.swift"; sourceTree = ""; }; C00508612F2139F6006016DE /* EnvironmentValues+FallbackComponentCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+FallbackComponentCache.swift"; sourceTree = ""; }; @@ -1581,7 +1581,7 @@ C005085B2F2139F6006016DE /* EnvironmentValues+ComponentAppearance.swift */, C005085C2F2139F6006016DE /* EnvironmentValues+ComponentContainerSize.swift */, C005085D2F2139F6006016DE /* EnvironmentValues+ComponentContext.swift */, - C005085E2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift */, + C005085E2F2139F6006016DE /* EnvironmentValues+ComponentID.swift */, C005085F2F2139F6006016DE /* EnvironmentValues+ComponentLayoutInvalidation.swift */, C00508602F2139F6006016DE /* EnvironmentValues+ComponentViewController.swift */, C00508612F2139F6006016DE /* EnvironmentValues+FallbackComponentCache.swift */, @@ -1589,13 +1589,6 @@ path = Environment; sourceTree = ""; }; - C00508632F2139F6006016DE /* Body */ = { - isa = PBXGroup; - children = ( - ); - path = Body; - sourceTree = ""; - }; C00508672F2139F6006016DE /* Cache */ = { isa = PBXGroup; children = ( @@ -1609,7 +1602,6 @@ C005086B2F2139F6006016DE /* Fallback */ = { isa = PBXGroup; children = ( - C00508632F2139F6006016DE /* Body */, C00508672F2139F6006016DE /* Cache */, C00508682F2139F6006016DE /* FallbackComponent.swift */, C00508692F2139F6006016DE /* FallbackComponentBody.swift */, @@ -3301,7 +3293,7 @@ C00508D72F2139F6006016DE /* ComponentPadding.swift in Sources */, C00508D82F2139F6006016DE /* ComponentHostingController.swift in Sources */, C00508D92F2139F6006016DE /* AnyComponentView.swift in Sources */, - C00508DA2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift in Sources */, + C00508DA2F2139F6006016DE /* EnvironmentValues+ComponentID.swift in Sources */, C00508DB2F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */, C00508DC2F2139F6006016DE /* AnyComponentPresenter.swift in Sources */, C00508DD2F2139F6006016DE /* ShadowModifier+ComponentTokenModifier.swift in Sources */, @@ -3663,7 +3655,7 @@ C00508A52F2139F6006016DE /* ComponentPadding.swift in Sources */, C00508A62F2139F6006016DE /* ComponentHostingController.swift in Sources */, C00508A72F2139F6006016DE /* AnyComponentView.swift in Sources */, - C00508A82F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift in Sources */, + C00508A82F2139F6006016DE /* EnvironmentValues+ComponentID.swift in Sources */, C00508A92F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */, C00508AA2F2139F6006016DE /* AnyComponentPresenter.swift in Sources */, C00508AB2F2139F6006016DE /* ShadowModifier+ComponentTokenModifier.swift in Sources */, @@ -4162,7 +4154,7 @@ C00509092F2139F6006016DE /* ComponentPadding.swift in Sources */, C005090A2F2139F6006016DE /* ComponentHostingController.swift in Sources */, C005090B2F2139F6006016DE /* AnyComponentView.swift in Sources */, - C005090C2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift in Sources */, + C005090C2F2139F6006016DE /* EnvironmentValues+ComponentID.swift in Sources */, C005090D2F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */, C005090E2F2139F6006016DE /* AnyComponentPresenter.swift in Sources */, C005090F2F2139F6006016DE /* ShadowModifier+ComponentTokenModifier.swift in Sources */, diff --git a/Sources/Component/Environment/EnvironmentValues+ComponentContainerSize.swift b/Sources/Component/Environment/EnvironmentValues+ComponentContainerSize.swift index 157b0fb0..f2469777 100644 --- a/Sources/Component/Environment/EnvironmentValues+ComponentContainerSize.swift +++ b/Sources/Component/Environment/EnvironmentValues+ComponentContainerSize.swift @@ -13,9 +13,6 @@ extension EnvironmentValues { /// Может использоваться в качестве максимальных размеров для определения размеров компонента /// в методе `sizing(fitting:context:)`. /// - /// Значением по умолчанию является размеры ближайшего экземпляра `UIViewController` в окружении, - /// либо размеры основного экрана устройства. - /// /// - Note: Нет необходимости самостоятельно устанавливать значение для этой переменной, /// его переопределяют встроенные компоненты. public var componentContainerSize: CGSize { @@ -23,4 +20,21 @@ extension EnvironmentValues { set { self[ComponentContainerSizeKey.self] = newValue } } } + +extension View { + + /// Устанавливает размеры ближайшего известного контейнера. + /// + /// Может использоваться в качестве максимальных размеров для определения размеров компонента + /// в методе `sizing(fitting:context:)`. + /// + /// - Note: Нет необходимости самостоятельно устанавливать значение для этой переменной, + /// его переопределяют встроенные компоненты. + /// + /// - Parameter size: Размеры ближайшего известного контейнера. + /// - Returns: Модифицированный экземпляр UI-представления. + public nonisolated func componentContainerSize(_ size: CGSize) -> some View { + environment(\.componentContainerSize, size) + } +} #endif diff --git a/Sources/Component/Environment/EnvironmentValues+ComponentIdentifier.swift b/Sources/Component/Environment/EnvironmentValues+ComponentID.swift similarity index 50% rename from Sources/Component/Environment/EnvironmentValues+ComponentIdentifier.swift rename to Sources/Component/Environment/EnvironmentValues+ComponentID.swift index 0f0ec9e8..d6e55109 100644 --- a/Sources/Component/Environment/EnvironmentValues+ComponentIdentifier.swift +++ b/Sources/Component/Environment/EnvironmentValues+ComponentID.swift @@ -21,4 +21,22 @@ extension EnvironmentValues { set { self[ComponentIDEnvironmentKey.self] = newValue } } } + +extension View { + + /// Устанавливает идентификатор для компонента. + /// + /// Используется в качестве идентификатора для встраивания SwiftUI-компонентов, + /// чтобы при переиспользовании родительского контейнера (например, reusable-ячейки) + /// компонент имел свое уникальное внешнее SwiftUI-хранилище данных. + /// + /// - Note: Является техническим значением окружения и не должно использоваться в обычных компонентах, + /// его переопределяют встроенные компоненты. + /// + /// - Parameter id: Идентификатор для компонента. + /// - Returns: Модифицированный экземпляр UI-представления. + public nonisolated func componentID(_ id: ComponentID) -> some View { + environment(\.componentID, id) + } +} #endif diff --git a/Sources/Component/Environment/EnvironmentValues+ComponentLayoutInvalidation.swift b/Sources/Component/Environment/EnvironmentValues+ComponentLayoutInvalidation.swift index 7bd091e8..fb065af9 100644 --- a/Sources/Component/Environment/EnvironmentValues+ComponentLayoutInvalidation.swift +++ b/Sources/Component/Environment/EnvironmentValues+ComponentLayoutInvalidation.swift @@ -62,4 +62,21 @@ extension ComponentContext { } } } + +extension View { + + /// Добавляет дополнительное действие для инвалидации лэйаута. + /// + /// Дополнительное действие будет выполнено до выполнения уже имеющихся действий инвалидации. + /// + /// - Parameter invalidation: Дополнительное действие для инвалидации лэйаута. + /// - Returns: Модифицированный экземпляр UI-представления. + public nonisolated func componentLayoutInvalidation( + _ invalidation: @escaping @MainActor () -> Void + ) -> some View { + transformEnvironment(\.componentLayoutInvalidation) { componentLayoutInvalidation in + componentLayoutInvalidation.prepend(invalidation) + } + } +} #endif diff --git a/Sources/Component/Environment/EnvironmentValues+ComponentViewController.swift b/Sources/Component/Environment/EnvironmentValues+ComponentViewController.swift index 4a84f0e8..41238cdc 100644 --- a/Sources/Component/Environment/EnvironmentValues+ComponentViewController.swift +++ b/Sources/Component/Environment/EnvironmentValues+ComponentViewController.swift @@ -83,4 +83,27 @@ extension ComponentContext { ) } } + +extension View { + + /// Устанавливает ближайший экземпляр `UIViewController` в окружении. + /// + /// Используется для встраивания SwiftUI-компонентов в UIKit-представление через `UIHostingController`. + /// Поэтому рекомендуется переопределять этот параметр в каждом экземпляре `UIViewController`, + /// иначе встраивание SwiftUI-представлений в UIKit может сработать некорректно. + /// + /// Если ближайший экземпляр `UIViewController` не определен, + /// то система попытается самостоятельно найти его по цепочке `UIResponder`. + /// + /// - Parameter viewController: Ближайший экземпляр `UIViewController`. + /// - Returns: Модифицированный экземпляр UI-представления. + public func componentViewController(_ viewController: UIViewController?) -> some View { + environment( + \.componentViewControllerStorage, + ComponentViewControllerStorage { [weak viewController] in + viewController + } + ) + } +} #endif diff --git a/Sources/Component/Environment/EnvironmentValues+FallbackComponentCache.swift b/Sources/Component/Environment/EnvironmentValues+FallbackComponentCache.swift index 37c87c12..cfe1ea52 100644 --- a/Sources/Component/Environment/EnvironmentValues+FallbackComponentCache.swift +++ b/Sources/Component/Environment/EnvironmentValues+FallbackComponentCache.swift @@ -10,9 +10,25 @@ internal struct FallbackComponentCacheEnvironmentKey: EnvironmentKey { extension EnvironmentValues { + /// Кэш для UIKit-компонентов. + /// + /// Используется для кэширования размеров UIKit-компонентов для использования внутри SwiftUI-представлений. public var fallbackComponentCache: FallbackComponentCache? { get { self[FallbackComponentCacheEnvironmentKey.self] } set { self[FallbackComponentCacheEnvironmentKey.self] = newValue } } } + +extension View { + + /// Устанавливает кэш для UIKit-компонентов. + /// + /// Используется для кэширования размеров UIKit-компонентов для использования внутри SwiftUI-представлений. + /// + /// - Parameter cache: Кэш для UIKit-компонентов. + /// - Returns: Модифицированный экземпляр UI-представления. + public nonisolated func fallbackComponentCache(_ cache: FallbackComponentCache?) -> some View { + environment(\.fallbackComponentCache, cache) + } +} #endif diff --git a/Sources/Text/Environment/EnvironmentValues+TextAnimation.swift b/Sources/Text/Environment/EnvironmentValues+TextAnimation.swift index 2cd23f4b..0e0e0008 100644 --- a/Sources/Text/Environment/EnvironmentValues+TextAnimation.swift +++ b/Sources/Text/Environment/EnvironmentValues+TextAnimation.swift @@ -13,4 +13,11 @@ extension EnvironmentValues { set { self[TextAnimationKey.self] = newValue } } } + +extension View { + + public nonisolated func textAnimation(_ animation: TextAnimation) -> some View { + environment(\.textAnimation, animation) + } +} #endif diff --git a/Sources/Text/Environment/EnvironmentValues+TextCache.swift b/Sources/Text/Environment/EnvironmentValues+TextCache.swift index b4b5e597..746d0d52 100644 --- a/Sources/Text/Environment/EnvironmentValues+TextCache.swift +++ b/Sources/Text/Environment/EnvironmentValues+TextCache.swift @@ -15,4 +15,11 @@ extension EnvironmentValues { set { self[TextCacheEnvironmentKey.self] = newValue } } } + +extension View { + + public nonisolated func textCache(_ cache: TextCache?) -> some View { + environment(\.textCache, cache) + } +} #endif diff --git a/Sources/Text/Environment/EnvironmentValues+TextDecoration.swift b/Sources/Text/Environment/EnvironmentValues+TextDecoration.swift index 4c9fa9a7..8d036662 100644 --- a/Sources/Text/Environment/EnvironmentValues+TextDecoration.swift +++ b/Sources/Text/Environment/EnvironmentValues+TextDecoration.swift @@ -13,4 +13,11 @@ extension EnvironmentValues { set { self[TextDecorationKey.self] = newValue } } } + +extension View { + + public nonisolated func textDecoration(_ decoration: [AnyTextDecorator]) -> some View { + environment(\.textDecoration, decoration) + } +} #endif diff --git a/Sources/Text/Environment/EnvironmentValues+TextTypography.swift b/Sources/Text/Environment/EnvironmentValues+TextTypography.swift index 2a195966..e21257ae 100644 --- a/Sources/Text/Environment/EnvironmentValues+TextTypography.swift +++ b/Sources/Text/Environment/EnvironmentValues+TextTypography.swift @@ -13,4 +13,11 @@ extension EnvironmentValues { set { self[TextTypographyKey.self] = newValue } } } + +extension View { + + public nonisolated func textTypography(_ typography: TypographyToken) -> some View { + environment(\.textTypography, typography) + } +} #endif From df7164234feb49e772d839e023c27d1600d670e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BB=D1=8C=D1=86=D0=B5=D0=B2=20=D0=92=D0=BB?= =?UTF-8?q?=D0=B0=D0=B4?= Date: Thu, 6 Aug 2026 14:30:28 +0300 Subject: [PATCH 3/8] Added eager component mounting policy --- Levitan.xcodeproj/project.pbxproj | 16 +++++++ ...onmentValues+ComponentMountingPolicy.swift | 22 ++++++++++ .../Hosting/ComponentHostingView.swift | 44 ++++++++++++++++++- .../Hosting/ComponentMountingPolicy.swift | 29 ++++++++++++ 4 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 Sources/Component/Environment/EnvironmentValues+ComponentMountingPolicy.swift create mode 100644 Sources/Component/Hosting/ComponentMountingPolicy.swift diff --git a/Levitan.xcodeproj/project.pbxproj b/Levitan.xcodeproj/project.pbxproj index 15cabe1d..f768070d 100755 --- a/Levitan.xcodeproj/project.pbxproj +++ b/Levitan.xcodeproj/project.pbxproj @@ -33,6 +33,8 @@ C00508A42F2139F6006016DE /* FallbackComponentCacheKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508652F2139F6006016DE /* FallbackComponentCacheKey.swift */; }; C00508A52F2139F6006016DE /* ComponentPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508702F2139F6006016DE /* ComponentPadding.swift */; }; C00508A62F2139F6006016DE /* ComponentHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005086C2F2139F6006016DE /* ComponentHostingController.swift */; }; + AB0000022F2139F6006016DE /* ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000012F2139F6006016DE /* ComponentMountingPolicy.swift */; }; + AB0000062F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000052F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift */; }; C00508A72F2139F6006016DE /* AnyComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005084E2F2139F6006016DE /* AnyComponentView.swift */; }; C00508A82F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005085E2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift */; }; C00508A92F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508812F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift */; }; @@ -83,6 +85,8 @@ C00508D62F2139F6006016DE /* FallbackComponentCacheKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508652F2139F6006016DE /* FallbackComponentCacheKey.swift */; }; C00508D72F2139F6006016DE /* ComponentPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508702F2139F6006016DE /* ComponentPadding.swift */; }; C00508D82F2139F6006016DE /* ComponentHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005086C2F2139F6006016DE /* ComponentHostingController.swift */; }; + AB0000032F2139F6006016DE /* ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000012F2139F6006016DE /* ComponentMountingPolicy.swift */; }; + AB0000072F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000052F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift */; }; C00508D92F2139F6006016DE /* AnyComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005084E2F2139F6006016DE /* AnyComponentView.swift */; }; C00508DA2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005085E2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift */; }; C00508DB2F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508812F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift */; }; @@ -133,6 +137,8 @@ C00509082F2139F6006016DE /* FallbackComponentCacheKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508652F2139F6006016DE /* FallbackComponentCacheKey.swift */; }; C00509092F2139F6006016DE /* ComponentPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508702F2139F6006016DE /* ComponentPadding.swift */; }; C005090A2F2139F6006016DE /* ComponentHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005086C2F2139F6006016DE /* ComponentHostingController.swift */; }; + AB0000042F2139F6006016DE /* ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000012F2139F6006016DE /* ComponentMountingPolicy.swift */; }; + AB0000082F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000052F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift */; }; C005090B2F2139F6006016DE /* AnyComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005084E2F2139F6006016DE /* AnyComponentView.swift */; }; C005090C2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005085E2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift */; }; C005090D2F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508812F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift */; }; @@ -1138,6 +1144,8 @@ C00508692F2139F6006016DE /* FallbackComponentBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FallbackComponentBody.swift; sourceTree = ""; }; C005086A2F2139F6006016DE /* FallbackComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FallbackComponentView.swift; sourceTree = ""; }; C005086C2F2139F6006016DE /* ComponentHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentHostingController.swift; sourceTree = ""; }; + AB0000012F2139F6006016DE /* ComponentMountingPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentMountingPolicy.swift; sourceTree = ""; }; + AB0000052F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+ComponentMountingPolicy.swift"; sourceTree = ""; }; C005086D2F2139F6006016DE /* ComponentHostingRoot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentHostingRoot.swift; sourceTree = ""; }; C005086E2F2139F6006016DE /* ComponentHostingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentHostingView.swift; sourceTree = ""; }; C00508702F2139F6006016DE /* ComponentPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentPadding.swift; sourceTree = ""; }; @@ -1584,6 +1592,7 @@ C005085E2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift */, C005085F2F2139F6006016DE /* EnvironmentValues+ComponentLayoutInvalidation.swift */, C00508602F2139F6006016DE /* EnvironmentValues+ComponentViewController.swift */, + AB0000052F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift */, C00508612F2139F6006016DE /* EnvironmentValues+FallbackComponentCache.swift */, ); path = Environment; @@ -1622,6 +1631,7 @@ isa = PBXGroup; children = ( C005086C2F2139F6006016DE /* ComponentHostingController.swift */, + AB0000012F2139F6006016DE /* ComponentMountingPolicy.swift */, C005086D2F2139F6006016DE /* ComponentHostingRoot.swift */, C005086E2F2139F6006016DE /* ComponentHostingView.swift */, ); @@ -3300,6 +3310,8 @@ C00508D62F2139F6006016DE /* FallbackComponentCacheKey.swift in Sources */, C00508D72F2139F6006016DE /* ComponentPadding.swift in Sources */, C00508D82F2139F6006016DE /* ComponentHostingController.swift in Sources */, + AB0000032F2139F6006016DE /* ComponentMountingPolicy.swift in Sources */, + AB0000072F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift in Sources */, C00508D92F2139F6006016DE /* AnyComponentView.swift in Sources */, C00508DA2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift in Sources */, C00508DB2F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */, @@ -3662,6 +3674,8 @@ C00508A42F2139F6006016DE /* FallbackComponentCacheKey.swift in Sources */, C00508A52F2139F6006016DE /* ComponentPadding.swift in Sources */, C00508A62F2139F6006016DE /* ComponentHostingController.swift in Sources */, + AB0000022F2139F6006016DE /* ComponentMountingPolicy.swift in Sources */, + AB0000062F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift in Sources */, C00508A72F2139F6006016DE /* AnyComponentView.swift in Sources */, C00508A82F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift in Sources */, C00508A92F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */, @@ -4161,6 +4175,8 @@ C00509082F2139F6006016DE /* FallbackComponentCacheKey.swift in Sources */, C00509092F2139F6006016DE /* ComponentPadding.swift in Sources */, C005090A2F2139F6006016DE /* ComponentHostingController.swift in Sources */, + AB0000042F2139F6006016DE /* ComponentMountingPolicy.swift in Sources */, + AB0000082F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift in Sources */, C005090B2F2139F6006016DE /* AnyComponentView.swift in Sources */, C005090C2F2139F6006016DE /* EnvironmentValues+ComponentIdentifier.swift in Sources */, C005090D2F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */, diff --git a/Sources/Component/Environment/EnvironmentValues+ComponentMountingPolicy.swift b/Sources/Component/Environment/EnvironmentValues+ComponentMountingPolicy.swift new file mode 100644 index 00000000..4d9e549a --- /dev/null +++ b/Sources/Component/Environment/EnvironmentValues+ComponentMountingPolicy.swift @@ -0,0 +1,22 @@ +#if canImport(UIKit) +import SwiftUI + +internal struct ComponentMountingPolicyEnvironmentKey: EnvironmentKey { + + internal static let defaultValue: ComponentMountingPolicy = .deferred +} + +extension EnvironmentValues { + + /// Политика монтирования SwiftUI-компонентов в UIKit-представление. + /// + /// Определяет момент, в который ``ComponentHostingView`` встраивает `UIHostingController`. + /// По умолчанию монтирование откладывается до попадания представления в иерархию окна. + /// + /// - SeeAlso: ``ComponentMountingPolicy`` + public var componentMountingPolicy: ComponentMountingPolicy { + get { self[ComponentMountingPolicyEnvironmentKey.self] } + set { self[ComponentMountingPolicyEnvironmentKey.self] = newValue } + } +} +#endif diff --git a/Sources/Component/Hosting/ComponentHostingView.swift b/Sources/Component/Hosting/ComponentHostingView.swift index db8bc161..8ce1aba2 100644 --- a/Sources/Component/Hosting/ComponentHostingView.swift +++ b/Sources/Component/Hosting/ComponentHostingView.swift @@ -4,14 +4,16 @@ import SwiftUI /// UIKit-представление для встраивания SwiftUI-компонентов. /// -/// Встраивает `UIHostingController` только после добавления в иерархию, +/// По умолчанию встраивает `UIHostingController` только после добавления в иерархию, /// чтобы гарантировать наличие экземпляра `UIViewController` в контексте или в цепочке `UIResponder`. +/// Момент встраивания настраивается политикой монтирования ``ComponentMountingPolicy``. /// /// Также самостоятельно синхронизирует SwiftUI-окружение с переопределенными значениями в UIKit-представлениях. /// /// - SeeAlso: ``Component`` /// - SeeAlso: ``ComponentView`` /// - SeeAlso: ``ComponentContext`` +/// - SeeAlso: ``ComponentMountingPolicy`` public final class ComponentHostingView: UIView { private typealias HostingRoot = ComponentHostingRoot @@ -24,6 +26,8 @@ public final class ComponentHostingView: UIView { private weak var componentViewController: UIViewController? + private var mountingPolicy: ComponentMountingPolicy = .deferred + public override var intrinsicContentSize: CGSize { hostingController? .viewIfLoaded? @@ -151,10 +155,14 @@ extension ComponentHostingView { with hostingRoot: HostingRoot, forced: Bool = false ) { - guard let superview, window != nil, appearance.isExist || forced else { + guard appearance.isExist || forced else { return } + guard let superview, window != nil else { + return setupHostingControllerEagerlyIfNeeded(with: hostingRoot) + } + if let hostingController { return setupHostingControllerIfNeeded( hostingController, @@ -177,6 +185,37 @@ extension ComponentHostingView { self.hostingController = hostingController } + private func setupHostingControllerEagerlyIfNeeded(with hostingRoot: HostingRoot) { + guard mountingPolicy == .eager else { + return + } + + if let hostingController { + guard hostingController.viewIfLoaded?.superview == nil else { + return + } + + return setupHostingController( + hostingController, + parentViewController: nil + ) + } + + let hostingController = HostingController( + rootView: hostingRoot, + intrinsicContentSizeInvalidation: { [weak self] in + self?.invalidateIntrinsicContentSize() + } + ) + + setupHostingController( + hostingController, + parentViewController: nil + ) + + self.hostingController = hostingController + } + private func resetHostingController(_ hostingController: HostingController) { hostingController.beginAppearanceTransition(false, animated: false) @@ -229,6 +268,7 @@ extension ComponentHostingView: ComponentView { let newID = context.componentID.value componentViewController = context.componentViewController + mountingPolicy = context.componentMountingPolicy.value let context = context .componentAppearance(appearance, of: self) diff --git a/Sources/Component/Hosting/ComponentMountingPolicy.swift b/Sources/Component/Hosting/ComponentMountingPolicy.swift new file mode 100644 index 00000000..093352af --- /dev/null +++ b/Sources/Component/Hosting/ComponentMountingPolicy.swift @@ -0,0 +1,29 @@ +#if canImport(UIKit) +import Foundation + +/// Политика монтирования SwiftUI-компонентов в UIKit-представление. +/// +/// Определяет момент, в который ``ComponentHostingView`` встраивает `UIHostingController` +/// по умолчанию ``deferred`` +/// +/// - SeeAlso: ``Component`` +/// - SeeAlso: ``ComponentContext`` +public enum ComponentMountingPolicy: Hashable, Sendable { + + /// Монтирование откладывается до попадания представления в иерархию окна. + /// + /// Гарантирует наличие экземпляра `UIViewController` в контексте или в цепочке `UIResponder` + /// на момент встраивания, поэтому используется по умолчанию. + /// + /// До монтирования представление не сообщает размер контента: + /// его `intrinsicContentSize` содержит `UIView.noIntrinsicMetric`. + case deferred + + /// Монтирование выполняется сразу при обновлении контента, не дожидаясь иерархии окна. + /// + /// Вне иерархии окна родительский `UIViewController` не резолвится, + /// поэтому контроллер встраивается без него, а при попадании в иерархию окна + /// выполняется его повторное встраивание уже с родителем. + case eager +} +#endif From d16ab2baa71c4fecc4dffd6964aeb090e0187d3c Mon Sep 17 00:00:00 2001 From: Almaz Ibragimov Date: Fri, 7 Aug 2026 14:00:54 +0300 Subject: [PATCH 4/8] Public ComponentHostingController --- .../Hosting/ComponentHostingController.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/Component/Hosting/ComponentHostingController.swift b/Sources/Component/Hosting/ComponentHostingController.swift index 6b4054b1..2537c364 100644 --- a/Sources/Component/Hosting/ComponentHostingController.swift +++ b/Sources/Component/Hosting/ComponentHostingController.swift @@ -2,18 +2,18 @@ import SwiftUI import UIKit -internal final class ComponentHostingController: UIHostingController { +public final class ComponentHostingController: UIHostingController { private var intrinsicContentSize: CGSize? { didSet { intrinsicContentSizeInvalidation?() } } - internal var intrinsicContentSizeInvalidation: (@MainActor () -> Void)? + public var intrinsicContentSizeInvalidation: (@MainActor () -> Void)? // До iOS 16 UIHostingController самовольно отображает скрытую панель навигации. // Чтобы это предотвратить, переопределяем для него navigationController // и возвращаем nil для iOS старше 16. - internal override var navigationController: UINavigationController? { + public override var navigationController: UINavigationController? { if #available(iOS 16.0, tvOS 16.0, *) { return super.navigationController } @@ -21,7 +21,7 @@ internal final class ComponentHostingController: UIHostingControl return nil } - internal init( + public init( rootView: Content, intrinsicContentSizeInvalidation: (@MainActor () -> Void)? = nil ) { @@ -31,11 +31,11 @@ internal final class ComponentHostingController: UIHostingControl } @available(*, unavailable) - internal required dynamic init?(coder aDecoder: NSCoder) { + public required dynamic init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - internal override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() if #available(iOS 16.4, tvOS 16.4, *) { @@ -47,7 +47,7 @@ internal final class ComponentHostingController: UIHostingControl view.backgroundColor = .clear } - internal override func viewDidLayoutSubviews() { + public override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if intrinsicContentSize != view.intrinsicContentSize { From baeaa706f5b8b9874557d2ef64ff333c956b6b26 Mon Sep 17 00:00:00 2001 From: Almaz Ibragimov Date: Wed, 12 Aug 2026 10:54:51 +0300 Subject: [PATCH 5/8] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EnvironmentValues+ComponentContainerSize.swift | 6 ------ .../EnvironmentValues+ComponentViewController.swift | 2 +- .../Component/Fallback/Cache/FallbackComponentCache.swift | 3 ++- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Sources/Component/Environment/EnvironmentValues+ComponentContainerSize.swift b/Sources/Component/Environment/EnvironmentValues+ComponentContainerSize.swift index f2469777..187d062b 100644 --- a/Sources/Component/Environment/EnvironmentValues+ComponentContainerSize.swift +++ b/Sources/Component/Environment/EnvironmentValues+ComponentContainerSize.swift @@ -12,9 +12,6 @@ extension EnvironmentValues { /// /// Может использоваться в качестве максимальных размеров для определения размеров компонента /// в методе `sizing(fitting:context:)`. - /// - /// - Note: Нет необходимости самостоятельно устанавливать значение для этой переменной, - /// его переопределяют встроенные компоненты. public var componentContainerSize: CGSize { get { self[ComponentContainerSizeKey.self] } set { self[ComponentContainerSizeKey.self] = newValue } @@ -28,9 +25,6 @@ extension View { /// Может использоваться в качестве максимальных размеров для определения размеров компонента /// в методе `sizing(fitting:context:)`. /// - /// - Note: Нет необходимости самостоятельно устанавливать значение для этой переменной, - /// его переопределяют встроенные компоненты. - /// /// - Parameter size: Размеры ближайшего известного контейнера. /// - Returns: Модифицированный экземпляр UI-представления. public nonisolated func componentContainerSize(_ size: CGSize) -> some View { diff --git a/Sources/Component/Environment/EnvironmentValues+ComponentViewController.swift b/Sources/Component/Environment/EnvironmentValues+ComponentViewController.swift index 41238cdc..4cf0230d 100644 --- a/Sources/Component/Environment/EnvironmentValues+ComponentViewController.swift +++ b/Sources/Component/Environment/EnvironmentValues+ComponentViewController.swift @@ -97,7 +97,7 @@ extension View { /// /// - Parameter viewController: Ближайший экземпляр `UIViewController`. /// - Returns: Модифицированный экземпляр UI-представления. - public func componentViewController(_ viewController: UIViewController?) -> some View { + public nonisolated func componentViewController(_ viewController: UIViewController?) -> some View { environment( \.componentViewControllerStorage, ComponentViewControllerStorage { [weak viewController] in diff --git a/Sources/Component/Fallback/Cache/FallbackComponentCache.swift b/Sources/Component/Fallback/Cache/FallbackComponentCache.swift index 823ed46e..07545e88 100644 --- a/Sources/Component/Fallback/Cache/FallbackComponentCache.swift +++ b/Sources/Component/Fallback/Cache/FallbackComponentCache.swift @@ -2,11 +2,12 @@ import CoreGraphics import Foundation +@MainActor public final class FallbackComponentCache { private var sizes: [FallbackComponentCacheKey: Set] = [:] - public init() { } + public nonisolated init() { } public func reset() { sizes.removeAll(keepingCapacity: true) From 01d67ee0ef099539f8dd94845e97abfba201ea39 Mon Sep 17 00:00:00 2001 From: Darya Viter <53571360+VDA22@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:00:34 +0300 Subject: [PATCH 6/8] Multicolor icons (#71) --- .../Part/Extensions/ImageValue+TextPart.swift | 2 +- .../DecorableByForegroundStyle.swift | 10 ++ .../ForegroundStyleDecorator.swift | 34 +++++++ .../DecorableByImageSymbolConfiguration.swift | 6 ++ .../ImageSymbolConfiguration.swift | 14 +++ .../ImageSymbolConfigurationDecorator.swift | 18 ++++ Sources/Token/Value/Image/ImageValue.swift | 93 +++++++++++++++++-- .../Extensions/SwiftUI/Image+Extensions.swift | 17 ++++ .../Tools/Extensions/UIKit/UIImage+Crop.swift | 23 +++++ 9 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 Sources/Token/Decorators/ForegroundStyle/DecorableByForegroundStyle.swift create mode 100644 Sources/Token/Decorators/ForegroundStyle/ForegroundStyleDecorator.swift create mode 100644 Sources/Token/Decorators/ImageSymbolConfiguration/DecorableByImageSymbolConfiguration.swift create mode 100644 Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfiguration.swift create mode 100644 Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfigurationDecorator.swift create mode 100644 Sources/Tools/Extensions/SwiftUI/Image+Extensions.swift create mode 100644 Sources/Tools/Extensions/UIKit/UIImage+Crop.swift diff --git a/Sources/Text/Part/Extensions/ImageValue+TextPart.swift b/Sources/Text/Part/Extensions/ImageValue+TextPart.swift index 1fc0c3b4..5e81ba9d 100644 --- a/Sources/Text/Part/Extensions/ImageValue+TextPart.swift +++ b/Sources/Text/Part/Extensions/ImageValue+TextPart.swift @@ -15,7 +15,7 @@ extension ImageValue: TextPart { let foregroundColor = typography.foregroundColor let uiImage = self - .foregroundColor(self.foregroundColor ?? foregroundColor) + .foregroundColor(self.primaryColor ?? foregroundColor) .uiImage let imageHeight = uiImage.size.height diff --git a/Sources/Token/Decorators/ForegroundStyle/DecorableByForegroundStyle.swift b/Sources/Token/Decorators/ForegroundStyle/DecorableByForegroundStyle.swift new file mode 100644 index 00000000..354a28ef --- /dev/null +++ b/Sources/Token/Decorators/ForegroundStyle/DecorableByForegroundStyle.swift @@ -0,0 +1,10 @@ +import Foundation + +public protocol DecorableByForegroundStyle { + + func foregroundStyle( + _ primaryColor: ColorValue, + _ secondaryColor: ColorValue?, + _ tertiaryColor: ColorValue? + ) -> Self +} diff --git a/Sources/Token/Decorators/ForegroundStyle/ForegroundStyleDecorator.swift b/Sources/Token/Decorators/ForegroundStyle/ForegroundStyleDecorator.swift new file mode 100644 index 00000000..259d252d --- /dev/null +++ b/Sources/Token/Decorators/ForegroundStyle/ForegroundStyleDecorator.swift @@ -0,0 +1,34 @@ +import Foundation + +internal struct ForegroundStyleDecorator: TokenDecorator { + + internal let primaryColor: ColorToken + internal let secondaryColor: ColorToken? + internal let tertiaryColor: ColorToken? + + internal func decorate(_ value: Value, theme: TokenTheme) -> Value { + value + .foregroundStyle( + primaryColor.resolve(for: theme), + secondaryColor?.resolve(for: theme), + tertiaryColor?.resolve(for: theme) + ) + } +} + +extension Token where Value: DecorableByForegroundStyle { + + public func foregroundStyle( + _ primaryColor: ColorToken, + _ secondaryColor: ColorToken? = nil, + _ tertiaryColor: ColorToken? = nil + ) -> Self { + decorated( + by: ForegroundStyleDecorator( + primaryColor: primaryColor, + secondaryColor: secondaryColor, + tertiaryColor: tertiaryColor + ) + ) + } +} diff --git a/Sources/Token/Decorators/ImageSymbolConfiguration/DecorableByImageSymbolConfiguration.swift b/Sources/Token/Decorators/ImageSymbolConfiguration/DecorableByImageSymbolConfiguration.swift new file mode 100644 index 00000000..b39a8dea --- /dev/null +++ b/Sources/Token/Decorators/ImageSymbolConfiguration/DecorableByImageSymbolConfiguration.swift @@ -0,0 +1,6 @@ +import Foundation + +public protocol DecorableByImageSymbolConfiguration { + + func imageSymbolConfiguration(_ imageSymbolConfiguration: ImageSymbolConfiguration?) -> Self +} diff --git a/Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfiguration.swift b/Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfiguration.swift new file mode 100644 index 00000000..ba88fdfb --- /dev/null +++ b/Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfiguration.swift @@ -0,0 +1,14 @@ +#if os(iOS) || os(tvOS) +import UIKit +#else +import AppKit +#endif + +public struct ImageSymbolConfiguration: Sendable, Hashable { + + let size: CGFloat + + public init(size: CGFloat) { + self.size = size + } +} diff --git a/Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfigurationDecorator.swift b/Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfigurationDecorator.swift new file mode 100644 index 00000000..b8daf702 --- /dev/null +++ b/Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfigurationDecorator.swift @@ -0,0 +1,18 @@ +import Foundation + +internal struct ImageSymbolConfigurationDecorator: TokenDecorator { + + internal let imageSymbolConfiguration: ImageSymbolConfiguration? + + internal func decorate(_ value: Value, theme: TokenTheme) -> Value { + value + .imageSymbolConfiguration(imageSymbolConfiguration) + } +} + +extension Token where Value: DecorableByImageSymbolConfiguration { + + public func imageSymbolConfiguration(_ imageSymbolConfiguration: ImageSymbolConfiguration?) -> Self { + decorated(by: ImageSymbolConfigurationDecorator(imageSymbolConfiguration: imageSymbolConfiguration)) + } +} diff --git a/Sources/Token/Value/Image/ImageValue.swift b/Sources/Token/Value/Image/ImageValue.swift index e55ba073..f6d6fca4 100644 --- a/Sources/Token/Value/Image/ImageValue.swift +++ b/Sources/Token/Value/Image/ImageValue.swift @@ -11,8 +11,11 @@ public struct ImageValue: public var source: ImageSource public var resizingMode: ImageResizingMode? - public var foregroundColor: ColorValue? + public var primaryColor: ColorValue? + public var secondaryColor: ColorValue? + public var tertiaryColor: ColorValue? public var insets: InsetsValue + public var imageSymbolConfiguration: ImageSymbolConfiguration? #if canImport(UIKit) public var uiImage: UIImage { @@ -25,15 +28,35 @@ public struct ImageValue: ) } - if let foregroundColor = foregroundColor?.uiColor { + let colors = [primaryColor?.uiColor, secondaryColor?.uiColor, tertiaryColor?.uiColor] + .compactMap(\.self) + + if colors.count == 1, let foregroundColor = colors.first { uiImage = uiImage.withTintColor( foregroundColor, renderingMode: .alwaysOriginal ) + } else if let primaryColor = primaryColor?.uiColor { + let foregroundStyleConfiguration = UIImage.SymbolConfiguration( + paletteColors: colors + ) + uiImage = uiImage.applyingSymbolConfiguration(foregroundStyleConfiguration) + ?? uiImage } else { uiImage = uiImage.withRenderingMode(.alwaysOriginal) } + if let imageSymbolConfigurationSize = imageSymbolConfiguration?.size { + let fontSizeConfiguration = UIImage.SymbolConfiguration( + font: .systemFont(ofSize: imageSymbolConfigurationSize) + ) + + uiImage = uiImage + .applyingSymbolConfiguration(fontSizeConfiguration)? + .crop(to: CGSize(width: imageSymbolConfigurationSize, height: imageSymbolConfigurationSize)) + ?? uiImage + } + if insets != .zero { uiImage = uiImage.withAlignmentRectInsets(insets.uiEdgeInsets) } @@ -45,9 +68,43 @@ public struct ImageValue: public var image: some View { source .image - .renderingMode(foregroundColor == nil ? .original : .template) - .iflet(resizingMode) { $0.resizable(resizingMode: $1.resizingMode) } - .iflet(foregroundColor) { $0.foregroundColor($1.color) } + .renderingMode(primaryColor == nil ? .original : .template) + .ifImageLet(resizingMode) { $0 = $0.resizable(resizingMode: $1.resizingMode) } + .iflet(primaryColor) { image, primaryColor in + switch (secondaryColor, tertiaryColor) { + case let (secondaryColor?, nil): + image + .symbolRenderingMode(.palette) + .foregroundStyle( + primaryColor.color, + secondaryColor.color + ) + case let (nil, tertiaryColor?): + image + .symbolRenderingMode(.palette) + .foregroundStyle( + primaryColor.color, + tertiaryColor.color + ) + case let (secondaryColor?, tertiaryColor?): + image + .symbolRenderingMode(.palette) + .foregroundStyle( + primaryColor.color, + secondaryColor.color, + tertiaryColor.color, + ) + case (nil, nil): + image + .foregroundStyle(primaryColor.color) + } + } + .iflet(imageSymbolConfiguration) { + $0 + .font(.system(size: $1.size)) + .frame(width: $1.size, height: $1.size) + .clipped() + } .if(insets != .zero) { $0.padding(insets.edgeInsets) } } @@ -59,7 +116,7 @@ public struct ImageValue: ) { self.source = source self.resizingMode = resizingMode - self.foregroundColor = foregroundColor + self.primaryColor = foregroundColor self.insets = insets } } @@ -67,6 +124,8 @@ public struct ImageValue: extension ImageValue: DecorableByResizingMode, DecorableByForegroundColor, + DecorableByForegroundStyle, + DecorableByImageSymbolConfiguration, DecorableByInsets { public func resizable(_ resizingMode: ImageResizingMode?) -> Self { @@ -74,12 +133,32 @@ extension ImageValue: } public func foregroundColor(_ foregroundColor: ColorValue?) -> Self { - changing { $0.foregroundColor = foregroundColor } + changing { + $0.primaryColor = foregroundColor + $0.secondaryColor = nil + $0.tertiaryColor = nil + } + } + + public func foregroundStyle( + _ primaryColor: ColorValue, + _ secondaryColor: ColorValue?, + _ tertiaryColor: ColorValue? + ) -> Self { + changing { + $0.primaryColor = primaryColor + $0.secondaryColor = secondaryColor + $0.tertiaryColor = tertiaryColor + } } public func inset(by insets: InsetsValue) -> Self { changing { $0.insets = insets } } + + public func imageSymbolConfiguration(_ imageSymbolConfiguration: ImageSymbolConfiguration?) -> Self { + changing { $0.imageSymbolConfiguration = imageSymbolConfiguration } + } } extension ImageValue { diff --git a/Sources/Tools/Extensions/SwiftUI/Image+Extensions.swift b/Sources/Tools/Extensions/SwiftUI/Image+Extensions.swift new file mode 100644 index 00000000..ca927f32 --- /dev/null +++ b/Sources/Tools/Extensions/SwiftUI/Image+Extensions.swift @@ -0,0 +1,17 @@ +import SwiftUI + +extension Image { + + @ViewBuilder + internal nonisolated func ifImageLet( + _ condition: T?, + _ apply: (inout Image, T) -> Void + ) -> Image { + var result = self + + if let value = condition { + apply(&result, value) + } + return result + } +} diff --git a/Sources/Tools/Extensions/UIKit/UIImage+Crop.swift b/Sources/Tools/Extensions/UIKit/UIImage+Crop.swift new file mode 100644 index 00000000..d43e5a98 --- /dev/null +++ b/Sources/Tools/Extensions/UIKit/UIImage+Crop.swift @@ -0,0 +1,23 @@ +#if canImport(UIKit) +import UIKit + +extension UIImage { + + func crop(to targetSize: CGSize) -> UIImage? { + let format = UIGraphicsImageRendererFormat.default() + format.scale = scale + let renderer = UIGraphicsImageRenderer(size: targetSize, format: format) + + return renderer.image { context in + let widthDiff = size.width - targetSize.width + let heightDiff = size.height - targetSize.height + + let drawOrigin = CGPoint( + x: widthDiff.isZero ? .zero : -widthDiff / 2, + y: heightDiff.isZero ? .zero : -heightDiff / 2 + ) + self.draw(at: drawOrigin) + } + } +} +#endif From 6dc6e4a10154a182a6169fd92f6ce89ee7a90fc3 Mon Sep 17 00:00:00 2001 From: Almaz Ibragimov Date: Fri, 2 Oct 2026 23:16:11 +0300 Subject: [PATCH 7/8] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=80=D0=B5=D0=B4=D1=83=D0=BF=D1=80=D0=B5=D0=B6?= =?UTF-8?q?=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Levitan.xcodeproj/project.pbxproj | 90 +++++++++++++++++-- .../DecorableByImageSymbolConfiguration.swift | 2 +- .../ImageSymbolConfigurationDecorator.swift | 9 +- .../Image}/ImageSymbolConfiguration.swift | 2 +- Sources/Token/Value/Image/ImageValue.swift | 71 ++++++++------- .../Extensions/SwiftUI/Image+Extensions.swift | 12 ++- ...ge+Crop.swift => UIImage+Extensions.swift} | 7 +- 7 files changed, 135 insertions(+), 58 deletions(-) rename Sources/Token/{Decorators/ImageSymbolConfiguration => Value/Image}/ImageSymbolConfiguration.swift (86%) rename Sources/Tools/Extensions/UIKit/{UIImage+Crop.swift => UIImage+Extensions.swift} (85%) diff --git a/Levitan.xcodeproj/project.pbxproj b/Levitan.xcodeproj/project.pbxproj index 1fe6037c..4be92c4e 100755 --- a/Levitan.xcodeproj/project.pbxproj +++ b/Levitan.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + AB0000022F2139F6006016DE /* ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000012F2139F6006016DE /* ComponentMountingPolicy.swift */; }; + AB0000032F2139F6006016DE /* ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000012F2139F6006016DE /* ComponentMountingPolicy.swift */; }; + AB0000042F2139F6006016DE /* ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000012F2139F6006016DE /* ComponentMountingPolicy.swift */; }; + AB0000062F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000052F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift */; }; + AB0000072F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000052F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift */; }; + AB0000082F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000052F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift */; }; C005088D2F2139F6006016DE /* ComponentFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508762F2139F6006016DE /* ComponentFrame.swift */; }; C005088E2F2139F6006016DE /* ComponentModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508862F2139F6006016DE /* ComponentModifier.swift */; }; C005088F2F2139F6006016DE /* AnyComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005084C2F2139F6006016DE /* AnyComponent.swift */; }; @@ -33,8 +39,6 @@ C00508A42F2139F6006016DE /* FallbackComponentCacheKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508652F2139F6006016DE /* FallbackComponentCacheKey.swift */; }; C00508A52F2139F6006016DE /* ComponentPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508702F2139F6006016DE /* ComponentPadding.swift */; }; C00508A62F2139F6006016DE /* ComponentHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005086C2F2139F6006016DE /* ComponentHostingController.swift */; }; - AB0000022F2139F6006016DE /* ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000012F2139F6006016DE /* ComponentMountingPolicy.swift */; }; - AB0000062F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000052F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift */; }; C00508A72F2139F6006016DE /* AnyComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005084E2F2139F6006016DE /* AnyComponentView.swift */; }; C00508A82F2139F6006016DE /* EnvironmentValues+ComponentID.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005085E2F2139F6006016DE /* EnvironmentValues+ComponentID.swift */; }; C00508A92F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508812F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift */; }; @@ -85,8 +89,6 @@ C00508D62F2139F6006016DE /* FallbackComponentCacheKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508652F2139F6006016DE /* FallbackComponentCacheKey.swift */; }; C00508D72F2139F6006016DE /* ComponentPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508702F2139F6006016DE /* ComponentPadding.swift */; }; C00508D82F2139F6006016DE /* ComponentHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005086C2F2139F6006016DE /* ComponentHostingController.swift */; }; - AB0000032F2139F6006016DE /* ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000012F2139F6006016DE /* ComponentMountingPolicy.swift */; }; - AB0000072F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000052F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift */; }; C00508D92F2139F6006016DE /* AnyComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005084E2F2139F6006016DE /* AnyComponentView.swift */; }; C00508DA2F2139F6006016DE /* EnvironmentValues+ComponentID.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005085E2F2139F6006016DE /* EnvironmentValues+ComponentID.swift */; }; C00508DB2F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508812F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift */; }; @@ -137,8 +139,6 @@ C00509082F2139F6006016DE /* FallbackComponentCacheKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508652F2139F6006016DE /* FallbackComponentCacheKey.swift */; }; C00509092F2139F6006016DE /* ComponentPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508702F2139F6006016DE /* ComponentPadding.swift */; }; C005090A2F2139F6006016DE /* ComponentHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005086C2F2139F6006016DE /* ComponentHostingController.swift */; }; - AB0000042F2139F6006016DE /* ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000012F2139F6006016DE /* ComponentMountingPolicy.swift */; }; - AB0000082F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0000052F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift */; }; C005090B2F2139F6006016DE /* AnyComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005084E2F2139F6006016DE /* AnyComponentView.swift */; }; C005090C2F2139F6006016DE /* EnvironmentValues+ComponentID.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005085E2F2139F6006016DE /* EnvironmentValues+ComponentID.swift */; }; C005090D2F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508812F2139F6006016DE /* ScaleEffectModifier+ComponentTokenModifier.swift */; }; @@ -163,6 +163,27 @@ C00509202F2139F6006016DE /* FontModifier+ComponentTokenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005087D2F2139F6006016DE /* FontModifier+ComponentTokenModifier.swift */; }; C00509212F2139F6006016DE /* EnvironmentValues+ComponentContainerSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005085C2F2139F6006016DE /* EnvironmentValues+ComponentContainerSize.swift */; }; C00509222F2139F6006016DE /* Component.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00508892F2139F6006016DE /* Component.swift */; }; + C0211A2E30703CB90006C777 /* DecorableByForegroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2730703CB90006C777 /* DecorableByForegroundStyle.swift */; }; + C0211A2F30703CB90006C777 /* ImageSymbolConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2B30703CB90006C777 /* ImageSymbolConfiguration.swift */; }; + C0211A3030703CB90006C777 /* DecorableByImageSymbolConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2A30703CB90006C777 /* DecorableByImageSymbolConfiguration.swift */; }; + C0211A3130703CB90006C777 /* ImageSymbolConfigurationDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2C30703CB90006C777 /* ImageSymbolConfigurationDecorator.swift */; }; + C0211A3230703CB90006C777 /* ForegroundStyleDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2830703CB90006C777 /* ForegroundStyleDecorator.swift */; }; + C0211A3330703CB90006C777 /* DecorableByForegroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2730703CB90006C777 /* DecorableByForegroundStyle.swift */; }; + C0211A3430703CB90006C777 /* ImageSymbolConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2B30703CB90006C777 /* ImageSymbolConfiguration.swift */; }; + C0211A3530703CB90006C777 /* DecorableByImageSymbolConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2A30703CB90006C777 /* DecorableByImageSymbolConfiguration.swift */; }; + C0211A3630703CB90006C777 /* ImageSymbolConfigurationDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2C30703CB90006C777 /* ImageSymbolConfigurationDecorator.swift */; }; + C0211A3730703CB90006C777 /* ForegroundStyleDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2830703CB90006C777 /* ForegroundStyleDecorator.swift */; }; + C0211A3830703CB90006C777 /* DecorableByForegroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2730703CB90006C777 /* DecorableByForegroundStyle.swift */; }; + C0211A3930703CB90006C777 /* ImageSymbolConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2B30703CB90006C777 /* ImageSymbolConfiguration.swift */; }; + C0211A3A30703CB90006C777 /* DecorableByImageSymbolConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2A30703CB90006C777 /* DecorableByImageSymbolConfiguration.swift */; }; + C0211A3B30703CB90006C777 /* ImageSymbolConfigurationDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2C30703CB90006C777 /* ImageSymbolConfigurationDecorator.swift */; }; + C0211A3C30703CB90006C777 /* ForegroundStyleDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A2830703CB90006C777 /* ForegroundStyleDecorator.swift */; }; + C0211A3E30703D1A0006C777 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A3D30703D1A0006C777 /* UIImage+Extensions.swift */; }; + C0211A3F30703D1A0006C777 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A3D30703D1A0006C777 /* UIImage+Extensions.swift */; }; + C0211A4030703D1A0006C777 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A3D30703D1A0006C777 /* UIImage+Extensions.swift */; }; + C0211A4230703F200006C777 /* Image+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A4130703F200006C777 /* Image+Extensions.swift */; }; + C0211A4330703F200006C777 /* Image+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A4130703F200006C777 /* Image+Extensions.swift */; }; + C0211A4430703F200006C777 /* Image+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0211A4130703F200006C777 /* Image+Extensions.swift */; }; C02C897E2F0266B400A2FE9F /* Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02C89572F0266B400A2FE9F /* Diffable.swift */; }; C02C897F2F0266B400A2FE9F /* OutsetsDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02C886A2F0266B400A2FE9F /* OutsetsDecorator.swift */; }; C02C89802F0266B400A2FE9F /* CGFloat+TokenValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02C89012F0266B400A2FE9F /* CGFloat+TokenValue.swift */; }; @@ -1118,6 +1139,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + AB0000012F2139F6006016DE /* ComponentMountingPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentMountingPolicy.swift; sourceTree = ""; }; + AB0000052F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+ComponentMountingPolicy.swift"; sourceTree = ""; }; C0007FFA23CBAAA5005F95E0 /* .swift-version */ = {isa = PBXFileReference; lastKnownFileType = text; path = ".swift-version"; sourceTree = ""; }; C005084C2F2139F6006016DE /* AnyComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyComponent.swift; sourceTree = ""; }; C005084D2F2139F6006016DE /* AnyComponentPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyComponentPresenter.swift; sourceTree = ""; }; @@ -1144,8 +1167,6 @@ C00508692F2139F6006016DE /* FallbackComponentBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FallbackComponentBody.swift; sourceTree = ""; }; C005086A2F2139F6006016DE /* FallbackComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FallbackComponentView.swift; sourceTree = ""; }; C005086C2F2139F6006016DE /* ComponentHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentHostingController.swift; sourceTree = ""; }; - AB0000012F2139F6006016DE /* ComponentMountingPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentMountingPolicy.swift; sourceTree = ""; }; - AB0000052F2139F6006016DE /* EnvironmentValues+ComponentMountingPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+ComponentMountingPolicy.swift"; sourceTree = ""; }; C005086D2F2139F6006016DE /* ComponentHostingRoot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentHostingRoot.swift; sourceTree = ""; }; C005086E2F2139F6006016DE /* ComponentHostingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentHostingView.swift; sourceTree = ""; }; C00508702F2139F6006016DE /* ComponentPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentPadding.swift; sourceTree = ""; }; @@ -1171,6 +1192,13 @@ C00508892F2139F6006016DE /* Component.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Component.swift; sourceTree = ""; }; C005088A2F2139F6006016DE /* ComponentID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentID.swift; sourceTree = ""; }; C005088B2F2139F6006016DE /* ComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentView.swift; sourceTree = ""; }; + C0211A2730703CB90006C777 /* DecorableByForegroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorableByForegroundStyle.swift; sourceTree = ""; }; + C0211A2830703CB90006C777 /* ForegroundStyleDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForegroundStyleDecorator.swift; sourceTree = ""; }; + C0211A2A30703CB90006C777 /* DecorableByImageSymbolConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorableByImageSymbolConfiguration.swift; sourceTree = ""; }; + C0211A2B30703CB90006C777 /* ImageSymbolConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSymbolConfiguration.swift; sourceTree = ""; }; + C0211A2C30703CB90006C777 /* ImageSymbolConfigurationDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSymbolConfigurationDecorator.swift; sourceTree = ""; }; + C0211A3D30703D1A0006C777 /* UIImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extensions.swift"; sourceTree = ""; }; + C0211A4130703F200006C777 /* Image+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Image+Extensions.swift"; sourceTree = ""; }; C02C87C12F02667300A2FE9F /* Gemfile.lock */ = {isa = PBXFileReference; lastKnownFileType = text; path = Gemfile.lock; sourceTree = ""; }; C02C87C22F02667300A2FE9F /* Levitan.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = Levitan.podspec; sourceTree = ""; }; C02C87C32F0266B400A2FE9F /* Array+Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Changeable.swift"; sourceTree = ""; }; @@ -1706,6 +1734,24 @@ path = Component; sourceTree = ""; }; + C0211A2930703CB90006C777 /* ForegroundStyle */ = { + isa = PBXGroup; + children = ( + C0211A2730703CB90006C777 /* DecorableByForegroundStyle.swift */, + C0211A2830703CB90006C777 /* ForegroundStyleDecorator.swift */, + ); + path = ForegroundStyle; + sourceTree = ""; + }; + C0211A2D30703CB90006C777 /* ImageSymbolConfiguration */ = { + isa = PBXGroup; + children = ( + C0211A2A30703CB90006C777 /* DecorableByImageSymbolConfiguration.swift */, + C0211A2C30703CB90006C777 /* ImageSymbolConfigurationDecorator.swift */, + ); + path = ImageSymbolConfiguration; + sourceTree = ""; + }; C02C87C62F0266B400A2FE9F /* Extensions */ = { isa = PBXGroup; children = ( @@ -2109,6 +2155,8 @@ C02C88482F0266B400A2FE9F /* FontScale */, C02C884B2F0266B400A2FE9F /* FontSize */, C02C884E2F0266B400A2FE9F /* ForegroundColor */, + C0211A2930703CB90006C777 /* ForegroundStyle */, + C0211A2D30703CB90006C777 /* ImageSymbolConfiguration */, C02C88512F0266B400A2FE9F /* Insets */, C02C88542F0266B400A2FE9F /* LetterSpacing */, C02C88572F0266B400A2FE9F /* LineBreakMode */, @@ -2126,8 +2174,8 @@ C02C887A2F0266B400A2FE9F /* ResizingMode */, C02C887D2F0266B400A2FE9F /* Strikethrough */, C02C88802F0266B400A2FE9F /* Stroke */, - C02C88832F0266B400A2FE9F /* Underline */, C02C88842F0266B400A2FE9F /* TokenDecorator.swift */, + C02C88832F0266B400A2FE9F /* Underline */, ); path = Decorators; sourceTree = ""; @@ -2492,6 +2540,7 @@ C02C89212F0266B400A2FE9F /* Image */ = { isa = PBXGroup; children = ( + C0211A2B30703CB90006C777 /* ImageSymbolConfiguration.swift */, C02C891D2F0266B400A2FE9F /* ImageResizingMode.swift */, C02C891E2F0266B400A2FE9F /* ImageSource.swift */, C02C891F2F0266B400A2FE9F /* ImageToken.swift */, @@ -2716,6 +2765,7 @@ C06EE5572F0D205800AD1F6A /* EdgeInsets+Extensions.swift */, C0C8B8032F0917DF0029D19F /* EnvironmentValues+Extensions.swift */, C02C89662F0266B400A2FE9F /* Image.DynamicRange+Extensions.swift */, + C0211A4130703F200006C777 /* Image+Extensions.swift */, C02C89672F0266B400A2FE9F /* UserInterfaceSizeClass+Extensions.swift */, C02C89682F0266B400A2FE9F /* View+Extensions.swift */, ); @@ -2729,6 +2779,7 @@ C02C896B2F0266B400A2FE9F /* NSLayoutConstraint+Extensions.swift */, C02C896C2F0266B400A2FE9F /* UICollectionView+Extensions.swift */, C02C896D2F0266B400A2FE9F /* UIEdgeInsets+Extensions.swift */, + C0211A3D30703D1A0006C777 /* UIImage+Extensions.swift */, C02C896E2F0266B400A2FE9F /* UILayoutPriority+Extensions.swift */, C02C896F2F0266B400A2FE9F /* UIResponder+Extensions.swift */, C0C8A8E12F07B96D001651F6 /* UIView+Extensions.swift */, @@ -3196,6 +3247,7 @@ C0C8B86E2F09A02F0029D19F /* ScaleEffectModifier.swift in Sources */, C02C8C502F0266B400A2FE9F /* TokenShapedView.swift in Sources */, C02C8C512F0266B400A2FE9F /* DecorableByStrikethrough.swift in Sources */, + C0211A4330703F200006C777 /* Image+Extensions.swift in Sources */, C02C8C522F0266B400A2FE9F /* MaskLayer.swift in Sources */, C02C8C532F0266B400A2FE9F /* TextAnimation.swift in Sources */, C02C8C542F0266B400A2FE9F /* CGSize+Extensions.swift in Sources */, @@ -3378,6 +3430,11 @@ C02C8CE72F0266B400A2FE9F /* Dictionary+Changeable.swift in Sources */, C02C8CE92F0266B400A2FE9F /* TextPart.swift in Sources */, C02C8CEA2F0266B400A2FE9F /* ImageValue+TextPart.swift in Sources */, + C0211A2E30703CB90006C777 /* DecorableByForegroundStyle.swift in Sources */, + C0211A2F30703CB90006C777 /* ImageSymbolConfiguration.swift in Sources */, + C0211A3030703CB90006C777 /* DecorableByImageSymbolConfiguration.swift in Sources */, + C0211A3130703CB90006C777 /* ImageSymbolConfigurationDecorator.swift in Sources */, + C0211A3230703CB90006C777 /* ForegroundStyleDecorator.swift in Sources */, C02C8CEB2F0266B400A2FE9F /* ShadowLayer.swift in Sources */, C02C8CEC2F0266B400A2FE9F /* DecorableByNegation.swift in Sources */, C02C8CED2F0266B400A2FE9F /* CGSize+TokenValue.swift in Sources */, @@ -3386,6 +3443,7 @@ C02C8CF02F0266B400A2FE9F /* AnyTokenView.swift in Sources */, C02C8CF22F0266B400A2FE9F /* TextInterpolation.swift in Sources */, C02C8CF32F0266B400A2FE9F /* Collection+Extensions.swift in Sources */, + C0211A3F30703D1A0006C777 /* UIImage+Extensions.swift in Sources */, C02C8CF42F0266B400A2FE9F /* TokenViewDSL.swift in Sources */, C02C8CF52F0266B400A2FE9F /* TokenViewProperties+CALayer.swift in Sources */, C02C8CF72F0266B400A2FE9F /* String+TextPart.swift in Sources */, @@ -3560,6 +3618,7 @@ C0C8B85F2F09A02F0029D19F /* ScaleEffectModifier.swift in Sources */, C02C89BA2F0266B400A2FE9F /* TokenShapedView.swift in Sources */, C02C89BB2F0266B400A2FE9F /* DecorableByStrikethrough.swift in Sources */, + C0211A4430703F200006C777 /* Image+Extensions.swift in Sources */, C02C89BC2F0266B400A2FE9F /* MaskLayer.swift in Sources */, C02C89BD2F0266B400A2FE9F /* TextAnimation.swift in Sources */, C02C89BE2F0266B400A2FE9F /* CGSize+Extensions.swift in Sources */, @@ -3742,6 +3801,11 @@ C02C8A512F0266B400A2FE9F /* Dictionary+Changeable.swift in Sources */, C02C8A532F0266B400A2FE9F /* TextPart.swift in Sources */, C02C8A542F0266B400A2FE9F /* ImageValue+TextPart.swift in Sources */, + C0211A3330703CB90006C777 /* DecorableByForegroundStyle.swift in Sources */, + C0211A3430703CB90006C777 /* ImageSymbolConfiguration.swift in Sources */, + C0211A3530703CB90006C777 /* DecorableByImageSymbolConfiguration.swift in Sources */, + C0211A3630703CB90006C777 /* ImageSymbolConfigurationDecorator.swift in Sources */, + C0211A3730703CB90006C777 /* ForegroundStyleDecorator.swift in Sources */, C02C8A552F0266B400A2FE9F /* ShadowLayer.swift in Sources */, C02C8A562F0266B400A2FE9F /* DecorableByNegation.swift in Sources */, C02C8A572F0266B400A2FE9F /* CGSize+TokenValue.swift in Sources */, @@ -3750,6 +3814,7 @@ C02C8A5A2F0266B400A2FE9F /* AnyTokenView.swift in Sources */, C02C8A5C2F0266B400A2FE9F /* TextInterpolation.swift in Sources */, C02C8A5D2F0266B400A2FE9F /* Collection+Extensions.swift in Sources */, + C0211A4030703D1A0006C777 /* UIImage+Extensions.swift in Sources */, C02C8A5E2F0266B400A2FE9F /* TokenViewDSL.swift in Sources */, C02C8A5F2F0266B400A2FE9F /* TokenViewProperties+CALayer.swift in Sources */, C02C8A612F0266B400A2FE9F /* String+TextPart.swift in Sources */, @@ -3875,6 +3940,7 @@ C02C8ADA2F0266B400A2FE9F /* OpacityDecorator.swift in Sources */, C02C8ADB2F0266B400A2FE9F /* UnderlineDecorator+TextDecorator.swift in Sources */, C02C8ADC2F0266B400A2FE9F /* ShapeValue.swift in Sources */, + C0211A3E30703D1A0006C777 /* UIImage+Extensions.swift in Sources */, C02C8ADD2F0266B400A2FE9F /* CGRect+TokenValue.swift in Sources */, C02C8ADE2F0266B400A2FE9F /* Sequence+Extensions.swift in Sources */, C02C8ADF2F0266B400A2FE9F /* DivisionDecorator.swift in Sources */, @@ -4030,6 +4096,7 @@ C02C8B842F0266B400A2FE9F /* RangeReplaceableCollection+Extensions.swift in Sources */, C02C8B852F0266B400A2FE9F /* ImageValue.swift in Sources */, C02C8B862F0266B400A2FE9F /* CALayer+TokenView.swift in Sources */, + C0211A4230703F200006C777 /* Image+Extensions.swift in Sources */, C02C8B872F0266B400A2FE9F /* InsetsToken.swift in Sources */, C02C8B882F0266B400A2FE9F /* ParagraphSpacingDecorator.swift in Sources */, C02C8B892F0266B400A2FE9F /* TokenViewProperties+UISwitch.swift in Sources */, @@ -4113,6 +4180,11 @@ C02C8BE32F0266B400A2FE9F /* ShadowValue.swift in Sources */, C02C8BE42F0266B400A2FE9F /* String+TokenValue.swift in Sources */, C02C8BE52F0266B400A2FE9F /* ColorDecorator.swift in Sources */, + C0211A3830703CB90006C777 /* DecorableByForegroundStyle.swift in Sources */, + C0211A3930703CB90006C777 /* ImageSymbolConfiguration.swift in Sources */, + C0211A3A30703CB90006C777 /* DecorableByImageSymbolConfiguration.swift in Sources */, + C0211A3B30703CB90006C777 /* ImageSymbolConfigurationDecorator.swift in Sources */, + C0211A3C30703CB90006C777 /* ForegroundStyleDecorator.swift in Sources */, C02C8BE62F0266B400A2FE9F /* SpringAnimationToken.swift in Sources */, C02C8BE72F0266B400A2FE9F /* TokenViewProperties+UIView.swift in Sources */, C02C8BE82F0266B400A2FE9F /* ShapeToken.swift in Sources */, diff --git a/Sources/Token/Decorators/ImageSymbolConfiguration/DecorableByImageSymbolConfiguration.swift b/Sources/Token/Decorators/ImageSymbolConfiguration/DecorableByImageSymbolConfiguration.swift index b39a8dea..abfa307d 100644 --- a/Sources/Token/Decorators/ImageSymbolConfiguration/DecorableByImageSymbolConfiguration.swift +++ b/Sources/Token/Decorators/ImageSymbolConfiguration/DecorableByImageSymbolConfiguration.swift @@ -2,5 +2,5 @@ import Foundation public protocol DecorableByImageSymbolConfiguration { - func imageSymbolConfiguration(_ imageSymbolConfiguration: ImageSymbolConfiguration?) -> Self + func symbolConfiguration(_ symbolConfiguration: ImageSymbolConfiguration?) -> Self } diff --git a/Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfigurationDecorator.swift b/Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfigurationDecorator.swift index b8daf702..d61db658 100644 --- a/Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfigurationDecorator.swift +++ b/Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfigurationDecorator.swift @@ -2,17 +2,16 @@ import Foundation internal struct ImageSymbolConfigurationDecorator: TokenDecorator { - internal let imageSymbolConfiguration: ImageSymbolConfiguration? + internal let symbolConfiguration: ImageSymbolConfiguration? internal func decorate(_ value: Value, theme: TokenTheme) -> Value { - value - .imageSymbolConfiguration(imageSymbolConfiguration) + value.symbolConfiguration(symbolConfiguration) } } extension Token where Value: DecorableByImageSymbolConfiguration { - public func imageSymbolConfiguration(_ imageSymbolConfiguration: ImageSymbolConfiguration?) -> Self { - decorated(by: ImageSymbolConfigurationDecorator(imageSymbolConfiguration: imageSymbolConfiguration)) + public func symbolConfiguration(_ symbolConfiguration: ImageSymbolConfiguration?) -> Self { + decorated(by: ImageSymbolConfigurationDecorator(symbolConfiguration: symbolConfiguration)) } } diff --git a/Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfiguration.swift b/Sources/Token/Value/Image/ImageSymbolConfiguration.swift similarity index 86% rename from Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfiguration.swift rename to Sources/Token/Value/Image/ImageSymbolConfiguration.swift index ba88fdfb..d29aa471 100644 --- a/Sources/Token/Decorators/ImageSymbolConfiguration/ImageSymbolConfiguration.swift +++ b/Sources/Token/Value/Image/ImageSymbolConfiguration.swift @@ -6,7 +6,7 @@ import AppKit public struct ImageSymbolConfiguration: Sendable, Hashable { - let size: CGFloat + public let size: CGFloat public init(size: CGFloat) { self.size = size diff --git a/Sources/Token/Value/Image/ImageValue.swift b/Sources/Token/Value/Image/ImageValue.swift index f6d6fca4..54db04c7 100644 --- a/Sources/Token/Value/Image/ImageValue.swift +++ b/Sources/Token/Value/Image/ImageValue.swift @@ -15,7 +15,7 @@ public struct ImageValue: public var secondaryColor: ColorValue? public var tertiaryColor: ColorValue? public var insets: InsetsValue - public var imageSymbolConfiguration: ImageSymbolConfiguration? + public var symbolConfiguration: ImageSymbolConfiguration? #if canImport(UIKit) public var uiImage: UIImage { @@ -28,33 +28,36 @@ public struct ImageValue: ) } - let colors = [primaryColor?.uiColor, secondaryColor?.uiColor, tertiaryColor?.uiColor] + let colors = [primaryColor, secondaryColor, tertiaryColor] .compactMap(\.self) + .map(\.uiColor) - if colors.count == 1, let foregroundColor = colors.first { + switch colors.count { + case .zero: + uiImage = uiImage.withRenderingMode(.alwaysOriginal) + + case 1: uiImage = uiImage.withTintColor( - foregroundColor, + colors[.zero], renderingMode: .alwaysOriginal ) - } else if let primaryColor = primaryColor?.uiColor { + + default: let foregroundStyleConfiguration = UIImage.SymbolConfiguration( paletteColors: colors ) - uiImage = uiImage.applyingSymbolConfiguration(foregroundStyleConfiguration) - ?? uiImage - } else { - uiImage = uiImage.withRenderingMode(.alwaysOriginal) + + uiImage = uiImage.applyingSymbolConfiguration(foregroundStyleConfiguration) ?? uiImage } - if let imageSymbolConfigurationSize = imageSymbolConfiguration?.size { + if let symbolConfigurationSize = symbolConfiguration?.size { let fontSizeConfiguration = UIImage.SymbolConfiguration( - font: .systemFont(ofSize: imageSymbolConfigurationSize) + font: .systemFont(ofSize: symbolConfigurationSize) ) - uiImage = uiImage - .applyingSymbolConfiguration(fontSizeConfiguration)? - .crop(to: CGSize(width: imageSymbolConfigurationSize, height: imageSymbolConfigurationSize)) - ?? uiImage + uiImage = uiImage.applyingSymbolConfiguration(fontSizeConfiguration).flatMap { uiImage in + uiImage.cropped(to: CGSize(width: symbolConfigurationSize, height: symbolConfigurationSize)) + } ?? uiImage } if insets != .zero { @@ -69,7 +72,7 @@ public struct ImageValue: source .image .renderingMode(primaryColor == nil ? .original : .template) - .ifImageLet(resizingMode) { $0 = $0.resizable(resizingMode: $1.resizingMode) } + .iflet(resizingMode) { $0.resizable(resizingMode: $1.resizingMode) } .iflet(primaryColor) { image, primaryColor in switch (secondaryColor, tertiaryColor) { case let (secondaryColor?, nil): @@ -79,6 +82,7 @@ public struct ImageValue: primaryColor.color, secondaryColor.color ) + case let (nil, tertiaryColor?): image .symbolRenderingMode(.palette) @@ -86,23 +90,24 @@ public struct ImageValue: primaryColor.color, tertiaryColor.color ) + case let (secondaryColor?, tertiaryColor?): image .symbolRenderingMode(.palette) .foregroundStyle( primaryColor.color, secondaryColor.color, - tertiaryColor.color, + tertiaryColor.color ) + case (nil, nil): - image - .foregroundStyle(primaryColor.color) + image.foregroundStyle(primaryColor.color) } } - .iflet(imageSymbolConfiguration) { - $0 - .font(.system(size: $1.size)) - .frame(width: $1.size, height: $1.size) + .iflet(symbolConfiguration) { image, symbolConfiguration in + image + .font(.system(size: symbolConfiguration.size)) + .frame(width: symbolConfiguration.size, height: symbolConfiguration.size) .clipped() } .if(insets != .zero) { $0.padding(insets.edgeInsets) } @@ -133,10 +138,10 @@ extension ImageValue: } public func foregroundColor(_ foregroundColor: ColorValue?) -> Self { - changing { - $0.primaryColor = foregroundColor - $0.secondaryColor = nil - $0.tertiaryColor = nil + changing { image in + image.primaryColor = foregroundColor + image.secondaryColor = nil + image.tertiaryColor = nil } } @@ -145,10 +150,10 @@ extension ImageValue: _ secondaryColor: ColorValue?, _ tertiaryColor: ColorValue? ) -> Self { - changing { - $0.primaryColor = primaryColor - $0.secondaryColor = secondaryColor - $0.tertiaryColor = tertiaryColor + changing { image in + image.primaryColor = primaryColor + image.secondaryColor = secondaryColor + image.tertiaryColor = tertiaryColor } } @@ -156,8 +161,8 @@ extension ImageValue: changing { $0.insets = insets } } - public func imageSymbolConfiguration(_ imageSymbolConfiguration: ImageSymbolConfiguration?) -> Self { - changing { $0.imageSymbolConfiguration = imageSymbolConfiguration } + public func symbolConfiguration(_ symbolConfiguration: ImageSymbolConfiguration?) -> Self { + changing { $0.symbolConfiguration = symbolConfiguration } } } diff --git a/Sources/Tools/Extensions/SwiftUI/Image+Extensions.swift b/Sources/Tools/Extensions/SwiftUI/Image+Extensions.swift index ca927f32..c10c0b7d 100644 --- a/Sources/Tools/Extensions/SwiftUI/Image+Extensions.swift +++ b/Sources/Tools/Extensions/SwiftUI/Image+Extensions.swift @@ -2,16 +2,14 @@ import SwiftUI extension Image { - @ViewBuilder - internal nonisolated func ifImageLet( + internal nonisolated func iflet( _ condition: T?, - _ apply: (inout Image, T) -> Void + _ content: (Self, _ value: T) -> Self ) -> Image { - var result = self - if let value = condition { - apply(&result, value) + content(self, value) + } else { + self } - return result } } diff --git a/Sources/Tools/Extensions/UIKit/UIImage+Crop.swift b/Sources/Tools/Extensions/UIKit/UIImage+Extensions.swift similarity index 85% rename from Sources/Tools/Extensions/UIKit/UIImage+Crop.swift rename to Sources/Tools/Extensions/UIKit/UIImage+Extensions.swift index d43e5a98..86362251 100644 --- a/Sources/Tools/Extensions/UIKit/UIImage+Crop.swift +++ b/Sources/Tools/Extensions/UIKit/UIImage+Extensions.swift @@ -3,12 +3,14 @@ import UIKit extension UIImage { - func crop(to targetSize: CGSize) -> UIImage? { + internal func cropped(to targetSize: CGSize) -> UIImage? { let format = UIGraphicsImageRendererFormat.default() + format.scale = scale + let renderer = UIGraphicsImageRenderer(size: targetSize, format: format) - return renderer.image { context in + return renderer.image { _ in let widthDiff = size.width - targetSize.width let heightDiff = size.height - targetSize.height @@ -16,6 +18,7 @@ extension UIImage { x: widthDiff.isZero ? .zero : -widthDiff / 2, y: heightDiff.isZero ? .zero : -heightDiff / 2 ) + self.draw(at: drawOrigin) } } From ab2e690e8b997e3e331f0dbf423d1c532cbbab6d Mon Sep 17 00:00:00 2001 From: Almaz Ibragimov Date: Fri, 2 Oct 2026 23:17:33 +0300 Subject: [PATCH 8/8] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Levitan.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Levitan Documentation.xcscheme | 2 +- Levitan.xcodeproj/xcshareddata/xcschemes/Levitan iOS.xcscheme | 2 +- Levitan.xcodeproj/xcshareddata/xcschemes/Levitan tvOS.xcscheme | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Levitan.xcodeproj/project.pbxproj b/Levitan.xcodeproj/project.pbxproj index 4be92c4e..cc389951 100755 --- a/Levitan.xcodeproj/project.pbxproj +++ b/Levitan.xcodeproj/project.pbxproj @@ -3037,7 +3037,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1130; - LastUpgradeCheck = 2610; + LastUpgradeCheck = 2700; ORGANIZATIONNAME = HeadHunter; TargetAttributes = { C057F00D23CB8EEB00C2D895 = { diff --git a/Levitan.xcodeproj/xcshareddata/xcschemes/Levitan Documentation.xcscheme b/Levitan.xcodeproj/xcshareddata/xcschemes/Levitan Documentation.xcscheme index c59bc883..2365dc35 100644 --- a/Levitan.xcodeproj/xcshareddata/xcschemes/Levitan Documentation.xcscheme +++ b/Levitan.xcodeproj/xcshareddata/xcschemes/Levitan Documentation.xcscheme @@ -1,6 +1,6 @@