Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 90 additions & 2 deletions Levitan.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
LastUpgradeVersion = "2700"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
LastUpgradeVersion = "2700"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
LastUpgradeVersion = "2700"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
44 changes: 42 additions & 2 deletions Sources/Component/Hosting/ComponentHostingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Content: View>: UIView {

private typealias HostingRoot = ComponentHostingRoot<Content>
Expand All @@ -24,6 +26,8 @@ public final class ComponentHostingView<Content: View>: UIView {

private weak var componentViewController: UIViewController?

private var mountingPolicy: ComponentMountingPolicy = .deferred

public override var intrinsicContentSize: CGSize {
hostingController?
.viewIfLoaded?
Expand Down Expand Up @@ -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,
Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
29 changes: 29 additions & 0 deletions Sources/Component/Hosting/ComponentMountingPolicy.swift
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion Sources/Text/Part/Extensions/ImageValue+TextPart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

public protocol DecorableByForegroundStyle {

func foregroundStyle(
_ primaryColor: ColorValue,
_ secondaryColor: ColorValue?,
_ tertiaryColor: ColorValue?
) -> Self
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Foundation

internal struct ForegroundStyleDecorator<Value: DecorableByForegroundStyle>: 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
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

public protocol DecorableByImageSymbolConfiguration {

func symbolConfiguration(_ symbolConfiguration: ImageSymbolConfiguration?) -> Self
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

internal struct ImageSymbolConfigurationDecorator<Value: DecorableByImageSymbolConfiguration>: TokenDecorator {

internal let symbolConfiguration: ImageSymbolConfiguration?

internal func decorate(_ value: Value, theme: TokenTheme) -> Value {
value.symbolConfiguration(symbolConfiguration)
}
}

extension Token where Value: DecorableByImageSymbolConfiguration {

public func symbolConfiguration(_ symbolConfiguration: ImageSymbolConfiguration?) -> Self {
decorated(by: ImageSymbolConfigurationDecorator(symbolConfiguration: symbolConfiguration))
}
}
14 changes: 14 additions & 0 deletions Sources/Token/Value/Image/ImageSymbolConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#if os(iOS) || os(tvOS)
import UIKit
#else
import AppKit
#endif

public struct ImageSymbolConfiguration: Sendable, Hashable {

public let size: CGFloat

public init(size: CGFloat) {
self.size = size
}
}
Loading
Loading