From 956c055cd80d04f7fbf7472aecd3fb8a9df00dc6 Mon Sep 17 00:00:00 2001 From: babaevmm Date: Mon, 4 Aug 2025 16:20:27 +0300 Subject: [PATCH] update swiftformat, add organize rule commit_hash:0f7ba811b37db6d2a9fc05efe5f94502feda4856 --- client/ios/.swiftformat | 6 + client/ios/.swiftlint.yml | 2 +- client/ios/DivKit/Animators/Animator.swift | 17 +- .../Animators/DivAnimatorController.swift | 11 +- .../DivKit/Animators/ValueInterpolator.swift | 2 + .../DebugBlock+UIViewRenderableBlock.swift | 51 +-- client/ios/DivKit/Debug/DebugBlock.swift | 8 +- .../DivKit/Debug/DebugErrorCollector.swift | 28 +- client/ios/DivKit/Debug/ErrorListView.swift | 8 +- client/ios/DivKit/Debug/TimeMeasure.swift | 4 +- .../ios/DivKit/DivBlockModelingContext.swift | 34 +- client/ios/DivKit/DivBlockStateStorage.swift | 111 +++---- client/ios/DivKit/DivFlagsInfo.swift | 5 +- client/ios/DivKit/DivKitComponents.swift | 8 +- client/ios/DivKit/DivVisibilityCounter.swift | 15 +- .../CalcExpression/CalcExpression.swift | 66 ++-- .../CustomFunctions/CustomFunction.swift | 5 +- .../CustomFunctions/DivFunctionsStorage.swift | 4 +- .../DivKit/Expressions/ExpressionError.swift | 40 +-- .../Expressions/ExpressionResolver.swift | 30 +- .../Expressions/Functions/Function.swift | 43 +-- .../Expressions/FunctionsProvider.swift | 19 +- .../DivBase/DivBaseBlockBuilder.swift | 169 +++++----- .../SizeProviderBlock+UIViewRenderable.swift | 19 +- .../DivKit/Patches/DivPatchDownloader.swift | 7 +- .../ios/DivKit/States/DivStateManager.swift | 83 ++--- .../DivPersistentValuesStorage.swift | 17 +- .../ios/DivKit/Templates/DivTemplates.swift | 4 +- .../DivKit/Timers/DivTimerController.swift | 4 +- .../DivKit/Variables/DivVariableStorage.swift | 15 +- .../DivKit/Variables/DivVariableValue.swift | 45 +-- .../Variables/DivVariablesStorage.swift | 112 +++---- .../ios/DivKit/Views/DivBlockProvider.swift | 198 ++++++------ client/ios/DivKit/Views/DivHostingView.swift | 4 +- client/ios/DivKit/Views/DivView.swift | 175 ++++++----- .../ios/DivKit/Views/DivViewPreloader.swift | 28 +- client/ios/DivKit/Views/DivViewSize.swift | 12 +- .../Animations/AnimationBlockView.swift | 17 +- .../Lottie/LottieAnimationBlock.swift | 2 + .../Lottie/LottieExtensionHandler.swift | 40 +-- .../Animations/RemoteAnimationHolder.swift | 4 +- .../Animations/Rive/RiveAnimationBlock.swift | 27 +- .../CustomImagePreviewExtensionHandler.swift | 1 + .../ImageExtensionHandler.swift | 1 + .../ImageThemeExtensionHandler.swift | 1 + .../InputAccessoryViewExtensionHandler.swift | 1 + .../PinchToZoomExtensionHandler.swift | 4 +- .../TextExtensionHandler.swift | 9 +- .../ShimmerImagePreviewExtension.swift | 1 + .../LottieExtensionHandlerTests.swift | 1 + .../ios/DivKitPlayground/AppComponents.swift | 11 +- .../DivKit/RiveDivCustomData.swift | 1 + .../DivKitPlaygroundApp.swift | 9 +- .../QRScanner/MetadataCaptureSession.swift | 7 +- .../QRScanner/ScannerViewController.swift | 4 +- .../Regression/RegressionTestModel.swift | 123 ++++---- .../Regression/RegressionView.swift | 17 +- .../UI/DivView/DivViewController.swift | 10 +- .../UI/DivView/DivViewProvider.swift | 1 + .../ios/DivKitPlayground/UI/SamplesView.swift | 4 +- .../ios/DivKitPlayground/Utils/TestData.swift | 14 +- .../Utils/UserPreferences.swift | 6 +- .../WebPreview/Networking/Payload.swift | 9 +- .../WebPreview/UI/WebPreviewView.swift | 3 +- .../UI/WebPreviewViewController.swift | 10 +- .../DivKitSnapshotTests.swift | 7 +- .../DivKitSnapshotTests/ReferenceSet.swift | 7 +- .../SnapshotTestRunner.swift | 4 +- .../Expressions/CustomFunctionTests.swift | 21 +- .../Expressions/ExpressionTests.swift | 91 +++--- .../Expressions/FunctionSignaturesTests.swift | 31 +- .../XCTestCase+ViewVisibilityCallCount.swift | 8 +- client/ios/DivKitTests/IntegrationTests.swift | 21 +- .../Templates/TemplateToTypeTests.swift | 21 +- client/ios/DivKitTests/Timers/Utils.swift | 6 +- client/ios/DivKitTests/ViewSizeTests.swift | 129 ++++---- .../DivKitTestsSupport/FakeImageHolder.swift | 11 +- .../ios/DivKitUITests/TooltipsUITests.swift | 21 +- .../LayoutKit/Interface/UIElementPath.swift | 59 ++-- .../LayoutKit/Base/MaskedInputViewModel.swift | 3 +- .../LayoutKit/Base/Masks/MaskValidator.swift | 8 +- .../Base/Masks/PhoneMaskFormatter.swift | 13 +- .../LayoutKit/Base/TextInputValidator.swift | 6 +- client/ios/LayoutKit/LayoutKit/Binding.swift | 3 +- .../LayoutKit/Blocks/ActionAnimation.swift | 4 +- .../Anchor/AnchorBlock+UIViewRenderable.swift | 57 ++-- .../LayoutKit/Blocks/Anchor/AnchorBlock.swift | 111 +++---- .../LayoutKit/Blocks/AnchorPoint.swift | 1 + .../LayoutKit/Blocks/Aspect/AspectBlock.swift | 18 +- .../LayoutKit/Blocks/BackgroundBlock.swift | 12 +- .../LayoutKit/Blocks/Block+Animation.swift | 33 +- .../LayoutKit/Blocks/BlockAlignment2D.swift | 5 +- .../LayoutKit/Blocks/BlockTooltip.swift | 8 +- .../LayoutKit/Blocks/BlockTooltipParams.swift | 3 +- .../Blocks/Container/ContainerBlock.swift | 296 +++++++++--------- .../Container/ContainerBlockLayout.swift | 70 +++-- .../Blocks/Container/WrapLayoutGroups.swift | 34 +- .../Blocks/Decorations/DecoratingBlock.swift | 26 +- .../LayoutKit/Blocks/EmptyBlock.swift | 16 +- .../LayoutKit/Blocks/GalleryBlock.swift | 28 +- .../LayoutKit/Blocks/GradientBlock.swift | 28 +- .../LayoutKit/Blocks/Grid/Grid.swift | 21 +- .../LayoutKit/Blocks/Grid/GridBlock.swift | 147 ++++----- .../LayoutKit/Blocks/ImageBaseBlock.swift | 1 + .../LayoutKit/Blocks/LaidOutBlock.swift | 49 +-- .../LayoutKit/Blocks/LayeredBlock.swift | 28 +- .../LayoutKit/Blocks/LayoutTrait.swift | 4 +- .../LayoutKit/Blocks/PageControlBlock.swift | 36 +-- .../LayoutKit/Blocks/PagerBlock.swift | 33 +- .../LayoutKit/Blocks/SeparatorBlock.swift | 32 +- .../LayoutKit/Blocks/Shadow/BlockShadow.swift | 4 +- .../Blocks/Slider/MarksConfiguration.swift | 7 +- .../Slider/MarksConfigurationModel.swift | 30 +- .../LayoutKit/Blocks/Slider/MarksLayer.swift | 35 ++- .../LayoutKit/Blocks/Slider/MarksView.swift | 11 +- .../LayoutKit/Blocks/Slider/SliderBlock.swift | 36 ++- .../LayoutKit/Blocks/Slider/SliderModel.swift | 50 +-- .../LayoutKit/Blocks/Slider/SliderView.swift | 178 +++++------ .../Blocks/SwipeContainerBlock.swift | 13 +- .../LayoutKit/Blocks/SwitchBlock.swift | 56 ++-- .../Blocks/SwitchableContainerBlock.swift | 114 +++---- .../SwitchableContainerBlockLayout.swift | 25 +- .../LayoutKit/Blocks/TabsBlock.swift | 28 +- .../LayoutKit/Blocks/TextBlock.swift | 66 ++-- .../LayoutKit/Blocks/TextFieldBlock.swift | 140 +++++---- .../LayoutKit/Blocks/TextInputBlock.swift | 56 ++-- .../Blocks/TransitioningAnimation.swift | 14 +- .../LayoutKit/Blocks/TransitioningBlock.swift | 15 +- .../Video/CorePlayer/CorePlayerImpl.swift | 46 +-- .../Video/DefaultPlayer/DefaultPlayer.swift | 8 +- .../DefaultPlayer/DefaultPlayerFactory.swift | 7 +- .../DefaultPlayer/DefaultPlayerView.swift | 31 +- .../Blocks/Video/PlayerFactory.swift | 16 +- .../LayoutKit/Blocks/Video/VideoBlock.swift | 21 +- .../LayoutKit/Blocks/VideoBlockLegacy.swift | 11 +- .../LayoutKit/Blocks/VisibilityAction.swift | 1 + .../LayoutKit/GenericViewBlock.swift | 23 +- .../LayoutKit/Tooltips/TooltipManager.swift | 26 +- .../UI/Actions/UIActionEventPerforming.swift | 8 +- .../Base/GenericCollectionReusableView.swift | 9 +- .../UI/Base/GenericCollectionViewCell.swift | 13 +- .../UI/Base/GenericCollectionViewLayout.swift | 15 +- .../IndicatorStateAnimator.swift | 19 +- .../PageIndicator/PageIndicatorLayer.swift | 1 + .../PageIndicator/PageIndicatorView.swift | 11 +- .../UI/Base/ScrollableContentPager.swift | 9 +- .../LayoutKit/UI/Base/VisibilityState.swift | 11 +- ...ssibilityBlock+UIViewRenderableBlock.swift | 23 +- ...ableImageBlock+UIViewRenderableBlock.swift | 52 +-- ...ackgroundBlock+UIViewRenderableBlock.swift | 43 +-- ...ContainerBlock+UIViewRenderableBlock.swift | 98 +++--- ...DebugInfoBlock+UIViewRenderableBlock.swift | 10 +- ...ecoratingBlock+UIViewRenderableBlock.swift | 269 ++++++++-------- ...AnimationBlock+UIViewRenderableBlock.swift | 72 ++--- .../GestureBlock+UIViewRenderableBlock.swift | 10 +- .../GridBlock+UIViewRenderableBlock.swift | 62 ++-- .../LayeredBlock+UIViewRenderableBlock.swift | 54 ++-- .../Blocks/MaskedBlock+UIViewRenderable.swift | 17 +- ...geControlBlock+UIViewRenderableBlock.swift | 4 +- .../PagerBlock+UIViewRenderableBlock.swift | 31 +- ...nchToZoomBlock+UIViewRenderableBlock.swift | 34 +- .../StateBlock+UIViewRenderableBlock.swift | 24 +- ...SwipeContainerBlock+UIViewRenderable.swift | 48 +-- .../SwitchBlock+UIViewRenderableBlock.swift | 4 +- ...ContainerBlock+UIViewRenderableBlock.swift | 9 +- .../TextBlock+UIViewRenderableBlock.swift | 259 +++++++-------- ...TextFieldBlock+UIViewRenderableBlock.swift | 191 +++++------ ...TextInputBlock+UIViewRenderableBlock.swift | 107 +++---- .../LayoutKit/UI/Blocks/TextSelection.swift | 159 +++++----- .../UI/Blocks/TooltipContainerView.swift | 34 +- ...sitioningBlock+UIViewRenderableBlock.swift | 50 +-- .../VideoBlock+UIViewRenderableBlock.swift | 79 ++--- ...deoBlockLegacy+UIViewRenderableBlock.swift | 20 +- .../UI/Views/AnimatingGradientView.swift | 30 +- .../UI/Views/Background+UIViewRendering.swift | 6 +- .../LayoutKit/UI/Views/BlockHostingView.swift | 4 +- .../LayoutKit/UI/Views/BoxShadowView.swift | 17 +- .../UI/Views/ContextMenuDelegate.swift | 16 +- .../LayoutKit/UI/Views/GalleryView.swift | 133 ++++---- .../UI/Views/NinePatchImageView.swift | 4 +- .../UI/Views/SegmentedProgressView.swift | 6 +- .../Views/TabbedPages/TabContentsView.swift | 159 +++++----- .../TabListSelectionDataSourceImpl.swift | 8 +- .../UI/Views/TabbedPages/TabListView.swift | 26 +- .../TabbedPages/TabListViewDataSource.swift | 17 +- .../TabbedPages/TabListViewDelegate.swift | 29 +- .../TabTitleDelimiterReusableView.swift | 12 +- .../Views/TabbedPages/TabbedPagesView.swift | 77 ++--- .../TabsCollectionViewFlowLayout.swift | 8 +- .../VisibleBoundsTrackingCollectionView.swift | 14 +- .../ViewModels/GalleryViewLayout.swift | 7 +- .../ViewModels/GalleryViewModel.swift | 15 +- .../LayoutKit/ViewModels/InsetMode.swift | 5 +- .../ViewModels/PagerViewLayout.swift | 7 +- .../LayoutKit/ViewModels/PagerViewState.swift | 11 +- .../TabbedPages/TabContentsViewModel.swift | 19 +- .../ViewModels/TabbedPages/TabViewState.swift | 4 +- .../VisibilityActionPerformer.swift | 7 +- .../Blocks/BlockWithFixedWrapContent.swift | 16 +- .../Blocks/GridBlockTests.swift | 125 ++++---- .../Blocks/Array+UIViewRenderableTests.swift | 19 +- .../UI/Views/TextSelectionTests.swift | 93 +++--- .../ios/LayoutKit/LayoutKitTests/Utils.swift | 6 +- .../ViewModels/GalleryViewLayoutTests.swift | 79 ++--- .../TabContentsViewModelTests.swift | 7 +- .../Contents.swift | 11 +- .../LayoutKitSnapshotTests/ReferenceSet.swift | 5 +- .../SampleDivCustomBlockFactory.swift | 11 +- .../SampleDivCustomBlockFactory.swift | 11 +- .../Serialization/DeserializationError.swift | 83 ++--- 210 files changed, 3773 insertions(+), 3579 deletions(-) diff --git a/client/ios/.swiftformat b/client/ios/.swiftformat index d4a63982f..7f353e5b0 100644 --- a/client/ios/.swiftformat +++ b/client/ios/.swiftformat @@ -21,3 +21,9 @@ --wraparguments before-first --wrapcollections before-first --wrapparameters before-first + + +--rules organizeDeclarations +# options for organizeDeclarations +--organization-mode type +--mark-categories false \ No newline at end of file diff --git a/client/ios/.swiftlint.yml b/client/ios/.swiftlint.yml index e07af25f1..0e49b1c8c 100644 --- a/client/ios/.swiftlint.yml +++ b/client/ios/.swiftlint.yml @@ -58,7 +58,7 @@ custom_rules: message: "Such a cast to CF* type is unsafe. Use Serialization.safeCFCast (or add one if missing)" no_direct_use_of_repeating_count_initializer: name: "Dangerous repeating initializer" - regex: '\(\s*repeating\s*:\s*\S.*,\s*count\s*:\s*\S.*\)' + regex: '\(\s*repeating\s*:\s*[^,]*,\s*count\s*:\s*' message: "Use init(repeating:times:) instead" severity: warning no_direct_use_of_unique_keys_with_values_initializer: diff --git a/client/ios/DivKit/Animators/Animator.swift b/client/ios/DivKit/Animators/Animator.swift index d163f63bc..79dc6dc9a 100644 --- a/client/ios/DivKit/Animators/Animator.swift +++ b/client/ios/DivKit/Animators/Animator.swift @@ -46,6 +46,7 @@ final class ValueAnimator: Animator { } let id: String + private var configuration: Configuration private let animationBlock: (AnimatedType) -> Void private let valueInterpolator: I @@ -135,6 +136,14 @@ final class ValueAnimator: Animator { } } + func stop() { + item?.cancel() + item = nil + displayLink?.invalidate() + displayLink = nil + cancelAction() + } + @objc private func update() { guard let startTime, displayLink != nil else { return } @@ -180,13 +189,6 @@ final class ValueAnimator: Animator { } } - func stop() { - item?.cancel() - item = nil - displayLink?.invalidate() - displayLink = nil - cancelAction() - } } #if os(iOS) @@ -201,6 +203,7 @@ extension CADisplayLink { private let currentMediaTime = { Double(0) } private class DisplayLink { init(target _: Any, selector _: Selector) {} + func invalidate() {} func addToCurrentRunLoop() {} } diff --git a/client/ios/DivKit/Animators/DivAnimatorController.swift b/client/ios/DivKit/Animators/DivAnimatorController.swift index 724947301..00d68b06e 100644 --- a/client/ios/DivKit/Animators/DivAnimatorController.swift +++ b/client/ios/DivKit/Animators/DivAnimatorController.swift @@ -6,6 +6,12 @@ final class DivAnimatorController { private var animators = [UIElementPath: Animator]() private let lock = AllocatedUnfairLock() + deinit { + lock.withLock { + animators.values.forEach { $0.stop() } + } + } + func startAnimator( path: UIElementPath, id: String, @@ -61,9 +67,4 @@ final class DivAnimatorController { } } - deinit { - lock.withLock { - animators.values.forEach { $0.stop() } - } - } } diff --git a/client/ios/DivKit/Animators/ValueInterpolator.swift b/client/ios/DivKit/Animators/ValueInterpolator.swift index 48314c795..e89a981c9 100644 --- a/client/ios/DivKit/Animators/ValueInterpolator.swift +++ b/client/ios/DivKit/Animators/ValueInterpolator.swift @@ -28,6 +28,7 @@ extension Double: BuildFromDivVariable { struct DoubleInterpolator: ValueInterpolator { typealias ValueType = Double + func interpolate(from: Double, to: Double, progress: CGFloat) -> Double { from + (to - from) * progress } @@ -49,6 +50,7 @@ extension Color: BuildFromDivVariable { struct ColorInterpolator: ValueInterpolator { typealias ValueType = Color + func interpolate(from: Color, to: Color, progress: CGFloat) -> Color { guard let fromComponents = from.cgColor.components, let toComponents = to.cgColor.components, fromComponents.count >= 3, toComponents.count >= 3 else { diff --git a/client/ios/DivKit/Debug/DebugBlock+UIViewRenderableBlock.swift b/client/ios/DivKit/Debug/DebugBlock+UIViewRenderableBlock.swift index 014096c43..3ad4b8584 100644 --- a/client/ios/DivKit/Debug/DebugBlock+UIViewRenderableBlock.swift +++ b/client/ios/DivKit/Debug/DebugBlock+UIViewRenderableBlock.swift @@ -32,14 +32,6 @@ extension DebugBlock: UIViewRenderable { private final class DebugBlockView: BlockView, VisibleBoundsTrackingContainer { private var childView: BlockView? - var effectiveBackgroundColor: UIColor? { - childView?.backgroundColor - } - - var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { - childView.map { [$0] } ?? [] - } - private var showDebugInfo: ((ViewType) -> Void)? private var errorCollector: DebugErrorCollector? private let disposePool = AutodisposePool() @@ -54,6 +46,14 @@ private final class DebugBlockView: BlockView, VisibleBoundsTrackingContainer { return button }() + var effectiveBackgroundColor: UIColor? { + childView?.backgroundColor + } + + var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { + childView.map { [$0] } ?? [] + } + init() { super.init(frame: .zero) addSubview(errorsButton) @@ -65,6 +65,21 @@ private final class DebugBlockView: BlockView, VisibleBoundsTrackingContainer { fatalError("init(coder:) has not been implemented") } + override func layoutSubviews() { + super.layoutSubviews() + childView?.frame = bounds + errorsButton.frame = CGRect( + center: CGPoint(x: buttonSize / 2.0, y: bounds.midY), + size: CGSize(squareDimension: buttonSize) + ) + errorsButton.layer.cornerRadius = buttonSize / 2.0 + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + return result === self ? nil : result + } + func configure( child: Block, errorCollector: DebugErrorCollector, @@ -91,14 +106,9 @@ private final class DebugBlockView: BlockView, VisibleBoundsTrackingContainer { setNeedsLayout() } - override func layoutSubviews() { - super.layoutSubviews() - childView?.frame = bounds - errorsButton.frame = CGRect( - center: CGPoint(x: buttonSize / 2.0, y: bounds.midY), - size: CGSize(squareDimension: buttonSize) - ) - errorsButton.layer.cornerRadius = buttonSize / 2.0 + @objc func errorsButtonTapped() { + guard let showDebugInfo, let errorCollector, errorCollector.totalErrorCount > 0 else { return } + showDebugInfo(ErrorListView(errors: errorCollector.errorList)) } private func updateCountLabel() { @@ -108,15 +118,6 @@ private final class DebugBlockView: BlockView, VisibleBoundsTrackingContainer { errorsButton.setTitle("\(min(maxCount, errorsCount))", for: .normal) } - @objc func errorsButtonTapped() { - guard let showDebugInfo, let errorCollector, errorCollector.totalErrorCount > 0 else { return } - showDebugInfo(ErrorListView(errors: errorCollector.errorList)) - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let result = super.hitTest(point, with: event) - return result === self ? nil : result - } } private let showOverlayURL = URL(string: "debugInfo://show")! diff --git a/client/ios/DivKit/Debug/DebugBlock.swift b/client/ios/DivKit/Debug/DebugBlock.swift index 4de506451..3451c7e0a 100644 --- a/client/ios/DivKit/Debug/DebugBlock.swift +++ b/client/ios/DivKit/Debug/DebugBlock.swift @@ -8,6 +8,10 @@ final class DebugBlock: WrapperBlock, LayoutCachingDefaultImpl { let errorCollector: DebugErrorCollector let showDebugInfo: (ViewType) -> Void + var debugDescription: String { + "DebugBlock errors: \(errorCollector.debugDescription). Child: \(child)" + } + init( child: Block, errorCollector: DebugErrorCollector, @@ -32,10 +36,6 @@ final class DebugBlock: WrapperBlock, LayoutCachingDefaultImpl { return errorCollector === other.errorCollector && child.equals(other.child) } - var debugDescription: String { - "DebugBlock errors: \(errorCollector.debugDescription). Child: \(child)" - } - func getImageHolders() -> [any VGSLUI.ImageHolder] { child.getImageHolders() } diff --git a/client/ios/DivKit/Debug/DebugErrorCollector.swift b/client/ios/DivKit/Debug/DebugErrorCollector.swift index abe1ca505..9c7e0bb7d 100644 --- a/client/ios/DivKit/Debug/DebugErrorCollector.swift +++ b/client/ios/DivKit/Debug/DebugErrorCollector.swift @@ -2,13 +2,25 @@ import LayoutKit import VGSLFundamentals final class DebugErrorCollector: DivReporter { - private let wrappedDivReporter: DivReporter - var errorStorage: DivErrorsStorage private(set) var layoutErrors = [DivError]() let observableErrorCount = ObservableProperty(initialValue: 0) + private let wrappedDivReporter: DivReporter + + var totalErrorCount: Int { + errorStorage.errors.count + layoutErrors.count + } + + var errorList: [String] { + errorStorage.errors.map(\.prettyMessage) + layoutErrors.map(\.prettyMessage) + } + + var debugDescription: String { + "Errors: \(errorList)" + } + init( wrappedDivReporter: DivReporter, errorStorage: DivErrorsStorage @@ -28,18 +40,6 @@ final class DebugErrorCollector: DivReporter { wrappedDivReporter.reportAction(cardId: cardId, info: info) } - var totalErrorCount: Int { - errorStorage.errors.count + layoutErrors.count - } - - var errorList: [String] { - errorStorage.errors.map(\.prettyMessage) + layoutErrors.map(\.prettyMessage) - } - - var debugDescription: String { - "Errors: \(errorList)" - } - private func hasError(_ error: DivError) -> Bool { layoutErrors.contains { $0.kind == error.kind diff --git a/client/ios/DivKit/Debug/ErrorListView.swift b/client/ios/DivKit/Debug/ErrorListView.swift index 2c1b3050d..ae1db7561 100644 --- a/client/ios/DivKit/Debug/ErrorListView.swift +++ b/client/ios/DivKit/Debug/ErrorListView.swift @@ -18,15 +18,15 @@ final class ErrorListView: UIView { addSubview(errorView) } - public override func layoutSubviews() { - self.errorView.frame = self.bounds.inset(by: safeAreaInsets) - } - @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } + public override func layoutSubviews() { + self.errorView.frame = self.bounds.inset(by: safeAreaInsets) + } + private func copyToPasteboard() { UIPasteboard.general.string = errorString } diff --git a/client/ios/DivKit/Debug/TimeMeasure.swift b/client/ios/DivKit/Debug/TimeMeasure.swift index 429f57b78..ee1cd3173 100644 --- a/client/ios/DivKit/Debug/TimeMeasure.swift +++ b/client/ios/DivKit/Debug/TimeMeasure.swift @@ -16,11 +16,11 @@ public final class TimeMeasure: @unchecked Sendable { case warm } + public private(set) var time: Time? + private var startTime: Date? private var status: Status = .cold - public private(set) var time: Time? - init() {} public func updateMeasure(action: () throws -> T) throws -> T { diff --git a/client/ios/DivKit/DivBlockModelingContext.swift b/client/ios/DivKit/DivBlockModelingContext.swift index df327b558..87aaeeb91 100644 --- a/client/ios/DivKit/DivBlockModelingContext.swift +++ b/client/ios/DivKit/DivBlockModelingContext.swift @@ -9,17 +9,24 @@ import AppKit #endif public struct DivBlockModelingContext { + public let actionHandler: DivActionHandler? + public let blockStateStorage: DivBlockStateStorage + public let imageHolderFactory: DivImageHolderFactory + public let fontProvider: DivFontProvider + public private(set) var errorsStorage: DivErrorsStorage + public let variablesStorage: DivVariablesStorage + public private(set) var expressionResolver: ExpressionResolver + public let variableTracker: DivVariableTracker? + public private(set) var path: UIElementPath + public private(set) var currentDivId: String? + private(set) var viewId: DivViewId private(set) var cardLogId: String? private(set) var parentDivStatePath: DivStatePath? let stateManager: DivStateManager - public let actionHandler: DivActionHandler? - public let blockStateStorage: DivBlockStateStorage let visibilityCounter: DivVisibilityCounter let lastVisibleBoundsCache: DivLastVisibleBoundsCache - public let imageHolderFactory: DivImageHolderFactory let divCustomBlockFactory: DivCustomBlockFactory - public let fontProvider: DivFontProvider let flagsInfo: DivFlagsInfo let extensionHandlers: [String: DivExtensionHandler] let layoutDirection: UserInterfaceLayoutDirection @@ -27,18 +34,10 @@ public struct DivBlockModelingContext { let scheduler: Scheduling let playerFactory: PlayerFactory? private(set) weak var parentScrollView: ScrollView? - public private(set) var errorsStorage: DivErrorsStorage let debugErrorCollector: DebugErrorCollector? - private let persistentValuesStorage: DivPersistentValuesStorage let tooltipViewFactory: DivTooltipViewFactory? let functionsStorage: DivFunctionsStorage? - public let variablesStorage: DivVariablesStorage let triggersStorage: DivTriggersStorage? - public private(set) var expressionResolver: ExpressionResolver - private let functionsProvider: FunctionsProvider - public let variableTracker: DivVariableTracker? - public private(set) var path: UIElementPath - public private(set) var currentDivId: String? // Overriden id for modified contexts of child divs, used in prototypes private(set) var overridenId: String? private(set) var sizeModifier: DivSizeModifier? @@ -48,11 +47,18 @@ public struct DivBlockModelingContext { let animatorController: DivAnimatorController? private(set) var accessibilityElementsStorage = DivAccessibilityElementsStorage() + private let persistentValuesStorage: DivPersistentValuesStorage + private let functionsProvider: FunctionsProvider + // Deprecated, `parentPath` was changed to `path` public var parentPath: UIElementPath { path } + public var cardId: DivCardID { + viewId.cardId + } + @_spi(Internal) public init( cardId: DivCardID, @@ -171,10 +177,6 @@ public struct DivBlockModelingContext { ) } - public var cardId: DivCardID { - viewId.cardId - } - public func getExtensionHandlers(for div: DivBase) -> [DivExtensionHandler] { (div.extensions ?? []).compactMap { let id = $0.id diff --git a/client/ios/DivKit/DivBlockStateStorage.swift b/client/ios/DivKit/DivBlockStateStorage.swift index 6b9c5758e..084bf0241 100644 --- a/client/ios/DivKit/DivBlockStateStorage.swift +++ b/client/ios/DivKit/DivBlockStateStorage.swift @@ -29,9 +29,26 @@ public final class DivBlockStateStorage { case idFocused(IdAndCardId) } + private(set) var isInputFocused = false + private var shouldRefreshCachedStates = false private var _cachedStates: BlocksState? + private var _states: [StateKey: ElementState] { + didSet { + shouldRefreshCachedStates = true + } + } + + private var focusedElement: FocusedElement = .none { + didSet { + isInputFocused = false + } + } + + private let lock = AllocatedUnfairLock() + private let stateUpdatesPipe = SignalPipe() + public var states: BlocksState { if let cached = _cachedStates, !shouldRefreshCachedStates { @@ -56,23 +73,6 @@ public final class DivBlockStateStorage { } } - private var _states: [StateKey: ElementState] { - didSet { - shouldRefreshCachedStates = true - } - } - - private var focusedElement: FocusedElement = .none { - didSet { - isInputFocused = false - } - } - - private let lock = AllocatedUnfairLock() - private let stateUpdatesPipe = SignalPipe() - - private(set) var isInputFocused = false - var stateUpdates: Signal { stateUpdatesPipe.signal } @@ -147,12 +147,6 @@ public final class DivBlockStateStorage { } } - func setFocused(isFocused: Bool, element: IdAndCardId) { - lock.withLock { - focusedElement = isFocused ? .idFocused(element) : removeFocus(from: element) - } - } - public func setFocused( isFocused: Bool, path: UIElementPath @@ -168,22 +162,55 @@ public final class DivBlockStateStorage { } } - func isFocused(element: IdAndCardId) -> Bool { - lock.withLock { - isFocusedInternal(checkedElement: .idFocused(element)) - } - } - public func isFocused(path: UIElementPath) -> Bool { lock.withLock { isFocusedInternal(checkedElement: .pathFocused(path)) } } + public func reset() { + lock.withLock { + _states = [:] + focusedElement = .none + } + } + + public func reset(cardId: DivCardID) { + lock.withLock { + _states = _states.filter { $0.key.cardID != cardId } + if getFocusedElement()?.cardId == cardId { + focusedElement = .none + } + } + } + + func setFocused(isFocused: Bool, element: IdAndCardId) { + lock.withLock { + focusedElement = isFocused ? .idFocused(element) : removeFocus(from: element) + } + } + + func isFocused(element: IdAndCardId) -> Bool { + lock.withLock { + isFocusedInternal(checkedElement: .idFocused(element)) + } + } + func setInputFocused() { isInputFocused = true } + func getFocusedElement() -> IdAndCardId? { + switch focusedElement { + case .none: + nil + case let .pathFocused(focusedPath): + IdAndCardId(path: focusedPath) + case let .idFocused(focusedId): + focusedId + } + } + private func isFocusedInternal(checkedElement: FocusedElement) -> Bool { switch (focusedElement, checkedElement) { case (.none, _), (_, .none): @@ -203,32 +230,6 @@ public final class DivBlockStateStorage { isFocusedInternal(checkedElement: FocusedElement.idFocused(element)) ? .none : focusedElement } - func getFocusedElement() -> IdAndCardId? { - switch focusedElement { - case .none: - nil - case let .pathFocused(focusedPath): - IdAndCardId(path: focusedPath) - case let .idFocused(focusedId): - focusedId - } - } - - public func reset() { - lock.withLock { - _states = [:] - focusedElement = .none - } - } - - public func reset(cardId: DivCardID) { - lock.withLock { - _states = _states.filter { $0.key.cardID != cardId } - if getFocusedElement()?.cardId == cardId { - focusedElement = .none - } - } - } } extension DivBlockStateStorage: ElementStateObserver { diff --git a/client/ios/DivKit/DivFlagsInfo.swift b/client/ios/DivKit/DivFlagsInfo.swift index f1c39b433..4f134a996 100644 --- a/client/ios/DivKit/DivFlagsInfo.swift +++ b/client/ios/DivKit/DivFlagsInfo.swift @@ -6,6 +6,9 @@ /// included in the framework. /// You can access the default `DivFlagsInfo` instance using the static property `default`. public struct DivFlagsInfo { + /// The default instance of `DivFlagsInfo`. + public static let `default` = DivFlagsInfo() + /// Defines the behavior of the visibility/disappear actions. /// /// `true` - visibility action URLs will be handled by `DivUrlHandler` the same way regular @@ -65,6 +68,4 @@ public struct DivFlagsInfo { self.fontCacheEnabled = fontCacheEnabled } - /// The default instance of `DivFlagsInfo`. - public static let `default` = DivFlagsInfo() } diff --git a/client/ios/DivKit/DivKitComponents.swift b/client/ios/DivKit/DivKitComponents.swift index 684266e34..d98e8815c 100644 --- a/client/ios/DivKit/DivKitComponents.swift +++ b/client/ios/DivKit/DivKitComponents.swift @@ -32,10 +32,6 @@ public final class DivKitComponents { public let visibilityCounter = DivVisibilityCounter() public let resourcesPreloader: DivDataResourcesPreloader? - public var updateCardSignal: Signal<[DivCardUpdateReason]> { - updateCardPipe.signal - } - private let animatorController = DivAnimatorController() private let disposePool = AutodisposePool() private let idToPath = IdToPath() @@ -50,6 +46,10 @@ public final class DivKitComponents { private let variableTracker = DivVariableTracker() private var debugErrorCollectors = [DivCardID: DebugErrorCollector]() + public var updateCardSignal: Signal<[DivCardUpdateReason]> { + updateCardPipe.signal + } + /// You can create an instance of `DivKitComponents` with various optional parameters that allow /// you to customize the behavior and functionality of `DivKit` to suit your specific needs. /// diff --git a/client/ios/DivKit/DivVisibilityCounter.swift b/client/ios/DivKit/DivVisibilityCounter.swift index 20671ad18..cf684846f 100644 --- a/client/ios/DivKit/DivVisibilityCounter.swift +++ b/client/ios/DivKit/DivVisibilityCounter.swift @@ -6,6 +6,14 @@ public final class DivVisibilityCounter { init() {} + public func reset() { + storage.removeAll() + } + + public func reset(cardId: DivCardID) { + storage = storage.filter { $0.key.root != cardId.rawValue } + } + func visibilityCount(for path: UIElementPath) -> UInt { storage[path] ?? 0 } @@ -14,11 +22,4 @@ public final class DivVisibilityCounter { storage[path] = visibilityCount(for: path) + 1 } - public func reset() { - storage.removeAll() - } - - public func reset(cardId: DivCardID) { - storage = storage.filter { $0.key.root != cardId.rawValue } - } } diff --git a/client/ios/DivKit/Expressions/CalcExpression/CalcExpression.swift b/client/ios/DivKit/Expressions/CalcExpression/CalcExpression.swift index f3372660c..8489f2225 100644 --- a/client/ios/DivKit/Expressions/CalcExpression/CalcExpression.swift +++ b/client/ios/DivKit/Expressions/CalcExpression/CalcExpression.swift @@ -35,11 +35,6 @@ import Foundation import VGSL struct CalcExpression { - static func parse(_ expression: String) throws -> CalcExpression { - var unicodeScalarView = UnicodeScalarView(expression.unicodeScalars) - return try CalcExpression(root: unicodeScalarView.parseSubexpression(upTo: [])) - } - private let root: Subexpression var variableNames: [String] { @@ -51,6 +46,11 @@ struct CalcExpression { } } + static func parse(_ expression: String) throws -> CalcExpression { + var unicodeScalarView = UnicodeScalarView(expression.unicodeScalars) + return try CalcExpression(root: unicodeScalarView.parseSubexpression(upTo: [])) + } + func extractDynamicVariableNames(_ context: ExpressionContext) throws -> [String] { try root.extractDynamicVariableNames(context) } @@ -78,18 +78,6 @@ enum Subexpression { } } - func evaluate(_ context: ExpressionContext) throws -> Any { - switch self { - case let .literal(value): - return value - case let .symbol(symbol, args): - if let evaluator = context.evaluators(symbol) { - return try evaluator.invoke(args: args, context: context) - } - throw ExpressionError("Undefined symbol: \(symbol.name).") - } - } - var symbols: Set { switch self { case .literal: @@ -103,6 +91,18 @@ enum Subexpression { } } + func evaluate(_ context: ExpressionContext) throws -> Any { + switch self { + case let .literal(value): + return value + case let .symbol(symbol, args): + if let evaluator = context.evaluators(symbol) { + return try evaluator.invoke(args: args, context: context) + } + throw ExpressionError("Undefined symbol: \(symbol.name).") + } + } + func extractDynamicVariableNames(_ context: ExpressionContext) throws -> [String] { switch self { case .literal: @@ -129,14 +129,14 @@ enum Subexpression { fileprivate struct UnicodeScalarView { typealias Index = String.UnicodeScalarView.Index - private let characters: String.UnicodeScalarView private(set) var startIndex: Index private(set) var endIndex: Index - init(_ unicodeScalars: String.UnicodeScalarView) { - characters = unicodeScalars - startIndex = characters.startIndex - endIndex = characters.endIndex + private let characters: String.UnicodeScalarView + + /// Returns the remaining characters + var unicodeScalars: Substring.UnicodeScalarView { + characters[startIndex..= endIndex } - private subscript(_ index: Index) -> UnicodeScalar { - characters[index] - } - - private func index(after index: Index) -> Index { - characters.index(after: index) + init(_ unicodeScalars: String.UnicodeScalarView) { + characters = unicodeScalars + startIndex = characters.startIndex + endIndex = characters.endIndex } func prefix(upTo index: Index) -> UnicodeScalarView { @@ -169,6 +167,14 @@ fileprivate struct UnicodeScalarView { return view } + private subscript(_ index: Index) -> UnicodeScalar { + characters[index] + } + + private func index(after index: Index) -> Index { + characters.index(after: index) + } + private mutating func popFirst() -> UnicodeScalar? { if isEmpty { return nil @@ -178,10 +184,6 @@ fileprivate struct UnicodeScalarView { return char } - /// Returns the remaining characters - var unicodeScalars: Substring.UnicodeScalarView { - characters[startIndex.. Error { ExpressionError("Integer overflow.") } @@ -31,23 +52,4 @@ public struct ExpressionError: LocalizedError, CustomStringConvertible { ) } - public let description: String - let message: String - - init( - _ message: String, - expression: String? = nil - ) { - self.message = message - - description = if let expression { - "\(message) Expression: \(expression)" - } else { - message - } - } - - public var errorDescription: String? { - description - } } diff --git a/client/ios/DivKit/Expressions/ExpressionResolver.swift b/client/ios/DivKit/Expressions/ExpressionResolver.swift index f6e59f005..f9458db59 100644 --- a/client/ios/DivKit/Expressions/ExpressionResolver.swift +++ b/client/ios/DivKit/Expressions/ExpressionResolver.swift @@ -18,6 +18,21 @@ public final class ExpressionResolver { errorTracker: errorTracker ) + @_spi(Legacy) + public convenience init( + variableValueProvider: @escaping (String) -> Any?, + persistentValuesStorage: DivPersistentValuesStorage = DivPersistentValuesStorage(), + errorTracker: ExpressionErrorTracker? = nil + ) { + self.init( + functionsProvider: FunctionsProvider( + persistentValuesStorage: persistentValuesStorage + ), + variableValueProvider: variableValueProvider, + errorTracker: { errorTracker?($0) } + ) + } + init( functionsProvider: FunctionsProvider, customFunctionsStorageProvider: @escaping (String) -> DivFunctionsStorage? = { _ in nil }, @@ -50,21 +65,6 @@ public final class ExpressionResolver { self.errorTracker = reporter.asExpressionErrorTracker(cardId: path.cardId) } - @_spi(Legacy) - public convenience init( - variableValueProvider: @escaping (String) -> Any?, - persistentValuesStorage: DivPersistentValuesStorage = DivPersistentValuesStorage(), - errorTracker: ExpressionErrorTracker? = nil - ) { - self.init( - functionsProvider: FunctionsProvider( - persistentValuesStorage: persistentValuesStorage - ), - variableValueProvider: variableValueProvider, - errorTracker: { errorTracker?($0) } - ) - } - public func resolve(_ expression: String) -> Any? { if let link: ExpressionLink = makeLink(expression) { return resolveAnyLink(link) diff --git a/client/ios/DivKit/Expressions/Functions/Function.swift b/client/ios/DivKit/Expressions/Functions/Function.swift index cbad36d59..8edffb51e 100644 --- a/client/ios/DivKit/Expressions/Functions/Function.swift +++ b/client/ios/DivKit/Expressions/Functions/Function.swift @@ -34,17 +34,17 @@ extension SimpleFunction { struct NoMatchingSignatureError: Error {} struct ConstantFunction: SimpleFunction { + let signature = FunctionSignature( + arguments: [], + resultType: R.self + ) + private let value: R init(_ value: R) { self.value = value } - let signature = FunctionSignature( - arguments: [], - resultType: R.self - ) - func invoke(_: [Any], context _: ExpressionContext) throws -> Any { value } @@ -67,13 +67,13 @@ struct LazyFunction: Function { } struct FunctionNullary: SimpleFunction { - private let impl: (ExpressionContext) throws -> R - let signature = FunctionSignature( arguments: [], resultType: R.self ) + private let impl: (ExpressionContext) throws -> R + init(impl: @escaping () throws -> R) { self.impl = { _ in try impl() } } @@ -88,8 +88,6 @@ struct FunctionNullary: SimpleFunction { } struct FunctionUnary: SimpleFunction { - private let impl: (T1) throws -> R - let signature = FunctionSignature( arguments: [ .init(type: T1.self), @@ -97,6 +95,8 @@ struct FunctionUnary: SimpleFunction { resultType: R.self ) + private let impl: (T1) throws -> R + init(impl: @escaping (T1) throws -> R) { self.impl = impl } @@ -108,8 +108,6 @@ struct FunctionUnary: SimpleFunction { } struct FunctionBinary: SimpleFunction { - private let impl: (T1, T2, ExpressionContext) throws -> R - let signature = FunctionSignature( arguments: [ .init(type: T1.self), @@ -118,6 +116,8 @@ struct FunctionBinary: SimpleFunction { resultType: R.self ) + private let impl: (T1, T2, ExpressionContext) throws -> R + init(impl: @escaping (T1, T2) throws -> R) { self.impl = { arg1, arg2, _ in try impl(arg1, arg2) } } @@ -137,8 +137,6 @@ struct FunctionBinary: SimpleFunction { } struct FunctionTernary: SimpleFunction { - private let impl: (T1, T2, T3) throws -> R - let signature = FunctionSignature( arguments: [ .init(type: T1.self), @@ -148,6 +146,8 @@ struct FunctionTernary: SimpleFunction { resultType: R.self ) + private let impl: (T1, T2, T3) throws -> R + init(impl: @escaping (T1, T2, T3) throws -> R) { self.impl = impl } @@ -163,8 +163,6 @@ struct FunctionTernary: SimpleFunction { } struct FunctionQuaternary: SimpleFunction { - private let impl: (T1, T2, T3, T4) throws -> R - let signature = FunctionSignature( arguments: [ .init(type: T1.self), @@ -175,6 +173,8 @@ struct FunctionQuaternary: SimpleFunction { resultType: R.self ) + private let impl: (T1, T2, T3, T4) throws -> R + init(impl: @escaping (T1, T2, T3, T4) throws -> R) { self.impl = impl } @@ -191,8 +191,6 @@ struct FunctionQuaternary: SimpleFunction { } struct FunctionVarUnary: SimpleFunction { - private let impl: ([T1]) throws -> R - let signature = FunctionSignature( arguments: [ .init(type: T1.self, vararg: true), @@ -200,6 +198,8 @@ struct FunctionVarUnary: SimpleFunction { resultType: R.self ) + private let impl: ([T1]) throws -> R + init(impl: @escaping ([T1]) throws -> R) { self.impl = impl } @@ -211,8 +211,6 @@ struct FunctionVarUnary: SimpleFunction { } struct FunctionVarBinary: SimpleFunction { - private let impl: (T1, [T2]) throws -> R - let signature = FunctionSignature( arguments: [ .init(type: T1.self), @@ -221,6 +219,8 @@ struct FunctionVarBinary: SimpleFunction { resultType: R.self ) + private let impl: (T1, [T2]) throws -> R + init(impl: @escaping (T1, [T2]) throws -> R) { self.impl = impl } @@ -235,8 +235,6 @@ struct FunctionVarBinary: SimpleFunction { } struct FunctionVarTernary: SimpleFunction { - private let impl: (T1, T2, [T3]) throws -> R - let signature = FunctionSignature( arguments: [ .init(type: T1.self), @@ -246,6 +244,8 @@ struct FunctionVarTernary: SimpleFunction { resultType: R.self ) + private let impl: (T1, T2, [T3]) throws -> R + init(impl: @escaping (T1, T2, [T3]) throws -> R) { self.impl = impl } @@ -262,6 +262,7 @@ struct FunctionVarTernary: SimpleFunction { struct OverloadedFunction: Function { let functions: [SimpleFunction] + private let makeError: ([Any]) -> Error init(functions: [SimpleFunction], makeError: (([Any]) -> Error)? = nil) { diff --git a/client/ios/DivKit/Expressions/FunctionsProvider.swift b/client/ios/DivKit/Expressions/FunctionsProvider.swift index 811bb1522..8f8515887 100644 --- a/client/ios/DivKit/Expressions/FunctionsProvider.swift +++ b/client/ios/DivKit/Expressions/FunctionsProvider.swift @@ -2,15 +2,6 @@ import Foundation import VGSL final class FunctionsProvider { - private let persistentValuesStorage: DivPersistentValuesStorage - private let lock = AllocatedUnfairLock() - - init( - persistentValuesStorage: DivPersistentValuesStorage - ) { - self.persistentValuesStorage = persistentValuesStorage - } - static let methods: [String: Function] = { var methods: [String: Function] = [:] methods.addArrayMethods() @@ -64,6 +55,16 @@ final class FunctionsProvider { guard getValueFunctions.contains(symbol.name) else { return nil } return DynamicVariablesEvaluator() } + + private let persistentValuesStorage: DivPersistentValuesStorage + private let lock = AllocatedUnfairLock() + + init( + persistentValuesStorage: DivPersistentValuesStorage + ) { + self.persistentValuesStorage = persistentValuesStorage + } + } private struct CustomFunctionEvaluator: Function { diff --git a/client/ios/DivKit/Extensions/DivBase/DivBaseBlockBuilder.swift b/client/ios/DivKit/Extensions/DivBase/DivBaseBlockBuilder.swift index 4fd059f72..08e066ea1 100644 --- a/client/ios/DivKit/Extensions/DivBase/DivBaseBlockBuilder.swift +++ b/client/ios/DivKit/Extensions/DivBase/DivBaseBlockBuilder.swift @@ -21,18 +21,6 @@ final class DivBaseBlockBuilder { private let identity: String private let isFocused: Bool - private var expressionResolver: ExpressionResolver { - context.expressionResolver - } - - private var path: UIElementPath { - context.path - } - - private var statePath: DivStatePath { - context.parentDivStatePath ?? DivData.rootPath - } - private lazy var rotation = self.div.transform?.resolveRotation(self.expressionResolver) private lazy var border = isFocused ? div.focus?.border ?? div.border : div.border @@ -56,6 +44,18 @@ final class DivBaseBlockBuilder { makeGoneBlock(div: div) } + private var expressionResolver: ExpressionResolver { + context.expressionResolver + } + + private var path: UIElementPath { + context.path + } + + private var statePath: DivStatePath { + context.parentDivStatePath ?? DivData.rootPath + } + init( context: DivBlockModelingContext, visibility: Visibility, @@ -74,77 +74,6 @@ final class DivBaseBlockBuilder { self.isFocused = isFocused } - private func makeGoneBlock( - div: DivBase - ) -> Block { - context.stateManager.setBlockVisibility( - statePath: statePath, - div: div, - isVisible: false - ) - - if let visibilityParams = context.makeVisibilityParams( - actions: div.makeVisibilityActions( - actionsType: .disappear, - context: context - ), - isVisible: false - ) { - return EmptyBlock.zeroSized.addingDecorations( - visibilityParams: visibilityParams, - isEmpty: true - ) - } - - context.lastVisibleBoundsCache.onBecomeInvisible(path) - return EmptyBlock.zeroSized - } - - private func getBackground(_ isFocused: Bool) -> [DivBackground]? { - guard isFocused else { - return div.background - } - return div.focus?.background ?? div.background - } - - private func isAppearing() -> Bool { - let stateManager = context.stateManager - if stateManager.shouldBlockAppearWithTransition(path: statePath + identity) { - return true - } - - return stateManager.isBlockAdded(identity, stateBlockPath: statePath.stateBlockPath) - } - - private func addAnimationWarningsIfNeeded(isPresentAnimationIn: Bool) { - guard context.currentDivId == nil, - let block = block as? DetachableAnimationBlock else { return } - - func warningMessage(_ animation: String) -> String { - "The component id with the \(animation) property for state change is missing. Either specify the id, or specify the \"transition_trigger\" property without \"state_change\" value." - } - - if isPresentAnimationIn, - div.transitionTriggersOrDefault.contains(.stateChange) { - context.addWarning( - message: warningMessage("\"transition_in\"") - ) - } - - if block.animationOut != nil, - div.transitionTriggersOrDefault.contains(.stateChange) { - context.addWarning( - message: warningMessage("\"transition_out\"") - ) - } - - if block.animationChange != nil { - context.addWarning( - message: warningMessage("\"transition_change\"") - ) - } - } - func applyExtensionHandlers( stage: ApplyExtensionHandlersStage ) -> Self { @@ -162,7 +91,7 @@ final class DivBaseBlockBuilder { applyPaddings: Bool ) -> Self { block = block.addingEdgeInsets( - applyPaddings ? div.paddings.resolve(context) : .zero, + applyPaddings ? div.paddings.resolve(context): .zero, clipsToBounds: clipToBounds ) @@ -371,4 +300,76 @@ final class DivBaseBlockBuilder { func build() -> Block { block } + + private func makeGoneBlock( + div: DivBase + ) -> Block { + context.stateManager.setBlockVisibility( + statePath: statePath, + div: div, + isVisible: false + ) + + if let visibilityParams = context.makeVisibilityParams( + actions: div.makeVisibilityActions( + actionsType: .disappear, + context: context + ), + isVisible: false + ) { + return EmptyBlock.zeroSized.addingDecorations( + visibilityParams: visibilityParams, + isEmpty: true + ) + } + + context.lastVisibleBoundsCache.onBecomeInvisible(path) + return EmptyBlock.zeroSized + } + + private func getBackground(_ isFocused: Bool) -> [DivBackground]? { + guard isFocused else { + return div.background + } + return div.focus?.background ?? div.background + } + + private func isAppearing() -> Bool { + let stateManager = context.stateManager + if stateManager.shouldBlockAppearWithTransition(path: statePath + identity) { + return true + } + + return stateManager.isBlockAdded(identity, stateBlockPath: statePath.stateBlockPath) + } + + private func addAnimationWarningsIfNeeded(isPresentAnimationIn: Bool) { + guard context.currentDivId == nil, + let block = block as? DetachableAnimationBlock else { return } + + func warningMessage(_ animation: String) -> String { + "The component id with the \(animation) property for state change is missing. Either specify the id, or specify the \"transition_trigger\" property without \"state_change\" value." + } + + if isPresentAnimationIn, + div.transitionTriggersOrDefault.contains(.stateChange) { + context.addWarning( + message: warningMessage("\"transition_in\"") + ) + } + + if block.animationOut != nil, + div.transitionTriggersOrDefault.contains(.stateChange) { + context.addWarning( + message: warningMessage("\"transition_out\"") + ) + } + + if block.animationChange != nil { + context.addWarning( + message: warningMessage("\"transition_change\"") + ) + } + } + } diff --git a/client/ios/DivKit/LayoutProvider/SizeProviderBlock+UIViewRenderable.swift b/client/ios/DivKit/LayoutProvider/SizeProviderBlock+UIViewRenderable.swift index 536bbf957..41e601122 100644 --- a/client/ios/DivKit/LayoutProvider/SizeProviderBlock+UIViewRenderable.swift +++ b/client/ios/DivKit/LayoutProvider/SizeProviderBlock+UIViewRenderable.swift @@ -29,9 +29,13 @@ extension SizeProviderBlock { } private final class SizeProviderBlockView: BlockView { + var childMarginsSize = CGSize.zero + private var block: SizeProviderBlock! private var childView: BlockView! + var effectiveBackgroundColor: UIColor? { childView.backgroundColor } + init() { super.init(frame: .zero) } @@ -41,9 +45,12 @@ private final class SizeProviderBlockView: BlockView { fatalError("init(coder:) has not been implemented") } - var effectiveBackgroundColor: UIColor? { childView.backgroundColor } - - var childMarginsSize = CGSize.zero + override func layoutSubviews() { + super.layoutSubviews() + childView.frame = bounds + block.widthUpdater?(Int(bounds.width - childMarginsSize.width)) + block.heightUpdater?(Int(bounds.height - childMarginsSize.height)) + } func configure( block: SizeProviderBlock, @@ -64,12 +71,6 @@ private final class SizeProviderBlockView: BlockView { setNeedsLayout() } - override func layoutSubviews() { - super.layoutSubviews() - childView.frame = bounds - block.widthUpdater?(Int(bounds.width - childMarginsSize.width)) - block.heightUpdater?(Int(bounds.height - childMarginsSize.height)) - } } extension SizeProviderBlockView: VisibleBoundsTrackingContainer { diff --git a/client/ios/DivKit/Patches/DivPatchDownloader.swift b/client/ios/DivKit/Patches/DivPatchDownloader.swift index 37aa31593..37d35b5c8 100644 --- a/client/ios/DivKit/Patches/DivPatchDownloader.swift +++ b/client/ios/DivKit/Patches/DivPatchDownloader.swift @@ -10,6 +10,10 @@ public class DivPatchDownloader: DivPatchProvider { self.requestPerformer = requestPerformer } + deinit { + cancelRequests() + } + public func getPatch( url: URL, completion: @escaping DivPatchProviderCompletion @@ -33,9 +37,6 @@ public class DivPatchDownloader: DivPatchProvider { requests.removeAll() } - deinit { - cancelRequests() - } } private func parseResult( diff --git a/client/ios/DivKit/States/DivStateManager.swift b/client/ios/DivKit/States/DivStateManager.swift index f17355484..9b9e89db7 100644 --- a/client/ios/DivKit/States/DivStateManager.swift +++ b/client/ios/DivKit/States/DivStateManager.swift @@ -58,26 +58,6 @@ public class DivStateManager { self._items = items } - func get(stateBlockPath: DivStatePath) -> Item? { - lock.withLock { - _items[stateBlockPath] - } - } - - func setState(stateBlockPath: DivStatePath, stateBinding: Binding) { - lock.withLock { - _stateBindings[stateBlockPath] = stateBinding - guard stateBinding.value != _items[stateBlockPath]?.currentStateID.rawValue else { return } - updateState(path: stateBlockPath, stateID: DivStateID(rawValue: stateBinding.value)) - } - } - - func resetBinding(for stateBlockPath: DivStatePath) { - lock.withLock { - _ = _stateBindings.removeValue(forKey: stateBlockPath) - } - } - public func setState(stateBlockPath: DivStatePath, stateID: DivStateID) { lock.withLock { _stateBindings[stateBlockPath]?.value = stateID.rawValue @@ -94,21 +74,6 @@ public class DivStateManager { } } - private func updateState(path: DivStatePath, stateID: DivStateID) { - // need to take a write lock before - let previousItem = _items[path] - let previousState: PreviousState = if let previousStateID = previousItem?.currentStateID { - .withID(previousStateID) - } else { - .initial - } - _stateBindings[path]?.value = stateID.rawValue - _items[path] = Item( - currentStateID: stateID, - previousState: previousState - ) - } - public func removeState(path: DivStatePath) { lock.withLock { _ = _items.removeValue(forKey: path) @@ -182,6 +147,35 @@ public class DivStateManager { } } + public func reset() { + lock.withLock { + _items = [:] + _blockIds = [:] + _blockVisibility = [:] + _stateBindings = [:] + } + } + + func get(stateBlockPath: DivStatePath) -> Item? { + lock.withLock { + _items[stateBlockPath] + } + } + + func setState(stateBlockPath: DivStatePath, stateBinding: Binding) { + lock.withLock { + _stateBindings[stateBlockPath] = stateBinding + guard stateBinding.value != _items[stateBlockPath]?.currentStateID.rawValue else { return } + updateState(path: stateBlockPath, stateID: DivStateID(rawValue: stateBinding.value)) + } + } + + func resetBinding(for stateBlockPath: DivStatePath) { + lock.withLock { + _ = _stateBindings.removeValue(forKey: stateBlockPath) + } + } + func setBlockVisibility( statePath: DivStatePath, resolvedId: String, @@ -200,14 +194,21 @@ public class DivStateManager { } } - public func reset() { - lock.withLock { - _items = [:] - _blockIds = [:] - _blockVisibility = [:] - _stateBindings = [:] + private func updateState(path: DivStatePath, stateID: DivStateID) { + // need to take a write lock before + let previousItem = _items[path] + let previousState: PreviousState = if let previousStateID = previousItem?.currentStateID { + .withID(previousStateID) + } else { + .initial } + _stateBindings[path]?.value = stateID.rawValue + _items[path] = Item( + currentStateID: stateID, + previousState: previousState + ) } + } extension DivStateManager: Equatable { diff --git a/client/ios/DivKit/StoredValues/DivPersistentValuesStorage.swift b/client/ios/DivKit/StoredValues/DivPersistentValuesStorage.swift index 18fe1f41c..88fa7950e 100644 --- a/client/ios/DivKit/StoredValues/DivPersistentValuesStorage.swift +++ b/client/ios/DivKit/StoredValues/DivPersistentValuesStorage.swift @@ -3,8 +3,17 @@ import VGSL public final class DivPersistentValuesStorage { static let storageFileName = "divkit.values_storage" + private let timestampProvider: Variable + private let storage = Property( + fileName: storageFileName, + initialValue: StoredValues(items: [:]), + onError: { error in + DivKitLogger.error("Failed to create storage: \(error)") + } + ) + public init( timestampProvider: Variable = Variable { Date().timeIntervalSince1970.milliseconds @@ -14,14 +23,6 @@ public final class DivPersistentValuesStorage { removeOutdatedStoredValues() } - private let storage = Property( - fileName: storageFileName, - initialValue: StoredValues(items: [:]), - onError: { error in - DivKitLogger.error("Failed to create storage: \(error)") - } - ) - func set(value: DivStoredValue) { var items = storage.value.items items[value.name] = StoredValue( diff --git a/client/ios/DivKit/Templates/DivTemplates.swift b/client/ios/DivKit/Templates/DivTemplates.swift index a2b115257..f27311801 100644 --- a/client/ios/DivKit/Templates/DivTemplates.swift +++ b/client/ios/DivKit/Templates/DivTemplates.swift @@ -3,11 +3,11 @@ import Serialization import VGSL public struct DivTemplates: Deserializable, @unchecked Sendable { + public static let empty = DivTemplates(dictionary: [:]) + public let templates: [TemplateName: Any] public let templateToType: [TemplateName: String] - public static let empty = DivTemplates(dictionary: [:]) - public init( templates: [TemplateName: Any], templatesToType: [TemplateName: String] diff --git a/client/ios/DivKit/Timers/DivTimerController.swift b/client/ios/DivKit/Timers/DivTimerController.swift index aae1a9c9f..f55f1efaa 100644 --- a/client/ios/DivKit/Timers/DivTimerController.swift +++ b/client/ios/DivKit/Timers/DivTimerController.swift @@ -11,6 +11,8 @@ final class DivTimerController { case paused } + private(set) var state: State = .stopped + private let cardId: DivCardID private let divTimer: DivTimer private let timerScheduler: Scheduling @@ -22,8 +24,6 @@ final class DivTimerController { private let persistentValuesStorage: DivPersistentValuesStorage private let reporter: DivReporter - private(set) var state: State = .stopped - private var savedDuration: TimeInterval? private var savedInterval: TimeInterval? private var tickTimer: TimerType? diff --git a/client/ios/DivKit/Variables/DivVariableStorage.swift b/client/ios/DivKit/Variables/DivVariableStorage.swift index 8c1454c1e..2a148d9cc 100644 --- a/client/ios/DivKit/Variables/DivVariableStorage.swift +++ b/client/ios/DivKit/Variables/DivVariableStorage.swift @@ -11,11 +11,15 @@ public final class DivVariableStorage { } let initialPath: UIElementPath? + let changeEvents: Signal + private let outerStorage: DivVariableStorage? private var _values = DivVariables() private let lock = AllocatedUnfairLock() + private let changeEventsPipe = SignalPipe() + /// Gets all available variables including variables from outer storage. public var allValues: DivVariables { lock.withLock { @@ -33,9 +37,6 @@ public final class DivVariableStorage { (outerStorage?.allValues ?? [:]) + _values } - private let changeEventsPipe = SignalPipe() - let changeEvents: Signal - /// Initializes a new instance of ``DivVariableStorage``. /// /// - Parameters: @@ -186,6 +187,10 @@ public final class DivVariableStorage { } } + public func addObserver(_ action: @escaping (ChangeEvent) -> Void) -> Disposable { + changeEvents.addObserver(action) + } + func update( name: DivVariableName, valueFactory: (DivVariableValue) -> DivVariableValue? @@ -218,10 +223,6 @@ public final class DivVariableStorage { return isUpdated } - public func addObserver(_ action: @escaping (ChangeEvent) -> Void) -> Disposable { - changeEvents.addObserver(action) - } - private func notify(_ event: ChangeEvent) { onMainThread { [weak self] in self?.changeEventsPipe.send(event) diff --git a/client/ios/DivKit/Variables/DivVariableValue.swift b/client/ios/DivKit/Variables/DivVariableValue.swift index 2b9694cde..5cd68beb1 100644 --- a/client/ios/DivKit/Variables/DivVariableValue.swift +++ b/client/ios/DivKit/Variables/DivVariableValue.swift @@ -12,28 +12,6 @@ public enum DivVariableValue: Hashable { case dict(DivDictionary) case array(DivArray) - @inlinable - public func typedValue() -> T? { - switch self { - case let .string(value): - return value as? T - case let .number(value): - return value as? T - case let .integer(value): - return value as? T - case let .bool(value): - return value as? T - case let .color(value): - return value as? T - case let .url(value): - return value as? T - case let .dict(value): - return value as? T - case let .array(value): - return value as? T - } - } - init?(_ value: some Any) { switch value { case let value as String: @@ -57,4 +35,27 @@ public enum DivVariableValue: Hashable { return nil } } + + @inlinable + public func typedValue() -> T? { + switch self { + case let .string(value): + return value as? T + case let .number(value): + return value as? T + case let .integer(value): + return value as? T + case let .bool(value): + return value as? T + case let .color(value): + return value as? T + case let .url(value): + return value as? T + case let .dict(value): + return value as? T + case let .array(value): + return value as? T + } + } + } diff --git a/client/ios/DivKit/Variables/DivVariablesStorage.swift b/client/ios/DivKit/Variables/DivVariablesStorage.swift index 696b99448..33d2a4921 100644 --- a/client/ios/DivKit/Variables/DivVariablesStorage.swift +++ b/client/ios/DivKit/Variables/DivVariablesStorage.swift @@ -17,10 +17,6 @@ public final class DivVariablesStorage { public let kind: Kind - init(_ kind: Kind) { - self.kind = kind - } - public var changedVariables: Set { switch kind { case let .global(names): @@ -29,14 +25,20 @@ public final class DivVariablesStorage { names } } + + init(_ kind: Kind) { + self.kind = kind + } + } + public let changeEvents: Signal + private let globalStorage: DivVariableStorage private var localStorages: [UIElementPath: DivVariableStorage] = [:] private let lock = AllocatedUnfairLock() private let changeEventsPipe = SignalPipe() - public let changeEvents: Signal public convenience init() { self.init(outerStorage: nil) @@ -51,38 +53,6 @@ public final class DivVariablesStorage { changeEvents = Signal.merge(globalStorageEvents, changeEventsPipe.signal) } - func getOnlyElementVariables(cardId: DivCardID, elementId: String) -> DivVariables? { - lock.withLock { - let storages = localStorages.filter { - $0.key.leaf == elementId && $0.key.cardId == cardId - }.map(\.value) - - guard let storage = storages.first else { - DivKitLogger.error("Element with id \(elementId) not found") - return nil - } - guard storages.count == 1 else { - DivKitLogger.error("Found multiple elements that respond to id: \(elementId)") - return nil - } - - guard storage.initialPath?.leaf == elementId else { - return [:] - } - - return storage.values - } - } - - func getVariableValue( - path: UIElementPath, - name: DivVariableName - ) -> T? { - lock.withLock { - getNearestStorage(path).getValue(name) - } - } - public func getVariableValue( cardId: DivCardID, name: DivVariableName @@ -106,24 +76,6 @@ public final class DivVariablesStorage { return localStorage?.hasValue(name) ?? globalStorage.hasValue(name) } - func initializeIfNeeded(path: UIElementPath, variables: DivVariables) { - lock.withLock { - if localStorages[path] != nil { - // storage is already initialized - return - } - let nearestStorage = getNearestStorage(path.parent) - if variables.isEmpty { - // optimization that allows to access the local storage for one operation - localStorages[path] = nearestStorage - } else { - let localStorage = DivVariableStorage(outerStorage: nearestStorage, initialPath: path) - localStorage.replaceAll(variables, notifyObservers: false) - localStorages[path] = localStorage - } - } - } - /// Replaces all card variables with new ones. /// Does not affect global variables. public func set( @@ -210,6 +162,56 @@ public final class DivVariablesStorage { changeEvents.addObserver(action) } + func getOnlyElementVariables(cardId: DivCardID, elementId: String) -> DivVariables? { + lock.withLock { + let storages = localStorages.filter { + $0.key.leaf == elementId && $0.key.cardId == cardId + }.map(\.value) + + guard let storage = storages.first else { + DivKitLogger.error("Element with id \(elementId) not found") + return nil + } + guard storages.count == 1 else { + DivKitLogger.error("Found multiple elements that respond to id: \(elementId)") + return nil + } + + guard storage.initialPath?.leaf == elementId else { + return [:] + } + + return storage.values + } + } + + func getVariableValue( + path: UIElementPath, + name: DivVariableName + ) -> T? { + lock.withLock { + getNearestStorage(path).getValue(name) + } + } + + func initializeIfNeeded(path: UIElementPath, variables: DivVariables) { + lock.withLock { + if localStorages[path] != nil { + // storage is already initialized + return + } + let nearestStorage = getNearestStorage(path.parent) + if variables.isEmpty { + // optimization that allows to access the local storage for one operation + localStorages[path] = nearestStorage + } else { + let localStorage = DivVariableStorage(outerStorage: nearestStorage, initialPath: path) + localStorage.replaceAll(variables, notifyObservers: false) + localStorages[path] = localStorage + } + } + } + func getNearestStorage(_ path: UIElementPath?) -> DivVariableStorage { var currentPath: UIElementPath? = path while let path = currentPath { diff --git a/client/ios/DivKit/Views/DivBlockProvider.swift b/client/ios/DivKit/Views/DivBlockProvider.swift index 98ee745a6..c494b470b 100644 --- a/client/ios/DivKit/Views/DivBlockProvider.swift +++ b/client/ios/DivKit/Views/DivBlockProvider.swift @@ -6,17 +6,6 @@ import VGSL @MainActor final class DivBlockProvider { - private let divKitComponents: DivKitComponents - private let onCardSizeChanged: (DivCardID, DivViewSize) -> Void - private let disposePool = AutodisposePool() - - private var divData: DivData? { - didSet { - guard oldValue !== divData else { return } - update(reasons: []) - } - } - private(set) var id: DivViewId! private(set) var cardSize: DivViewSize? { @@ -27,16 +16,6 @@ final class DivBlockProvider { } } - private var debugParams = DebugParams() - private var dataErrors = [DeserializationError]() - private var updateInProgress = false - - private let measurements = DebugParams.Measurements( - divDataParsingTime: TimeMeasure(), - renderTime: TimeMeasure(), - templateParsingTime: TimeMeasure() - ) - @ObservableProperty private(set) var block: Block = noDataBlock { didSet { @@ -51,15 +30,36 @@ final class DivBlockProvider { } } - var cardId: DivCardID { - id.cardId - } - @ObservableProperty private(set) var shouldRecalculateVisibility: Bool = true var accessibilityElementsStorage: DivAccessibilityElementsStorage? + private let divKitComponents: DivKitComponents + private let onCardSizeChanged: (DivCardID, DivViewSize) -> Void + private let disposePool = AutodisposePool() + + private var divData: DivData? { + didSet { + guard oldValue !== divData else { return } + update(reasons: []) + } + } + + private var debugParams = DebugParams() + private var dataErrors = [DeserializationError]() + private var updateInProgress = false + + private let measurements = DebugParams.Measurements( + divDataParsingTime: TimeMeasure(), + renderTime: TimeMeasure(), + templateParsingTime: TimeMeasure() + ) + + var cardId: DivCardID { + id.cardId + } + init( divKitComponents: DivKitComponents, onCardSizeChanged: @escaping (DivCardID, DivViewSize) -> Void @@ -108,6 +108,80 @@ final class DivBlockProvider { } } + func update(reasons: [DivCardUpdateReason]) { + guard var divData else { + block = debugParams.isDebugInfoEnabled ? makeErrorsBlock(dataErrors) : noDataBlock + return + } + + guard needUpdateBlock(reasons: reasons) else { + return + } + + guard !updateInProgress else { + return + } + + updateInProgress = true + defer { + updateInProgress = false + } + + let context = makeCurrentContext() + + reasons.compactMap { $0.patch(for: self.cardId) }.forEach { patch in + divData = divData.applyPatchWithActions( + patch, + context: context + ) + } + + self.divData = divData + + if reasons.filter(\.isVariable).isEmpty { + context.layoutProviderHandler?.resetUpdatedVariables() + shouldRecalculateVisibility = true + } + dataErrors.forEach { context.errorsStorage.add($0) } + do { + block = try measurements.renderTime.updateMeasure { + try divData.makeBlock( + context: context + ) + } + accessibilityElementsStorage = context.accessibilityElementsStorage + debugParams.processMeasurements((cardId: cardId, measurements: measurements)) + for error in context.errorsStorage.errors { + divKitComponents.reporter.reportError(cardId: cardId, error: error) + } + if !divKitComponents.flagsInfo.initializeTriggerOnSet { + divKitComponents.triggersStorage.initialize(cardId: cardId) + } + } catch { + divKitComponents.reporter.reportError( + cardId: cardId, + error: DivUnknownError(error, path: UIElementPath(cardId.rawValue)) + ) + block = handleError(error: error, context: context) + } + } + + func update(withStates blockStates: BlocksState) { + do { + block = try block.updated(withStates: blockStates) + } catch { + block = handleError(error: error) + } + } + + func update(path: UIElementPath, isFocused: Bool) { + do { + block = try block.updated(path: path, isFocused: isFocused) + } catch { + block = handleError(error: error) + } + } + private func update(data: Data) { dataErrors = [] do { @@ -189,64 +263,6 @@ final class DivBlockProvider { self.divData = divData } - func update(reasons: [DivCardUpdateReason]) { - guard var divData else { - block = debugParams.isDebugInfoEnabled ? makeErrorsBlock(dataErrors) : noDataBlock - return - } - - guard needUpdateBlock(reasons: reasons) else { - return - } - - guard !updateInProgress else { - return - } - - updateInProgress = true - defer { - updateInProgress = false - } - - let context = makeCurrentContext() - - reasons.compactMap { $0.patch(for: self.cardId) }.forEach { patch in - divData = divData.applyPatchWithActions( - patch, - context: context - ) - } - - self.divData = divData - - if reasons.filter(\.isVariable).isEmpty { - context.layoutProviderHandler?.resetUpdatedVariables() - shouldRecalculateVisibility = true - } - dataErrors.forEach { context.errorsStorage.add($0) } - do { - block = try measurements.renderTime.updateMeasure { - try divData.makeBlock( - context: context - ) - } - accessibilityElementsStorage = context.accessibilityElementsStorage - debugParams.processMeasurements((cardId: cardId, measurements: measurements)) - for error in context.errorsStorage.errors { - divKitComponents.reporter.reportError(cardId: cardId, error: error) - } - if !divKitComponents.flagsInfo.initializeTriggerOnSet { - divKitComponents.triggersStorage.initialize(cardId: cardId) - } - } catch { - divKitComponents.reporter.reportError( - cardId: cardId, - error: DivUnknownError(error, path: UIElementPath(cardId.rawValue)) - ) - block = handleError(error: error, context: context) - } - } - private func needUpdateBlock(reasons: [DivCardUpdateReason]) -> Bool { guard !reasons.isEmpty else { return true } for reason in reasons { @@ -283,22 +299,6 @@ final class DivBlockProvider { ) } - func update(withStates blockStates: BlocksState) { - do { - block = try block.updated(withStates: blockStates) - } catch { - block = handleError(error: error) - } - } - - func update(path: UIElementPath, isFocused: Bool) { - do { - block = try block.updated(path: path, isFocused: isFocused) - } catch { - block = handleError(error: error) - } - } - private func parseDivDataWithTemplates( _ jsonDict: [String: Any], cardId: DivCardID diff --git a/client/ios/DivKit/Views/DivHostingView.swift b/client/ios/DivKit/Views/DivHostingView.swift index 8fd5860ac..42954f492 100644 --- a/client/ios/DivKit/Views/DivHostingView.swift +++ b/client/ios/DivKit/Views/DivHostingView.swift @@ -48,12 +48,12 @@ public struct DivHostingView: UIViewRepresentable { } private class VisibilityTrackingView: UIView { - let divView: DivView - override var intrinsicContentSize: CGSize { divView.intrinsicContentSize } + let divView: DivView + init(divView: DivView) { self.divView = divView diff --git a/client/ios/DivKit/Views/DivView.swift b/client/ios/DivKit/Views/DivView.swift index d010258af..f0f26d45e 100644 --- a/client/ios/DivKit/Views/DivView.swift +++ b/client/ios/DivKit/Views/DivView.swift @@ -14,6 +14,34 @@ import VGSL /// the demo app. public final class DivView: VisibleBoundsTrackingView { + /// Returns the intrinsic content size of the ``DivView``. + /// + /// - Returns: The calculated intrinsic content size. + public override var intrinsicContentSize: CGSize { + guard let cardSize else { + return CGSize(width: DivView.noIntrinsicMetric, height: DivView.noIntrinsicMetric) + } + let width: CGFloat + switch cardSize.width { + case .matchParent: + width = bounds.width == 0 ? DivView.noIntrinsicMetric : bounds.width + case let .desired(value): + width = value + case .dependsOnOtherDimensionSize: + assertionFailure("Width depends on other dimension size") + width = DivView.noIntrinsicMetric + } + let height: CGFloat = switch cardSize.height { + case .matchParent: + bounds.height == 0 ? DivView.noIntrinsicMetric : bounds.height + case let .desired(value): + value + case let .dependsOnOtherDimensionSize(heightForWidth): + heightForWidth(width) + } + return CGSize(width: width, height: height) + } + private let divKitComponents: DivKitComponents private let preloader: DivViewPreloader private var blockSubscription: Disposable? @@ -42,6 +70,13 @@ public final class DivView: VisibleBoundsTrackingView { private let boundsTracker = BoundsTracker() + /// Returns ``DivCardSize`` of the ``DivView``. + /// + /// - Returns: The calculated ``DivCardSize``. + public var cardSize: DivViewSize? { + blockProvider?.cardSize + } + /// Initializes a new `DivView` instance. /// /// - Parameters: @@ -60,6 +95,43 @@ public final class DivView: VisibleBoundsTrackingView { addGestureRecognizer(tapGestureRecognizer) } + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func layoutSubviews() { + super.layoutSubviews() + guard let blockView else { return } + + blockView.frame = bounds + blockView.layoutIfNeeded() + + if shouldRecalculateVisibility { + blockView.onVisibleBoundsChanged( + from: boundsTracker.previousBounds, + to: boundsTracker.lastVisibleBounds + ) + + shouldRecalculateVisibility = false + boundsTracker.updatePreviousBounds() + } + } + + public override func layoutSublayers(of layer: CALayer) { + super.layoutSublayers(of: layer) + + invalidateIntrinsicContentSizeIfBoundsChanged() + } + + public override func removeFromSuperview() { + super.removeFromSuperview() + blockView?.onVisibleBoundsChanged( + from: boundsTracker.lastVisibleBounds, + to: .zero + ) + } + /// Sets the source of the ``DivView`` and updates the layout. /// - Parameters: /// - source: The source of the ``DivView``, specified using `JSON` data, a `Data` object, or @@ -149,50 +221,6 @@ public final class DivView: VisibleBoundsTrackingView { blockProvider?.update(reasons: [.patch(cardId, patch)]) } - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func layoutSubviews() { - super.layoutSubviews() - guard let blockView else { return } - - blockView.frame = bounds - blockView.layoutIfNeeded() - - if shouldRecalculateVisibility { - blockView.onVisibleBoundsChanged( - from: boundsTracker.previousBounds, - to: boundsTracker.lastVisibleBounds - ) - - shouldRecalculateVisibility = false - boundsTracker.updatePreviousBounds() - } - } - - public override func layoutSublayers(of layer: CALayer) { - super.layoutSublayers(of: layer) - - invalidateIntrinsicContentSizeIfBoundsChanged() - } - - public override func removeFromSuperview() { - super.removeFromSuperview() - blockView?.onVisibleBoundsChanged( - from: boundsTracker.lastVisibleBounds, - to: .zero - ) - } - - /// Returns ``DivCardSize`` of the ``DivView``. - /// - /// - Returns: The calculated ``DivCardSize``. - public var cardSize: DivViewSize? { - blockProvider?.cardSize - } - /// Adds an observer to listen for ``DivView`` estimated size changes. /// /// - Parameters: @@ -208,32 +236,23 @@ public final class DivView: VisibleBoundsTrackingView { } } - /// Returns the intrinsic content size of the ``DivView``. - /// - /// - Returns: The calculated intrinsic content size. - public override var intrinsicContentSize: CGSize { - guard let cardSize else { - return CGSize(width: DivView.noIntrinsicMetric, height: DivView.noIntrinsicMetric) + /// Notifies the DivView about changes in its visible bounds. + /// - Parameters: + /// - to: The new bounds rectangle. + public func onVisibleBoundsChanged(to: CGRect) { + guard boundsTracker.append(to) else { return } + + shouldRecalculateVisibility = true + if window == nil { + forceLayout() + } else { + setNeedsLayout() } - let width: CGFloat - switch cardSize.width { - case .matchParent: - width = bounds.width == 0 ? DivView.noIntrinsicMetric : bounds.width - case let .desired(value): - width = value - case .dependsOnOtherDimensionSize: - assertionFailure("Width depends on other dimension size") - width = DivView.noIntrinsicMetric - } - let height: CGFloat = switch cardSize.height { - case .matchParent: - bounds.height == 0 ? DivView.noIntrinsicMetric : bounds.height - case let .desired(value): - value - case let .dependsOnOtherDimensionSize(heightForWidth): - heightForWidth(width) - } - return CGSize(width: width, height: height) + } + + /// Use ``onVisibleBoundsChanged(to:)`` instead. + public func onVisibleBoundsChanged(from _: CGRect, to: CGRect) { + onVisibleBoundsChanged(to: to) } private func update(block: Block) { @@ -272,24 +291,6 @@ public final class DivView: VisibleBoundsTrackingView { clearFocus() } - /// Notifies the DivView about changes in its visible bounds. - /// - Parameters: - /// - to: The new bounds rectangle. - public func onVisibleBoundsChanged(to: CGRect) { - guard boundsTracker.append(to) else { return } - - shouldRecalculateVisibility = true - if window == nil { - forceLayout() - } else { - setNeedsLayout() - } - } - - /// Use ``onVisibleBoundsChanged(to:)`` instead. - public func onVisibleBoundsChanged(from _: CGRect, to: CGRect) { - onVisibleBoundsChanged(to: to) - } } extension DivView: ElementStateObserver { diff --git a/client/ios/DivKit/Views/DivViewPreloader.swift b/client/ios/DivKit/Views/DivViewPreloader.swift index 23ac18134..e2d86387d 100644 --- a/client/ios/DivKit/Views/DivViewPreloader.swift +++ b/client/ios/DivKit/Views/DivViewPreloader.swift @@ -14,10 +14,12 @@ public final class DivViewPreloader { public let estimatedSize: DivViewSize } + private(set) var setSourceTask: Task? + private var blockProviders = [DivCardID: DivBlockProvider]() private let divKitComponents: DivKitComponents private let changeEventsPipe = SignalPipe() - private(set) var setSourceTask: Task? + var changeEvents: Signal { changeEventsPipe.signal } @@ -30,18 +32,6 @@ public final class DivViewPreloader { self.divKitComponents = divKitComponents } - func blockProvider(for cardId: DivCardID) -> DivBlockProvider { - if let blockProvider = blockProviders[cardId] { - return blockProvider - } else { - let blockProvider = DivBlockProvider(divKitComponents: divKitComponents) { [weak self] in - self?.changeEventsPipe.send(DivViewSizeChange(cardId: $0, estimatedSize: $1)) - } - blockProviders[cardId] = blockProvider - return blockProvider - } - } - /// Sets the source for ``DivViewPreloader`` and updates the layout. /// - Parameters: /// - source: The source of the ``DivView``. @@ -131,6 +121,18 @@ public final class DivViewPreloader { changeEvents.addObserver(onCardSizeChanged) } + func blockProvider(for cardId: DivCardID) -> DivBlockProvider { + if let blockProvider = blockProviders[cardId] { + return blockProvider + } else { + let blockProvider = DivBlockProvider(divKitComponents: divKitComponents) { [weak self] in + self?.changeEventsPipe.send(DivViewSizeChange(cardId: $0, estimatedSize: $1)) + } + blockProviders[cardId] = blockProvider + return blockProvider + } + } + func reset(cardId: DivCardID) { blockProviders.removeValue(forKey: cardId) } diff --git a/client/ios/DivKit/Views/DivViewSize.swift b/client/ios/DivKit/Views/DivViewSize.swift index ac84d4a71..5f317292e 100644 --- a/client/ios/DivKit/Views/DivViewSize.swift +++ b/client/ios/DivKit/Views/DivViewSize.swift @@ -30,6 +30,12 @@ public struct DivViewSize: Equatable { } } + public let width: DivDimension + public let height: DivDimension + + private let constrainedWidth: Bool + private let constrainedHeight: Bool + public init(block: Block) { let width: DivDimension = block .isHorizontallyResizable ? .matchParent : .desired(block.widthOfHorizontallyNonResizableBlock) @@ -40,12 +46,6 @@ public struct DivViewSize: Equatable { self.constrainedHeight = block.isVerticallyConstrained } - public let width: DivDimension - public let height: DivDimension - - private let constrainedWidth: Bool - private let constrainedHeight: Bool - /// Computes the actual size for a ``DivView`` given its parent's size. /// /// - Parameters: diff --git a/client/ios/DivKitExtensions/Animations/AnimationBlockView.swift b/client/ios/DivKitExtensions/Animations/AnimationBlockView.swift index e7ccdf708..ebf858244 100644 --- a/client/ios/DivKitExtensions/Animations/AnimationBlockView.swift +++ b/client/ios/DivKitExtensions/Animations/AnimationBlockView.swift @@ -19,8 +19,6 @@ final class AnimationBlockView: BlockView { } } - private var animationRequest: Cancellable? - var isPlaying: Bool = true { didSet { guard oldValue != isPlaying else { return } @@ -57,21 +55,24 @@ final class AnimationBlockView: BlockView { } } + let effectiveBackgroundColor: UIColor? = nil + + private var animationRequest: Cancellable? + init() { super.init(frame: .zero) } - override func layoutSubviews() { - super.layoutSubviews() - animatableView?.frame = bounds - } - @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - let effectiveBackgroundColor: UIColor? = nil + override func layoutSubviews() { + super.layoutSubviews() + animatableView?.frame = bounds + } + } extension AnimationBlockView: VisibleBoundsTrackingLeaf {} diff --git a/client/ios/DivKitExtensions/Animations/Lottie/LottieAnimationBlock.swift b/client/ios/DivKitExtensions/Animations/Lottie/LottieAnimationBlock.swift index 319e0c762..800bec183 100644 --- a/client/ios/DivKitExtensions/Animations/Lottie/LottieAnimationBlock.swift +++ b/client/ios/DivKitExtensions/Animations/Lottie/LottieAnimationBlock.swift @@ -11,6 +11,8 @@ final class LottieAnimationBlock: SizeForwardingBlock { let scale: DivImageScale var isPlaying: Bool + let intrinsicContentWidth: CGFloat = 0 + var debugDescription: String { "Animation Block playing animation with view: \(animatableView)" } diff --git a/client/ios/DivKitExtensions/Animations/Lottie/LottieExtensionHandler.swift b/client/ios/DivKitExtensions/Animations/Lottie/LottieExtensionHandler.swift index eb831222a..d90a55a21 100644 --- a/client/ios/DivKitExtensions/Animations/Lottie/LottieExtensionHandler.swift +++ b/client/ios/DivKitExtensions/Animations/Lottie/LottieExtensionHandler.swift @@ -21,6 +21,18 @@ public final class LottieExtensionHandler: DivExtensionHandler { self.localAnimationDataProvider = localAnimationDataProvider } + static func getPreloadURL(div: DivBase, expressionResolver: ExpressionResolver) -> URL? { + let extensionData = div.extensions?.first { $0.id == "lottie" } + guard let paramsDict = extensionData?.params, + let params = LottieExtensionParams( + paramsDictionary: paramsDict, + expressionResolver: expressionResolver + ) else { + return nil + } + return params.source.url + } + public func applyAfterBaseProperties( to block: Block, div: DivBase, @@ -67,22 +79,20 @@ public final class LottieExtensionHandler: DivExtensionHandler { [Self.getPreloadURL(div: div, expressionResolver: expressionResolver)].compactMap { $0 } } - static func getPreloadURL(div: DivBase, expressionResolver: ExpressionResolver) -> URL? { - let extensionData = div.extensions?.first { $0.id == "lottie" } - guard let paramsDict = extensionData?.params, - let params = LottieExtensionParams( - paramsDictionary: paramsDict, - expressionResolver: expressionResolver - ) else { - return nil - } - return params.source.url - } } private class JSONAnimationHolder: AnimationHolder { let animation: AnimationSourceType? + var debugDescription: String { + guard let animation = animation as? LottieAnimationSourceType, + case let .json(json) = animation else { + assertionFailure("JSONAnimation holder can hold only json ") + return "" + } + return "JSON Animation holder with json \(json)" + } + init(json: [String: Any]) { self.animation = LottieAnimationSourceType.json(json) } @@ -102,12 +112,4 @@ private class JSONAnimationHolder: AnimationHolder { return false } - var debugDescription: String { - guard let animation = animation as? LottieAnimationSourceType, - case let .json(json) = animation else { - assertionFailure("JSONAnimation holder can hold only json ") - return "" - } - return "JSON Animation holder with json \(json)" - } } diff --git a/client/ios/DivKitExtensions/Animations/RemoteAnimationHolder.swift b/client/ios/DivKitExtensions/Animations/RemoteAnimationHolder.swift index 72ae6fa74..6624cda5e 100644 --- a/client/ios/DivKitExtensions/Animations/RemoteAnimationHolder.swift +++ b/client/ios/DivKitExtensions/Animations/RemoteAnimationHolder.swift @@ -10,8 +10,10 @@ public final class RemoteAnimationHolder: AnimationHolder { private typealias AsyncAnimationRequester = (@escaping (AnimationSourceType?) -> Void) -> Cancellable? - let url: URL public private(set) var animation: AnimationSourceType? + + let url: URL + private let animationType: AnimationType private let resourceRequester: AsyncAnimationRequester diff --git a/client/ios/DivKitExtensions/Animations/Rive/RiveAnimationBlock.swift b/client/ios/DivKitExtensions/Animations/Rive/RiveAnimationBlock.swift index 6a02dd32b..4003db375 100644 --- a/client/ios/DivKitExtensions/Animations/Rive/RiveAnimationBlock.swift +++ b/client/ios/DivKitExtensions/Animations/Rive/RiveAnimationBlock.swift @@ -4,15 +4,27 @@ import LayoutKit import VGSL public final class RiveAnimationBlock: BlockWithTraits { - let animationHolder: AnimationHolder - let animatableView: Lazy public let widthTrait: LayoutTrait public let heightTrait: LayoutTrait + let animationHolder: AnimationHolder + let animatableView: Lazy + public var debugDescription: String { "Sized Animation Block is playing animation with \(animationHolder)" } + public var intrinsicContentWidth: CGFloat { + switch widthTrait { + case let .fixed(value): + value + case let .intrinsic(_, minSize, _): + minSize + case .weighted: + 0 + } + } + public init( animationHolder: AnimationHolder, animatableView: Lazy, @@ -25,17 +37,6 @@ public final class RiveAnimationBlock: BlockWithTraits { self.heightTrait = heightTrait } - public var intrinsicContentWidth: CGFloat { - switch widthTrait { - case let .fixed(value): - value - case let .intrinsic(_, minSize, _): - minSize - case .weighted: - 0 - } - } - public func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { switch heightTrait { case let .fixed(value): diff --git a/client/ios/DivKitExtensions/ExtensionHandlers/CustomImagePreviewExtensionHandler.swift b/client/ios/DivKitExtensions/ExtensionHandlers/CustomImagePreviewExtensionHandler.swift index e367a9371..e3a5bdb5c 100644 --- a/client/ios/DivKitExtensions/ExtensionHandlers/CustomImagePreviewExtensionHandler.swift +++ b/client/ios/DivKitExtensions/ExtensionHandlers/CustomImagePreviewExtensionHandler.swift @@ -5,6 +5,7 @@ import VGSL public final class CustomImagePreviewExtensionHandler: DivExtensionHandler { public let id: String + private let viewProvider: ViewProvider public init(id: String, viewProvider: ViewProvider) { diff --git a/client/ios/DivKitExtensions/ExtensionHandlers/ImageExtensionHandler.swift b/client/ios/DivKitExtensions/ExtensionHandlers/ImageExtensionHandler.swift index 63c3a609e..9dc767a14 100644 --- a/client/ios/DivKitExtensions/ExtensionHandlers/ImageExtensionHandler.swift +++ b/client/ios/DivKitExtensions/ExtensionHandlers/ImageExtensionHandler.swift @@ -4,6 +4,7 @@ import VGSL public final class ImageExtensionHandler: DivExtensionHandler { public let id: String + private let image: ImageHolder? public init( diff --git a/client/ios/DivKitExtensions/ExtensionHandlers/ImageThemeExtensionHandler.swift b/client/ios/DivKitExtensions/ExtensionHandlers/ImageThemeExtensionHandler.swift index 3727c0950..462c7366b 100644 --- a/client/ios/DivKitExtensions/ExtensionHandlers/ImageThemeExtensionHandler.swift +++ b/client/ios/DivKitExtensions/ExtensionHandlers/ImageThemeExtensionHandler.swift @@ -5,6 +5,7 @@ import VGSL public final class ImageThemeExtensionHandler: DivExtensionHandler { public let id = extensionID + private let theme: Variable public init(theme: Variable) { diff --git a/client/ios/DivKitExtensions/ExtensionHandlers/InputAccessoryViewExtensionHandler.swift b/client/ios/DivKitExtensions/ExtensionHandlers/InputAccessoryViewExtensionHandler.swift index 149aadf57..d9f2d57e8 100644 --- a/client/ios/DivKitExtensions/ExtensionHandlers/InputAccessoryViewExtensionHandler.swift +++ b/client/ios/DivKitExtensions/ExtensionHandlers/InputAccessoryViewExtensionHandler.swift @@ -10,6 +10,7 @@ public protocol InputAccessoryViewProvider { public final class InputAccessoryViewExtensionHandler: DivExtensionHandler { public let id = "input_accessory_view" + private let viewProvider: InputAccessoryViewProvider public init(viewProvider: InputAccessoryViewProvider) { diff --git a/client/ios/DivKitExtensions/ExtensionHandlers/PinchToZoomExtensionHandler.swift b/client/ios/DivKitExtensions/ExtensionHandlers/PinchToZoomExtensionHandler.swift index 9fc43908f..543a97518 100644 --- a/client/ios/DivKitExtensions/ExtensionHandlers/PinchToZoomExtensionHandler.swift +++ b/client/ios/DivKitExtensions/ExtensionHandlers/PinchToZoomExtensionHandler.swift @@ -4,10 +4,10 @@ import LayoutKit import UIKit public final class PinchToZoomExtensionHandler: DivExtensionHandler { - private weak var overlayView: UIView? - public let id = "pinch-to-zoom" + private weak var overlayView: UIView? + public init(overlayView: UIView) { self.overlayView = overlayView } diff --git a/client/ios/DivKitExtensions/ExtensionHandlers/TextExtensionHandler.swift b/client/ios/DivKitExtensions/ExtensionHandlers/TextExtensionHandler.swift index 3fb599804..758026f87 100644 --- a/client/ios/DivKitExtensions/ExtensionHandlers/TextExtensionHandler.swift +++ b/client/ios/DivKitExtensions/ExtensionHandlers/TextExtensionHandler.swift @@ -4,8 +4,13 @@ import VGSL public final class TextExtensionHandler: DivExtensionHandler { public let id: String + private let text: String? + public var accessibilityElement: AccessibilityElement? { + .none(label: text) + } + public init( id: String, text: String? @@ -14,10 +19,6 @@ public final class TextExtensionHandler: DivExtensionHandler { self.text = text } - public var accessibilityElement: AccessibilityElement? { - .none(label: text) - } - public func applyBeforeBaseProperties( to block: Block, div _: DivBase, diff --git a/client/ios/DivKitExtensions/Shimmers/Shimmer/ShimmerImagePreviewExtension.swift b/client/ios/DivKitExtensions/Shimmers/Shimmer/ShimmerImagePreviewExtension.swift index 048bb7d64..862e48a3e 100644 --- a/client/ios/DivKitExtensions/Shimmers/Shimmer/ShimmerImagePreviewExtension.swift +++ b/client/ios/DivKitExtensions/Shimmers/Shimmer/ShimmerImagePreviewExtension.swift @@ -6,6 +6,7 @@ import VGSL public final class ShimmerImagePreviewExtension: DivExtensionHandler { public let id: String = extensionID + private let effectBeginTime = CACurrentMediaTime() public init() {} diff --git a/client/ios/DivKitExtensionsTests/LottieExtensionHandlerTests.swift b/client/ios/DivKitExtensionsTests/LottieExtensionHandlerTests.swift index ebda79873..99d753a7a 100644 --- a/client/ios/DivKitExtensionsTests/LottieExtensionHandlerTests.swift +++ b/client/ios/DivKitExtensionsTests/LottieExtensionHandlerTests.swift @@ -85,6 +85,7 @@ final class LottieExtensionHandlerTests: XCTestCase { private final class MockLottieAnimationFactory: AsyncSourceAnimatableViewFactory { var returnView = MockAnimatableView(frame: .zero) + func createAsyncSourceAnimatableView(withMode _: AnimationRepeatMode, repeatCount _: Float) -> AsyncSourceAnimatableView { returnView diff --git a/client/ios/DivKitPlayground/AppComponents.swift b/client/ios/DivKitPlayground/AppComponents.swift index 5a4053855..c3ffa1a2a 100644 --- a/client/ios/DivKitPlayground/AppComponents.swift +++ b/client/ios/DivKitPlayground/AppComponents.swift @@ -9,6 +9,12 @@ import VGSL enum AppComponents { static let fontProvider = YSFontProvider() + static var debugParams: DebugParams { + DebugParams( + isDebugInfoEnabled: true + ) + } + static func makeDivKitComponents( layoutDirection: UserInterfaceLayoutDirection = .system, reporter: DivReporter = PlaygroundReporter(), @@ -54,11 +60,6 @@ enum AppComponents { ) } - static var debugParams: DebugParams { - DebugParams( - isDebugInfoEnabled: true - ) - } } private func makeCachingPlayerFactory(requester: URLResourceRequesting) -> PlayerFactory { diff --git a/client/ios/DivKitPlayground/DivKit/RiveDivCustomData.swift b/client/ios/DivKitPlayground/DivKit/RiveDivCustomData.swift index d6042a66d..08a9f6330 100644 --- a/client/ios/DivKitPlayground/DivKit/RiveDivCustomData.swift +++ b/client/ios/DivKitPlayground/DivKit/RiveDivCustomData.swift @@ -37,6 +37,7 @@ struct RiveDivCustomData { } static let divCustomType = "rive_animation" + let url: URL let fit: Fit let alignment: Alignment diff --git a/client/ios/DivKitPlayground/DivKitPlaygroundApp.swift b/client/ios/DivKitPlayground/DivKitPlaygroundApp.swift index 0d04fb7d5..46f600543 100644 --- a/client/ios/DivKitPlayground/DivKitPlaygroundApp.swift +++ b/client/ios/DivKitPlayground/DivKitPlaygroundApp.swift @@ -3,13 +3,14 @@ import SwiftUI @main struct DivKitPlaygroundApp: App { - init() { - DivKitLogger.isEnabled = true - } - var body: some Scene { WindowGroup { MainView() } } + + init() { + DivKitLogger.isEnabled = true + } + } diff --git a/client/ios/DivKitPlayground/QRScanner/MetadataCaptureSession.swift b/client/ios/DivKitPlayground/QRScanner/MetadataCaptureSession.swift index 45f5d0a07..df84a42e1 100644 --- a/client/ios/DivKitPlayground/QRScanner/MetadataCaptureSession.swift +++ b/client/ios/DivKitPlayground/QRScanner/MetadataCaptureSession.swift @@ -5,16 +5,17 @@ import VGSL typealias LogError = (String) -> Void final class MetadataCaptureSession: NSObject, AVCaptureMetadataOutputObjectsDelegate { + let result: ObservableProperty + private let captureSession = AVCaptureSession() private let previewLayer: AVCaptureVideoPreviewLayer - let result: ObservableProperty private let logError: LogError - var layer: CALayer { previewLayer } - private var isInitialized = false + var layer: CALayer { previewLayer } + init( result: ObservableProperty, logError: @escaping LogError diff --git a/client/ios/DivKitPlayground/QRScanner/ScannerViewController.swift b/client/ios/DivKitPlayground/QRScanner/ScannerViewController.swift index 5e167914b..d1fac1f4f 100644 --- a/client/ios/DivKitPlayground/QRScanner/ScannerViewController.swift +++ b/client/ios/DivKitPlayground/QRScanner/ScannerViewController.swift @@ -2,10 +2,10 @@ import UIKit import VGSL final class ScannerViewController: UIViewController { - private let captureSession: Lazy - let result = ObservableProperty(initialValue: "") + private let captureSession: Lazy + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") diff --git a/client/ios/DivKitPlayground/Regression/RegressionTestModel.swift b/client/ios/DivKitPlayground/Regression/RegressionTestModel.swift index 0cd8405f3..ca4af523d 100644 --- a/client/ios/DivKitPlayground/Regression/RegressionTestModel.swift +++ b/client/ios/DivKitPlayground/Regression/RegressionTestModel.swift @@ -1,15 +1,6 @@ import SwiftUI struct RegressionTestsModel: Decodable { - let tests: [RegressionTestModel] - - init(from decoder: Decoder) throws { - tests = try decoder - .container(keyedBy: CodingKeys.self) - .decode([SafeDecodable].self, forKey: .tests) - .compactMap(\.value) - } - private enum CodingKeys: String, CodingKey { case tests } @@ -26,9 +17,50 @@ struct RegressionTestsModel: Decodable { } } } + + let tests: [RegressionTestModel] + + init(from decoder: Decoder) throws { + tests = try decoder + .container(keyedBy: CodingKeys.self) + .decode([SafeDecodable].self, forKey: .tests) + .compactMap(\.value) + } + } struct RegressionTestModel: Decodable, Hashable { + private enum CodingKeys: String, CodingKey { + case expectedResults = "expected_results" + case file + case platforms + case steps + case tags + case title + } + + private struct DecodingError: LocalizedError { + let description: String + + var errorDescription: String? { + NSLocalizedString(description, comment: "DecodingError") + } + + init(_ description: String) { + self.description = description + } + + init(key: CodingKeys, error: Error) { + description = "Failed to read \(key.rawValue): \(error.localizedDescription)" + } + + init(key: CodingKeys, title: String, error: Error) { + description = + "Failed to read \(key.rawValue) in test '\(title)': \(error.localizedDescription)" + } + + } + let expectedResults: [String] let platforms: [Platform] let steps: [String] @@ -36,6 +68,28 @@ struct RegressionTestModel: Decodable, Hashable { let title: String let url: URL + var description: String { + var description = "" + if !steps.isEmpty { + description += "Steps:" + for step in steps { + description += "\n • \(step)" + } + } + + if !expectedResults.isEmpty { + if !steps.isEmpty { + description += "\n\n" + } + description += "Expected results:" + for result in expectedResults { + description += "\n • \(result)" + } + } + + return description + } + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -73,57 +127,6 @@ struct RegressionTestModel: Decodable, Hashable { tags = (try? container.decodeIfPresent([String].self, forKey: .tags)) ?? [] } - var description: String { - var description = "" - if !steps.isEmpty { - description += "Steps:" - for step in steps { - description += "\n • \(step)" - } - } - - if !expectedResults.isEmpty { - if !steps.isEmpty { - description += "\n\n" - } - description += "Expected results:" - for result in expectedResults { - description += "\n • \(result)" - } - } - - return description - } - - private enum CodingKeys: String, CodingKey { - case expectedResults = "expected_results" - case file - case platforms - case steps - case tags - case title - } - - private struct DecodingError: LocalizedError { - let description: String - - init(_ description: String) { - self.description = description - } - - init(key: CodingKeys, error: Error) { - description = "Failed to read \(key.rawValue): \(error.localizedDescription)" - } - - init(key: CodingKeys, title: String, error: Error) { - description = - "Failed to read \(key.rawValue) in test '\(title)': \(error.localizedDescription)" - } - - var errorDescription: String? { - NSLocalizedString(description, comment: "DecodingError") - } - } } enum Platform: String, Decodable { diff --git a/client/ios/DivKitPlayground/Regression/RegressionView.swift b/client/ios/DivKitPlayground/Regression/RegressionView.swift index 35e4e9556..d94528f3d 100644 --- a/client/ios/DivKitPlayground/Regression/RegressionView.swift +++ b/client/ios/DivKitPlayground/Regression/RegressionView.swift @@ -9,6 +9,15 @@ struct RegressionView: View { private let divViewProvider: DivViewProvider + private var tests: [RegressionTestModel] { + if query.isEmpty { + return TestData.regressionTests + } + return TestData.regressionTests.filter { + $0.title.range(of: query, options: .caseInsensitive) != nil + } + } + init( divViewProvider: DivViewProvider ) { @@ -40,14 +49,6 @@ struct RegressionView: View { } } - private var tests: [RegressionTestModel] { - if query.isEmpty { - return TestData.regressionTests - } - return TestData.regressionTests.filter { - $0.title.range(of: query, options: .caseInsensitive) != nil - } - } } private struct NavigationButton: View where Destination: View { diff --git a/client/ios/DivKitPlayground/UI/DivView/DivViewController.swift b/client/ios/DivKitPlayground/UI/DivView/DivViewController.swift index 6ebeab89e..505fd0ad1 100644 --- a/client/ios/DivKitPlayground/UI/DivView/DivViewController.swift +++ b/client/ios/DivKitPlayground/UI/DivView/DivViewController.swift @@ -14,6 +14,11 @@ open class DivViewController: UIViewController { private let identifier: String = "baseDivView" + @available(*, unavailable) + public required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + init( jsonPublisher: JsonPublisher, divKitComponents: DivKitComponents, @@ -33,11 +38,6 @@ open class DivViewController: UIViewController { .store(in: &cancellables) } - @available(*, unavailable) - public required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - public override func loadView() { scrollView.backgroundColor = .white view = scrollView diff --git a/client/ios/DivKitPlayground/UI/DivView/DivViewProvider.swift b/client/ios/DivKitPlayground/UI/DivView/DivViewProvider.swift index fae645ad0..3d4a34747 100644 --- a/client/ios/DivKitPlayground/UI/DivView/DivViewProvider.swift +++ b/client/ios/DivKitPlayground/UI/DivView/DivViewProvider.swift @@ -4,6 +4,7 @@ import SwiftUI final class DivViewProvider { let jsonProvider: PlaygroundJsonProvider + private let divKitComponents: DivKitComponents private let layoutDirection: UIUserInterfaceLayoutDirection diff --git a/client/ios/DivKitPlayground/UI/SamplesView.swift b/client/ios/DivKitPlayground/UI/SamplesView.swift index da0acd582..a767c53e2 100644 --- a/client/ios/DivKitPlayground/UI/SamplesView.swift +++ b/client/ios/DivKitPlayground/UI/SamplesView.swift @@ -35,10 +35,10 @@ struct SampleView: View { } final class SamplesModel { - private let divKitComponents = AppComponents.makeDivKitComponents() - private(set) var items: [SampleModel]! + private let divKitComponents = AppComponents.makeDivKitComponents() + init() { items = TestData.samples .map { diff --git a/client/ios/DivKitPlayground/Utils/TestData.swift b/client/ios/DivKitPlayground/Utils/TestData.swift index 47e07aacb..cfb96b436 100644 --- a/client/ios/DivKitPlayground/Utils/TestData.swift +++ b/client/ios/DivKitPlayground/Utils/TestData.swift @@ -5,13 +5,6 @@ enum TestData { static let samplesPath = "samples" static let patchesPath = "regression_test_data/patches" - private static var cachedRegressionTests: [RegressionTestModel]? - - static var samples: [URL] { - getAllFiles(path: samplesPath) - .map { url, _ in url } - } - static var regressionTests: [RegressionTestModel] = { let indexFileUrl = Bundle.main .url(forResource: "index", withExtension: "json", subdirectory: regressionPath)! @@ -23,6 +16,13 @@ enum TestData { .sorted { $0.title < $1.title } }() + private static var cachedRegressionTests: [RegressionTestModel]? + + static var samples: [URL] { + getAllFiles(path: samplesPath) + .map { url, _ in url } + } + private static func getAllFiles(path: String) -> [(URL, String)] { getItems(path: path, extension: "") .flatMap { _, folderName in diff --git a/client/ios/DivKitPlayground/Utils/UserPreferences.swift b/client/ios/DivKitPlayground/Utils/UserPreferences.swift index 981cb1dd6..62e3342a8 100644 --- a/client/ios/DivKitPlayground/Utils/UserPreferences.swift +++ b/client/ios/DivKitPlayground/Utils/UserPreferences.swift @@ -18,17 +18,17 @@ enum UserPreferences { static let isRTLEnabledDefault: Bool = UIUserInterfaceLayoutDirection .system == .rightToLeft ? true : false + static let showRenderingTimeDefault = false + static let playgroundThemeDefault = Theme.system + static var isRTLEnabled: Bool { defaults.value(forKey: isRTLEnabledKey) as? Bool ?? isRTLEnabledDefault } - static let showRenderingTimeDefault = false static var showRenderingTime: Bool { defaults.value(forKey: showRenderingTimeKey) as? Bool ?? showRenderingTimeDefault } - static let playgroundThemeDefault = Theme.system - static var playgroundTheme: Theme { let value = Theme(rawValue: defaults.value(forKey: playgroundThemeKey) as? String ?? "") ?? playgroundThemeDefault diff --git a/client/ios/DivKitPlayground/WebPreview/Networking/Payload.swift b/client/ios/DivKitPlayground/WebPreview/Networking/Payload.swift index 2662a3c53..17c263af2 100644 --- a/client/ios/DivKitPlayground/WebPreview/Networking/Payload.swift +++ b/client/ios/DivKitPlayground/WebPreview/Networking/Payload.swift @@ -54,10 +54,6 @@ struct UIStatePayload: Encodable { } struct RenderingTime: Encodable { - let div_render_total: Time - let div_parsing_data: Time - let div_parsing_templates: Time - enum HistogramType: String, Encodable { case cold case warm @@ -67,6 +63,11 @@ struct UIStatePayload: Encodable { let value: Int let histogram_type: HistogramType } + + let div_render_total: Time + let div_parsing_data: Time + let div_parsing_templates: Time + } let type: String = "ui_state" diff --git a/client/ios/DivKitPlayground/WebPreview/UI/WebPreviewView.swift b/client/ios/DivKitPlayground/WebPreview/UI/WebPreviewView.swift index 5d54af8da..43f9f7c47 100644 --- a/client/ios/DivKitPlayground/WebPreview/UI/WebPreviewView.swift +++ b/client/ios/DivKitPlayground/WebPreview/UI/WebPreviewView.swift @@ -34,8 +34,9 @@ struct WebPreviewView: View { private final class WebPreviewModel { let divKitComponents: DivKitComponents - private let socket = WebPreviewSocket() private(set) var debugParams: DebugParams! + + private let socket = WebPreviewSocket() private let payloadFactory: UIStatePayloadFactory private var renderingTime: UIStatePayload.RenderingTime? private var cancellables = Set() diff --git a/client/ios/DivKitPlayground/WebPreview/UI/WebPreviewViewController.swift b/client/ios/DivKitPlayground/WebPreview/UI/WebPreviewViewController.swift index 765a60bd9..11cad94e9 100644 --- a/client/ios/DivKitPlayground/WebPreview/UI/WebPreviewViewController.swift +++ b/client/ios/DivKitPlayground/WebPreview/UI/WebPreviewViewController.swift @@ -50,11 +50,6 @@ private final class WebPreviewViewController: DivViewController { fatalError("init(coder:) has not been implemented") } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - takeScreenshot(afterScreenUpdates: false) - } - public override func viewDidAppear(_: Bool) { isAppeared = true } @@ -66,6 +61,11 @@ private final class WebPreviewViewController: DivViewController { isAppeared = false } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + takeScreenshot(afterScreenUpdates: false) + } + override func onViewUpdated() { super.onViewUpdated() takeScreenshot(afterScreenUpdates: true) diff --git a/client/ios/DivKitSnapshotTests/DivKitSnapshotTests.swift b/client/ios/DivKitSnapshotTests/DivKitSnapshotTests.swift index 6c1cfd6b7..754386618 100644 --- a/client/ios/DivKitSnapshotTests/DivKitSnapshotTests.swift +++ b/client/ios/DivKitSnapshotTests/DivKitSnapshotTests.swift @@ -76,6 +76,10 @@ private class LabelImagePreviewProvider: ViewProvider { return label! } + func equals(other: ViewProvider) -> Bool { + loadView() == other.loadView() + } + private func makeLabel() -> UILabel { let label = UILabel() label.text = "Preview" @@ -83,9 +87,6 @@ private class LabelImagePreviewProvider: ViewProvider { return label } - func equals(other: ViewProvider) -> Bool { - loadView() == other.loadView() - } } private let defaultPagerViewState = [ diff --git a/client/ios/DivKitSnapshotTests/ReferenceSet.swift b/client/ios/DivKitSnapshotTests/ReferenceSet.swift index ad8fd8406..1acee09fc 100644 --- a/client/ios/DivKitSnapshotTests/ReferenceSet.swift +++ b/client/ios/DivKitSnapshotTests/ReferenceSet.swift @@ -21,11 +21,12 @@ enum ReferenceSet { } private struct PlistContents: Decodable { - let referenceSnapshotsPath: String - let resultSnapshotsPath: String - enum CodingKeys: String, CodingKey { case referenceSnapshotsPath = "REFERENCE_SNAPSHOTS_PATH" case resultSnapshotsPath = "RESULT_SNAPSHOTS_PATH" } + + let referenceSnapshotsPath: String + let resultSnapshotsPath: String + } diff --git a/client/ios/DivKitSnapshotTests/SnapshotTestRunner.swift b/client/ios/DivKitSnapshotTests/SnapshotTestRunner.swift index 511cef767..e7fa35057 100644 --- a/client/ios/DivKitSnapshotTests/SnapshotTestRunner.swift +++ b/client/ios/DivKitSnapshotTests/SnapshotTestRunner.swift @@ -16,12 +16,12 @@ final class SnapshotTestRunner { let mode = TestMode.verify #endif + private let file: JsonFile + init(file: JsonFile) { self.file = file } - private let file: JsonFile - func run( caseName: String, blocksState: [IdAndCardId: ElementState] = [:], diff --git a/client/ios/DivKitTests/Expressions/CustomFunctionTests.swift b/client/ios/DivKitTests/Expressions/CustomFunctionTests.swift index 08d7588b8..5e86327c8 100644 --- a/client/ios/DivKitTests/Expressions/CustomFunctionTests.swift +++ b/client/ios/DivKitTests/Expressions/CustomFunctionTests.swift @@ -87,16 +87,6 @@ final class CustomFunctionTests: XCTestCase { private lazy var functionsStorage = DivFunctionsStorage(reporter: mockReporter) private lazy var resolver = makeExpressionResolver(path: path) - private func makeExpressionResolver(path: UIElementPath) -> ExpressionResolver { - ExpressionResolver( - path: path, - variablesStorage: variablesStorage, - functionsStorage: functionsStorage, - persistentValuesStorage: DivPersistentValuesStorage(), - reporter: mockReporter - ) - } - override func setUp() { variablesStorage.set(cardId: "card_id", variables: variables) } @@ -315,6 +305,17 @@ final class CustomFunctionTests: XCTestCase { XCTAssertNil(resolver.resolve("@{array_var.increment(2)}")) } + + private func makeExpressionResolver(path: UIElementPath) -> ExpressionResolver { + ExpressionResolver( + path: path, + variablesStorage: variablesStorage, + functionsStorage: functionsStorage, + persistentValuesStorage: DivPersistentValuesStorage(), + reporter: mockReporter + ) + } + } private let variables: DivVariables = [ diff --git a/client/ios/DivKitTests/Expressions/ExpressionTests.swift b/client/ios/DivKitTests/Expressions/ExpressionTests.swift index fb6194e02..a2630dc26 100644 --- a/client/ios/DivKitTests/Expressions/ExpressionTests.swift +++ b/client/ios/DivKitTests/Expressions/ExpressionTests.swift @@ -66,11 +66,26 @@ private struct TestCases: Decodable { } private struct ExpressionTestCase: Decodable { + private enum CodingKeys: String, CodingKey { + case expression, variables, expected, platforms + } + let expression: String let variables: DivVariables let expected: ExpectedValue let platforms: [Platform] + var description: String { + let formattedExpression = if expression.starts(with: "@{"), + expression.last == "}", + !expression.dropFirst(2).contains("@{") { + String(expression.dropFirst(2).dropLast()) + } else { + expression + } + return "\(formattedExpression) -> \(expected.description): \(hashValue)" + } + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) expression = try container.decode(String.self, forKey: .expression) @@ -98,21 +113,6 @@ private struct ExpressionTestCase: Decodable { ) } - var description: String { - let formattedExpression = if expression.starts(with: "@{"), - expression.last == "}", - !expression.dropFirst(2).contains("@{") { - String(expression.dropFirst(2).dropLast()) - } else { - expression - } - return "\(formattedExpression) -> \(expected.description): \(hashValue)" - } - - private enum CodingKeys: String, CodingKey { - case expression, variables, expected, platforms - } - func resolveValue( errorTracker: ExpressionErrorTracker? = nil ) -> String? { @@ -194,6 +194,37 @@ enum ExpectedValue: Decodable { case unorderedArray(DivArray) case error(String) + private enum CodingKeys: String, CodingKey { + case type, value + } + + var description: String { + switch self { + case let .string(value): + formatArgForError(value) + case let .double(value): + formatArgForError(value) + case let .integer(value): + formatArgForError(value) + case let .bool(value): + formatArgForError(value) + case let .color(value): + formatArgForError(value) + case let .datetime(value): + formatArgForError(value) + case let .url(value): + formatArgForError(value) + case let .array(value): + formatArgForError(value) + case let .dict(value): + formatArgForError(value) + case let .unorderedArray(value): + formatArgForError(value) + case .error: + "error" + } + } + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(String.self, forKey: .type) @@ -254,36 +285,6 @@ enum ExpectedValue: Decodable { } } - var description: String { - switch self { - case let .string(value): - formatArgForError(value) - case let .double(value): - formatArgForError(value) - case let .integer(value): - formatArgForError(value) - case let .bool(value): - formatArgForError(value) - case let .color(value): - formatArgForError(value) - case let .datetime(value): - formatArgForError(value) - case let .url(value): - formatArgForError(value) - case let .array(value): - formatArgForError(value) - case let .dict(value): - formatArgForError(value) - case let .unorderedArray(value): - formatArgForError(value) - case .error: - "error" - } - } - - private enum CodingKeys: String, CodingKey { - case type, value - } } extension DivVariable: Swift.Decodable { diff --git a/client/ios/DivKitTests/Expressions/FunctionSignaturesTests.swift b/client/ios/DivKitTests/Expressions/FunctionSignaturesTests.swift index 36bb4a7d5..edbdd2044 100644 --- a/client/ios/DivKitTests/Expressions/FunctionSignaturesTests.swift +++ b/client/ios/DivKitTests/Expressions/FunctionSignaturesTests.swift @@ -48,21 +48,20 @@ private struct TestCases: Decodable { } private struct SignatureTestCase: Decodable { + private enum CodingKeys: String, CodingKey { + case functionName = "function_name" + case isMethod = "is_method" + case arguments + case resultType = "result_type" + case platforms + } + let functionName: String let isMethod: Bool let arguments: [ArgumentSignature] let resultType: Any.Type let platforms: [Platform] - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - functionName = try container.decode(String.self, forKey: .functionName) - isMethod = (try? container.decode(Bool.self, forKey: .isMethod)) ?? false - arguments = (try? container.decode([ArgumentSignature].self, forKey: .arguments)) ?? [] - resultType = try parseType(container.decode(String.self, forKey: .resultType)) - platforms = try container.decode([Platform].self, forKey: .platforms) - } - var toSignature: FunctionSignature { FunctionSignature(arguments: arguments, resultType: resultType) } @@ -82,13 +81,15 @@ private struct SignatureTestCase: Decodable { return name } - private enum CodingKeys: String, CodingKey { - case functionName = "function_name" - case isMethod = "is_method" - case arguments - case resultType = "result_type" - case platforms + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + functionName = try container.decode(String.self, forKey: .functionName) + isMethod = (try? container.decode(Bool.self, forKey: .isMethod)) ?? false + arguments = (try? container.decode([ArgumentSignature].self, forKey: .arguments)) ?? [] + resultType = try parseType(container.decode(String.self, forKey: .resultType)) + platforms = try container.decode([Platform].self, forKey: .platforms) } + } extension Function { diff --git a/client/ios/DivKitTests/Extensions/XCTestCase+ViewVisibilityCallCount.swift b/client/ios/DivKitTests/Extensions/XCTestCase+ViewVisibilityCallCount.swift index 07938ec37..c35aface9 100644 --- a/client/ios/DivKitTests/Extensions/XCTestCase+ViewVisibilityCallCount.swift +++ b/client/ios/DivKitTests/Extensions/XCTestCase+ViewVisibilityCallCount.swift @@ -37,6 +37,10 @@ final class VisibilityTester { let timer: TestTimerScheduler let performer = UIActionEventPerformerMock() + var callsCount: Int { + performer.callCount + } + init( block: Block, timer: TestTimerScheduler @@ -46,10 +50,6 @@ final class VisibilityTester { performer.addSubview(view) } - var callsCount: Int { - performer.callCount - } - func setViewAppear() { updateViewVisibility(isVisible: true) } diff --git a/client/ios/DivKitTests/IntegrationTests.swift b/client/ios/DivKitTests/IntegrationTests.swift index 97106440b..f834a12c6 100644 --- a/client/ios/DivKitTests/IntegrationTests.swift +++ b/client/ios/DivKitTests/IntegrationTests.swift @@ -123,6 +123,10 @@ private struct IntegrationTestData { } private struct IntegrationTest: Decodable, @unchecked Sendable { + let description: String + let divData: DivData + let cases: [IntegrationTestCase] + init(_ data: Data) throws { let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] let casesJson = json["cases"]! @@ -138,25 +142,27 @@ private struct IntegrationTest: Decodable, @unchecked Sendable { ) } - let description: String - let divData: DivData - let cases: [IntegrationTestCase] } private struct IntegrationTestCase: Decodable { + private enum CodingKeys: String, CodingKey { + case divActions = "div_actions", expected, platforms + } + let divActions: [DivAction]? let expected: [Expected] let platforms: [Platform] - private enum CodingKeys: String, CodingKey { - case divActions = "div_actions", expected, platforms - } } private enum Expected: Decodable { case variable(String, ExpectedValue) case error(String) + private enum CodingKeys: String, CodingKey { + case variableName = "variable_name", type, value + } + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -179,9 +185,6 @@ private enum Expected: Decodable { } } - private enum CodingKeys: String, CodingKey { - case variableName = "variable_name", type, value - } } extension DivAction: Swift.Decodable { diff --git a/client/ios/DivKitTests/Templates/TemplateToTypeTests.swift b/client/ios/DivKitTests/Templates/TemplateToTypeTests.swift index 2d5863ce4..c0bb0836a 100644 --- a/client/ios/DivKitTests/Templates/TemplateToTypeTests.swift +++ b/client/ios/DivKitTests/Templates/TemplateToTypeTests.swift @@ -2,16 +2,6 @@ import XCTest final class TemplateToTypeTests: XCTestCase { - private func performTestCase( - json: String, - templateToType: [TemplateName: String] - ) throws { - let dict = try JSONSerialization.jsonObject( - with: json.data(using: .utf8)! - ) as! [String: Any] - XCTAssertEqual(calculateTemplateToType(in: dict), templateToType) - } - func test_IndependentTypes() throws { let json = """ { @@ -101,4 +91,15 @@ final class TemplateToTypeTests: XCTestCase { ] try performTestCase(json: json, templateToType: templateToType) } + + private func performTestCase( + json: String, + templateToType: [TemplateName: String] + ) throws { + let dict = try JSONSerialization.jsonObject( + with: json.data(using: .utf8)! + ) as! [String: Any] + XCTAssertEqual(calculateTemplateToType(in: dict), templateToType) + } + } diff --git a/client/ios/DivKitTests/Timers/Utils.swift b/client/ios/DivKitTests/Timers/Utils.swift index 1255a09bf..cd71d051e 100644 --- a/client/ios/DivKitTests/Timers/Utils.swift +++ b/client/ios/DivKitTests/Timers/Utils.swift @@ -5,9 +5,11 @@ import XCTest public final class TestTimer: TimerType { public let timeInterval: TimeInterval + public var fireDate: Date + public private(set) var isValid = true + private let block: () -> Void private let repeating: Bool - public var fireDate: Date public init( fireDate: Date = .distantFuture, @@ -21,8 +23,6 @@ public final class TestTimer: TimerType { self.repeating = repeating } - public private(set) var isValid = true - public func fire() { guard isValid else { XCTFail("Timer is not valid.") diff --git a/client/ios/DivKitTests/ViewSizeTests.swift b/client/ios/DivKitTests/ViewSizeTests.swift index 87bf265d6..7014d432f 100644 --- a/client/ios/DivKitTests/ViewSizeTests.swift +++ b/client/ios/DivKitTests/ViewSizeTests.swift @@ -4,6 +4,72 @@ import VGSL import XCTest final class DivViewSizeTests: XCTestCase { + private final class TestBlock: BlockWithTraits { + var widthTrait: LayoutKit.LayoutTrait + + var heightTrait: LayoutKit.LayoutTrait + + var intrinsicContentWidth: CGFloat + + var debugDescription: String = "" + + init( + widthTrait: LayoutTrait, + heightTrait: LayoutTrait, + intrinsicContentWidth: CGFloat + ) { + self.widthTrait = widthTrait + self.heightTrait = heightTrait + self.intrinsicContentWidth = intrinsicContentWidth + } + + static func makeBlockView() -> LayoutKit.BlockView { + TestView() + } + + func configureBlockView( + _: LayoutKit.BlockView, + observer _: LayoutKit.ElementStateObserver?, + overscrollDelegate _: VGSLUI.ScrollDelegate?, + renderingDelegate _: LayoutKit.RenderingDelegate? + ) {} + + func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { + 0.0 + } + + func equals(_: LayoutKit.Block) -> Bool { + true + } + + func canConfigureBlockView(_: LayoutKit.BlockView) -> Bool { false } + + func getImageHolders() -> [VGSLUI.ImageHolder] { + [] + } + + func laidOut(for _: CGFloat) -> LayoutKit.Block { + self + } + + func laidOut(for _: CGSize) -> LayoutKit.Block { + self + } + + func updated(withStates _: LayoutKit.BlocksState) throws -> Self { + self + } + } + + private final class TestView: BlockView { + var block: TestBlock? + + var effectiveBackgroundColor: UIColor? { backgroundColor } + + func onVisibleBoundsChanged(from _: CGRect, to _: CGRect) {} + + } + func test_sizeForParentViewSizeForWeightedBlock() { let weightedBlock = TestBlock( widthTrait: LayoutTrait.weighted(.default), @@ -60,67 +126,4 @@ final class DivViewSizeTests: XCTestCase { XCTAssertTrue(size == CGSize(width: 500.0, height: 0.0)) } - private final class TestBlock: BlockWithTraits { - init( - widthTrait: LayoutTrait, - heightTrait: LayoutTrait, - intrinsicContentWidth: CGFloat - ) { - self.widthTrait = widthTrait - self.heightTrait = heightTrait - self.intrinsicContentWidth = intrinsicContentWidth - } - - func configureBlockView( - _: LayoutKit.BlockView, - observer _: LayoutKit.ElementStateObserver?, - overscrollDelegate _: VGSLUI.ScrollDelegate?, - renderingDelegate _: LayoutKit.RenderingDelegate? - ) {} - - var widthTrait: LayoutKit.LayoutTrait - - var heightTrait: LayoutKit.LayoutTrait - - var intrinsicContentWidth: CGFloat - - func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { - 0.0 - } - - func equals(_: LayoutKit.Block) -> Bool { - true - } - - var debugDescription: String = "" - - static func makeBlockView() -> LayoutKit.BlockView { - TestView() - } - - func canConfigureBlockView(_: LayoutKit.BlockView) -> Bool { false } - - func getImageHolders() -> [VGSLUI.ImageHolder] { - [] - } - - func laidOut(for _: CGFloat) -> LayoutKit.Block { - self - } - - func laidOut(for _: CGSize) -> LayoutKit.Block { - self - } - - func updated(withStates _: LayoutKit.BlocksState) throws -> Self { - self - } - } - - private final class TestView: BlockView { - func onVisibleBoundsChanged(from _: CGRect, to _: CGRect) {} - - var block: TestBlock? - var effectiveBackgroundColor: UIColor? { backgroundColor } - } } diff --git a/client/ios/DivKitTestsSupport/FakeImageHolder.swift b/client/ios/DivKitTestsSupport/FakeImageHolder.swift index f7c356dc3..72c70611b 100644 --- a/client/ios/DivKitTestsSupport/FakeImageHolder.swift +++ b/client/ios/DivKitTestsSupport/FakeImageHolder.swift @@ -11,8 +11,6 @@ public final class FakeImageHolderFactory: DivImageHolderFactory { } public final class FakeImageHolder: ImageHolder { - public init() {} - public var image: Image? { nil } @@ -21,6 +19,12 @@ public final class FakeImageHolder: ImageHolder { nil } + public var debugDescription: String { + "FakeImageHolder" + } + + public init() {} + public func requestImageWithCompletion(_: @escaping @MainActor (Image?) -> Void) -> Cancellable? { nil } @@ -33,7 +37,4 @@ public final class FakeImageHolder: ImageHolder { true } - public var debugDescription: String { - "FakeImageHolder" - } } diff --git a/client/ios/DivKitUITests/TooltipsUITests.swift b/client/ios/DivKitUITests/TooltipsUITests.swift index 07969e11b..f8dd294ff 100644 --- a/client/ios/DivKitUITests/TooltipsUITests.swift +++ b/client/ios/DivKitUITests/TooltipsUITests.swift @@ -1,11 +1,15 @@ import XCTest final class TooltipsUITests: XCTestCase { - private let app = XCUIApplication() - private var elementsQuery: XCUIElementTypeQueryProvider { - app.scrollViews.otherElements + private enum TooltipTest { + case tooltipPosition, closeOnSwitchOrientation } + private enum Position: CaseIterable { + case bottom, top, right + } + + private let app = XCUIApplication() private lazy var baseDivView: XCUIElement = { let mainWindow = app.windows.element(boundBy: 0) return mainWindow.descendants(matching: .any).matching(identifier: "baseDivView").element @@ -18,6 +22,10 @@ final class TooltipsUITests: XCTestCase { private lazy var closeTooltipButton: XCUIElement = app.buttons["close tooltip"] + private var elementsQuery: XCUIElementTypeQueryProvider { + app.scrollViews.otherElements + } + override func setUpWithError() throws { try super.setUpWithError() @@ -85,11 +93,4 @@ final class TooltipsUITests: XCTestCase { } } - private enum TooltipTest { - case tooltipPosition, closeOnSwitchOrientation - } - - private enum Position: CaseIterable { - case bottom, top, right - } } diff --git a/client/ios/LayoutKit/Interface/UIElementPath.swift b/client/ios/LayoutKit/Interface/UIElementPath.swift index 39c6db0c1..451079933 100644 --- a/client/ios/LayoutKit/Interface/UIElementPath.swift +++ b/client/ios/LayoutKit/Interface/UIElementPath.swift @@ -4,22 +4,6 @@ import VGSL public struct UIElementPath: CustomStringConvertible, ExpressibleByStringLiteral, Codable { private let address: ListNode - private init(address: ListNode) { - self.address = address - } - - public init(_ root: String) { - address = ListNode(value: root) - } - - public init(parent: UIElementPath, child: String) { - address = ListNode(value: child, next: parent.address) - } - - public init(stringLiteral value: StringLiteralType) { - self.init(value) - } - public var description: String { address.joined(separator: "/") } @@ -39,11 +23,20 @@ public struct UIElementPath: CustomStringConvertible, ExpressibleByStringLiteral address.value } - public func starts(with path: UIElementPath) -> Bool { - if path == self { - return true - } - return parent?.starts(with: path) == true + public init(_ root: String) { + address = ListNode(value: root) + } + + public init(parent: UIElementPath, child: String) { + address = ListNode(value: child, next: parent.address) + } + + public init(stringLiteral value: StringLiteralType) { + self.init(value) + } + + private init(address: ListNode) { + self.address = address } public static func parse(_ path: String) -> UIElementPath { @@ -53,6 +46,14 @@ public struct UIElementPath: CustomStringConvertible, ExpressibleByStringLiteral } return UIElementPath(path) } + + public func starts(with path: UIElementPath) -> Bool { + if path == self { + return true + } + return parent?.starts(with: path) == true + } + } extension UIElementPath: Hashable { @@ -102,8 +103,13 @@ extension UIElementPath { } private final class ListNode: Codable { + enum CodingKeys: CodingKey { + case value, next + } + let value: String let next: ListNode? + private var _root: String? private lazy var cachedHash: Int = { @@ -114,11 +120,6 @@ private final class ListNode: Codable { return hasher.finalize() }() - init(value: String, next: ListNode? = nil) { - self.value = value - self.next = next - } - var root: String { if let _root { return _root @@ -131,9 +132,11 @@ private final class ListNode: Codable { return result } - enum CodingKeys: CodingKey { - case value, next + init(value: String, next: ListNode? = nil) { + self.value = value + self.next = next } + } extension ListNode: Hashable { diff --git a/client/ios/LayoutKit/LayoutKit/Base/MaskedInputViewModel.swift b/client/ios/LayoutKit/LayoutKit/Base/MaskedInputViewModel.swift index 048531469..c9abb2e7d 100644 --- a/client/ios/LayoutKit/LayoutKit/Base/MaskedInputViewModel.swift +++ b/client/ios/LayoutKit/LayoutKit/Base/MaskedInputViewModel.swift @@ -10,12 +10,13 @@ public class MaskedInputViewModel { case clearRange(range: Range) } - @ObservableProperty private var rawCursorPosition: CursorData? @ObservableVariable var text: String @ObservableVariable var cursorPosition: NSRange? @ObservableProperty var rawText: String @ObservableProperty var maskValidator: MaskValidator @ObservableProperty var typo: Typo? + + @ObservableProperty private var rawCursorPosition: CursorData? private let disposePool = AutodisposePool() init( diff --git a/client/ios/LayoutKit/LayoutKit/Base/Masks/MaskValidator.swift b/client/ios/LayoutKit/LayoutKit/Base/Masks/MaskValidator.swift index 5bbae75da..9a06e54cc 100644 --- a/client/ios/LayoutKit/LayoutKit/Base/Masks/MaskValidator.swift +++ b/client/ios/LayoutKit/LayoutKit/Base/Masks/MaskValidator.swift @@ -14,6 +14,10 @@ public final class MaskValidator: Equatable { self.formatter = formatter } + public static func ==(lhs: MaskValidator, rhs: MaskValidator) -> Bool { + lhs.formatter.equals(rhs.formatter) + } + public func formatted(rawText: String, rawCursorPosition: CursorData? = nil) -> InputData { formatter.formatted(rawText: rawText, rawCursorPosition: rawCursorPosition) } @@ -80,9 +84,6 @@ public final class MaskValidator: Equatable { ) } - public static func ==(lhs: MaskValidator, rhs: MaskValidator) -> Bool { - lhs.formatter.equals(rhs.formatter) - } } public enum CursorPositionTag {} @@ -102,6 +103,7 @@ public struct InputData { public let text: String public let cursorPosition: CursorPosition? public var rawData: [RawCharacter] + public var rawText: String { String(rawData.map(\.char)) } diff --git a/client/ios/LayoutKit/LayoutKit/Base/Masks/PhoneMaskFormatter.swift b/client/ios/LayoutKit/LayoutKit/Base/Masks/PhoneMaskFormatter.swift index 9876afe7d..b53fb9aca 100644 --- a/client/ios/LayoutKit/LayoutKit/Base/Masks/PhoneMaskFormatter.swift +++ b/client/ios/LayoutKit/LayoutKit/Base/Masks/PhoneMaskFormatter.swift @@ -62,6 +62,13 @@ public final class PhoneMaskFormatter: MaskFormatter { return InputData(text: textString, cursorPosition: newCursorPosition, rawData: rawData) } + public func equals(_ other: MaskFormatter) -> Bool { + guard let other = other as? PhoneMaskFormatter else { + return false + } + return self.masksByCountryCode == other.masksByCountryCode + } + private func findMask(for rawText: String) -> String { guard !rawText.isEmpty else { return "" @@ -95,12 +102,6 @@ public final class PhoneMaskFormatter: MaskFormatter { return resultMask + extraSymbols } - public func equals(_ other: MaskFormatter) -> Bool { - guard let other = other as? PhoneMaskFormatter else { - return false - } - return self.masksByCountryCode == other.masksByCountryCode - } } extension Character { diff --git a/client/ios/LayoutKit/LayoutKit/Base/TextInputValidator.swift b/client/ios/LayoutKit/LayoutKit/Base/TextInputValidator.swift index 6fc5a6fe6..9d2f37115 100644 --- a/client/ios/LayoutKit/LayoutKit/Base/TextInputValidator.swift +++ b/client/ios/LayoutKit/LayoutKit/Base/TextInputValidator.swift @@ -1,12 +1,12 @@ import Foundation public final class TextInputValidator { - private let allowEmpty: Bool - private let validator: (String) -> Bool - public let message: () -> String? public var isValid: Binding + private let allowEmpty: Bool + private let validator: (String) -> Bool + public init( isValid: Binding, allowEmpty: Bool, diff --git a/client/ios/LayoutKit/LayoutKit/Binding.swift b/client/ios/LayoutKit/LayoutKit/Binding.swift index f7d399828..b550c3674 100644 --- a/client/ios/LayoutKit/LayoutKit/Binding.swift +++ b/client/ios/LayoutKit/LayoutKit/Binding.swift @@ -2,9 +2,10 @@ import Foundation import VGSL public struct Binding: Equatable { - private let name: String @Property public var value: T + private let name: String + public init( name: String, value: Property diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/ActionAnimation.swift b/client/ios/LayoutKit/LayoutKit/Blocks/ActionAnimation.swift index 54239054b..5afa56bae 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/ActionAnimation.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/ActionAnimation.swift @@ -1,9 +1,9 @@ public struct ActionAnimation: Equatable { + public static let empty = Self(touchDown: [.empty], touchUp: [.empty]) + public let touchDown: [TransitioningAnimation] public let touchUp: [TransitioningAnimation] - public static let empty = Self(touchDown: [.empty], touchUp: [.empty]) - public init( touchDown: [TransitioningAnimation], touchUp: [TransitioningAnimation] diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Anchor/AnchorBlock+UIViewRenderable.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Anchor/AnchorBlock+UIViewRenderable.swift index 8a5830142..7b42a14d8 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Anchor/AnchorBlock+UIViewRenderable.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Anchor/AnchorBlock+UIViewRenderable.swift @@ -44,16 +44,41 @@ private final class AnchorView: BlockView, VisibleBoundsTrackingContainer { private var preventLayout = false private weak var observer: ElementStateObserver? - private var block: AnchorBlock! { - modelAndLastLayoutSize.model?.block - } - var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { [leadingView, centerView, trailingView].compactMap { $0 } } var effectiveBackgroundColor: UIColor? { backgroundColor } + private var block: AnchorBlock! { + modelAndLastLayoutSize.model?.block + } + + override func layoutSubviews() { + guard !preventLayout else { return } + + super.layoutSubviews() + + if let lastLayoutSize = modelAndLastLayoutSize.lastLayoutSize, bounds.size == lastLayoutSize { + return + } + + guard let model = modelAndLastLayoutSize.model else { + return + } + + let layout = model.layout ?? model.block.makeLayout(for: bounds.size) + + leadingView?.frame = layout.leadingFrame + centerView?.frame = layout.centerFrame + trailingView?.frame = layout.trailingFrame + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + return result === self ? nil : result + } + func configure( model: Model, observer: ElementStateObserver?, @@ -93,29 +118,5 @@ private final class AnchorView: BlockView, VisibleBoundsTrackingContainer { setNeedsLayout() } - override func layoutSubviews() { - guard !preventLayout else { return } - - super.layoutSubviews() - - if let lastLayoutSize = modelAndLastLayoutSize.lastLayoutSize, bounds.size == lastLayoutSize { - return - } - - guard let model = modelAndLastLayoutSize.model else { - return - } - - let layout = model.layout ?? model.block.makeLayout(for: bounds.size) - - leadingView?.frame = layout.leadingFrame - centerView?.frame = layout.centerFrame - trailingView?.frame = layout.trailingFrame - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let result = super.hitTest(point, with: event) - return result === self ? nil : result - } } #endif diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Anchor/AnchorBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Anchor/AnchorBlock.swift index 894f28f4a..5a1c9d12a 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Anchor/AnchorBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Anchor/AnchorBlock.swift @@ -16,33 +16,32 @@ public final class AnchorBlock: BlockWithLayout, BlockWithTraits { public let center: Block public let trailing: Block? - private var contents: [Block] { - [leading, center, trailing].compactMap { $0 } + public var intrinsicContentWidth: CGFloat { + if case let .fixed(value) = widthTrait { + return value + } + + let widths = contents.map(\.intrinsicContentWidth) + var result: CGFloat = switch direction { + case .horizontal: + widths.reduce(0, +) + case .vertical: + widths.max()! + } + + if case let .intrinsic(_, minSize, maxSize) = widthTrait { + result = clamp(result, min: minSize, max: maxSize) + } + + return result } - private init( - direction: ContainerBlock.LayoutDirection, - widthTrait: LayoutTrait, - heightTrait: LayoutTrait, - crossAlignment: Alignment, - leading: Block?, - center: Block, - trailing: Block? - ) { - self.direction = direction - self.widthTrait = widthTrait - self.heightTrait = heightTrait - self.crossAlignment = crossAlignment - self.leading = leading - self.center = center - self.trailing = trailing + public var debugDescription: String { + "Anchored" + } - for content in contents { - switch direction { - case .vertical: precondition(!content.isVerticallyResizable) - case .horizontal: precondition(!content.isHorizontallyResizable) - } - } + private var contents: [Block] { + [leading, center, trailing].compactMap { $0 } } public convenience init( @@ -76,24 +75,29 @@ public final class AnchorBlock: BlockWithLayout, BlockWithTraits { ) } - public var intrinsicContentWidth: CGFloat { - if case let .fixed(value) = widthTrait { - return value - } + private init( + direction: ContainerBlock.LayoutDirection, + widthTrait: LayoutTrait, + heightTrait: LayoutTrait, + crossAlignment: Alignment, + leading: Block?, + center: Block, + trailing: Block? + ) { + self.direction = direction + self.widthTrait = widthTrait + self.heightTrait = heightTrait + self.crossAlignment = crossAlignment + self.leading = leading + self.center = center + self.trailing = trailing - let widths = contents.map(\.intrinsicContentWidth) - var result: CGFloat = switch direction { - case .horizontal: - widths.reduce(0, +) - case .vertical: - widths.max()! + for content in contents { + switch direction { + case .vertical: precondition(!content.isVerticallyResizable) + case .horizontal: precondition(!content.isHorizontallyResizable) + } } - - if case let .intrinsic(_, minSize, maxSize) = widthTrait { - result = clamp(result, min: minSize, max: maxSize) - } - - return result } public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { @@ -118,17 +122,6 @@ public final class AnchorBlock: BlockWithLayout, BlockWithTraits { return result } - func makeLayout(for size: CGSize) -> Layout { - Layout( - size: size, - direction: direction, - crossAlignment: crossAlignment, - leading: leading, - center: center, - trailing: trailing - ) - } - public func laidOutHierarchy(for size: CGSize) -> (AnchorBlock, Layout) { let layout = makeLayout(for: size) let laidOutSelf = AnchorBlock( @@ -155,10 +148,6 @@ public final class AnchorBlock: BlockWithLayout, BlockWithTraits { && trailing == other.trailing } - public var debugDescription: String { - "Anchored" - } - public func getImageHolders() -> [ImageHolder] { contents.flatMap { $0.getImageHolders() } } @@ -186,4 +175,16 @@ public final class AnchorBlock: BlockWithLayout, BlockWithTraits { trailing: trailing?.updated(path: path, isFocused: isFocused) ) } + + func makeLayout(for size: CGSize) -> Layout { + Layout( + size: size, + direction: direction, + crossAlignment: crossAlignment, + leading: leading, + center: center, + trailing: trailing + ) + } + } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/AnchorPoint.swift b/client/ios/LayoutKit/LayoutKit/Blocks/AnchorPoint.swift index 76a1614ac..2b02b8eac 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/AnchorPoint.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/AnchorPoint.swift @@ -2,6 +2,7 @@ import CoreGraphics public struct AnchorPoint: Equatable { let x, y: AnchorValue + public init(x: AnchorValue, y: AnchorValue) { self.x = x self.y = y diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Aspect/AspectBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Aspect/AspectBlock.swift index 46479330b..1cab3f039 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Aspect/AspectBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Aspect/AspectBlock.swift @@ -6,14 +6,21 @@ public final class AspectBlock: WrapperBlock { public var child: Block { content } + public var isVerticallyResizable: Bool { false } + + public var debugDescription: String { + """ + Aspect \(aspectRatio): + \(child) + """ + } + public init(content: Content, aspectRatio: CGFloat) { assert(content.isVerticallyResizable) self.content = content self.aspectRatio = aspectRatio } - public var isVerticallyResizable: Bool { false } - public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { width * aspectRatio } @@ -31,13 +38,6 @@ public final class AspectBlock: WrapperBlock { return child == other.child && aspectRatio == other.aspectRatio } - public var debugDescription: String { - """ - Aspect \(aspectRatio): - \(child) - """ - } - public func laidOut(for width: CGFloat) -> Block { makeCopy(wrapping: child.laidOut(for: width)) } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/BackgroundBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/BackgroundBlock.swift index 4851c66fa..de27486e4 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/BackgroundBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/BackgroundBlock.swift @@ -19,6 +19,12 @@ public final class BackgroundBlock: BlockWithLayout, WrapperBlock { self.cornerRadius = cornerRadius } + public static func ==(lhs: BackgroundBlock, rhs: BackgroundBlock) -> Bool { + lhs.background == rhs.background && + lhs.child == rhs.child && + lhs.cornerRadius == rhs.cornerRadius + } + public func equals(_ other: Block) -> Bool { guard let other = other as? BackgroundBlock else { return false @@ -35,12 +41,6 @@ public final class BackgroundBlock: BlockWithLayout, WrapperBlock { ) } - public static func ==(lhs: BackgroundBlock, rhs: BackgroundBlock) -> Bool { - lhs.background == rhs.background && - lhs.child == rhs.child && - lhs.cornerRadius == rhs.cornerRadius - } - func laidOutHierarchy(for size: CGSize) -> (BackgroundBlock, Layout) { let laidOutChild = child.laidOut(for: size) let block = BackgroundBlock( diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Block+Animation.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Block+Animation.swift index 221a606d4..0ed417307 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Block+Animation.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Block+Animation.swift @@ -12,6 +12,23 @@ public func ==(lhs: CATransform3D, rhs: CATransform3D) -> Bool { } public struct BlockAnimation: Equatable { + public struct KeyTime: ExpressibleByFloatLiteral, ExpressibleByIntegerLiteral, Equatable { + public let value: Double + + public init(_ value: Double) { + precondition(value >= 0 && value <= 1.0, "KeyTime value must be in [0; 1]") + self.value = value + } + + public init(floatLiteral value: Double) { + self.init(value) + } + + public init(integerLiteral value: IntegerLiteralType) { + self.init(Double(value)) + } + } + public let changes: AnimationChanges public let keyTimes: [KeyTime] public let duration: TimeInterval @@ -38,22 +55,6 @@ public struct BlockAnimation: Equatable { self.timingFunction = timingFunction } - public struct KeyTime: ExpressibleByFloatLiteral, ExpressibleByIntegerLiteral, Equatable { - public let value: Double - - public init(_ value: Double) { - precondition(value >= 0 && value <= 1.0, "KeyTime value must be in [0; 1]") - self.value = value - } - - public init(floatLiteral value: Double) { - self.init(value) - } - - public init(integerLiteral value: IntegerLiteralType) { - self.init(Double(value)) - } - } } extension BlockAnimation { diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/BlockAlignment2D.swift b/client/ios/LayoutKit/LayoutKit/Blocks/BlockAlignment2D.swift index f8a710250..62548acdd 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/BlockAlignment2D.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/BlockAlignment2D.swift @@ -2,8 +2,6 @@ import CoreGraphics import VGSL public struct BlockAlignment2D: Equatable { - public let horizontal: Alignment - public let vertical: Alignment public static let `default` = BlockAlignment2D.topLeft public static let topLeft = BlockAlignment2D(horizontal: .leading, vertical: .leading) public static let topCenter = BlockAlignment2D(horizontal: .center, vertical: .leading) @@ -15,6 +13,9 @@ public struct BlockAlignment2D: Equatable { public static let bottomCenter = BlockAlignment2D(horizontal: .center, vertical: .trailing) public static let bottomRight = BlockAlignment2D(horizontal: .trailing, vertical: .trailing) + public let horizontal: Alignment + public let vertical: Alignment + public init( horizontal: Alignment = .leading, vertical: Alignment = .leading diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/BlockTooltip.swift b/client/ios/LayoutKit/LayoutKit/Blocks/BlockTooltip.swift index 09baa635b..0e3192ee7 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/BlockTooltip.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/BlockTooltip.swift @@ -34,6 +34,10 @@ public struct BlockTooltip: Equatable { public let useLegacyWidth: Bool public let tooltipViewFactory: TooltipViewFactory? + public var id: String { + params.id + } + public init( block: Block, params: BlockTooltipParams, @@ -50,10 +54,6 @@ public struct BlockTooltip: Equatable { self.params = params } - public var id: String { - params.id - } - public static func ==(lhs: BlockTooltip, rhs: BlockTooltip) -> Bool { lhs.params == rhs.params && lhs.offset == rhs.offset && diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/BlockTooltipParams.swift b/client/ios/LayoutKit/LayoutKit/Blocks/BlockTooltipParams.swift index da5e1d004..19e008c17 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/BlockTooltipParams.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/BlockTooltipParams.swift @@ -5,11 +5,12 @@ public struct BlockTooltipParams: Equatable { public let mode: BlockTooltip.Mode public let duration: TimeInterval public let closeByTapOutside: Bool - let tapOutsideActions: [UserInterfaceAction] public let backgroundAccessibilityDescription: String? public let animationIn: [TransitioningAnimation]? public let animationOut: [TransitioningAnimation]? + let tapOutsideActions: [UserInterfaceAction] + public init( id: String, mode: BlockTooltip.Mode, diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift index e6444fa67..2442ced27 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift @@ -9,12 +9,6 @@ import AppKit #endif public final class ContainerBlock: BlockWithLayout { - typealias Layout = ContainerBlockLayout - public static let defaultAnchorPoint = AnchorPoint( - x: .relative(value: 50), - y: .relative(value: 50) - ) - /// Determines direction in which child blocks are laid out in a container @frozen public enum LayoutDirection: CaseIterable { @@ -82,12 +76,19 @@ public final class ContainerBlock: BlockWithLayout { } } + typealias Layout = ContainerBlockLayout + private struct CachedSizes { var intrinsicWidth: CGFloat? var intrinsicHeight: (width: CGFloat, height: CGFloat)? var nonResizableSize: (width: CGFloat, height: CGFloat?)? } + public static let defaultAnchorPoint = AnchorPoint( + x: .relative(value: 50), + y: .relative(value: 50) + ) + public let blockLayoutDirection: UserInterfaceLayoutDirection public let layoutDirection: LayoutDirection public let layoutMode: LayoutMode @@ -108,6 +109,98 @@ public final class ContainerBlock: BlockWithLayout { private var cached = CachedSizes() + public var isVerticallyResizable: Bool { heightTrait.isResizable } + public var isHorizontallyResizable: Bool { widthTrait.isResizable } + + public var calculateWidthFirst: Bool { + switch widthTrait { + case .fixed, .weighted: + true + case .intrinsic: + !(layoutDirection == .vertical && layoutMode == .wrap) + } + } + + public var isVerticallyConstrained: Bool { heightTrait.isConstrained } + public var isHorizontallyConstrained: Bool { widthTrait.isConstrained } + + public var intrinsicContentWidth: CGFloat { + if case let .fixed(width) = widthTrait { + return width + } + + if let cached = cached.intrinsicWidth { + return cached + } + + var result: CGFloat = switch layoutDirection { + case .horizontal: + (children.map(\.content.intrinsicContentWidth) + gaps).reduce(0, +) + case .vertical: + children.map(\.content.intrinsicContentWidth).max() ?? 0 + } + + if case let .intrinsic(_, minSize, maxSize) = widthTrait { + result = clamp(result, min: minSize, max: maxSize) + } + + cached.intrinsicWidth = result + return result + } + + public var widthOfHorizontallyNonResizableBlock: CGFloat { + if case let .fixed(value) = widthTrait { + return value + } + + guard case .intrinsic = widthTrait else { + assertionFailure("cannot get widthOfHorizontallyNonResizableBlock for resizable block") + return 0 + } + + if let cached = cached.nonResizableSize, cached.height == nil { + return cached.width + } + + let result: CGFloat = switch layoutDirection { + case .horizontal: + (children.map(\.content.widthOfHorizontallyNonResizableBlock) + gaps) + .reduce(0, +) + case .vertical: + // MOBYANDEXIOS-1092: Only non-resizable children can influence the width of a container + // because the widths of resizable children depend on the width of container itself + children.filter { !$0.content.isHorizontallyResizable } + .map(\.content.widthOfHorizontallyNonResizableBlock).max() ?? 0 + } + + cached.nonResizableSize = (width: result, height: nil) + return result + } + + public var heightOfVerticallyNonResizableBlock: CGFloat { + assert( + layoutMode == .wrap && layoutDirection == .vertical, + "First height calculation should only be used for vertical container with wrap layout mode" + ) + return heightOfVerticallyNonResizableBlock(forWidth: .zero) + } + + public var weightOfVerticallyResizableBlock: LayoutTrait.Weight { + guard case let .weighted(value) = heightTrait else { + assertionFailure("try to get weight for non resizable block") + return LayoutTrait.Weight.default + } + return value + } + + public var weightOfHorizontallyResizableBlock: LayoutTrait.Weight { + guard case let .weighted(value) = widthTrait else { + assertionFailure("try to get weight for non resizable block") + return LayoutTrait.Weight.default + } + return value + } + public init( blockLayoutDirection: UserInterfaceLayoutDirection = .leftToRight, layoutDirection: LayoutDirection, @@ -186,94 +279,6 @@ public final class ContainerBlock: BlockWithLayout { return layout.ascent } - private func validateLayoutTraits() throws { - if layoutMode == .wrap { - switch layoutDirection { - case .horizontal: - guard children.map(\.content).allVerticallyNonResizable else { - throw BlockError( - "Container block error: horizontal wrap container has children with resizable height" - ) - } - case .vertical: - guard children.map(\.content).allHorizontallyNonResizable else { - throw BlockError( - "Container block error: vertical wrap container has children with resizable width" - ) - } - } - } - - if case .intrinsic = widthTrait { - switch layoutDirection { - case .horizontal: - guard children.map(\.content).allHorizontallyNonResizable else { - throw BlockError( - "Container block error: horizontal intrinsic-width container has children with resizable width" - ) - } - case .vertical: - guard children.map(\.content).hasHorizontallyNonResizable else { - throw BlockError( - "Container block error: in vertical intrinsic-width container all children have resizable width" - ) - } - } - } - - if case .intrinsic = heightTrait { - switch layoutDirection { - case .horizontal: - break // this is currently a valid case, see `.max() ?? 0` on line 163 - case .vertical: - guard children.map(\.content).allVerticallyNonResizable else { - throw BlockError( - "Container block error: vertical intrinsic-height container has children with resizable height" - ) - } - } - } - } - - public var isVerticallyResizable: Bool { heightTrait.isResizable } - public var isHorizontallyResizable: Bool { widthTrait.isResizable } - - public var calculateWidthFirst: Bool { - switch widthTrait { - case .fixed, .weighted: - true - case .intrinsic: - !(layoutDirection == .vertical && layoutMode == .wrap) - } - } - - public var isVerticallyConstrained: Bool { heightTrait.isConstrained } - public var isHorizontallyConstrained: Bool { widthTrait.isConstrained } - - public var intrinsicContentWidth: CGFloat { - if case let .fixed(width) = widthTrait { - return width - } - - if let cached = cached.intrinsicWidth { - return cached - } - - var result: CGFloat = switch layoutDirection { - case .horizontal: - (children.map(\.content.intrinsicContentWidth) + gaps).reduce(0, +) - case .vertical: - children.map(\.content.intrinsicContentWidth).max() ?? 0 - } - - if case let .intrinsic(_, minSize, maxSize) = widthTrait { - result = clamp(result, min: minSize, max: maxSize) - } - - cached.intrinsicWidth = result - return result - } - public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { if case let .fixed(height) = heightTrait { return height @@ -320,43 +325,6 @@ public final class ContainerBlock: BlockWithLayout { return result } - public var widthOfHorizontallyNonResizableBlock: CGFloat { - if case let .fixed(value) = widthTrait { - return value - } - - guard case .intrinsic = widthTrait else { - assertionFailure("cannot get widthOfHorizontallyNonResizableBlock for resizable block") - return 0 - } - - if let cached = cached.nonResizableSize, cached.height == nil { - return cached.width - } - - let result: CGFloat = switch layoutDirection { - case .horizontal: - (children.map(\.content.widthOfHorizontallyNonResizableBlock) + gaps) - .reduce(0, +) - case .vertical: - // MOBYANDEXIOS-1092: Only non-resizable children can influence the width of a container - // because the widths of resizable children depend on the width of container itself - children.filter { !$0.content.isHorizontallyResizable } - .map(\.content.widthOfHorizontallyNonResizableBlock).max() ?? 0 - } - - cached.nonResizableSize = (width: result, height: nil) - return result - } - - public var heightOfVerticallyNonResizableBlock: CGFloat { - assert( - layoutMode == .wrap && layoutDirection == .vertical, - "First height calculation should only be used for vertical container with wrap layout mode" - ) - return heightOfVerticallyNonResizableBlock(forWidth: .zero) - } - public func widthOfHorizontallyNonResizableBlock(forHeight height: CGFloat) -> CGFloat { assert( layoutMode == .wrap && layoutDirection == .vertical, @@ -406,22 +374,6 @@ public final class ContainerBlock: BlockWithLayout { } } - public var weightOfVerticallyResizableBlock: LayoutTrait.Weight { - guard case let .weighted(value) = heightTrait else { - assertionFailure("try to get weight for non resizable block") - return LayoutTrait.Weight.default - } - return value - } - - public var weightOfHorizontallyResizableBlock: LayoutTrait.Weight { - guard case let .weighted(value) = widthTrait else { - assertionFailure("try to get weight for non resizable block") - return LayoutTrait.Weight.default - } - return value - } - public func equals(_ other: Block) -> Bool { guard let other = other as? ContainerBlock else { return false @@ -450,6 +402,56 @@ public final class ContainerBlock: BlockWithLayout { return (block, layout) } + + private func validateLayoutTraits() throws { + if layoutMode == .wrap { + switch layoutDirection { + case .horizontal: + guard children.map(\.content).allVerticallyNonResizable else { + throw BlockError( + "Container block error: horizontal wrap container has children with resizable height" + ) + } + case .vertical: + guard children.map(\.content).allHorizontallyNonResizable else { + throw BlockError( + "Container block error: vertical wrap container has children with resizable width" + ) + } + } + } + + if case .intrinsic = widthTrait { + switch layoutDirection { + case .horizontal: + guard children.map(\.content).allHorizontallyNonResizable else { + throw BlockError( + "Container block error: horizontal intrinsic-width container has children with resizable width" + ) + } + case .vertical: + guard children.map(\.content).hasHorizontallyNonResizable else { + throw BlockError( + "Container block error: in vertical intrinsic-width container all children have resizable width" + ) + } + } + } + + if case .intrinsic = heightTrait { + switch layoutDirection { + case .horizontal: + break // this is currently a valid case, see `.max() ?? 0` on line 163 + case .vertical: + guard children.map(\.content).allVerticallyNonResizable else { + throw BlockError( + "Container block error: vertical intrinsic-height container has children with resizable height" + ) + } + } + } + } + } private func makeGapsWithSeparators( diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlockLayout.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlockLayout.swift index 956b4096b..23ef4c43f 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlockLayout.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlockLayout.swift @@ -7,6 +7,15 @@ struct ContainerBlockLayout { case fits case doesNotFit(notFittingPartSize: CGFloat) + var insetValue: CGFloat { + switch self { + case .fits: + 0 + case let .doesNotFit(notFittingPartSize: inset): + inset + } + } + init(offsets: [CGFloat], margin: CGFloat) { let minOffset = (offsets.min() ?? 0) - margin if minOffset.isApproximatelyLessThan(0) { @@ -16,19 +25,12 @@ struct ContainerBlockLayout { } } - var insetValue: CGFloat { - switch self { - case .fits: - 0 - case let .doesNotFit(notFittingPartSize: inset): - inset - } - } } public private(set) var childrenWithSeparators: [ContainerBlock.Child] = [] public private(set) var blockFrames: [CGRect] = [] public private(set) var ascent: CGFloat? + let gaps: [CGFloat] let blockLayoutDirection: UserInterfaceLayoutDirection let layoutDirection: ContainerBlock.LayoutDirection @@ -38,6 +40,32 @@ struct ContainerBlockLayout { let needCompressConstrainedBlocks: Bool let axialAlignmentManager: AxialAlignmentManager + public var leftInset: CGFloat { + let leftMargin = layoutDirection == .horizontal ? gaps.first! : 0 + return ContentFitting( + offsets: blockFrames.map(\.minX), + margin: leftMargin + ).insetValue + } + + public var topInset: CGFloat { + let topMargin = layoutDirection == .vertical ? gaps.first! : 0 + return ContentFitting( + offsets: blockFrames.map(\.minY), + margin: topMargin + ).insetValue + } + + public var bottomInset: CGFloat { layoutDirection == .vertical ? gaps.last! : 0 } + public var rightInset: CGFloat { layoutDirection == .horizontal ? gaps.last! : 0 } + + public var contentSize: CGSize { + CGSize( + width: blockFrames.map(\.maxX).max() ?? 0, + height: blockFrames.map(\.maxY).max() ?? 0 + ) + } + private var sizeInDirection: CGFloat { switch layoutDirection { case .horizontal: @@ -380,32 +408,6 @@ struct ContainerBlockLayout { } } - public var leftInset: CGFloat { - let leftMargin = layoutDirection == .horizontal ? gaps.first! : 0 - return ContentFitting( - offsets: blockFrames.map(\.minX), - margin: leftMargin - ).insetValue - } - - public var topInset: CGFloat { - let topMargin = layoutDirection == .vertical ? gaps.first! : 0 - return ContentFitting( - offsets: blockFrames.map(\.minY), - margin: topMargin - ).insetValue - } - - public var bottomInset: CGFloat { layoutDirection == .vertical ? gaps.last! : 0 } - public var rightInset: CGFloat { layoutDirection == .horizontal ? gaps.last! : 0 } - - public var contentSize: CGSize { - CGSize( - width: blockFrames.map(\.maxX).max() ?? 0, - height: blockFrames.map(\.maxY).max() ?? 0 - ) - } - private func getMaxAscent( current containerAscent: CGFloat?, child: ContainerBlock.Child, diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Container/WrapLayoutGroups.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Container/WrapLayoutGroups.swift index ca00f419c..393fce1d5 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Container/WrapLayoutGroups.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Container/WrapLayoutGroups.swift @@ -20,6 +20,23 @@ struct WrapLayoutGroups { private var offset: CGFloat = 0 private var separatorAdded = false + private var separatorOffset: CGFloat { + guard let separator else { + return 0 + } + let separatorSize = separatorSize[keyPath: keyPath] + var offset: CGFloat = 0 + if line.count > 0, separator.showBetween { + offset = separatorSize + } else if separator.showAtStart { + offset = separatorSize + } + if separator.showAtEnd { + offset = offset + separatorSize + } + return offset + } + init( blockLayoutDirection: UserInterfaceLayoutDirection, children: [ContainerBlock.Child], @@ -93,23 +110,6 @@ struct WrapLayoutGroups { childrenWithSeparators = groups.flatMap { $0 }.map(\.child) } - private var separatorOffset: CGFloat { - guard let separator else { - return 0 - } - let separatorSize = separatorSize[keyPath: keyPath] - var offset: CGFloat = 0 - if line.count > 0, separator.showBetween { - offset = separatorSize - } else if separator.showAtStart { - offset = separatorSize - } - if separator.showAtEnd { - offset = offset + separatorSize - } - return offset - } - private mutating func addChild(child: ContainerBlock.Child, size: CGSize) { if line.count > 0 { addBetweenSeparator() diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/DecoratingBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/DecoratingBlock.swift index dec6c67d5..d640a4e54 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/DecoratingBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/DecoratingBlock.swift @@ -34,6 +34,14 @@ final class DecoratingBlock: WrapperBlock { let isFocused: Bool let captureFocusOnAction: Bool + var intrinsicContentWidth: CGFloat { + child.intrinsicContentWidth.roundedToScreenScale + paddings.horizontal.sum + } + + var widthOfHorizontallyNonResizableBlock: CGFloat { + intrinsicContentWidth + } + init( child: Block, backgroundColor: Color = DecoratingBlock.defaultBackgroundColor, @@ -86,8 +94,11 @@ final class DecoratingBlock: WrapperBlock { self.captureFocusOnAction = captureFocusOnAction } - var intrinsicContentWidth: CGFloat { - child.intrinsicContentWidth.roundedToScreenScale + paddings.horizontal.sum + public func ascent(forWidth width: CGFloat) -> CGFloat? { + guard let childAscent = child.ascent(forWidth: width) else { + return nil + } + return childAscent + paddings.vertical.leading } func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { @@ -96,21 +107,10 @@ final class DecoratingBlock: WrapperBlock { + paddings.vertical.sum } - var widthOfHorizontallyNonResizableBlock: CGFloat { - intrinsicContentWidth - } - func heightOfVerticallyNonResizableBlock(forWidth width: CGFloat) -> CGFloat { intrinsicContentHeight(forWidth: width) } - public func ascent(forWidth width: CGFloat) -> CGFloat? { - guard let childAscent = child.ascent(forWidth: width) else { - return nil - } - return childAscent + paddings.vertical.leading - } - func laidOut(for width: CGFloat) -> Block { let childWidth = width - paddings.horizontal.sum return updatingChild(child.laidOut(for: childWidth)) diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/EmptyBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/EmptyBlock.swift index 03122399d..177abea15 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/EmptyBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/EmptyBlock.swift @@ -6,6 +6,14 @@ public final class EmptyBlock: BlockWithTraits { public let widthTrait: LayoutTrait public let heightTrait: LayoutTrait + public var intrinsicContentWidth: CGFloat { + widthTrait.intrinsicSize + } + + public var isEmpty: Bool { + self == EmptyBlock.zeroSized + } + public init( widthTrait: LayoutTrait = .resizable, heightTrait: LayoutTrait = .resizable @@ -14,20 +22,12 @@ public final class EmptyBlock: BlockWithTraits { self.heightTrait = heightTrait } - public var intrinsicContentWidth: CGFloat { - widthTrait.intrinsicSize - } - public func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { heightTrait.intrinsicSize } public func getImageHolders() -> [ImageHolder] { [] } - public var isEmpty: Bool { - self == EmptyBlock.zeroSized - } - public func equals(_ other: Block) -> Bool { guard let other = other as? EmptyBlock else { return false diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/GalleryBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/GalleryBlock.swift index 61db6bdeb..b05ad243e 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/GalleryBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/GalleryBlock.swift @@ -3,17 +3,29 @@ import Foundation import VGSL public final class GalleryBlock: BlockWithTraits { - private lazy var contentSize: CGSize = model.intrinsicSize - public let model: GalleryViewModel public let state: GalleryViewState public let widthTrait: LayoutTrait public let heightTrait: LayoutTrait + private lazy var contentSize: CGSize = model.intrinsicSize + public var path: UIElementPath? { model.path } + public var intrinsicContentWidth: CGFloat { + switch widthTrait { + case let .fixed(value): + return value + case let .intrinsic(_, minSize, maxSize): + let width = contentSize.width + return clamp(width, min: minSize, max: maxSize) + case .weighted: + return 0 + } + } + public init( model: GalleryViewModel, state: GalleryViewState, @@ -34,18 +46,6 @@ public final class GalleryBlock: BlockWithTraits { } } - public var intrinsicContentWidth: CGFloat { - switch widthTrait { - case let .fixed(value): - return value - case let .intrinsic(_, minSize, maxSize): - let width = contentSize.width - return clamp(width, min: minSize, max: maxSize) - case .weighted: - return 0 - } - } - public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { switch heightTrait { case let .fixed(value): diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/GradientBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/GradientBlock.swift index 08038c70b..d2f54cff9 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/GradientBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/GradientBlock.swift @@ -6,16 +6,6 @@ public final class GradientBlock: BlockWithTraits { public let widthTrait: LayoutTrait public let heightTrait: LayoutTrait - public init( - gradient: Gradient, - widthTrait: LayoutTrait = .resizable, - heightTrait: LayoutTrait = .resizable - ) { - self.gradient = gradient - self.widthTrait = widthTrait - self.heightTrait = heightTrait - } - public var intrinsicContentWidth: CGFloat { switch widthTrait { case let .fixed(value): @@ -27,6 +17,20 @@ public final class GradientBlock: BlockWithTraits { } } + public var debugDescription: String { + "\(widthTrait) x \(heightTrait) Gradient \(gradient)" + } + + public init( + gradient: Gradient, + widthTrait: LayoutTrait = .resizable, + heightTrait: LayoutTrait = .resizable + ) { + self.gradient = gradient + self.widthTrait = widthTrait + self.heightTrait = heightTrait + } + public func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { switch heightTrait { case let .fixed(value): @@ -45,10 +49,6 @@ public final class GradientBlock: BlockWithTraits { && heightTrait == other.heightTrait } - public var debugDescription: String { - "\(widthTrait) x \(heightTrait) Gradient \(gradient)" - } - public func getImageHolders() -> [ImageHolder] { [] } } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Grid/Grid.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Grid/Grid.swift index aa2a7726d..7fb95deba 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Grid/Grid.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Grid/Grid.swift @@ -2,19 +2,12 @@ import VGSL struct Grid { typealias GridItem = GridBlock.Item + let itemsIndices: [[Int?]] var rowCount: Int { itemsIndices.count } var columnCount: Int { itemsIndices[0].count } - func forEachCell(_ block: ((row: Int, column: Int)) -> Void) { - for row in itemsIndices.indices { - for column in itemsIndices[0].indices { - block((row, column)) - } - } - } - init(spans: [GridBlock.Span], columnCount: Int) throws { guard !spans.isEmpty else { throw BlockError("Grid block error: empty items") @@ -62,14 +55,24 @@ struct Grid { itemsIndices = result } + + func forEachCell(_ block: ((row: Int, column: Int)) -> Void) { + for row in itemsIndices.indices { + for column in itemsIndices[0].indices { + block((row, column)) + } + } + } + } private struct GridIterator { typealias Element = (row: Int, column: Int) - private let columnCount: Int private(set) var current: Element = (0, 0) + private let columnCount: Int + init(columnCount: Int) { self.columnCount = columnCount } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Grid/GridBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Grid/GridBlock.swift index 22e6d0ef3..cbfb1921c 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Grid/GridBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Grid/GridBlock.swift @@ -3,20 +3,21 @@ import VGSL public final class GridBlock: BlockWithTraits, BlockWithLayout { public struct Span: Equatable { - public let rows: Int - public let columns: Int public static let `default` = Span() - private init() { - rows = 1 - columns = 1 - } + public let rows: Int + public let columns: Int public init(rows: Int = 1, columns: Int = 1) { self.rows = rows self.columns = columns } + private init() { + rows = 1 + columns = 1 + } + func validate() throws { if rows < 1 { throw BlockError("Grid block error: invalid rows span \(rows) < 1") @@ -72,27 +73,29 @@ public final class GridBlock: BlockWithTraits, BlockWithLayout { public let items: [Item] public let columnCount: Int public let path: UIElementPath? + let grid: Grid private var cachedIntrinsicWidth: CGFloat? private var cachedIntrinsicHeight: (width: CGFloat, height: CGFloat)? - private init( - widthTrait: LayoutTrait, - heightTrait: LayoutTrait, - contentAlignment: BlockAlignment2D, - items: [Item], - columnCount: Int, - grid: Grid, - path: UIElementPath? - ) { - self.widthTrait = widthTrait - self.heightTrait = heightTrait - self.contentAlignment = contentAlignment - self.items = items - self.columnCount = columnCount - self.grid = grid - self.path = path + public var intrinsicContentWidth: CGFloat { + if case let .fixed(value) = widthTrait { + return value + } + + if let cached = cachedIntrinsicWidth { + return cached + } + + var result = Layout.calculateIntrinsicColumnWidths(items: items, grid: grid).reduce(0, +) + + if case let .intrinsic(_, minSize, maxSize) = widthTrait { + result = clamp(result, min: minSize, max: maxSize) + } + + cachedIntrinsicWidth = result + return result } public convenience init( @@ -115,60 +118,28 @@ public final class GridBlock: BlockWithTraits, BlockWithLayout { try validateLayoutTraits() } + private init( + widthTrait: LayoutTrait, + heightTrait: LayoutTrait, + contentAlignment: BlockAlignment2D, + items: [Item], + columnCount: Int, + grid: Grid, + path: UIElementPath? + ) { + self.widthTrait = widthTrait + self.heightTrait = heightTrait + self.contentAlignment = contentAlignment + self.items = items + self.columnCount = columnCount + self.grid = grid + self.path = path + } + public func getImageHolders() -> [ImageHolder] { items.flatMap { $0.contents.getImageHolders() } } - private func validateLayoutTraits() throws { - if case .intrinsic = widthTrait, Layout.isGrid( - grid, - with: items, - resizableAtDirection: .horizontal - ) { - throw BlockError( - "Grid block error: cannot create horizontally resizable grid with intrinsic width trait" - ) - } - - if case .intrinsic = heightTrait, Layout.isGrid( - grid, - with: items, - resizableAtDirection: .vertical - ) { - throw BlockError( - "Grid block error: cannot create vertically resizable grid with intrinsic height trait" - ) - } - } - - private func makeLayout(constrainedTo size: CGSize = .infinite) -> Layout { - Layout( - size: size, - items: items, - grid: grid, - contentAlignment: contentAlignment - ) - } - - public var intrinsicContentWidth: CGFloat { - if case let .fixed(value) = widthTrait { - return value - } - - if let cached = cachedIntrinsicWidth { - return cached - } - - var result = Layout.calculateIntrinsicColumnWidths(items: items, grid: grid).reduce(0, +) - - if case let .intrinsic(_, minSize, maxSize) = widthTrait { - result = clamp(result, min: minSize, max: maxSize) - } - - cachedIntrinsicWidth = result - return result - } - public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { if case let .fixed(value) = heightTrait { return value @@ -220,6 +191,38 @@ public final class GridBlock: BlockWithTraits, BlockWithLayout { return (block, layout) } + + private func validateLayoutTraits() throws { + if case .intrinsic = widthTrait, Layout.isGrid( + grid, + with: items, + resizableAtDirection: .horizontal + ) { + throw BlockError( + "Grid block error: cannot create horizontally resizable grid with intrinsic width trait" + ) + } + + if case .intrinsic = heightTrait, Layout.isGrid( + grid, + with: items, + resizableAtDirection: .vertical + ) { + throw BlockError( + "Grid block error: cannot create vertically resizable grid with intrinsic height trait" + ) + } + } + + private func makeLayout(constrainedTo size: CGSize = .infinite) -> Layout { + Layout( + size: size, + items: items, + grid: grid, + contentAlignment: contentAlignment + ) + } + } extension GridBlock { diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/ImageBaseBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/ImageBaseBlock.swift index 30ad680d4..eda79cc52 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/ImageBaseBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/ImageBaseBlock.swift @@ -19,6 +19,7 @@ public enum ImageBlockHeight: Equatable { public struct ImageBaseBlockState: ElementState, Equatable { public let intrinsicContentSize: CGSize? + public init(widthTrait: LayoutTrait, height: ImageBlockHeight, imageHolder: ImageHolder) { let hasIntrinsicSize = widthTrait == .intrinsic || height == .trait(.intrinsic) self.intrinsicContentSize = hasIntrinsicSize ? imageHolder.currentImageSize : nil diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/LaidOutBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/LaidOutBlock.swift index 18adeebeb..94ab17e9d 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/LaidOutBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/LaidOutBlock.swift @@ -4,8 +4,33 @@ import VGSL final class LaidOutBlock: Block { public let block: T public let layout: T.Layout + public let isVerticallyResizable = false + public let isHorizontallyResizable = false + private let size: CGSize + public var isVerticallyConstrained: Bool { block.isVerticallyConstrained } + public var isHorizontallyConstrained: Bool { block.isHorizontallyConstrained } + + public var intrinsicContentWidth: CGFloat { + size.width + } + + public var widthOfHorizontallyNonResizableBlock: CGFloat { size.width } + public var weightOfVerticallyResizableBlock: LayoutTrait.Weight { + assertionFailure("try to get weight for non resizable block") + return .default + } + + public var weightOfHorizontallyResizableBlock: LayoutTrait.Weight { + assertionFailure("try to get weight for non resizable block") + return .default + } + + public var debugDescription: String { + "L\(size.width)x\(size.height) \(block.debugDescription)" + } + public convenience init(block: T, width: CGFloat) { let height = block.heightOfVerticallyNonResizableBlock(forWidth: width) self.init(block: block, size: CGSize(width: width, height: height)) @@ -17,35 +42,14 @@ final class LaidOutBlock: Block { (self.block, layout) = block.laidOutHierarchy(for: size) } - public let isVerticallyResizable = false - public let isHorizontallyResizable = false - - public var isVerticallyConstrained: Bool { block.isVerticallyConstrained } - public var isHorizontallyConstrained: Bool { block.isHorizontallyConstrained } - - public var intrinsicContentWidth: CGFloat { - size.width - } - public func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { size.height } - public var widthOfHorizontallyNonResizableBlock: CGFloat { size.width } public func heightOfVerticallyNonResizableBlock(forWidth _: CGFloat) -> CGFloat { size.height } - public var weightOfVerticallyResizableBlock: LayoutTrait.Weight { - assertionFailure("try to get weight for non resizable block") - return .default - } - - public var weightOfHorizontallyResizableBlock: LayoutTrait.Weight { - assertionFailure("try to get weight for non resizable block") - return .default - } - public func equals(_ other: Block) -> Bool { guard let other = other as? LaidOutBlock else { return false @@ -54,9 +58,6 @@ final class LaidOutBlock: Block { return block == other.block && size == other.size } - public var debugDescription: String { - "L\(size.width)x\(size.height) \(block.debugDescription)" - } } extension LaidOutBlock: ImageContaining { diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/LayeredBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/LayeredBlock.swift index 71fde96f0..37cd2f4c2 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/LayeredBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/LayeredBlock.swift @@ -2,8 +2,6 @@ import CoreGraphics import VGSL public final class LayeredBlock: BlockWithTraits, BlockWithLayout { - typealias Layout = [CGRect] - public struct Child: Equatable { public var content: Block public let alignment: BlockAlignment2D @@ -14,10 +12,24 @@ public final class LayeredBlock: BlockWithTraits, BlockWithLayout { } } + typealias Layout = [CGRect] + public let widthTrait: LayoutTrait public let heightTrait: LayoutTrait public let children: [Child] + public var intrinsicContentWidth: CGFloat { + switch widthTrait { + case let .fixed(width): + return width + case let .intrinsic(_, minSize, maxSize): + let width = children.map(\.content.intrinsicContentWidth).max() ?? 0 + return clamp(width, min: minSize, max: maxSize) + case .weighted: + return children.map(\.content.intrinsicContentWidth).max() ?? 0 + } + } + public convenience init( widthTrait: LayoutTrait = .resizable, heightTrait: LayoutTrait = .intrinsic, @@ -49,18 +61,6 @@ public final class LayeredBlock: BlockWithTraits, BlockWithLayout { self.children = children } - public var intrinsicContentWidth: CGFloat { - switch widthTrait { - case let .fixed(width): - return width - case let .intrinsic(_, minSize, maxSize): - let width = children.map(\.content.intrinsicContentWidth).max() ?? 0 - return clamp(width, min: minSize, max: maxSize) - case .weighted: - return children.map(\.content.intrinsicContentWidth).max() ?? 0 - } - } - public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { switch heightTrait { case let .fixed(height): diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/LayoutTrait.swift b/client/ios/LayoutKit/LayoutKit/Blocks/LayoutTrait.swift index a35b3ad1f..0a4c3f9b6 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/LayoutTrait.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/LayoutTrait.swift @@ -15,6 +15,8 @@ public enum LayoutTrait: Equatable { public struct Weight: RawRepresentable, ExpressibleByFloatLiteral, ExpressibleByIntegerLiteral, Equatable, Comparable { + public static let `default`: Weight = 1 + public let rawValue: CGFloat public init?(rawValue: CGFloat) { @@ -30,8 +32,6 @@ public enum LayoutTrait: Equatable { self.init(rawValue: CGFloat(value))! } - public static let `default`: Weight = 1 - public static func <(lhs: Weight, rhs: Weight) -> Bool { lhs.rawValue < rhs.rawValue } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/PageControlBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/PageControlBlock.swift index f38a35857..e2a3c0817 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/PageControlBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/PageControlBlock.swift @@ -23,24 +23,6 @@ public final class PageControlBlock: BlockWithTraits { pageControlPath } - public init( - layoutDirection: UserInterfaceLayoutDirection = .leftToRight, - pageControlPath: UIElementPath? = nil, - pagerPath: PagerPath?, - widthTrait: LayoutTrait, - heightTrait: LayoutTrait, - configuration: PageIndicatorConfiguration, - state: State - ) { - self.layoutDirection = layoutDirection - self.pageControlPath = pageControlPath - self.pagerPath = pagerPath - self.widthTrait = widthTrait - self.heightTrait = heightTrait - self.configuration = configuration - self.state = state - } - public var intrinsicContentWidth: CGFloat { switch widthTrait { case let .fixed(value): @@ -59,6 +41,24 @@ public final class PageControlBlock: BlockWithTraits { } } + public init( + layoutDirection: UserInterfaceLayoutDirection = .leftToRight, + pageControlPath: UIElementPath? = nil, + pagerPath: PagerPath?, + widthTrait: LayoutTrait, + heightTrait: LayoutTrait, + configuration: PageIndicatorConfiguration, + state: State + ) { + self.layoutDirection = layoutDirection + self.pageControlPath = pageControlPath + self.pagerPath = pagerPath + self.widthTrait = widthTrait + self.heightTrait = heightTrait + self.configuration = configuration + self.state = state + } + public func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { switch heightTrait { case let .fixed(value): diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/PagerBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/PagerBlock.swift index bfffe1404..a4d1df274 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/PagerBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/PagerBlock.swift @@ -16,11 +16,27 @@ public final class PagerBlock: BlockWithTraits { public let state: PagerViewState public let widthTrait: LayoutTrait public let heightTrait: LayoutTrait + + let selectedActions: [[UserInterfaceAction]] + public var path: UIElementPath? { gallery.path } - let selectedActions: [[UserInterfaceAction]] + public var intrinsicContentWidth: CGFloat { + switch widthTrait { + case let .fixed(value): + return value + case let .intrinsic(_, minSize, maxSize): + let width = gallery.intrinsicPagerSize( + forWidth: CGFloat.greatestFiniteMagnitude, + layoutMode: layoutMode + ).width + return clamp(width, min: minSize, max: maxSize) + case .weighted: + return 0 + } + } public init( pagerPath: PagerPath?, @@ -50,21 +66,6 @@ public final class PagerBlock: BlockWithTraits { } } - public var intrinsicContentWidth: CGFloat { - switch widthTrait { - case let .fixed(value): - return value - case let .intrinsic(_, minSize, maxSize): - let width = gallery.intrinsicPagerSize( - forWidth: CGFloat.greatestFiniteMagnitude, - layoutMode: layoutMode - ).width - return clamp(width, min: minSize, max: maxSize) - case .weighted: - return 0 - } - } - public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { switch heightTrait { case let .fixed(value): diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/SeparatorBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/SeparatorBlock.swift index 07baa9d83..b2c771817 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/SeparatorBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/SeparatorBlock.swift @@ -12,20 +12,8 @@ public final class SeparatorBlock: BlockWithTraits { public let widthTrait: LayoutTrait public let heightTrait: LayoutTrait - private init( - color: Color = .clear, - direction: Direction = .horizontal, - trait: LayoutTrait = .resizable - ) { - self.color = color - switch direction { - case .horizontal: - widthTrait = trait - heightTrait = .fixed(1) - case .vertical: - widthTrait = .fixed(1) - heightTrait = trait - } + public var intrinsicContentWidth: CGFloat { + widthTrait.intrinsicSize } public convenience init( @@ -44,8 +32,20 @@ public final class SeparatorBlock: BlockWithTraits { self.init(color: color, direction: direction, trait: .fixed(size)) } - public var intrinsicContentWidth: CGFloat { - widthTrait.intrinsicSize + private init( + color: Color = .clear, + direction: Direction = .horizontal, + trait: LayoutTrait = .resizable + ) { + self.color = color + switch direction { + case .horizontal: + widthTrait = trait + heightTrait = .fixed(1) + case .vertical: + widthTrait = .fixed(1) + heightTrait = trait + } } public func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Shadow/BlockShadow.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Shadow/BlockShadow.swift index c0735d0b0..211fa85fc 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Shadow/BlockShadow.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Shadow/BlockShadow.swift @@ -17,12 +17,12 @@ public struct BlockShadow: Equatable { public let opacity: Float public let color: Color + private let absoluteBlurRadius: CGFloat + public var blurRadius: CGFloat { absoluteBlurRadius / 2 } - private let absoluteBlurRadius: CGFloat - public init( cornerRadii: CornerRadii, blurRadius: CGFloat = Defaults.blurRadius, diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksConfiguration.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksConfiguration.swift index 36083ede0..6b86037ac 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksConfiguration.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksConfiguration.swift @@ -2,11 +2,12 @@ import Foundation import VGSL public struct MarksConfiguration: Equatable { - let modelConfiguration: MarksConfigurationModel - let horizontalInset: CGFloat - public static let empty = MarksConfiguration( modelConfiguration: .empty, horizontalInset: 0 ) + + let modelConfiguration: MarksConfigurationModel + let horizontalInset: CGFloat + } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksConfigurationModel.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksConfigurationModel.swift index d688d6000..ae1c9a2cc 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksConfigurationModel.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksConfigurationModel.swift @@ -2,6 +2,14 @@ import Foundation import VGSL public struct MarksConfigurationModel: Equatable { + public static let empty = Self( + minValue: 0, + maxValue: 0, + activeMark: .empty, + inactiveMark: .empty, + layoutDirection: .leftToRight + ) + let minValue: CGFloat let maxValue: CGFloat let activeMark: RoundedRectangle @@ -22,17 +30,18 @@ public struct MarksConfigurationModel: Equatable { self.layoutDirection = layoutDirection } - public static let empty = Self( - minValue: 0, - maxValue: 0, - activeMark: .empty, - inactiveMark: .empty, - layoutDirection: .leftToRight - ) } extension MarksConfigurationModel { public struct RoundedRectangle: Equatable { + public static let empty = RoundedRectangle( + size: CGSize.zero, + cornerRadius: 0, + color: .clear, + borderWidth: 0, + borderColor: .clear + ) + var size: CGSize let cornerRadius: CGFloat let color: Color @@ -53,12 +62,5 @@ extension MarksConfigurationModel { self.borderColor = borderColor } - public static let empty = RoundedRectangle( - size: CGSize.zero, - cornerRadius: 0, - color: .clear, - borderWidth: 0, - borderColor: .clear - ) } } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksLayer.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksLayer.swift index 93287948b..431b1a8d0 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksLayer.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksLayer.swift @@ -22,6 +22,24 @@ final class MarksLayer: CALayer { configuration.modelConfiguration.inactiveMark } + override init() { + super.init() + } + + override init(layer: Any) { + super.init(layer: layer) + guard let markLayer = layer as? MarksLayer else { + assertionFailure("unknown layer inside init(layer: Any)") + return + } + configuration = markLayer.configuration + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func draw(in ctx: CGContext) { configureMarks(in: ctx) @@ -92,23 +110,6 @@ final class MarksLayer: CALayer { } } - override init() { - super.init() - } - - override init(layer: Any) { - super.init(layer: layer) - guard let markLayer = layer as? MarksLayer else { - assertionFailure("unknown layer inside init(layer: Any)") - return - } - configuration = markLayer.configuration - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } } extension MarksConfigurationModel.RoundedRectangle { diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksView.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksView.swift index c308cd390..a176d607a 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksView.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksView.swift @@ -2,6 +2,8 @@ import UIKit final class MarksView: UIView { + override class var layerClass: AnyClass { MarksLayer.self } + var configuration: MarksConfiguration { get { contentLayer.configuration @@ -36,8 +38,6 @@ final class MarksView: UIView { layer as! MarksLayer } - override class var layerClass: AnyClass { MarksLayer.self } - init() { super.init(frame: .zero) commonInit() @@ -53,12 +53,13 @@ final class MarksView: UIView { commonInit() } + override func draw(_: CGRect) { + // overriden draw is required for redrawing of layer + } + private func commonInit() { backgroundColor = .clear } - override func draw(_: CGRect) { - // overriden draw is required for redrawing of layer - } } #endif diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderBlock.swift index caf6740ec..f5f19504f 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderBlock.swift @@ -10,6 +10,7 @@ public final class SliderBlock: BlockWithTraits { // MARK: - SliderBlock let sliderModel: SliderModel + public var path: UIElementPath? { sliderModel.path } @@ -26,16 +27,12 @@ public final class SliderBlock: BlockWithTraits { } } - public func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { - switch heightTrait { - case let .fixed(value): - return value - case let .intrinsic(_, minSize, maxSize): - let height = sliderModel.sliderHeight - return clamp(height, min: minSize, max: maxSize) - case .weighted: - return 0 - } + public var debugDescription: String { + """ + Slider min value: \(sliderModel.minValue) + Slider max value: \(sliderModel.maxValue) + First thumb value: \(sliderModel.firstThumb.value) + """ + (sliderModel.secondThumb.flatMap { "Second thumb value: \($0.value)" } ?? "") } public init( @@ -48,6 +45,18 @@ public final class SliderBlock: BlockWithTraits { self.heightTrait = heightTrait } + public func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { + switch heightTrait { + case let .fixed(value): + return value + case let .intrinsic(_, minSize, maxSize): + let height = sliderModel.sliderHeight + return clamp(height, min: minSize, max: maxSize) + case .weighted: + return 0 + } + } + public func equals(_ other: Block) -> Bool { guard let otherSlider = (other as? SliderBlock) else { return false } @@ -60,13 +69,6 @@ public final class SliderBlock: BlockWithTraits { [] } - public var debugDescription: String { - """ - Slider min value: \(sliderModel.minValue) - Slider max value: \(sliderModel.maxValue) - First thumb value: \(sliderModel.firstThumb.value) - """ + (sliderModel.secondThumb.flatMap { "Second thumb value: \($0.value)" } ?? "") - } } extension SliderBlock: LayoutCachingDefaultImpl {} diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderModel.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderModel.swift index c2d14927d..e45fb3303 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderModel.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderModel.swift @@ -10,6 +10,16 @@ import AppKit public struct SliderModel: Equatable { public struct ThumbModel: Equatable { + static var empty: Self { + ThumbModel( + block: EmptyBlock.zeroSized, + value: .zero, + size: .zero, + offsetX: 0, + offsetY: 0 + ) + } + public let block: Block public var value: Binding public let size: CGSize @@ -30,16 +40,6 @@ public struct SliderModel: Equatable { self.offsetY = offsetY } - static var empty: Self { - ThumbModel( - block: EmptyBlock.zeroSized, - value: .zero, - size: .zero, - offsetX: 0, - offsetY: 0 - ) - } - public static func ==(lhs: SliderModel.ThumbModel, rhs: SliderModel.ThumbModel) -> Bool { lhs.value == rhs.value && lhs.size == rhs.size && @@ -92,6 +92,19 @@ public struct SliderModel: Equatable { } } + static var empty: Self { + SliderModel( + firstThumb: .empty, + secondThumb: nil, + minValue: 0, + maxValue: 0, + marksConfiguration: .empty, + ranges: [], + layoutDirection: .leftToRight, + isEnabled: true + ) + } + public var firstThumb: ThumbModel public var secondThumb: ThumbModel? @@ -102,6 +115,8 @@ public struct SliderModel: Equatable { public let path: UIElementPath? public let isEnabled: Bool + private let marksModelConfiguration: MarksConfigurationModel + public var marksConfiguration: MarksConfiguration { MarksConfiguration( modelConfiguration: marksModelConfiguration, @@ -113,8 +128,6 @@ public struct SliderModel: Equatable { maxValue - minValue } - private let marksModelConfiguration: MarksConfigurationModel - public var sliderHeight: CGFloat { max( marksModelConfiguration.activeMark.size.height, @@ -176,19 +189,6 @@ public struct SliderModel: Equatable { self.isEnabled = isEnabled } - static var empty: Self { - SliderModel( - firstThumb: .empty, - secondThumb: nil, - minValue: 0, - maxValue: 0, - marksConfiguration: .empty, - ranges: [], - layoutDirection: .leftToRight, - isEnabled: true - ) - } - public static func ==(lhs: SliderModel, rhs: SliderModel) -> Bool { lhs.firstThumb == rhs.firstThumb && lhs.secondThumb == rhs.secondThumb && diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderView.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderView.swift index e2ec2d630..86add2ddc 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderView.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderView.swift @@ -3,6 +3,8 @@ import UIKit import VGSL final class SliderView: BlockView, VisibleBoundsTrackingLeaf { + var layoutReporter: LayoutReporter? + private var sliderModel: SliderModel = .empty private var thumbAnimator: UIViewPropertyAnimator? private var firstThumbProgress: CGFloat = .zero { @@ -19,7 +21,93 @@ final class SliderView: BlockView, VisibleBoundsTrackingLeaf { } } - var layoutReporter: LayoutReporter? + private var firstThumb: BlockView? + private var secondThumb: BlockView? + private var activeRangeTracks: [BlockView] = [] + private var inactiveRangeTracks: [BlockView] = [] + private lazy var inactiveTrackView = makeAndInsertView(UIView(), at: 0) + private lazy var activeTrackView = makeAndInsertView(UIView(), at: 1) + private lazy var marksView = makeAndInsertView(MarksView(), at: 2) + + private let recognizer = UILongPressGestureRecognizer() + private var activeThumb: SliderThumbNumber = .first + + var effectiveBackgroundColor: UIColor? { + backgroundColor + } + + private var pointWidth: CGFloat { + let width = bounds.width - sliderModel.horizontalInset - max( + abs(sliderModel.firstThumb.offsetX), + abs(sliderModel.secondThumb?.offsetX ?? 0) + ) + if sliderModel.valueRange == 0 { + return width + } else { + return CGFloat(width) / CGFloat(sliderModel.valueRange) + } + } + + init() { + super.init(frame: .zero) + recognizer.addTarget(self, action: #selector(handleTap)) + isExclusiveTouch = true + recognizer.minimumPressDuration = 0 + addGestureRecognizer(recognizer) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + let firstThumbProgress = clampSliderValue(firstThumbProgress, sliderModel: sliderModel) + let secondThumbProgress = clampSliderValue(secondThumbProgress, sliderModel: sliderModel) + + guard thumbAnimator?.state != .active else { + return + } + + super.layoutSubviews() + layoutReporter?.willLayoutSubviews() + + configureThumb( + thumbView: firstThumb, + progress: firstThumbProgress, + thumbModel: sliderModel.firstThumb + ) + configureThumb( + thumbView: secondThumb, + progress: secondThumbProgress, + thumbModel: sliderModel.secondThumb + ) + + configureSliderView(marksView) + + configureSliderView(inactiveTrackView, with: sliderModel.horizontalInset) + configureSliderView(activeTrackView, with: sliderModel.horizontalInset) + + configureRangeViews( + inactiveRangeTracks, + with: sliderModel.ranges, + in: inactiveTrackView, + isActive: false + ) + configureRangeViews( + activeRangeTracks, + with: sliderModel.ranges, + in: activeTrackView, + isActive: true + ) + + configureTracks( + progressFirstThumb: firstThumbProgress, + progressSecondThumb: sliderModel + .secondThumb != nil ? secondThumbProgress : CGFloat(sliderModel.minValue) + ) + layoutReporter?.didLayoutSubviews() + } func setSliderModel( _ sliderModel: SliderModel, @@ -119,46 +207,6 @@ final class SliderView: BlockView, VisibleBoundsTrackingLeaf { secondThumbProgress = value } - var effectiveBackgroundColor: UIColor? { - backgroundColor - } - - private var pointWidth: CGFloat { - let width = bounds.width - sliderModel.horizontalInset - max( - abs(sliderModel.firstThumb.offsetX), - abs(sliderModel.secondThumb?.offsetX ?? 0) - ) - if sliderModel.valueRange == 0 { - return width - } else { - return CGFloat(width) / CGFloat(sliderModel.valueRange) - } - } - - private var firstThumb: BlockView? - private var secondThumb: BlockView? - private var activeRangeTracks: [BlockView] = [] - private var inactiveRangeTracks: [BlockView] = [] - private lazy var inactiveTrackView = makeAndInsertView(UIView(), at: 0) - private lazy var activeTrackView = makeAndInsertView(UIView(), at: 1) - private lazy var marksView = makeAndInsertView(MarksView(), at: 2) - - private let recognizer = UILongPressGestureRecognizer() - private var activeThumb: SliderThumbNumber = .first - - init() { - super.init(frame: .zero) - recognizer.addTarget(self, action: #selector(handleTap)) - isExclusiveTouch = true - recognizer.minimumPressDuration = 0 - addGestureRecognizer(recognizer) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - private func makeAndInsertView(_ view: T, at position: Int) -> T { view.clipsToBounds = true self.insertSubview(view, at: position) @@ -261,54 +309,6 @@ final class SliderView: BlockView, VisibleBoundsTrackingLeaf { ) } - override func layoutSubviews() { - let firstThumbProgress = clampSliderValue(firstThumbProgress, sliderModel: sliderModel) - let secondThumbProgress = clampSliderValue(secondThumbProgress, sliderModel: sliderModel) - - guard thumbAnimator?.state != .active else { - return - } - - super.layoutSubviews() - layoutReporter?.willLayoutSubviews() - - configureThumb( - thumbView: firstThumb, - progress: firstThumbProgress, - thumbModel: sliderModel.firstThumb - ) - configureThumb( - thumbView: secondThumb, - progress: secondThumbProgress, - thumbModel: sliderModel.secondThumb - ) - - configureSliderView(marksView) - - configureSliderView(inactiveTrackView, with: sliderModel.horizontalInset) - configureSliderView(activeTrackView, with: sliderModel.horizontalInset) - - configureRangeViews( - inactiveRangeTracks, - with: sliderModel.ranges, - in: inactiveTrackView, - isActive: false - ) - configureRangeViews( - activeRangeTracks, - with: sliderModel.ranges, - in: activeTrackView, - isActive: true - ) - - configureTracks( - progressFirstThumb: firstThumbProgress, - progressSecondThumb: sliderModel - .secondThumb != nil ? secondThumbProgress : CGFloat(sliderModel.minValue) - ) - layoutReporter?.didLayoutSubviews() - } - private func configureRangeViews( _ rangeViews: [UIView], with models: [SliderModel.RangeModel], diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/SwipeContainerBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/SwipeContainerBlock.swift index 67bbaa276..2416c267d 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/SwipeContainerBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/SwipeContainerBlock.swift @@ -27,6 +27,13 @@ public final class SwipeContainerBlock: WrapperBlock { self.swipeOutActions = swipeOutActions } + public static func ==(lhs: SwipeContainerBlock, rhs: SwipeContainerBlock) -> Bool { + lhs.child == rhs.child + && lhs.swipeOutActions == rhs.swipeOutActions + && lhs.path == rhs.path + && lhs.state == rhs.state + } + public func intrinsicContentHeight(forWidth: CGFloat) -> CGFloat { switch state { case .normal: @@ -53,12 +60,6 @@ public final class SwipeContainerBlock: WrapperBlock { return self == other } - public static func ==(lhs: SwipeContainerBlock, rhs: SwipeContainerBlock) -> Bool { - lhs.child == rhs.child - && lhs.swipeOutActions == rhs.swipeOutActions - && lhs.path == rhs.path - && lhs.state == rhs.state - } } extension SwipeContainerBlock: LayoutCaching { diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/SwitchBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/SwitchBlock.swift index 969098c21..c13f4cbb5 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/SwitchBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/SwitchBlock.swift @@ -11,6 +11,34 @@ public final class SwitchBlock: Block { public let onTintColor: Color? public let accessibilityElement: AccessibilityElement? + public let isVerticallyConstrained = false + public let isHorizontallyConstrained = false + + public var isVerticallyResizable: Bool { heightTrait.isResizable } + public var isHorizontallyResizable: Bool { widthTrait.isResizable } + + public var intrinsicContentWidth: CGFloat { + switch widthTrait { + case let .fixed(value): + value + case .intrinsic: + uiSwitchSize.width + case .weighted: + 0 + } + } + + public var widthOfHorizontallyNonResizableBlock: CGFloat { uiSwitchSize.width } + public var weightOfVerticallyResizableBlock: LayoutTrait.Weight { + assertionFailure("try to get weight for non resizable block") + return .default + } + + public var weightOfHorizontallyResizableBlock: LayoutTrait.Weight { + assertionFailure("try to get weight for non resizable block") + return .default + } + public init( widthTrait: LayoutTrait = .intrinsic, heightTrait: LayoutTrait = .intrinsic, @@ -47,23 +75,6 @@ public final class SwitchBlock: Block { self.accessibilityElement = accessibilityElement } - public var isVerticallyResizable: Bool { heightTrait.isResizable } - public var isHorizontallyResizable: Bool { widthTrait.isResizable } - - public let isVerticallyConstrained = false - public let isHorizontallyConstrained = false - - public var intrinsicContentWidth: CGFloat { - switch widthTrait { - case let .fixed(value): - value - case .intrinsic: - uiSwitchSize.width - case .weighted: - 0 - } - } - public func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { switch heightTrait { case let .fixed(value): @@ -75,21 +86,10 @@ public final class SwitchBlock: Block { } } - public var widthOfHorizontallyNonResizableBlock: CGFloat { uiSwitchSize.width } public func heightOfVerticallyNonResizableBlock(forWidth _: CGFloat) -> CGFloat { uiSwitchSize.height } - public var weightOfVerticallyResizableBlock: LayoutTrait.Weight { - assertionFailure("try to get weight for non resizable block") - return .default - } - - public var weightOfHorizontallyResizableBlock: LayoutTrait.Weight { - assertionFailure("try to get weight for non resizable block") - return .default - } - public func equals(_ other: Block) -> Bool { guard let other = other as? SwitchBlock else { return false diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/SwitchableContainerBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/SwitchableContainerBlock.swift index 199f60a9f..e7b7cdf01 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/SwitchableContainerBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/SwitchableContainerBlock.swift @@ -47,6 +47,63 @@ public final class SwitchableContainerBlock: Block { public let switchAction: UserInterfaceAction? public let path: UIElementPath + public let isVerticallyConstrained = false + public let isHorizontallyConstrained = false + + public var isVerticallyResizable: Bool { + items.0.content.isVerticallyResizable && items.1.content.isVerticallyResizable + } + + public var isHorizontallyResizable: Bool { + items.0.content.isHorizontallyResizable && items.1.content.isHorizontallyResizable + } + + public var intrinsicContentWidth: CGFloat { + let layout = SwitchableContainerBlockLayout( + width: .infinity, + titles: (items.0.title, items.1.title), + titleGaps: titleGaps, + selectorSideGaps: selectorSideGaps + ) + let contentMaxWidth = max( + items.0.content.intrinsicContentWidth, + items.1.content.intrinsicContentWidth + ) + return max(layout.selectorIntrinsicWidth, contentMaxWidth) + } + + public var widthOfHorizontallyNonResizableBlock: CGFloat { + guard !isHorizontallyResizable else { + assertionFailure("Should be at least one non resizable block") + return 0 + } + + var width: CGFloat = 0 + if !items.0.content.isHorizontallyResizable { + width = max(items.0.content.widthOfHorizontallyNonResizableBlock, width) + } + if !items.1.content.isHorizontallyResizable { + width = max(items.1.content.widthOfHorizontallyNonResizableBlock, width) + } + return width + } + + public var weightOfVerticallyResizableBlock: LayoutTrait.Weight { + precondition(isVerticallyResizable) + return max( + items.0.content.weightOfVerticallyResizableBlock, + items.1.content.weightOfVerticallyResizableBlock + ) + } + + public var weightOfHorizontallyResizableBlock: LayoutTrait.Weight { + precondition(isHorizontallyResizable) + return max( + items.0.content.weightOfHorizontallyResizableBlock, + items.1.content.weightOfHorizontallyResizableBlock + ) + } + public init( selectedItem: Selection, items: Items, @@ -69,31 +126,6 @@ public final class SwitchableContainerBlock: Block { self.path = path } - public var isVerticallyResizable: Bool { - items.0.content.isVerticallyResizable && items.1.content.isVerticallyResizable - } - - public var isHorizontallyResizable: Bool { - items.0.content.isHorizontallyResizable && items.1.content.isHorizontallyResizable - } - - public let isVerticallyConstrained = false - public let isHorizontallyConstrained = false - - public var intrinsicContentWidth: CGFloat { - let layout = SwitchableContainerBlockLayout( - width: .infinity, - titles: (items.0.title, items.1.title), - titleGaps: titleGaps, - selectorSideGaps: selectorSideGaps - ) - let contentMaxWidth = max( - items.0.content.intrinsicContentWidth, - items.1.content.intrinsicContentWidth - ) - return max(layout.selectorIntrinsicWidth, contentMaxWidth) - } - public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { var total: CGFloat = 0 @@ -114,22 +146,6 @@ public final class SwitchableContainerBlock: Block { return total } - public var widthOfHorizontallyNonResizableBlock: CGFloat { - guard !isHorizontallyResizable else { - assertionFailure("Should be at least one non resizable block") - return 0 - } - - var width: CGFloat = 0 - if !items.0.content.isHorizontallyResizable { - width = max(items.0.content.widthOfHorizontallyNonResizableBlock, width) - } - if !items.1.content.isHorizontallyResizable { - width = max(items.1.content.widthOfHorizontallyNonResizableBlock, width) - } - return width - } - public func heightOfVerticallyNonResizableBlock(forWidth width: CGFloat) -> CGFloat { guard !isVerticallyResizable else { assertionFailure("Should be at leat one non resizable block") @@ -163,22 +179,6 @@ public final class SwitchableContainerBlock: Block { return height + contentHeight } - public var weightOfVerticallyResizableBlock: LayoutTrait.Weight { - precondition(isVerticallyResizable) - return max( - items.0.content.weightOfVerticallyResizableBlock, - items.1.content.weightOfVerticallyResizableBlock - ) - } - - public var weightOfHorizontallyResizableBlock: LayoutTrait.Weight { - precondition(isHorizontallyResizable) - return max( - items.0.content.weightOfHorizontallyResizableBlock, - items.1.content.weightOfHorizontallyResizableBlock - ) - } - public func equals(_ other: Block) -> Bool { guard let other = other as? SwitchableContainerBlock else { return false } return self == other diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/SwitchableContainerBlockLayout.swift b/client/ios/LayoutKit/LayoutKit/Blocks/SwitchableContainerBlockLayout.swift index 182a10021..d0d9f2164 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/SwitchableContainerBlockLayout.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/SwitchableContainerBlockLayout.swift @@ -7,18 +7,6 @@ struct SwitchableContainerBlockLayout { private let titleGaps: CGFloat private let selectorSideGaps: CGFloat - init( - width: CGFloat, - titles: SwitchableContainerBlock.Titles, - titleGaps: CGFloat, - selectorSideGaps: CGFloat - ) { - self.width = width - self.titles = titles - self.titleGaps = titleGaps - self.selectorSideGaps = selectorSideGaps - } - var selectorIntrinsicWidth: CGFloat { let leftTitleWidth = max( titles.0.selected.sizeForWidth(.infinity).width, @@ -41,4 +29,17 @@ struct SwitchableContainerBlockLayout { var selectorIntrinsicHeight: CGFloat { titlesHeight + 2.0 * titleGaps } + + init( + width: CGFloat, + titles: SwitchableContainerBlock.Titles, + titleGaps: CGFloat, + selectorSideGaps: CGFloat + ) { + self.width = width + self.titles = titles + self.titleGaps = titleGaps + self.selectorSideGaps = selectorSideGaps + } + } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/TabsBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/TabsBlock.swift index 6f811a580..a177ede8f 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/TabsBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/TabsBlock.swift @@ -12,20 +12,6 @@ public final class TabsBlock: BlockWithTraits { model.contentsModel.path } - public init( - model: TabViewModel, - state: TabViewState, - widthTrait: LayoutTrait = .resizable, - heightTrait: LayoutTrait = .intrinsic - ) throws { - self.model = model - self.state = state - self.widthTrait = widthTrait - self.heightTrait = heightTrait - - try checkConstraints() - } - public var intrinsicContentWidth: CGFloat { if case let .fixed(value) = widthTrait { return value @@ -45,6 +31,20 @@ public final class TabsBlock: BlockWithTraits { return layout.size.width } + public init( + model: TabViewModel, + state: TabViewState, + widthTrait: LayoutTrait = .resizable, + heightTrait: LayoutTrait = .intrinsic + ) throws { + self.model = model + self.state = state + self.widthTrait = widthTrait + self.heightTrait = heightTrait + + try checkConstraints() + } + public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { if case let .fixed(value) = heightTrait { return value diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/TextBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/TextBlock.swift index b25864848..043e75494 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/TextBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/TextBlock.swift @@ -60,6 +60,27 @@ public final class TextBlock: BlockWithTraits { private var cachedIntrinsicWidth: CGFloat? private var cachedIntrinsicHeight: (width: CGFloat, height: CGFloat)? + public var intrinsicContentWidth: CGFloat { + switch widthTrait { + case let .intrinsic(_, minSize, maxSize): + if let cached = cachedIntrinsicWidth { + return cached + } + + let width = ceil( + text.sizeForWidth(tightenWidth ? maxSize : .infinity) + .width + additionalTextInsets.horizontal.sum + ) + let result = clamp(width, min: minSize, max: maxSize) + cachedIntrinsicWidth = result + return result + case let .fixed(value): + return value + case .weighted: + return 0 + } + } + public init( widthTrait: LayoutTrait, heightTrait: LayoutTrait = .intrinsic, @@ -144,25 +165,18 @@ public final class TextBlock: BlockWithTraits { ) } - public var intrinsicContentWidth: CGFloat { - switch widthTrait { - case let .intrinsic(_, minSize, maxSize): - if let cached = cachedIntrinsicWidth { - return cached - } - - let width = ceil( - text.sizeForWidth(tightenWidth ? maxSize : .infinity) - .width + additionalTextInsets.horizontal.sum - ) - let result = clamp(width, min: minSize, max: maxSize) - cachedIntrinsicWidth = result - return result - case let .fixed(value): - return value - case .weighted: - return 0 - } + public static func ==(lhs: TextBlock, rhs: TextBlock) -> Bool { + lhs.widthTrait == rhs.widthTrait + && lhs.heightTrait == rhs.heightTrait + && lhs.text == rhs.text + && lhs.gradientModel?.gradient == rhs.gradientModel?.gradient + && lhs.maxIntrinsicNumberOfLines == rhs.maxIntrinsicNumberOfLines + && lhs.minNumberOfHiddenLines == rhs.minNumberOfHiddenLines + && lhs.images == rhs.images + && lhs.accessibilityElement == rhs.accessibilityElement + && lhs.tightenWidth == rhs.tightenWidth + && lhs.autoEllipsize == rhs.autoEllipsize + && lhs.path == rhs.path } public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { @@ -196,20 +210,6 @@ public final class TextBlock: BlockWithTraits { images.map(\.holder) } - public static func ==(lhs: TextBlock, rhs: TextBlock) -> Bool { - lhs.widthTrait == rhs.widthTrait - && lhs.heightTrait == rhs.heightTrait - && lhs.text == rhs.text - && lhs.gradientModel?.gradient == rhs.gradientModel?.gradient - && lhs.maxIntrinsicNumberOfLines == rhs.maxIntrinsicNumberOfLines - && lhs.minNumberOfHiddenLines == rhs.minNumberOfHiddenLines - && lhs.images == rhs.images - && lhs.accessibilityElement == rhs.accessibilityElement - && lhs.tightenWidth == rhs.tightenWidth - && lhs.autoEllipsize == rhs.autoEllipsize - && lhs.path == rhs.path - } - public func calculateTextIntrinsicContentHeight( for width: CGFloat ) -> CGFloat { diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/TextFieldBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/TextFieldBlock.swift index aff722569..cdb073d4f 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/TextFieldBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/TextFieldBlock.swift @@ -36,6 +36,9 @@ public final class TextFieldBlock: Block { } public enum Placeholders: Equatable { + case separate(Separate) + case fieldOnly(NSAttributedString) + public struct Separate: Equatable { public struct AnimatableAttributes: Equatable { public let size: FontSize @@ -85,8 +88,6 @@ public final class TextFieldBlock: Block { } } - case separate(Separate) - case fieldOnly(NSAttributedString) } public enum TextAutocapitalizationType { @@ -123,6 +124,12 @@ public final class TextFieldBlock: Block { case disabled } + public enum KeyboardAppearance { + case `default` + case light + case dark + } + public static let defaultKeyboardAppearance = KeyboardAppearance.light public let widthTrait: LayoutTrait @@ -144,6 +151,59 @@ public final class TextFieldBlock: Block { public let interaction: Interaction public let accessibilityElement: AccessibilityElement? + public var isVerticallyResizable: Bool { heightTrait.isResizable } + public var isHorizontallyResizable: Bool { widthTrait.isResizable } + + public var isVerticallyConstrained: Bool { heightTrait.isConstrained } + public var isHorizontallyConstrained: Bool { widthTrait.isConstrained } + + public var intrinsicContentWidth: CGFloat { + switch widthTrait { + case let .fixed(value): + value + case let .intrinsic(_, minSize, _): + minSize + case .weighted: + 0 + } + } + + public var widthOfHorizontallyNonResizableBlock: CGFloat { + switch widthTrait { + case let .fixed(value): + return value + case .intrinsic, + .weighted: + assertionFailure("cannot get widthOfHorizontallyNonResizableBlock for text block") + return 0 + } + } + + public var weightOfVerticallyResizableBlock: LayoutTrait.Weight { + guard case let .weighted(value) = heightTrait else { + assertionFailure("try to get weight for non resizable block") + return .default + } + return value + } + + public var weightOfHorizontallyResizableBlock: LayoutTrait.Weight { + guard case let .weighted(value) = widthTrait else { + assertionFailure("try to get weight for non resizable block") + return .default + } + return value + } + + private var gap: CGFloat { + switch placeholders { + case let .separate(placeholders): + placeholders.gap + case .fieldOnly, .none: + 0 + } + } + public init( widthTrait: LayoutTrait = .resizable, heightTrait: LayoutTrait = .intrinsic, @@ -182,41 +242,6 @@ public final class TextFieldBlock: Block { self.accessibilityElement = accessibilityElement } - public var isVerticallyResizable: Bool { heightTrait.isResizable } - public var isHorizontallyResizable: Bool { widthTrait.isResizable } - - public var isVerticallyConstrained: Bool { heightTrait.isConstrained } - public var isHorizontallyConstrained: Bool { widthTrait.isConstrained } - - public var intrinsicContentWidth: CGFloat { - switch widthTrait { - case let .fixed(value): - value - case let .intrinsic(_, minSize, _): - minSize - case .weighted: - 0 - } - } - - private var gap: CGFloat { - switch placeholders { - case let .separate(placeholders): - placeholders.gap - case .fieldOnly, .none: - 0 - } - } - - private func getHeaderHeight(forWidth _: CGFloat) -> CGFloat { - switch placeholders { - case let .separate(placeholders): - placeholders.headerAttributes.height.rawValue - case .fieldOnly, .none: - 0 - } - } - public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { switch heightTrait { case let .fixed(value): @@ -231,17 +256,6 @@ public final class TextFieldBlock: Block { } } - public var widthOfHorizontallyNonResizableBlock: CGFloat { - switch widthTrait { - case let .fixed(value): - return value - case .intrinsic, - .weighted: - assertionFailure("cannot get widthOfHorizontallyNonResizableBlock for text block") - return 0 - } - } - public func heightOfVerticallyNonResizableBlock(forWidth width: CGFloat) -> CGFloat { switch heightTrait { case .intrinsic: @@ -256,22 +270,6 @@ public final class TextFieldBlock: Block { } } - public var weightOfVerticallyResizableBlock: LayoutTrait.Weight { - guard case let .weighted(value) = heightTrait else { - assertionFailure("try to get weight for non resizable block") - return .default - } - return value - } - - public var weightOfHorizontallyResizableBlock: LayoutTrait.Weight { - guard case let .weighted(value) = widthTrait else { - assertionFailure("try to get weight for non resizable block") - return .default - } - return value - } - public func equals(_ other: Block) -> Bool { guard let other = other as? TextFieldBlock else { return false @@ -279,13 +277,17 @@ public final class TextFieldBlock: Block { return self == other } - public enum KeyboardAppearance { - case `default` - case light - case dark + public func getImageHolders() -> [ImageHolder] { [] } + + private func getHeaderHeight(forWidth _: CGFloat) -> CGFloat { + switch placeholders { + case let .separate(placeholders): + placeholders.headerAttributes.height.rawValue + case .fieldOnly, .none: + 0 + } } - public func getImageHolders() -> [ImageHolder] { [] } } extension TextFieldBlock { diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/TextInputBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/TextInputBlock.swift index 959335aa6..d03b26467 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/TextInputBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/TextInputBlock.swift @@ -6,6 +6,9 @@ public typealias TextInputFilter = (String) -> Bool public final class TextInputBlock: BlockWithTraits { public enum InputType: Equatable { + case keyboard(KeyboardType) + case selection([SelectionItem]) + public enum KeyboardType: Equatable { case `default` case URL @@ -25,9 +28,6 @@ public final class TextInputBlock: BlockWithTraits { } } - case keyboard(KeyboardType) - case selection([SelectionItem]) - public static let `default`: Self = .keyboard(.default) } @@ -96,6 +96,31 @@ public final class TextInputBlock: BlockWithTraits { let shouldClearFocus: Variable + public var intrinsicContentWidth: CGFloat { + switch widthTrait { + case let .fixed(value): + value + case let .intrinsic(_, minSize, _): + minSize + case .weighted: + 0 + } + } + + private var textForMeasuring: NSAttributedString { + guard multiLineMode else { + return defaultTextForMeasuring.with(typo: textTypo) + } + let text = textValue.value + if text.isEmpty { + if hint.isEmpty { + return defaultTextForMeasuring.with(typo: textTypo) + } + return hint + } + return text.with(typo: textTypo) + } + public init( widthTrait: LayoutTrait = .resizable, heightTrait: LayoutTrait = .intrinsic, @@ -168,17 +193,6 @@ public final class TextInputBlock: BlockWithTraits { self.spellChecking = spellChecking } - public var intrinsicContentWidth: CGFloat { - switch widthTrait { - case let .fixed(value): - value - case let .intrinsic(_, minSize, _): - minSize - case .weighted: - 0 - } - } - public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { switch heightTrait { case let .fixed(value): @@ -200,20 +214,6 @@ public final class TextInputBlock: BlockWithTraits { } } - private var textForMeasuring: NSAttributedString { - guard multiLineMode else { - return defaultTextForMeasuring.with(typo: textTypo) - } - let text = textValue.value - if text.isEmpty { - if hint.isEmpty { - return defaultTextForMeasuring.with(typo: textTypo) - } - return hint - } - return text.with(typo: textTypo) - } - public func equals(_ other: Block) -> Bool { guard let other = other as? TextInputBlock else { return false diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/TransitioningAnimation.swift b/client/ios/LayoutKit/LayoutKit/Blocks/TransitioningAnimation.swift index 5ac5e4074..947965c26 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/TransitioningAnimation.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/TransitioningAnimation.swift @@ -11,13 +11,6 @@ public struct TransitioningAnimation: Equatable { case translationY } - public let kind: Kind - public let start: Double - public let end: Double - public let duration: TimeInterval - public let delay: TimeInterval - public let timingFunction: TimingFunction - public static let empty = Self( kind: .fade, start: 1, @@ -30,6 +23,13 @@ public struct TransitioningAnimation: Equatable { public static let defaultLeadingSlideDistance = -Double.infinity public static let defaultTrailingSlideDistance = Double.infinity + public let kind: Kind + public let start: Double + public let end: Double + public let duration: TimeInterval + public let delay: TimeInterval + public let timingFunction: TimingFunction + public init( kind: Kind, start: Double, diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/TransitioningBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/TransitioningBlock.swift index cf1dec4f6..974d795ff 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/TransitioningBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/TransitioningBlock.swift @@ -4,6 +4,7 @@ import VGSL public final class TransitioningBlock: BlockWithLayout, WrapperBlock { public typealias Layout = CGRect + public let from: Block? public let to: Block public let animationOut: [TransitioningAnimation]? @@ -23,6 +24,13 @@ public final class TransitioningBlock: BlockWithLayout, WrapperBlock { self.animationIn = animationIn } + public static func ==(lhs: TransitioningBlock, rhs: TransitioningBlock) -> Bool { + lhs.from == rhs.from && + lhs.to == rhs.to && + lhs.animationOut == rhs.animationOut && + lhs.animationIn == rhs.animationIn + } + public func equals(_ other: Block) -> Bool { guard let other = other as? Self else { return false @@ -40,13 +48,6 @@ public final class TransitioningBlock: BlockWithLayout, WrapperBlock { ) } - public static func ==(lhs: TransitioningBlock, rhs: TransitioningBlock) -> Bool { - lhs.from == rhs.from && - lhs.to == rhs.to && - lhs.animationOut == rhs.animationOut && - lhs.animationIn == rhs.animationIn - } - func laidOutHierarchy(for size: CGSize) -> (TransitioningBlock, Layout) { let block = Self( from: from?.laidOut(for: size), diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Video/CorePlayer/CorePlayerImpl.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Video/CorePlayer/CorePlayerImpl.swift index ff4129909..05a452a60 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Video/CorePlayer/CorePlayerImpl.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Video/CorePlayer/CorePlayerImpl.swift @@ -3,9 +3,19 @@ import Foundation import VGSL final class CorePlayerImpl: CorePlayer { - static func isMIMETypeSupported(_ mimeType: String) -> Bool { - AVURLAsset.isPlayableExtendedMIMEType(mimeType) - } + private let player: AVPlayer + private let itemsProvider: PlayerItemsProvider + + private let playerObservers = AutodisposePool() + private let itemObservers = AutodisposePool() + + private let playerStatusPipe = SignalPipe() + private let playerDurationPipe = SignalPipe() + private let playbackStatusPipe = SignalPipe() + private let playerErrorPipe = SignalPipe() + private let playbackFinishPipe = SignalPipe() + + private var currentTimePipes = [TimeInterval: SignalPipe]() var videoEngine: VideoEngine { VideoEngine(type: .avPlayer(player)) @@ -31,32 +41,14 @@ final class CorePlayerImpl: CorePlayer { playbackFinishPipe.signal } - private let player: AVPlayer - private let itemsProvider: PlayerItemsProvider - - private let playerObservers = AutodisposePool() - private let itemObservers = AutodisposePool() - - private let playerStatusPipe = SignalPipe() - private let playerDurationPipe = SignalPipe() - private let playbackStatusPipe = SignalPipe() - private let playerErrorPipe = SignalPipe() - private let playbackFinishPipe = SignalPipe() - - private var currentTimePipes = [TimeInterval: SignalPipe]() - init(itemsProvider: PlayerItemsProvider) { self.player = AVPlayer() self.itemsProvider = itemsProvider setup(player) } - private func resetPlayerObservers() { - playerObservers.drain() - } - - private func resetItemObservers() { - itemObservers.drain() + static func isMIMETypeSupported(_ mimeType: String) -> Bool { + AVURLAsset.isPlayableExtendedMIMEType(mimeType) } func set(source: Video) { @@ -111,6 +103,14 @@ final class CorePlayerImpl: CorePlayer { return pipe.signal } + private func resetPlayerObservers() { + playerObservers.drain() + } + + private func resetItemObservers() { + itemObservers.drain() + } + private func setup(_ player: AVPlayer) { player.actionAtItemEnd = .pause player.automaticallyWaitsToMinimizeStalling = true diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Video/DefaultPlayer/DefaultPlayer.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Video/DefaultPlayer/DefaultPlayer.swift index 1a61d2103..eaccff25a 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Video/DefaultPlayer/DefaultPlayer.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Video/DefaultPlayer/DefaultPlayer.swift @@ -5,16 +5,16 @@ import VGSL final class DefaultPlayer: Player { private let eventPipe = SignalPipe() - var signal: Signal { - eventPipe.signal - } - private let player: CorePlayer private var context: SourceContext? private let playerObservers = AutodisposePool() private let playbackConfigObservers = AutodisposePool() + var signal: Signal { + eventPipe.signal + } + init(itemsProvider: PlayerItemsProvider) { self.player = CorePlayerImpl(itemsProvider: itemsProvider) configureObservers(for: self.player) diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Video/DefaultPlayer/DefaultPlayerFactory.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Video/DefaultPlayer/DefaultPlayerFactory.swift index 67b895832..a60a45a51 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Video/DefaultPlayer/DefaultPlayerFactory.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Video/DefaultPlayer/DefaultPlayerFactory.swift @@ -7,6 +7,10 @@ import VGSL public final class DefaultPlayerFactory: PlayerFactory { private let itemsProvider: PlayerItemsProvider + public init(itemsProvider: PlayerItemsProvider = DefaultPlayerItemsProvider()) { + self.itemsProvider = itemsProvider + } + public func makePlayer(data: VideoData?, config: PlaybackConfig?) -> Player { let player = DefaultPlayer(itemsProvider: itemsProvider) guard let config, let data else { return player } @@ -18,8 +22,5 @@ public final class DefaultPlayerFactory: PlayerFactory { DefaultPlayerView() } - public init(itemsProvider: PlayerItemsProvider = DefaultPlayerItemsProvider()) { - self.itemsProvider = itemsProvider - } } #endif diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Video/DefaultPlayer/DefaultPlayerView.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Video/DefaultPlayer/DefaultPlayerView.swift index 1c8e188a9..55fda04bd 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Video/DefaultPlayer/DefaultPlayerView.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Video/DefaultPlayer/DefaultPlayerView.swift @@ -3,6 +3,22 @@ import AVFoundation import UIKit public final class DefaultPlayerView: UIView, PlayerView { + public override static var layerClass: AnyClass { AVPlayerLayer.self } + + public var videoRatio: CGFloat? { + guard let asset = playerLayer.player?.currentItem?.asset, + let videoSize = asset.tracks( + withMediaType: .video + ).first?.naturalSize + else { + return nil + } + + return videoSize.width / videoSize.height + } + + private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer } + public func onVisibleBoundsChanged(from _: CGRect, to _: CGRect) {} public func attach(player: Player) { @@ -23,21 +39,6 @@ public final class DefaultPlayerView: UIView, PlayerView { } } - public var videoRatio: CGFloat? { - guard let asset = playerLayer.player?.currentItem?.asset, - let videoSize = asset.tracks( - withMediaType: .video - ).first?.naturalSize - else { - return nil - } - - return videoSize.width / videoSize.height - } - - private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer } - - public override static var layerClass: AnyClass { AVPlayerLayer.self } } extension AVPlayer { diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Video/PlayerFactory.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Video/PlayerFactory.swift index 66bc245e7..1cb1726a4 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Video/PlayerFactory.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Video/PlayerFactory.swift @@ -34,6 +34,14 @@ public protocol PlayerFactory { } public struct PlaybackConfig: Equatable { + public static let `default` = PlaybackConfig( + autoPlay: true, + repeatable: false, + isMuted: false, + startPosition: .zero, + settingsPayload: [:] + ) + public let autoPlay: Bool public let repeatable: Bool public let isMuted: Bool @@ -54,14 +62,6 @@ public struct PlaybackConfig: Equatable { self.settingsPayload = settingsPayload } - public static let `default` = PlaybackConfig( - autoPlay: true, - repeatable: false, - isMuted: false, - startPosition: .zero, - settingsPayload: [:] - ) - public static func ==(lhs: PlaybackConfig, rhs: PlaybackConfig) -> Bool { lhs.autoPlay == rhs.autoPlay && lhs.repeatable == rhs.repeatable && diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Video/VideoBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Video/VideoBlock.swift index 3b950875e..af9d36f10 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Video/VideoBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Video/VideoBlock.swift @@ -8,10 +8,21 @@ public final class VideoBlock: BlockWithTraits { public let model: VideoBlockViewModel public let state: VideoBlockViewState public let playerFactory: PlayerFactory + public var debugDescription: String = "" + public var path: UIElementPath? { model.path } + public var intrinsicContentWidth: CGFloat { + switch widthTrait { + case let .fixed(value): + value + case .intrinsic, .weighted: + 0 + } + } + public init( widthTrait: LayoutTrait, heightTrait: LayoutTrait, @@ -26,15 +37,6 @@ public final class VideoBlock: BlockWithTraits { self.playerFactory = playerFactory } - public var intrinsicContentWidth: CGFloat { - switch widthTrait { - case let .fixed(value): - value - case .intrinsic, .weighted: - 0 - } - } - public func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { switch heightTrait { case let .fixed(value): @@ -54,7 +56,6 @@ public final class VideoBlock: BlockWithTraits { return self.model == other.model && self.state == other.state } - public var debugDescription: String = "" } extension VideoBlock: LayoutCachingDefaultImpl {} diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/VideoBlockLegacy.swift b/client/ios/LayoutKit/LayoutKit/Blocks/VideoBlockLegacy.swift index 8acf615a1..0692f2467 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/VideoBlockLegacy.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/VideoBlockLegacy.swift @@ -17,16 +17,18 @@ public final class VideoBlockLegacy: BlockWithTraits { } } - public var debugDescription: String { - "Video Block playing video with url \(videoAssetHolder.url)" - } - public let videoAssetHolder: VideoAssetHolder public let widthTrait: LayoutTrait public let heightTrait: LayoutTrait + public let intrinsicContentWidth: CGFloat = 0 + let autoplayAllowed: ObservableVariable + public var debugDescription: String { + "Video Block playing video with url \(videoAssetHolder.url)" + } + public init( videoAssetHolder: VideoAssetHolder, widthTrait: LayoutTrait, @@ -39,7 +41,6 @@ public final class VideoBlockLegacy: BlockWithTraits { self.autoplayAllowed = autoplayAllowed } - public let intrinsicContentWidth: CGFloat = 0 public func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { 0 } public func getImageHolders() -> [ImageHolder] { [] } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/VisibilityAction.swift b/client/ios/LayoutKit/LayoutKit/Blocks/VisibilityAction.swift index 32ac0ee49..fd0bb26bc 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/VisibilityAction.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/VisibilityAction.swift @@ -3,6 +3,7 @@ import Foundation public struct VisibilityAction { public let logId: String? + let uiAction: UserInterfaceAction let requiredDuration: TimeInterval let targetPercentage: Int diff --git a/client/ios/LayoutKit/LayoutKit/GenericViewBlock.swift b/client/ios/LayoutKit/LayoutKit/GenericViewBlock.swift index d85ea758f..34a5ff4ed 100644 --- a/client/ios/LayoutKit/LayoutKit/GenericViewBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/GenericViewBlock.swift @@ -41,6 +41,18 @@ public final class GenericViewBlock: BlockWithTraits { public var widthTrait: LayoutTrait { width.layoutTrait } public var heightTrait: LayoutTrait { height.layoutTrait } + public var intrinsicContentWidth: CGFloat { + switch width { + case .resizable: 0 + case let .fixed(width): width + case let .intrinsic(calculator): calculator.calculateWidth() + } + } + + public var debugDescription: String { + "Generic: \(dbgStr(content.currentValue)), W:\(dbgStr(width)), H:\(dbgStr(height))" + } + public init( lazyContent: Lazy, width: Trait, @@ -81,14 +93,6 @@ public final class GenericViewBlock: BlockWithTraits { self.init(content: .layer(layer), width: width.trait, height: height.trait) } - public var intrinsicContentWidth: CGFloat { - switch width { - case .resizable: 0 - case let .fixed(width): width - case let .intrinsic(calculator): calculator.calculateWidth() - } - } - public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { switch height { case .resizable: 0 @@ -108,9 +112,6 @@ public final class GenericViewBlock: BlockWithTraits { public func getImageHolders() -> [ImageHolder] { [] } - public var debugDescription: String { - "Generic: \(dbgStr(content.currentValue)), W:\(dbgStr(width)), H:\(dbgStr(height))" - } } extension GenericViewBlock: LayoutCachingDefaultImpl {} diff --git a/client/ios/LayoutKit/LayoutKit/Tooltips/TooltipManager.swift b/client/ios/LayoutKit/LayoutKit/Tooltips/TooltipManager.swift index 824ceeec8..0461ee5a4 100644 --- a/client/ios/LayoutKit/LayoutKit/Tooltips/TooltipManager.swift +++ b/client/ios/LayoutKit/LayoutKit/Tooltips/TooltipManager.swift @@ -70,10 +70,11 @@ public class DefaultTooltipManager: TooltipManager { public var shownTooltips: Property> + private(set) var tooltipWindowManager: TooltipWindowManager? + private var handleAction: (UIActionEvent) -> Void private var existingAnchorViews = WeakCollection() private var showingTooltips = [String: TooltipContainerView]() - private(set) var tooltipWindowManager: TooltipWindowManager? private var previousOrientation = UIDevice.current.orientation public init( @@ -91,6 +92,14 @@ public class DefaultTooltipManager: TooltipManager { ) } + deinit { + NotificationCenter.default.removeObserver( + self, + name: UIDevice.orientationDidChangeNotification, + object: nil + ) + } + public func showTooltip(info: TooltipInfo) { setupTooltipWindowManager() guard let tooltipWindowManager, @@ -175,14 +184,6 @@ public class DefaultTooltipManager: TooltipManager { handleAction = handler } - deinit { - NotificationCenter.default.removeObserver( - self, - name: UIDevice.orientationDidChangeNotification, - object: nil - ) - } - @objc func orientationDidChange(_: Notification) { let orientation = UIDevice.current.orientation guard orientation != previousOrientation, !orientation.isFlat else { return } @@ -242,6 +243,10 @@ extension TooltipAnchorView { } private final class ProxyViewController: UIViewController { + override var preferredStatusBarStyle: UIStatusBarStyle { + viewController.preferredStatusBarStyle + } + private let viewController: UIViewController init(viewController: UIViewController) { @@ -254,9 +259,6 @@ private final class ProxyViewController: UIViewController { fatalError("init(coder:) has not been implemented") } - override var preferredStatusBarStyle: UIStatusBarStyle { - viewController.preferredStatusBarStyle - } } final class TooltipWindowManager { diff --git a/client/ios/LayoutKit/LayoutKit/UI/Actions/UIActionEventPerforming.swift b/client/ios/LayoutKit/LayoutKit/UI/Actions/UIActionEventPerforming.swift index ee6aca0c5..1fd260621 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Actions/UIActionEventPerforming.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Actions/UIActionEventPerforming.swift @@ -15,15 +15,15 @@ public final class UIActionEvent: AppActionEventProtocol { public let originalSender: UIResponder public let uiAction: UserInterfaceAction + public var payload: UserInterfaceAction.Payload { + uiAction.payload + } + public init(uiAction: UserInterfaceAction, originalSender: UIResponder) { self.originalSender = originalSender self.uiAction = uiAction } - public var payload: UserInterfaceAction.Payload { - uiAction.payload - } - public func makeHandler(responder: UIResponder) -> Handler? { (responder as? UIActionEventPerforming)?.perform(uiActionEvent:from:) } diff --git a/client/ios/LayoutKit/LayoutKit/UI/Base/GenericCollectionReusableView.swift b/client/ios/LayoutKit/LayoutKit/UI/Base/GenericCollectionReusableView.swift index 363a1c7e9..fca639fda 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Base/GenericCollectionReusableView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Base/GenericCollectionReusableView.swift @@ -16,6 +16,11 @@ public final class GenericCollectionReusableView: UICollectionReusableView { } } + public override func layoutSubviews() { + super.layoutSubviews() + view?.frame = bounds + } + public func configure(model: UIViewRenderable, observer: ElementStateObserver? = nil) { self.model = model @@ -35,9 +40,5 @@ public final class GenericCollectionReusableView: UICollectionReusableView { } } - public override func layoutSubviews() { - super.layoutSubviews() - view?.frame = bounds - } } #endif diff --git a/client/ios/LayoutKit/LayoutKit/UI/Base/GenericCollectionViewCell.swift b/client/ios/LayoutKit/LayoutKit/UI/Base/GenericCollectionViewCell.swift index 7f3291eca..a9f4a664a 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Base/GenericCollectionViewCell.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Base/GenericCollectionViewCell.swift @@ -6,8 +6,6 @@ import VGSL open class GenericCollectionViewCell: UICollectionViewCell, VisibleBoundsTrackingContainer { public private(set) var model: UIViewRenderable! - public var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { view.asArray() } - public private(set) var view: BlockView? { didSet { oldValue?.removeFromSuperview() @@ -18,6 +16,13 @@ open class GenericCollectionViewCell: UICollectionViewCell, VisibleBoundsTrackin } } + public var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { view.asArray() } + + public override func layoutSubviews() { + super.layoutSubviews() + view?.frame = contentView.bounds + } + public func configure( model: UIViewRenderable, observer: ElementStateObserver?, @@ -45,9 +50,5 @@ open class GenericCollectionViewCell: UICollectionViewCell, VisibleBoundsTrackin applyAccessibilityFromScratch(accessibilityElement) } - public override func layoutSubviews() { - super.layoutSubviews() - view?.frame = contentView.bounds - } } #endif diff --git a/client/ios/LayoutKit/LayoutKit/UI/Base/GenericCollectionViewLayout.swift b/client/ios/LayoutKit/LayoutKit/UI/Base/GenericCollectionViewLayout.swift index b33af7bf7..b4d335d61 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Base/GenericCollectionViewLayout.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Base/GenericCollectionViewLayout.swift @@ -4,16 +4,20 @@ import UIKit import VGSL public final class GenericCollectionViewLayout: UICollectionViewLayout { + public override var collectionViewContentSize: CGSize { + layout?.contentSize ?? .zero + } + + public override var flipsHorizontallyInOppositeLayoutDirection: Bool { + true + } + public var layout: GenericCollectionLayout? { didSet { invalidateLayout() } } - public override var collectionViewContentSize: CGSize { - layout?.contentSize ?? .zero - } - public override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { guard let layout else { @@ -86,9 +90,6 @@ public final class GenericCollectionViewLayout: UICollectionViewLayout { return attributes } - public override var flipsHorizontallyInOppositeLayoutDirection: Bool { - true - } } extension UICollectionViewLayoutAttributes { diff --git a/client/ios/LayoutKit/LayoutKit/UI/Base/PageIndicator/IndicatorState/IndicatorStateAnimator.swift b/client/ios/LayoutKit/LayoutKit/UI/Base/PageIndicator/IndicatorState/IndicatorStateAnimator.swift index 6c63e4a50..51c546cd3 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Base/PageIndicator/IndicatorState/IndicatorStateAnimator.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Base/PageIndicator/IndicatorState/IndicatorStateAnimator.swift @@ -8,6 +8,16 @@ struct IndicatorStateAnimator { let boundsWidth: CGFloat let numberOfPages: Int + private var itemWidth: CGFloat { + switch configuration.itemPlacement { + case let .fixed(spaceBetweenCenters): + return spaceBetweenCenters + case let .stretch(_, maxVisibleItems): + let visiblePageCount = min(maxVisibleItems, numberOfPages) + return boundsWidth / CGFloat(visiblePageCount) + } + } + func indicatorColor(for state: IndicatorState) -> Color { let normalColor = configuration.normalColor let highlightedColor = configuration.highlightedColor @@ -78,14 +88,5 @@ struct IndicatorStateAnimator { } } - private var itemWidth: CGFloat { - switch configuration.itemPlacement { - case let .fixed(spaceBetweenCenters): - return spaceBetweenCenters - case let .stretch(_, maxVisibleItems): - let visiblePageCount = min(maxVisibleItems, numberOfPages) - return boundsWidth / CGFloat(visiblePageCount) - } - } } #endif diff --git a/client/ios/LayoutKit/LayoutKit/UI/Base/PageIndicator/PageIndicatorLayer.swift b/client/ios/LayoutKit/LayoutKit/UI/Base/PageIndicator/PageIndicatorLayer.swift index 693d5af03..9a2302b6c 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Base/PageIndicator/PageIndicatorLayer.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Base/PageIndicator/PageIndicatorLayer.swift @@ -28,6 +28,7 @@ final class ScrollPageIndicatorLayer: CALayer { } private var _cachedParams: PageIndicatorLayerParams? + private var params: PageIndicatorLayerParams { if let value = _cachedParams { return value } let result = PageIndicatorLayerParams( diff --git a/client/ios/LayoutKit/LayoutKit/UI/Base/PageIndicator/PageIndicatorView.swift b/client/ios/LayoutKit/LayoutKit/UI/Base/PageIndicator/PageIndicatorView.swift index de0ad0b2d..46b66a2c8 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Base/PageIndicator/PageIndicatorView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Base/PageIndicator/PageIndicatorView.swift @@ -3,6 +3,8 @@ import UIKit import VGSL final class PageIndicatorView: UIView { + override class var layerClass: AnyClass { ScrollPageIndicatorLayer.self } + var currentIndexPosition: CGFloat { get { contentLayer.currentIndexPosition @@ -37,8 +39,6 @@ final class PageIndicatorView: UIView { layer as! ScrollPageIndicatorLayer } - override class var layerClass: AnyClass { ScrollPageIndicatorLayer.self } - init() { super.init(frame: .zero) commonInit() @@ -54,12 +54,13 @@ final class PageIndicatorView: UIView { commonInit() } + override func draw(_: CGRect) { + // overriden draw is required for redrawing of layer + } + private func commonInit() { backgroundColor = .clear } - override func draw(_: CGRect) { - // overriden draw is required for redrawing of layer - } } #endif diff --git a/client/ios/LayoutKit/LayoutKit/UI/Base/ScrollableContentPager.swift b/client/ios/LayoutKit/LayoutKit/UI/Base/ScrollableContentPager.swift index 1767e1dec..467f932d1 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Base/ScrollableContentPager.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Base/ScrollableContentPager.swift @@ -2,9 +2,13 @@ import CoreGraphics import VGSL public final class ScrollableContentPager: NSObject { + public private(set) var currentPageIndex: Int? + public private(set) var lastTargetPageIndex: Int? + + private(set) var isHorizontal = true + private var indexedPageOrigins: [(origin: CGFloat, index: Int)] = [] private var isPagingEnabled = true - private(set) var isHorizontal = true public func setPageOrigins( _ pageOrigins: [CGFloat], @@ -26,9 +30,6 @@ public final class ScrollableContentPager: NSObject { currentPageIndex = pageIndex(forOffset: offset) } - public private(set) var currentPageIndex: Int? - public private(set) var lastTargetPageIndex: Int? - public func targetPageOffset(forProposedOffset offset: CGFloat, velocity: CGFloat) -> CGFloat? { guard let startPageIndex = currentPageIndex else { return nil } diff --git a/client/ios/LayoutKit/LayoutKit/UI/Base/VisibilityState.swift b/client/ios/LayoutKit/LayoutKit/UI/Base/VisibilityState.swift index b05004e7c..0fe84d51b 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Base/VisibilityState.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Base/VisibilityState.swift @@ -5,6 +5,12 @@ public struct VisibilityState: Equatable { public var maximumVisibleSize: CGSize public var visibleFrame: CGRect + public var visibility: CGFloat { + let visibleArea = visibleFrame.size.area + let maximumVisibleArea = maximumVisibleSize.area + return (maximumVisibleArea > 0) ? (visibleArea / maximumVisibleArea) : 0 + } + public init(visibleFrame: CGRect) { self.visibleFrame = visibleFrame maximumVisibleSize = visibleFrame.size @@ -27,11 +33,6 @@ public struct VisibilityState: Equatable { return visibilityState } - public var visibility: CGFloat { - let visibleArea = visibleFrame.size.area - let maximumVisibleArea = maximumVisibleSize.area - return (maximumVisibleArea > 0) ? (visibleArea / maximumVisibleArea) : 0 - } } extension CGSize { diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/AccessibilityBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/AccessibilityBlock+UIViewRenderableBlock.swift index eab2e55db..1e2871d9a 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/AccessibilityBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/AccessibilityBlock+UIViewRenderableBlock.swift @@ -43,9 +43,10 @@ private final class AccessibilityBlockView: UIView, BlockViewProtocol, let source: Variable } + private(set) var childView: BlockView? + private var model: Model! private weak var observer: ElementStateObserver? - private(set) var childView: BlockView? var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { childView.asArray() } var effectiveBackgroundColor: UIColor? { childView?.backgroundColor } @@ -59,6 +60,16 @@ private final class AccessibilityBlockView: UIView, BlockViewProtocol, fatalError("init(coder:) has not been implemented") } + override func layoutSubviews() { + super.layoutSubviews() + childView?.frame = bounds + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + return result === self ? nil : result + } + func configure( model: Model, observer: ElementStateObserver?, @@ -89,16 +100,6 @@ private final class AccessibilityBlockView: UIView, BlockViewProtocol, } } - override func layoutSubviews() { - super.layoutSubviews() - childView?.frame = bounds - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let result = super.hitTest(point, with: event) - return result === self ? nil : result - } - func onVisibleBoundsChanged(from: CGRect, to: CGRect) { passVisibleBoundsChanged(from: from, to: to) } diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/AnimatableImageBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/AnimatableImageBlock+UIViewRenderableBlock.swift index ddf546a71..aa5e5a977 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/AnimatableImageBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/AnimatableImageBlock+UIViewRenderableBlock.swift @@ -28,16 +28,6 @@ extension AnimatableImageBlock { private class AnimatableImageContainer: UIStackView, BlockViewProtocol, VisibleBoundsTrackingLeaf { var imageRequest: Cancellable? - var effectiveBackgroundColor: UIColor? { backgroundColor } - - private var backgroundModel: ImageViewBackgroundModel? { - didSet { - backgroundModel.applyTo(self, oldValue: oldValue) - } - } - - private var imageHolder: ImageHolder? - var imageContentMode = ImageContentMode.default { didSet { if imageView.imageContentMode != imageContentMode { @@ -47,8 +37,18 @@ private class AnimatableImageContainer: UIStackView, BlockViewProtocol, VisibleB } } + private var backgroundModel: ImageViewBackgroundModel? { + didSet { + backgroundModel.applyTo(self, oldValue: oldValue) + } + } + + private var imageHolder: ImageHolder? + private let imageView = AnimatableImageView(frame: .zero) + var effectiveBackgroundColor: UIColor? { backgroundColor } + public override init(frame: CGRect) { super.init(frame: frame) isUserInteractionEnabled = false @@ -86,6 +86,22 @@ private class AnimatableImageContainer: UIStackView, BlockViewProtocol, VisibleB private class AnimatableImageView: UIImageView { override class var layerClass: AnyClass { AnimatableImageLayer.self } + override var image: UIImage? { + willSet { + animatableImageLayer?.updateAllowed = true + } + didSet { + updateGravity() + animatableImageLayer?.updateAllowed = false + } + } + + var imageContentMode = ImageContentMode.default { + didSet { + updateGravity() + } + } + var animatableImageLayer: AnimatableImageLayer? { layer as? AnimatableImageLayer } @@ -103,22 +119,6 @@ private class AnimatableImageView: UIImageView { fatalError("init(coder:) has not been implemented") } - var imageContentMode = ImageContentMode.default { - didSet { - updateGravity() - } - } - - override var image: UIImage? { - willSet { - animatableImageLayer?.updateAllowed = true - } - didSet { - updateGravity() - animatableImageLayer?.updateAllowed = false - } - } - public override func layoutSubviews() { super.layoutSubviews() updateFramePosition() diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/BackgroundBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/BackgroundBlock+UIViewRenderableBlock.swift index cee526e21..94e0051e2 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/BackgroundBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/BackgroundBlock+UIViewRenderableBlock.swift @@ -36,6 +36,14 @@ extension BackgroundBlock { } private final class BackgroundBlockView: BlockView, VisibleBoundsTrackingContainer { + struct Model: ReferenceEquatable { + let background: Background + let child: Block + let source: Variable + let cornerRadius: CGFloat + let layout: BackgroundBlock.Layout? + } + private var backgroundView: BlockView? { didSet { guard oldValue !== backgroundView else { return } @@ -56,22 +64,28 @@ private final class BackgroundBlockView: BlockView, VisibleBoundsTrackingContain } } + private var model: Model! + private weak var observer: ElementStateObserver? + var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { [childView, backgroundView].compactMap { $0 } } var effectiveBackgroundColor: UIColor? { backgroundView?.effectiveBackgroundColor } - struct Model: ReferenceEquatable { - let background: Background - let child: Block - let source: Variable - let cornerRadius: CGFloat - let layout: BackgroundBlock.Layout? + override func layoutSubviews() { + super.layoutSubviews() + + layer.cornerRadius = clamp(model.cornerRadius, min: 0, max: bounds.size.minDimension.half) + + backgroundView?.frame = model?.layout ?? bounds + childView?.frame = model?.layout ?? bounds } - private var model: Model! - private weak var observer: ElementStateObserver? + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + return result === self ? nil : result + } func configure( model: Model, @@ -108,19 +122,6 @@ private final class BackgroundBlockView: BlockView, VisibleBoundsTrackingContain setNeedsLayout() } - override func layoutSubviews() { - super.layoutSubviews() - - layer.cornerRadius = clamp(model.cornerRadius, min: 0, max: bounds.size.minDimension.half) - - backgroundView?.frame = model?.layout ?? bounds - childView?.frame = model?.layout ?? bounds - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let result = super.hitTest(point, with: event) - return result === self ? nil : result - } } extension UIViewRenderable { diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/ContainerBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/ContainerBlock+UIViewRenderableBlock.swift index 04513faac..3f36a5072 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/ContainerBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/ContainerBlock+UIViewRenderableBlock.swift @@ -68,66 +68,24 @@ private final class ContainerBlockView: UIView, BlockViewProtocol, VisibleBounds let accessibility: AccessibilityElement? } - private var blockViews: [BlockView] = [] - var layoutReporter: LayoutReporter? - var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { blockViews } - var effectiveBackgroundColor: UIColor? { blockViews.first?.effectiveBackgroundColor } + + private var blockViews: [BlockView] = [] // layoutSubivews is called multiple times for same view size and model, so we optimize out // redundant calls private var modelAndLastLayoutSize: (model: Model?, lastLayoutSize: CGSize?) private var preventLayoutAndConfig = false - private var model: Model! { - modelAndLastLayoutSize.model - } - private weak var observer: ElementStateObserver? private weak var overscrollDelegate: ScrollDelegate? private weak var renderingDelegate: RenderingDelegate? - func configure( - model: Model, - observer: ElementStateObserver?, - overscrollDelegate: ScrollDelegate?, - renderingDelegate: RenderingDelegate? - ) { - guard model != modelAndLastLayoutSize.model || - observer !== self.observer, - !preventLayoutAndConfig - else { - return - } + var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { blockViews } + var effectiveBackgroundColor: UIColor? { blockViews.first?.effectiveBackgroundColor } - applyAccessibilityFromScratch(model.accessibility) - modelAndLastLayoutSize = (model: model, lastLayoutSize: nil) - self.observer = observer - self.overscrollDelegate = overscrollDelegate - self.renderingDelegate = renderingDelegate - - // Configuring views may lead to unpredictable side effects, - // including view hierarchy layout. - preventLayoutAndConfig = true - blockViews = blockViews.reused( - with: model.children.map(\.content), - attachTo: self, - observer: observer, - overscrollDelegate: overscrollDelegate, - renderingDelegate: renderingDelegate - ) - - preventLayoutAndConfig = false - - setNeedsLayout() - - clipsToBounds = model.clipContent - - let animationKey = "contentAnimation" - layer.removeAnimation(forKey: animationKey) - if let animation = model.contentAnimation?.keyFrameAnimation { - layer.add(animation, forKey: animationKey) - } + private var model: Model! { + modelAndLastLayoutSize.model } override func layoutSubviews() { @@ -186,5 +144,49 @@ private final class ContainerBlockView: UIView, BlockViewProtocol, VisibleBounds let result = super.hitTest(point, with: event) return result === self ? nil : result } + + func configure( + model: Model, + observer: ElementStateObserver?, + overscrollDelegate: ScrollDelegate?, + renderingDelegate: RenderingDelegate? + ) { + guard model != modelAndLastLayoutSize.model || + observer !== self.observer, + !preventLayoutAndConfig + else { + return + } + + applyAccessibilityFromScratch(model.accessibility) + modelAndLastLayoutSize = (model: model, lastLayoutSize: nil) + self.observer = observer + self.overscrollDelegate = overscrollDelegate + self.renderingDelegate = renderingDelegate + + // Configuring views may lead to unpredictable side effects, + // including view hierarchy layout. + preventLayoutAndConfig = true + blockViews = blockViews.reused( + with: model.children.map(\.content), + attachTo: self, + observer: observer, + overscrollDelegate: overscrollDelegate, + renderingDelegate: renderingDelegate + ) + + preventLayoutAndConfig = false + + setNeedsLayout() + + clipsToBounds = model.clipContent + + let animationKey = "contentAnimation" + layer.removeAnimation(forKey: animationKey) + if let animation = model.contentAnimation?.keyFrameAnimation { + layer.add(animation, forKey: animationKey) + } + } + } #endif diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/DebugInfoBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/DebugInfoBlock+UIViewRenderableBlock.swift index 24c282c80..531dca372 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/DebugInfoBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/DebugInfoBlock+UIViewRenderableBlock.swift @@ -44,6 +44,11 @@ private final class DebugInfoBlockView: BlockView { fatalError("init(coder:) has not been implemented") } + override func layoutSubviews() { + super.layoutSubviews() + self.childView.frame = bounds + } + func configure( child: Block, showDebugInfo: @escaping Action, @@ -62,11 +67,6 @@ private final class DebugInfoBlockView: BlockView { setNeedsLayout() } - override func layoutSubviews() { - super.layoutSubviews() - self.childView.frame = bounds - } - private func showInfo() { showDebugInfo() } diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift index 3fe04c076..d842019ae 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift @@ -168,10 +168,32 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT } } + override var isHighlighted: Bool { + didSet { + guard oldValue != isHighlighted else { + return + } + + updateHighlightState(animated: true) + } + } + + override var isEnabled: Bool { + didSet { + guard oldValue != isEnabled else { + return + } + + updateHighlightState(animated: false) + } + } + + private(set) var childView: BlockView? + fileprivate var model: Model! + private weak var observer: ElementStateObserver? private weak var renderingDelegate: RenderingDelegate? - private(set) var childView: BlockView? private var contextMenuDelegate: NSObjectProtocol? @@ -179,12 +201,7 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT private let animationMinimalDuration: TimeInterval = 0.125 private var visibilityActionPerformers: VisibilityActionPerformers? - var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { childView.asArray() } - var effectiveBackgroundColor: UIColor? { backgroundColor } - private var hasFocused = false - private var isViewOnWindow: Bool { window != nil } - private var tapRecognizer: UITapGestureRecognizer? { didSet { oldValue.flatMap(removeGestureRecognizer(_:)) @@ -213,32 +230,6 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT } } - override var isHighlighted: Bool { - didSet { - guard oldValue != isHighlighted else { - return - } - - updateHighlightState(animated: true) - } - } - - override var isEnabled: Bool { - didSet { - guard oldValue != isEnabled else { - return - } - - updateHighlightState(animated: false) - } - } - - private var highlightState: HighlightState { - (isHighlighted || !isEnabled) && model.hasResponsiveUI - ? .highlighted - : .normal - } - private var borderLayer: CALayer? { didSet { oldValue?.removeFromSuperlayer() @@ -263,82 +254,29 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT } } + var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { childView.asArray() } + var effectiveBackgroundColor: UIColor? { backgroundColor } + + private var isViewOnWindow: Bool { window != nil } + + private var highlightState: HighlightState { + (isHighlighted || !isEnabled) && model.hasResponsiveUI + ? .highlighted + : .normal + } + init() { super.init(frame: .zero) isExclusiveTouch = true } - private func configureRecognizers() { - guard model.shouldHandleAnyAction else { - tapRecognizer?.isEnabled = false - doubleTapRecognizer?.isEnabled = false - longPressRecognizer?.isEnabled = false - hoverRecognizer?.isEnabled = false - return - } - - if tapRecognizer == nil { - tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - } - tapRecognizer?.cancelsTouchesInView = false - - if model.shouldHandleDoubleTap, doubleTapRecognizer == nil { - let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - doubleTapRecognizer.numberOfTapsRequired = 2 - self.doubleTapRecognizer = doubleTapRecognizer - tapRecognizer?.require(toFail: doubleTapRecognizer) - } - - if model.shouldHandleLongTap, longPressRecognizer == nil { - longPressRecognizer = UILongPressGestureRecognizer( - target: self, - action: #selector(handleLongPress) - ) - } - - if model.shouldHandleHover, hoverRecognizer == nil { - hoverRecognizer = UIHoverGestureRecognizer( - target: self, - action: #selector(handleHover) - ) - } - - tapRecognizer?.isEnabled = model.shouldHandleTap - doubleTapRecognizer?.isEnabled = model.shouldHandleDoubleTap - longPressRecognizer?.isEnabled = model.shouldHandleLongTap - hoverRecognizer?.isEnabled = model.shouldHandleHover - } - - private func checkTouchableArea() { - guard tapRecognizer != nil || doubleTapRecognizer != nil || longPressRecognizer != nil else { - return - } - guard bounds.width > 0 && bounds.height > 0 else { return } - if bounds.width < 44 || bounds.height < 44 { - renderingDelegate?.reportRenderingError( - message: "Touchable view is too small: \(bounds.size), \(model.child)", - isWarning: true, - path: model.path ?? UIElementPath("") - ) - } - } - @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - @objc private func handleTap(recognizer: UITapGestureRecognizer) { - // Sometimes there are late touches that were performed before layout. - // This breaks UX due to UIView reusing, so just skip them here. - guard bounds.contains(recognizer.location(in: self)) else { return } - captureFocusIfNeeded() - let actions: [UserInterfaceAction]? = if recognizer === doubleTapRecognizer { - model.doubleTapActions?.asArray() - } else if recognizer === tapRecognizer { - model.actions?.asArray() - } else { - [] + deinit { + if model?.tooltips.isEmpty == false { + renderingDelegate?.tooltipAnchorViewRemoved(anchorView: self) } - actions?.perform(sendingFrom: self) } override func accessibilityActivate() -> Bool { @@ -490,6 +428,105 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT setNeedsLayout() } + func onVisibleBoundsChanged(from: CGRect, to: CGRect) { + if let child = childView as? DelayedVisibilityActionView { + child.visibilityAction = { [weak self] in + + guard let self else { return } + onVisibleBoundsChangedInternal(from: from, to: to) + } + } else { + onVisibleBoundsChangedInternal(from: from, to: to) + } + } + + func makeTooltipEvent(with info: TooltipInfo) -> TooltipEvent? { + guard let tooltipModel = model.tooltips.first(where: { $0.id == info.id }), let window else { + return nil + } + let tooltipView = tooltipModel.block.makeBlockView() + tooltipView.frame = tooltipModel.calculateFrame( + targeting: convert(bounds, to: nil), + constrainedBy: window.bounds + ) + return TooltipEvent( + info: info, + params: tooltipModel.params, + tooltipView: tooltipView, + tooltipAnchorView: self + ) + } + + private func configureRecognizers() { + guard model.shouldHandleAnyAction else { + tapRecognizer?.isEnabled = false + doubleTapRecognizer?.isEnabled = false + longPressRecognizer?.isEnabled = false + hoverRecognizer?.isEnabled = false + return + } + + if tapRecognizer == nil { + tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + } + tapRecognizer?.cancelsTouchesInView = false + + if model.shouldHandleDoubleTap, doubleTapRecognizer == nil { + let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + doubleTapRecognizer.numberOfTapsRequired = 2 + self.doubleTapRecognizer = doubleTapRecognizer + tapRecognizer?.require(toFail: doubleTapRecognizer) + } + + if model.shouldHandleLongTap, longPressRecognizer == nil { + longPressRecognizer = UILongPressGestureRecognizer( + target: self, + action: #selector(handleLongPress) + ) + } + + if model.shouldHandleHover, hoverRecognizer == nil { + hoverRecognizer = UIHoverGestureRecognizer( + target: self, + action: #selector(handleHover) + ) + } + + tapRecognizer?.isEnabled = model.shouldHandleTap + doubleTapRecognizer?.isEnabled = model.shouldHandleDoubleTap + longPressRecognizer?.isEnabled = model.shouldHandleLongTap + hoverRecognizer?.isEnabled = model.shouldHandleHover + } + + private func checkTouchableArea() { + guard tapRecognizer != nil || doubleTapRecognizer != nil || longPressRecognizer != nil else { + return + } + guard bounds.width > 0 && bounds.height > 0 else { return } + if bounds.width < 44 || bounds.height < 44 { + renderingDelegate?.reportRenderingError( + message: "Touchable view is too small: \(bounds.size), \(model.child)", + isWarning: true, + path: model.path ?? UIElementPath("") + ) + } + } + + @objc private func handleTap(recognizer: UITapGestureRecognizer) { + // Sometimes there are late touches that were performed before layout. + // This breaks UX due to UIView reusing, so just skip them here. + guard bounds.contains(recognizer.location(in: self)) else { return } + captureFocusIfNeeded() + let actions: [UserInterfaceAction]? = if recognizer === doubleTapRecognizer { + model.doubleTapActions?.asArray() + } else if recognizer === tapRecognizer { + model.actions?.asArray() + } else { + [] + } + actions?.perform(sendingFrom: self) + } + private func updateHighlightState(animated: Bool) { updateContentBackgroundColor(animated: animated) @@ -561,35 +598,6 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT } } - func onVisibleBoundsChanged(from: CGRect, to: CGRect) { - if let child = childView as? DelayedVisibilityActionView { - child.visibilityAction = { [weak self] in - - guard let self else { return } - onVisibleBoundsChangedInternal(from: from, to: to) - } - } else { - onVisibleBoundsChangedInternal(from: from, to: to) - } - } - - func makeTooltipEvent(with info: TooltipInfo) -> TooltipEvent? { - guard let tooltipModel = model.tooltips.first(where: { $0.id == info.id }), let window else { - return nil - } - let tooltipView = tooltipModel.block.makeBlockView() - tooltipView.frame = tooltipModel.calculateFrame( - targeting: convert(bounds, to: nil), - constrainedBy: window.bounds - ) - return TooltipEvent( - info: info, - params: tooltipModel.params, - tooltipView: tooltipView, - tooltipAnchorView: self - ) - } - private func updateVoiceOverFocus() { guard hasFocused, isViewOnWindow else { return } @@ -613,11 +621,6 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT } } - deinit { - if model?.tooltips.isEmpty == false { - renderingDelegate?.tooltipAnchorViewRemoved(anchorView: self) - } - } } extension DecoratingView { diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/DetachableAnimationBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/DetachableAnimationBlock+UIViewRenderableBlock.swift index 8f21f2b36..5fdc4bbab 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/DetachableAnimationBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/DetachableAnimationBlock+UIViewRenderableBlock.swift @@ -32,6 +32,14 @@ extension DetachableAnimationBlock { } final class DetachableAnimationBlockView: BlockView, DelayedVisibilityActionView { + var visibilityAction: Action? { + didSet { + if childView != nil, visibilityAction != nil { + applyVisibilityAction() + } + } + } + private var childView: BlockView? { didSet { guard childView !== oldValue else { return } @@ -55,14 +63,6 @@ final class DetachableAnimationBlockView: BlockView, DelayedVisibilityActionView } } - var visibilityAction: Action? { - didSet { - if childView != nil, visibilityAction != nil { - applyVisibilityAction() - } - } - } - private var animatedView: BlockView? private var animationIn: [TransitioningAnimation]? @@ -72,32 +72,12 @@ final class DetachableAnimationBlockView: BlockView, DelayedVisibilityActionView private var child: Block? private var isFirstChildLayout: Bool = true - var effectiveBackgroundColor: UIColor? { childView?.effectiveBackgroundColor } - - func configure( - child: Block, - animationIn: [TransitioningAnimation]?, - animationOut: [TransitioningAnimation]?, - animationChange: ChangeBoundsTransition?, - observer: ElementStateObserver?, - overscrollDelegate: ScrollDelegate?, - renderingDelegate: RenderingDelegate? - ) { - self.child = child - childView = child.reuse( - childView, - observer: observer, - overscrollDelegate: overscrollDelegate, - renderingDelegate: renderingDelegate, - superview: self - ) - self.animationIn = animationIn - self.animationOut = animationOut - self.animationChange = animationChange - - setNeedsLayout() + public var hasAnimationIn: Bool { + animationIn != nil } + var effectiveBackgroundColor: UIColor? { childView?.effectiveBackgroundColor } + override func layoutSubviews() { super.layoutSubviews() guard isFirstChildLayout || animationChange == nil else { @@ -110,10 +90,6 @@ final class DetachableAnimationBlockView: BlockView, DelayedVisibilityActionView } } - public var hasAnimationIn: Bool { - animationIn != nil - } - public func convertFrame(to container: UIView) -> CGRect { convert(bounds, to: container) } @@ -213,6 +189,30 @@ final class DetachableAnimationBlockView: BlockView, DelayedVisibilityActionView } } + func configure( + child: Block, + animationIn: [TransitioningAnimation]?, + animationOut: [TransitioningAnimation]?, + animationChange: ChangeBoundsTransition?, + observer: ElementStateObserver?, + overscrollDelegate: ScrollDelegate?, + renderingDelegate: RenderingDelegate? + ) { + self.child = child + childView = child.reuse( + childView, + observer: observer, + overscrollDelegate: overscrollDelegate, + renderingDelegate: renderingDelegate, + superview: self + ) + self.animationIn = animationIn + self.animationOut = animationOut + self.animationChange = animationChange + + setNeedsLayout() + } + func cancelAnimations() { queuedAnimation?.cancel() queuedAnimation = nil diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/GestureBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/GestureBlock+UIViewRenderableBlock.swift index bad4ae6d5..8254f0067 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/GestureBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/GestureBlock+UIViewRenderableBlock.swift @@ -57,6 +57,11 @@ private final class GestureView: BlockView { fatalError("init(coder:) has not been implemented") } + override func layoutSubviews() { + super.layoutSubviews() + self.childView.frame = bounds + } + func configure( child: Block, observer: ElementStateObserver?, @@ -82,11 +87,6 @@ private final class GestureView: BlockView { setNeedsLayout() } - override func layoutSubviews() { - super.layoutSubviews() - self.childView.frame = bounds - } - private func setupGestureRecognizers() { swipeRecognizers = Array.build { if upActions != nil { diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/GridBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/GridBlock+UIViewRenderableBlock.swift index 63c4718ec..7e284228d 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/GridBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/GridBlock+UIViewRenderableBlock.swift @@ -44,42 +44,15 @@ private final class GridView: BlockView, VisibleBoundsTrackingContainer { let source: Variable } - private var blockViews: [BlockView] = [] - var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { blockViews } - var effectiveBackgroundColor: UIColor? { backgroundColor } var layoutReporter: LayoutReporter? + private var blockViews: [BlockView] = [] private weak var observer: ElementStateObserver? private var modelAndLastLayoutSize: (model: Model?, lastLayoutSize: CGSize?) private var preventLayout = false - func setModel( - _ model: Model, - withObserver observer: ElementStateObserver?, - overscrollDelegate: ScrollDelegate?, - renderingDelegate: RenderingDelegate? - ) { - guard model != modelAndLastLayoutSize.model || observer !== self.observer else { - return - } - - modelAndLastLayoutSize = (model, nil) - self.observer = observer - - // Configuring views may lead to unpredictable side effects, - // including view hierarchy layout. - preventLayout = true - blockViews = blockViews.reused( - with: model.items.map(\.contents), - attachTo: self, - observer: observer, - overscrollDelegate: overscrollDelegate, - renderingDelegate: renderingDelegate - ) - preventLayout = false - - setNeedsLayout() - } + var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { blockViews } + var effectiveBackgroundColor: UIColor? { backgroundColor } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) @@ -110,5 +83,34 @@ private final class GridView: BlockView, VisibleBoundsTrackingContainer { } layoutReporter?.didLayoutSubviews() } + + func setModel( + _ model: Model, + withObserver observer: ElementStateObserver?, + overscrollDelegate: ScrollDelegate?, + renderingDelegate: RenderingDelegate? + ) { + guard model != modelAndLastLayoutSize.model || observer !== self.observer else { + return + } + + modelAndLastLayoutSize = (model, nil) + self.observer = observer + + // Configuring views may lead to unpredictable side effects, + // including view hierarchy layout. + preventLayout = true + blockViews = blockViews.reused( + with: model.items.map(\.contents), + attachTo: self, + observer: observer, + overscrollDelegate: overscrollDelegate, + renderingDelegate: renderingDelegate + ) + preventLayout = false + + setNeedsLayout() + } + } #endif diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/LayeredBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/LayeredBlock+UIViewRenderableBlock.swift index 0cec85d40..b2175fd99 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/LayeredBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/LayeredBlock+UIViewRenderableBlock.swift @@ -36,6 +36,7 @@ private final class LayeredBlockView: BlockView, VisibleBoundsTrackingContainer struct Model: ReferenceEquatable { let block: LayeredBlock let layout: LayeredBlock.Layout? + var source: Variable { Variable { [weak block] in block } } @@ -53,6 +54,33 @@ private final class LayeredBlockView: BlockView, VisibleBoundsTrackingContainer var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { blockViews } var effectiveBackgroundColor: UIColor? { blockViews.first?.effectiveBackgroundColor } + override func layoutSubviews() { + guard !preventLayout else { return } + + super.layoutSubviews() + + guard let model = modelAndLastLayoutSize.model, + modelAndLastLayoutSize.lastLayoutSize != bounds.size else { + return + } + + let childrenFrames = model.layout ?? model.block.makeChildrenFrames(size: bounds.size) + + for (view, frame) in zip(blockViews, childrenFrames) { + let currentTransform = view.transform + view.transform = .identity + view.frame = frame + view.transform = currentTransform + } + + modelAndLastLayoutSize = (model: model, lastLayoutSize: bounds.size) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + return result == self ? nil : result + } + func configure( model: Model, observer: ElementStateObserver?, @@ -81,31 +109,5 @@ private final class LayeredBlockView: BlockView, VisibleBoundsTrackingContainer setNeedsLayout() } - override func layoutSubviews() { - guard !preventLayout else { return } - - super.layoutSubviews() - - guard let model = modelAndLastLayoutSize.model, - modelAndLastLayoutSize.lastLayoutSize != bounds.size else { - return - } - - let childrenFrames = model.layout ?? model.block.makeChildrenFrames(size: bounds.size) - - for (view, frame) in zip(blockViews, childrenFrames) { - let currentTransform = view.transform - view.transform = .identity - view.frame = frame - view.transform = currentTransform - } - - modelAndLastLayoutSize = (model: model, lastLayoutSize: bounds.size) - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let result = super.hitTest(point, with: event) - return result == self ? nil : result - } } #endif diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/MaskedBlock+UIViewRenderable.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/MaskedBlock+UIViewRenderable.swift index 72dc8fd0a..f4f975647 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/MaskedBlock+UIViewRenderable.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/MaskedBlock+UIViewRenderable.swift @@ -54,6 +54,15 @@ private final class MaskedBlockView: BlockView, VisibleBoundsTrackingContainer { var effectiveBackgroundColor: UIColor? { maskedBlockView?.effectiveBackgroundColor } + override func layoutSubviews() { + super.layoutSubviews() + + maskedBlockView?.frame = bounds + maskBlockView?.frame = bounds + + maskedBlockView?.mask = maskBlockView + } + func configure( model: Model, observer: ElementStateObserver?, @@ -76,13 +85,5 @@ private final class MaskedBlockView: BlockView, VisibleBoundsTrackingContainer { setNeedsLayout() } - override func layoutSubviews() { - super.layoutSubviews() - - maskedBlockView?.frame = bounds - maskBlockView?.frame = bounds - - maskedBlockView?.mask = maskBlockView - } } #endif diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/PageControlBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/PageControlBlock+UIViewRenderableBlock.swift index 55f04cf1d..d5fb58c62 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/PageControlBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/PageControlBlock+UIViewRenderableBlock.swift @@ -33,8 +33,6 @@ private struct Model: ReferenceEquatable { } private final class PageControlBlockView: BlockView, VisibleBoundsTrackingLeaf { - private let indicatorView = PageIndicatorView() - var model: Model! { didSet { guard model != oldValue else { return } @@ -49,6 +47,8 @@ private final class PageControlBlockView: BlockView, VisibleBoundsTrackingLeaf { } } + private let indicatorView = PageIndicatorView() + var effectiveBackgroundColor: UIColor? { backgroundColor } init() { diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/PagerBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/PagerBlock+UIViewRenderableBlock.swift index ad52cde00..1ad62c8f6 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/PagerBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/PagerBlock+UIViewRenderableBlock.swift @@ -43,6 +43,22 @@ private final class PagerView: BlockView { var effectiveBackgroundColor: UIColor? { backgroundColor } + override init(frame: CGRect) { + galleryView = GalleryView(frame: frame) + super.init(frame: frame) + addSubview(galleryView) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + galleryView.frame = bounds + } + func configure( model: GalleryViewModel, selectedActions: [[UserInterfaceAction]], @@ -112,21 +128,6 @@ private final class PagerView: BlockView { } } - override init(frame: CGRect) { - galleryView = GalleryView(frame: frame) - super.init(frame: frame) - addSubview(galleryView) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - galleryView.frame = bounds - } } extension PagerView: ElementStateObserver { diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/PinchToZoomBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/PinchToZoomBlock+UIViewRenderableBlock.swift index 98c4f8aaa..9e285e8c5 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/PinchToZoomBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/PinchToZoomBlock+UIViewRenderableBlock.swift @@ -37,12 +37,12 @@ private final class PinchToZoomView: BlockView { private var translationToIgnore = CGPoint.zero private var originTranslation = CGPoint.zero + var effectiveBackgroundColor: UIColor? { childView.backgroundColor } + private var isZooming: Bool { childView.superview == shadeView } - var effectiveBackgroundColor: UIColor? { childView.backgroundColor } - init() { super.init(frame: .zero) @@ -68,6 +68,21 @@ private final class PinchToZoomView: BlockView { fatalError("init(coder:) has not been implemented") } + override func layoutSubviews() { + super.layoutSubviews() + + let currentTransform = childView.transform + childView.transform = .identity + if isZooming { + childView.frame = convert(bounds, to: overlayView) + shadeView.frame = overlayView.frame + } else { + childView.frame = bounds + shadeView.frame = .zero + } + childView.transform = currentTransform + } + func configure( child: Block, observer: ElementStateObserver?, @@ -86,21 +101,6 @@ private final class PinchToZoomView: BlockView { setNeedsLayout() } - override func layoutSubviews() { - super.layoutSubviews() - - let currentTransform = childView.transform - childView.transform = .identity - if isZooming { - childView.frame = convert(bounds, to: overlayView) - shadeView.frame = overlayView.frame - } else { - childView.frame = bounds - shadeView.frame = .zero - } - childView.transform = currentTransform - } - private func updateTransform() { let scaleDelta = scale - 1 childView.transform = CGAffineTransform(scaleX: scale, y: scale) diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift index 0405f441a..bedad0604 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift @@ -29,9 +29,10 @@ extension StateBlock { } private final class SubviewStorage: RenderingDelegate { - private typealias ViewWithID = (id: BlockViewID, view: DetachableAnimationBlockView) typealias FrameWithID = (id: BlockViewID, frame: CGRect) + private typealias ViewWithID = (id: BlockViewID, view: DetachableAnimationBlockView) + private let wrappedRenderingDelegate: RenderingDelegate? private let ids: Set // views are ordered, so can't use Dictionary here @@ -145,6 +146,17 @@ private final class StateBlockView: BlockView { var effectiveBackgroundColor: UIColor? { childView?.effectiveBackgroundColor } + override func layoutSubviews() { + super.layoutSubviews() + + childView?.setNonTransformedFrame(bounds) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + return result === self ? nil : result + } + func configure( child: Block, ids: Set, @@ -224,16 +236,6 @@ private final class StateBlockView: BlockView { } } - override func layoutSubviews() { - super.layoutSubviews() - - childView?.setNonTransformedFrame(bounds) - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let result = super.hitTest(point, with: event) - return result === self ? nil : result - } } extension StateBlockView: VisibleBoundsTrackingContainer { diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/SwipeContainerBlock+UIViewRenderable.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/SwipeContainerBlock+UIViewRenderable.swift index 3c94f53a4..75df5ee4d 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/SwipeContainerBlock+UIViewRenderable.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/SwipeContainerBlock+UIViewRenderable.swift @@ -48,12 +48,12 @@ private final class SwipeContainerBlockView: BlockView, VisibleBoundsTrackingCon } } - var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { view.asArray() } - var effectiveBackgroundColor: UIColor? { view?.effectiveBackgroundColor } - private weak var observer: ElementStateObserver? private weak var renderingDelegate: RenderingDelegate? + var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { view.asArray() } + var effectiveBackgroundColor: UIColor? { view?.effectiveBackgroundColor } + override init(frame: CGRect) { super.init(frame: frame) addSubview(swipeContainerView) @@ -64,6 +64,11 @@ private final class SwipeContainerBlockView: BlockView, VisibleBoundsTrackingCon fatalError("init(coder:) has not been implemented") } + override func layoutSubviews() { + super.layoutSubviews() + swipeContainerView.frame = CGRect(origin: .zero, size: frame.size) + } + func configure( model: Model, observer: ElementStateObserver?, @@ -95,10 +100,6 @@ private final class SwipeContainerBlockView: BlockView, VisibleBoundsTrackingCon setNeedsLayout() } - override func layoutSubviews() { - super.layoutSubviews() - swipeContainerView.frame = CGRect(origin: .zero, size: frame.size) - } } private final class SwipeContainerView: UIScrollView, UIScrollViewDelegate { @@ -138,6 +139,23 @@ private final class SwipeContainerView: UIScrollView, UIScrollViewDelegate { fatalError("init(coder:) has not been implemented") } + override func layoutSubviews() { + super.layoutSubviews() + contentSize = CGSize(width: bounds.width * 3, height: bounds.height) + layoutChild() + } + + func scrollViewDidEndDecelerating(_: UIScrollView) { + switch bounds.minX { + case let x where x < bounds.width * 0.5: + state = .left + case let x where x > bounds.width * 1.5: + state = .right + default: + state = .normal + } + } + private func updateContentOffset() { let x: CGFloat = switch state { case .left: @@ -162,22 +180,6 @@ private final class SwipeContainerView: UIScrollView, UIScrollViewDelegate { } } - override func layoutSubviews() { - super.layoutSubviews() - contentSize = CGSize(width: bounds.width * 3, height: bounds.height) - layoutChild() - } - - func scrollViewDidEndDecelerating(_: UIScrollView) { - switch bounds.minX { - case let x where x < bounds.width * 0.5: - state = .left - case let x where x > bounds.width * 1.5: - state = .right - default: - state = .normal - } - } } private let closeDuration: TimeInterval = 0.2 diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/SwitchBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/SwitchBlock+UIViewRenderableBlock.swift index 1cda04d03..35d2613fe 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/SwitchBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/SwitchBlock+UIViewRenderableBlock.swift @@ -44,10 +44,10 @@ private final class SwitchBlockView: BlockView, VisibleBoundsTrackingLeaf { } } - var effectiveBackgroundColor: UIColor? { backgroundColor } - private var aSwitch: UISwitch + var effectiveBackgroundColor: UIColor? { backgroundColor } + init() { aSwitch = UISwitch() aSwitch.tintColor = UIColor(white: 0, alpha: 0.1) diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/SwitchableContainerBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/SwitchableContainerBlock+UIViewRenderableBlock.swift index 241e9a654..6ce9a4e7a 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/SwitchableContainerBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/SwitchableContainerBlock+UIViewRenderableBlock.swift @@ -75,18 +75,19 @@ private final class SwitchableContainerView: BlockView, VisibleBoundsTrackingCon } } - var effectiveBackgroundColor: UIColor? { selectorView.backgroundColor } - - private var lastLayoutSize = CGRect.zero - var observer: ElementStateObserver? weak var overscrollDelegate: ScrollDelegate? weak var renderingDelegate: RenderingDelegate? + private var lastLayoutSize = CGRect.zero + private let selectorView = SegmentedSelectorView() private var blockViews: [BlockView] = [] + public var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { blockViews } + var effectiveBackgroundColor: UIColor? { selectorView.backgroundColor } + init() { super.init(frame: .zero) diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextBlock+UIViewRenderableBlock.swift index 772ce6a62..46df81d6d 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextBlock+UIViewRenderableBlock.swift @@ -45,9 +45,6 @@ extension TextBlock { private final class TextBlockContainer: BlockView, VisibleBoundsTrackingLeaf { var layoutReporter: LayoutReporter? - private let textBlockView = TextBlockView() - private var gradientContainerView: GradientContainerView? - var textGradientModel: TextBlock.GradientModel? { didSet { guard textGradientModel == nil || textGradientModel != oldValue else { @@ -59,19 +56,6 @@ private final class TextBlockContainer: BlockView, VisibleBoundsTrackingLeaf { } } - private var currentView: UIView? { - didSet { - guard currentView !== oldValue else { - return - } - oldValue?.removeFromSuperview() - if let currentView { - addSubview(currentView) - } - setNeedsLayout() - } - } - var model: TextBlockView.Model! { didSet { guard model == nil || model != oldValue else { @@ -84,6 +68,22 @@ private final class TextBlockContainer: BlockView, VisibleBoundsTrackingLeaf { } } + private let textBlockView = TextBlockView() + private var gradientContainerView: GradientContainerView? + + private var currentView: UIView? { + didSet { + guard currentView !== oldValue else { + return + } + oldValue?.removeFromSuperview() + if let currentView { + addSubview(currentView) + } + setNeedsLayout() + } + } + var effectiveBackgroundColor: UIColor? { backgroundColor } override init(frame: CGRect) { @@ -121,9 +121,10 @@ private final class TextBlockContainer: BlockView, VisibleBoundsTrackingLeaf { } private class GradientContainerView: UIView { + let gradientView: UIView + private let model: TextBlock.GradientModel private let rangedTextWithColorTextBlockView: TextBlockView - let gradientView: UIView init?(model: TextBlock.GradientModel?, mask: UIView) { guard let model else { @@ -175,6 +176,36 @@ private final class TextBlockView: UIView { let isFocused: Bool } + override var canBecomeFirstResponder: Bool { + model.canSelect + } + + var model: Model! { + didSet { + guard model == nil || model != oldValue else { + return + } + + precondition(model.images.count == model.attachments.count) + + if model.images == oldValue?.images { + imageRequests = model.images.indices + .filter { model.attachments[$0].image == nil } + .compactMap(requestImage) + } else { + imagesReferences = Array(repeating: nil, times: UInt(model.images.count)) + imageRequests = model.images.indices.compactMap(requestImage) + } + + isUserInteractionEnabled = model.isUserInteractionEnabled + + configureRecognizers() + selection = nil + + setNeedsDisplay() + } + } + private var lastElementIndex: Int? private var selection: TextSelection? { @@ -230,32 +261,6 @@ private final class TextBlockView: UIView { private var imagesReferences: [UIImage?] = [] - var model: Model! { - didSet { - guard model == nil || model != oldValue else { - return - } - - precondition(model.images.count == model.attachments.count) - - if model.images == oldValue?.images { - imageRequests = model.images.indices - .filter { model.attachments[$0].image == nil } - .compactMap(requestImage) - } else { - imagesReferences = Array(repeating: nil, times: UInt(model.images.count)) - imageRequests = model.images.indices.compactMap(requestImage) - } - - isUserInteractionEnabled = model.isUserInteractionEnabled - - configureRecognizers() - selection = nil - - setNeedsDisplay() - } - } - override init(frame: CGRect) { super.init(frame: frame) @@ -268,6 +273,88 @@ private final class TextBlockView: UIView { @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } + override func draw(_ rect: CGRect) { + guard let model else { return } + self.textLayout = model.text.drawAndGetLayout( + inContext: UIGraphicsGetCurrentContext()!, + verticalPosition: model.verticalPosition, + rect: rect, + textInsets: model.additionalTextInsets, + truncationToken: model.truncationToken, + actionKey: ActionsAttribute.Key, + backgroundKey: BackgroundAttribute.Key, + borderKey: BorderAttribute.Key, + rangeVerticalAlignmentKey: RangeVerticalAlignmentAttribute.Key, + selectedRange: selection?.range + ) + + self.selection?.draw(rect) + } + + override func point(inside point: CGPoint, with _: UIEvent?) -> Bool { + guard let textLayout else { + return false + } + return model.canSelect && bounds.contains(point) + || textLayout.runsWithAction.contains { $0.rect.contains(point) } + } + + override func resignFirstResponder() -> Bool { + let result = super.resignFirstResponder() + NotificationCenter.default.removeObserver( + self, + name: UIMenuController.willHideMenuNotification, + object: nil + ) + return result + } + + override func canPerformAction(_ action: Selector, withSender _: Any?) -> Bool { + action == #selector(copy(_:)) + } + + override func copy(_: Any?) { + if let selection { + UIPasteboard.general.string = selection.getSelectedText() + } + + selection = nil + setNeedsDisplay() + } + + func handleSelectionTapBegan(_ gesture: UIGestureRecognizer) { + becomeFirstResponder() + + guard let textLayout else { return } + + UIMenuController.shared.hideMenu(animated: true) + let textModel = TextSelection.TextModel(layout: textLayout, text: model.text) + let point = TextSelection.Point( + point: gesture.location(in: gesture.view), + viewBounds: bounds + ) + + selection = TextSelection(textModel: textModel, point: point) { [weak self] in + + guard let delayedSelectionTapGesture = self?.delayedSelectionTapGesture else { return } + self?.handleSelectionTapEnded(delayedSelectionTapGesture) + self?.delayedSelectionTapGesture = nil + } + } + + func handleSelectionTapEnded(_: UIGestureRecognizer) { + guard let selectionRect = selection?.rect else { + return + } + UIMenuController.shared.presentMenu(from: self, in: selectionRect) + NotificationCenter.default.addObserver( + self, + selector: #selector(resetSelecting), + name: UIMenuController.willHideMenuNotification, + object: nil + ) + } + private func configureRecognizers() { if model.canSelect { let doubleTapSelectionRecognizer = UITapGestureRecognizer( @@ -310,32 +397,6 @@ private final class TextBlockView: UIView { } } - override func draw(_ rect: CGRect) { - guard let model else { return } - self.textLayout = model.text.drawAndGetLayout( - inContext: UIGraphicsGetCurrentContext()!, - verticalPosition: model.verticalPosition, - rect: rect, - textInsets: model.additionalTextInsets, - truncationToken: model.truncationToken, - actionKey: ActionsAttribute.Key, - backgroundKey: BackgroundAttribute.Key, - borderKey: BorderAttribute.Key, - rangeVerticalAlignmentKey: RangeVerticalAlignmentAttribute.Key, - selectedRange: selection?.range - ) - - self.selection?.draw(rect) - } - - override func point(inside point: CGPoint, with _: UIEvent?) -> Bool { - guard let textLayout else { - return false - } - return model.canSelect && bounds.contains(point) - || textLayout.runsWithAction.contains { $0.rect.contains(point) } - } - @objc private func handleHideSelectionMenuTap(_: UITapGestureRecognizer) { UIMenuController.shared.hideMenu(animated: true) } @@ -373,39 +434,6 @@ private final class TextBlockView: UIView { } } - func handleSelectionTapBegan(_ gesture: UIGestureRecognizer) { - becomeFirstResponder() - - guard let textLayout else { return } - - UIMenuController.shared.hideMenu(animated: true) - let textModel = TextSelection.TextModel(layout: textLayout, text: model.text) - let point = TextSelection.Point( - point: gesture.location(in: gesture.view), - viewBounds: bounds - ) - - selection = TextSelection(textModel: textModel, point: point) { [weak self] in - - guard let delayedSelectionTapGesture = self?.delayedSelectionTapGesture else { return } - self?.handleSelectionTapEnded(delayedSelectionTapGesture) - self?.delayedSelectionTapGesture = nil - } - } - - func handleSelectionTapEnded(_: UIGestureRecognizer) { - guard let selectionRect = selection?.rect else { - return - } - UIMenuController.shared.presentMenu(from: self, in: selectionRect) - NotificationCenter.default.addObserver( - self, - selector: #selector(resetSelecting), - name: UIMenuController.willHideMenuNotification, - object: nil - ) - } - @objc private func handleSelectionPan(_ gesture: UIPanGestureRecognizer) { let point = gesture.location(in: gesture.view) @@ -439,37 +467,10 @@ private final class TextBlockView: UIView { setNeedsDisplay() } - override var canBecomeFirstResponder: Bool { - model.canSelect - } - - override func resignFirstResponder() -> Bool { - let result = super.resignFirstResponder() - NotificationCenter.default.removeObserver( - self, - name: UIMenuController.willHideMenuNotification, - object: nil - ) - return result - } - @objc private func resetSelecting() { selection = nil } - override func canPerformAction(_ action: Selector, withSender _: Any?) -> Bool { - action == #selector(copy(_:)) - } - - override func copy(_: Any?) { - if let selection { - UIPasteboard.general.string = selection.getSelectedText() - } - - selection = nil - setNeedsDisplay() - } - private func requestImage(at index: Int) -> Cancellable? { let modelImage = model.images[index] diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextFieldBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextFieldBlock+UIViewRenderableBlock.swift index d549cee8c..42a6cfcb7 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextFieldBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextFieldBlock+UIViewRenderableBlock.swift @@ -36,27 +36,22 @@ extension TextFieldBlock { } private final class TextFieldBlockView: BlockView, VisibleBoundsTrackingLeaf { - var text: NSAttributedString? { - get { - textField.attributedText - } - set { - updateSecureTextStyle(from: newValue) - textField.setTextPreservingPosition(newValue) - updatePlaceholdersVisibility() - } + struct Model: Equatable { + let placeholders: TextFieldBlock.Placeholders? + let text: NSAttributedString? + let size: CGSize + let headerIsHidden: Bool } - var textLabel: NSAttributedString? { - switch placeholders { - case let .separate(placeholders): - let attributes = headerIsHidden ? placeholders.field.attributes : placeholders - .headerAttributes - let typo = Typo(size: attributes.size, weight: placeholders.fontWeight) - .with(height: attributes.height).with(color: attributes.color) - return placeholders.text.with(typo: typo) - case .fieldOnly, .none: - return nil + struct Layout { + let labelFrame: CGRect + let fieldFrame: CGRect + } + + override var backgroundColor: UIColor? { + didSet { + placeholderLabel.backgroundColor = backgroundColor + textField.backgroundColor = backgroundColor } } @@ -73,75 +68,10 @@ private final class TextFieldBlockView: BlockView, VisibleBoundsTrackingLeaf { } } - var keyboardAppearance: TextFieldBlock.KeyboardAppearance { - get { textField.keyboardAppearance.blockValue } - set { textField.keyboardAppearance = newValue.uiValue } - } - var updateAction: UserInterfaceAction? var returnButtonPressedAction: UserInterfaceAction? var didBeginEditingAction: UserInterfaceAction? - private var lastModelAndLayout = (model: Model.empty, layout: Layout.empty) - - private func updateSecureTextStyle(from string: NSAttributedString?) { - let color: SystemColor = string.attribute(key: .foregroundColor, defaultValue: .black) - textField.defaultTextAttributes[.foregroundColor] = color - } - - override var backgroundColor: UIColor? { - didSet { - placeholderLabel.backgroundColor = backgroundColor - textField.backgroundColor = backgroundColor - } - } - - var effectiveBackgroundColor: UIColor? { backgroundColor } - - var autocapitalizationType: UITextAutocapitalizationType { - get { textField.autocapitalizationType } - set { textField.autocapitalizationType = newValue } - } - - var autocorrectionType: UITextAutocorrectionType { - get { textField.autocorrectionType } - set { textField.autocorrectionType = newValue } - } - - var isSecureTextEntry: Bool { - get { textField.isSecureTextEntry } - set { textField.isSecureTextEntry = newValue } - } - - var keyboardType: UIKeyboardType { - get { textField.keyboardType } - set { textField.keyboardType = newValue } - } - - func updateInteraction(_ interaction: TextFieldBlock.Interaction) { - postponedUpdateOfInteraction = nil - switch interaction { - case .unchanged: - break - case let .enabled(isFirstResponder): - textField.isUserInteractionEnabled = true - let setFirstResponder = { [weak self] in - if isFirstResponder { - _ = self?.textField.becomeFirstResponder() - } else { - _ = self?.textField.resignFirstResponder() - } - } - guard textField.window != nil else { - self.postponedUpdateOfInteraction = setFirstResponder - break - } - setFirstResponder() - case .disabled: - textField.isUserInteractionEnabled = false - } - } - var accessibility: AccessibilityElement? { didSet { textField.applyAccessibilityFromScratch(accessibility) @@ -212,11 +142,64 @@ private final class TextFieldBlockView: BlockView, VisibleBoundsTrackingLeaf { var maxNumberOfLines = 1 + private var lastModelAndLayout = (model: Model.empty, layout: Layout.empty) + private let placeholderLabel = Label() private let animationLabel = Label() private let textField = UITextField() private var postponedUpdateOfInteraction: Action? + var text: NSAttributedString? { + get { + textField.attributedText + } + set { + updateSecureTextStyle(from: newValue) + textField.setTextPreservingPosition(newValue) + updatePlaceholdersVisibility() + } + } + + var textLabel: NSAttributedString? { + switch placeholders { + case let .separate(placeholders): + let attributes = headerIsHidden ? placeholders.field.attributes : placeholders + .headerAttributes + let typo = Typo(size: attributes.size, weight: placeholders.fontWeight) + .with(height: attributes.height).with(color: attributes.color) + return placeholders.text.with(typo: typo) + case .fieldOnly, .none: + return nil + } + } + + var keyboardAppearance: TextFieldBlock.KeyboardAppearance { + get { textField.keyboardAppearance.blockValue } + set { textField.keyboardAppearance = newValue.uiValue } + } + + var effectiveBackgroundColor: UIColor? { backgroundColor } + + var autocapitalizationType: UITextAutocapitalizationType { + get { textField.autocapitalizationType } + set { textField.autocapitalizationType = newValue } + } + + var autocorrectionType: UITextAutocorrectionType { + get { textField.autocorrectionType } + set { textField.autocorrectionType = newValue } + } + + var isSecureTextEntry: Bool { + get { textField.isSecureTextEntry } + set { textField.isSecureTextEntry = newValue } + } + + var keyboardType: UIKeyboardType { + get { textField.keyboardType } + set { textField.keyboardType = newValue } + } + override init(frame: CGRect) { super.init(frame: frame) @@ -249,6 +232,35 @@ private final class TextFieldBlockView: BlockView, VisibleBoundsTrackingLeaf { } } + func updateInteraction(_ interaction: TextFieldBlock.Interaction) { + postponedUpdateOfInteraction = nil + switch interaction { + case .unchanged: + break + case let .enabled(isFirstResponder): + textField.isUserInteractionEnabled = true + let setFirstResponder = { [weak self] in + if isFirstResponder { + _ = self?.textField.becomeFirstResponder() + } else { + _ = self?.textField.resignFirstResponder() + } + } + guard textField.window != nil else { + self.postponedUpdateOfInteraction = setFirstResponder + break + } + setFirstResponder() + case .disabled: + textField.isUserInteractionEnabled = false + } + } + + private func updateSecureTextStyle(from string: NSAttributedString?) { + let color: SystemColor = string.attribute(key: .foregroundColor, defaultValue: .black) + textField.defaultTextAttributes[.foregroundColor] = color + } + private func layoutTextField(animated: Bool) { let currentModel = Model( placeholders: placeholders, @@ -445,17 +457,6 @@ private final class TextFieldBlockView: BlockView, VisibleBoundsTrackingLeaf { } } - struct Model: Equatable { - let placeholders: TextFieldBlock.Placeholders? - let text: NSAttributedString? - let size: CGSize - let headerIsHidden: Bool - } - - struct Layout { - let labelFrame: CGRect - let fieldFrame: CGRect - } } extension TextFieldBlockView: UITextFieldDelegate { diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextInputBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextInputBlock+UIViewRenderableBlock.swift index be9338d78..975bbf6a8 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextInputBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextInputBlock+UIViewRenderableBlock.swift @@ -56,6 +56,10 @@ extension TextInputBlock { } private final class TextInputBlockView: BlockView, VisibleBoundsTrackingLeaf { + var paddings: EdgeInsets = .zero + + var layoutReporter: LayoutReporter? + private let multiLineInput = PatchedUITextView() private let singleLineInput = PatchedUITextField() private let selectionView = UIPickerView() @@ -85,10 +89,14 @@ private final class TextInputBlockView: BlockView, VisibleBoundsTrackingLeaf { private var keyboardHeight: CGFloat? private var maxLength: Int? - var paddings: EdgeInsets = .zero - var effectiveBackgroundColor: UIColor? { backgroundColor } - var layoutReporter: LayoutReporter? + + private var currentText: String { + guard singleLineInput.isHidden else { + return singleLineInput.attributedText?.string ?? "" + } + return multiLineInput.attributedText.string + } override init(frame: CGRect) { textValue = .zero @@ -148,37 +156,28 @@ private final class TextInputBlockView: BlockView, VisibleBoundsTrackingLeaf { layoutReporter?.didLayoutSubviews() } + override func didMoveToWindow() { + if window != nil { + startKeyboardTracking() + if isInputFocused { + // The didMoveToWindow method in UIView is not a direct replacement for the viewDidAppear + // method in UIViewController. + // This causes the focusTextInput method to be called too early, before the view is actually + // in the view hierarchy. + // As a result, the keyboard does not appear as expected. + DispatchQueue.main.async { + self.focusTextInput() + } + } + } else { + stopAllTracking() + } + } + @objc func onTapGesture(sender _: UITapGestureRecognizer) { focusTextInput() } - private func focusTextInput() { - if singleLineInput.isHidden { - multiLineInput.becomeFirstResponder() - } else { - singleLineInput.becomeFirstResponder() - } - } - - private func clearFocus() { - if singleLineInput.isHidden { - if multiLineInput.isFirstResponder { - multiLineInput.resignFirstResponder() - } - } else { - if singleLineInput.isFirstResponder { - singleLineInput.resignFirstResponder() - } - } - } - - private var currentText: String { - guard singleLineInput.isHidden else { - return singleLineInput.attributedText?.string ?? "" - } - return multiLineInput.attributedText.string - } - func setLayoutDirection(_ layoutDirection: UserInterfaceLayoutDirection) { if layoutDirection != self.layoutDirection { self.layoutDirection = layoutDirection @@ -201,11 +200,6 @@ private final class TextInputBlockView: BlockView, VisibleBoundsTrackingLeaf { } } - private func setKeyboardType(_ type: TextInputBlock.InputType.KeyboardType) { - multiLineInput.keyboardType = type.uiType - singleLineInput.keyboardType = type.uiType - } - func setInputAccessoryView(_ accessoryView: ViewType?) { multiLineInput.inputAccessoryView = accessoryView singleLineInput.inputAccessoryView = accessoryView @@ -374,6 +368,31 @@ private final class TextInputBlockView: BlockView, VisibleBoundsTrackingLeaf { maxLength = value } + private func focusTextInput() { + if singleLineInput.isHidden { + multiLineInput.becomeFirstResponder() + } else { + singleLineInput.becomeFirstResponder() + } + } + + private func clearFocus() { + if singleLineInput.isHidden { + if multiLineInput.isFirstResponder { + multiLineInput.resignFirstResponder() + } + } else { + if singleLineInput.isFirstResponder { + singleLineInput.resignFirstResponder() + } + } + } + + private func setKeyboardType(_ type: TextInputBlock.InputType.KeyboardType) { + multiLineInput.keyboardType = type.uiType + singleLineInput.keyboardType = type.uiType + } + private func updateHintVisibility() { hintView.isHidden = !currentText.isEmpty } @@ -466,24 +485,6 @@ private final class TextInputBlockView: BlockView, VisibleBoundsTrackingLeaf { }.dispose(in: disposePool) } - override func didMoveToWindow() { - if window != nil { - startKeyboardTracking() - if isInputFocused { - // The didMoveToWindow method in UIView is not a direct replacement for the viewDidAppear - // method in UIViewController. - // This causes the focusTextInput method to be called too early, before the view is actually - // in the view hierarchy. - // As a result, the keyboard does not appear as expected. - DispatchQueue.main.async { - self.focusTextInput() - } - } - } else { - stopAllTracking() - } - } - private func startKeyboardTracking() { let notificationCenter = NotificationCenter.default notificationCenter.addObserver( diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextSelection.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextSelection.swift index 11b054434..a58814c8b 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextSelection.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/TextSelection.swift @@ -3,7 +3,53 @@ import UIKit import VGSL final class TextSelection { - private var activePointer: ActivePointer? + struct Point { + let point: CGPoint + let viewBounds: CGRect + + var x: CGFloat { + point.x + } + + var y: CGFloat { + point.y + } + + var verticallyInverted: CGPoint { + point + .applying(CGAffineTransform(translationX: 0, y: viewBounds.height).scaledBy(x: 1, y: -1)) + } + } + + struct TextModel { + let layout: AttributedStringLayout + let text: NSAttributedString + + var maxSymbolIndex: Int { + layout.lines + .compactMap { !$0.isTruncated ? $0.range.upperBound : -1 }.max() ?? Int.max + } + + func maxLineSymbolIndex(_ pointerIndex: Int) -> Int { + getLineRange(pointerIndex).upperBound + } + + func minLineSymbolIndex(_ pointerIndex: Int) -> Int { + getLineRange(pointerIndex).lowerBound + } + + private func getLineRange(_ index: Int) -> Range { + for line in layout.lines { + if line.range.contains(index), let range = Range(line.range) { + return range + } + } + + return 0.. private(set) var rect: CGRect? { didSet { @@ -11,6 +57,8 @@ final class TextSelection { } } + private var activePointer: ActivePointer? + private let textModel: TextModel private let rectSetAction: () -> Void @@ -28,47 +76,6 @@ final class TextSelection { makeRange(elementIndex) } - private func makeRange(_ elementIndex: Int) { - let prefix = textModel.text.string.prefix(elementIndex) - let suffix = textModel.text.string.suffix(from: prefix.endIndex) - let trailing = elementIndex + suffix.distance( - from: suffix.startIndex, - to: suffix.firstIndex { $0.isWhitespace || $0.isNewline } ?? suffix.endIndex - ) - let leading = prefix.lastIndex { $0.isWhitespace || $0.isNewline } - .flatMap { prefix.distance(from: prefix.startIndex, to: $0) + 1 } ?? 0 - - range = leading.. range.lowerBound: - range = range.lowerBound.. textModel.minLineSymbolIndex(elementIndex): - range = range.lowerBound - 1.. - let text: NSAttributedString + private func moveTrailing(_ elementIndex: Int) { + let elementIndex = max(elementIndex, range.lowerBound) - var maxSymbolIndex: Int { - layout.lines - .compactMap { !$0.isTruncated ? $0.range.upperBound : -1 }.max() ?? Int.max - } - - private func getLineRange(_ index: Int) -> Range { - for line in layout.lines { - if line.range.contains(index), let range = Range(line.range) { - return range - } - } - - return 0.. Int { - getLineRange(pointerIndex).upperBound - } - - func minLineSymbolIndex(_ pointerIndex: Int) -> Int { - getLineRange(pointerIndex).lowerBound + switch true { + case elementIndex > range.lowerBound: + range = range.lowerBound.. textModel.minLineSymbolIndex(elementIndex): + range = range.lowerBound - 1.. Void, @@ -57,23 +61,6 @@ public final class TooltipContainerView: UIView, UIActionEventPerforming { fatalError() } - var isModal: Bool { - tooltip.params.mode == .modal - } - - func animateAppear() { - if let animationIn = tooltip.params.animationIn { - setInitialParamsAndAnimate(animations: animationIn) - } - } - - @objc private func handleTap(_ sender: UITapGestureRecognizer) { - let point = sender.location(in: self) - if !isPointInsideTooltip(point) { - performTapOutsideActions() - } - } - public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !isPointInsideTooltip(point, event: event), !isModal { if tooltip.params.closeByTapOutside { @@ -131,6 +118,19 @@ public final class TooltipContainerView: UIView, UIActionEventPerforming { } } + func animateAppear() { + if let animationIn = tooltip.params.animationIn { + setInitialParamsAndAnimate(animations: animationIn) + } + } + + @objc private func handleTap(_ sender: UITapGestureRecognizer) { + let point = sender.location(in: self) + if !isPointInsideTooltip(point) { + performTapOutsideActions() + } + } + private func performTapOutsideActions() { let uiActionEvents = tooltip.params.tapOutsideActions.map { UIActionEvent(uiAction: $0, originalSender: self) diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/TransitioningBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/TransitioningBlock+UIViewRenderableBlock.swift index 246361f64..447356e44 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/TransitioningBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/TransitioningBlock+UIViewRenderableBlock.swift @@ -39,15 +39,16 @@ private final class TransitioningBlockView: BlockView, VisibleBoundsTrackingCont struct Model: Equatable { let block: TransitioningBlock let layout: Layout? + static func ==(lhs: Model, rhs: Model) -> Bool { lhs.block.equals(rhs.block) && lhs.layout == rhs.layout } } - private weak var observer: ElementStateObserver? - private(set) var model: Model? + private weak var observer: ElementStateObserver? + private var fromView: BlockView? { didSet { if fromView !== oldValue { @@ -69,6 +70,29 @@ private final class TransitioningBlockView: BlockView, VisibleBoundsTrackingCont var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { toView.asArray() } var effectiveBackgroundColor: UIColor? { toView?.effectiveBackgroundColor } + override func layoutSubviews() { + super.layoutSubviews() + + if let view = fromView { + let currentTransform = view.transform + view.transform = .identity + view.frame = bounds + view.transform = currentTransform + } + + if let view = toView { + let currentTransform = view.transform + view.transform = .identity + view.frame = bounds + view.transform = currentTransform + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + return result == self ? nil : result + } + func configure( model: Model, observer: ElementStateObserver?, @@ -152,27 +176,5 @@ private final class TransitioningBlockView: BlockView, VisibleBoundsTrackingCont view.setInitialParamsAndAnimate(animations: animations, completion: completion) } - override func layoutSubviews() { - super.layoutSubviews() - - if let view = fromView { - let currentTransform = view.transform - view.transform = .identity - view.frame = bounds - view.transform = currentTransform - } - - if let view = toView { - let currentTransform = view.transform - view.transform = .identity - view.frame = bounds - view.transform = currentTransform - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let result = super.hitTest(point, with: event) - return result == self ? nil : result - } } #endif diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/VideoBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/VideoBlock+UIViewRenderableBlock.swift index 97b70e775..0310bc394 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/VideoBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/VideoBlock+UIViewRenderableBlock.swift @@ -28,24 +28,24 @@ extension VideoBlock { } private final class VideoBlockView: BlockView, VisibleBoundsTrackingContainer { - var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { - [videoView].compactMap { $0 } - } - var layoutReporter: LayoutReporter? - init() { - super.init(frame: .zero) - - preview.whenLoaded { [weak self] value in - self?.videoView.map { $0.addSubview(value) } + var state: VideoBlockViewState = .init(state: .playing) { + didSet { + guard oldValue != state else { return } + switch state.state { + case .playing: + player?.play() + preview.currentValue?.isHidden = true + case .paused: + player?.pause() + } } } - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + weak var observer: ElementStateObserver? + var playerFactory: PlayerFactory? + var effectiveBackgroundColor: UIColor? private var model: VideoBlockViewModel = .zero private var playerSignal: Disposable? @@ -109,22 +109,36 @@ private final class VideoBlockView: BlockView, VisibleBoundsTrackingContainer { return view }() - var state: VideoBlockViewState = .init(state: .playing) { - didSet { - guard oldValue != state else { return } - switch state.state { - case .playing: - player?.play() - preview.currentValue?.isHidden = true - case .paused: - player?.pause() - } + var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { + [videoView].compactMap { $0 } + } + + init() { + super.init(frame: .zero) + + preview.whenLoaded { [weak self] value in + self?.videoView.map { $0.addSubview(value) } } } - weak var observer: ElementStateObserver? - var playerFactory: PlayerFactory? - var effectiveBackgroundColor: UIColor? + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + player?.pause() + } + + override func layoutSubviews() { + super.layoutSubviews() + layoutReporter?.willLayoutSubviews() + videoView?.frame = bounds + if preview.currentValue != nil { + preview.value.frame = adjustPreviewFrame() + } + layoutReporter?.didLayoutSubviews() + } func configure(with model: VideoBlockViewModel) { let oldValue = self.model @@ -153,16 +167,6 @@ private final class VideoBlockView: BlockView, VisibleBoundsTrackingContainer { } } - override func layoutSubviews() { - super.layoutSubviews() - layoutReporter?.willLayoutSubviews() - videoView?.frame = bounds - if preview.currentValue != nil { - preview.value.frame = adjustPreviewFrame() - } - layoutReporter?.didLayoutSubviews() - } - private func adjustPreviewFrame() -> CGRect { guard let videoView, let videoRatio = videoView.videoRatio else { @@ -193,9 +197,6 @@ private final class VideoBlockView: BlockView, VisibleBoundsTrackingContainer { } } - deinit { - player?.pause() - } } extension VideoBlockViewModel { diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/VideoBlockLegacy+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/VideoBlockLegacy+UIViewRenderableBlock.swift index c8d8a773a..f447b5431 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/VideoBlockLegacy+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/VideoBlockLegacy+UIViewRenderableBlock.swift @@ -28,6 +28,8 @@ extension VideoBlockLegacy { } private final class VideoBlockLegacyView: BlockView { + override static var layerClass: AnyClass { AVPlayerLayer.self } + var videoAssetHolder: VideoBlockLegacy.VideoAssetHolder! { didSet { let actualURL = videoAssetHolder.url @@ -41,6 +43,14 @@ private final class VideoBlockLegacyView: BlockView { let autoplayAllowed: ObservableVariableConnection + let effectiveBackgroundColor: UIColor? = nil + + private let disposePool = AutodisposePool() + private let avPlayer = AVQueuePlayer() + private var playerLooper: AVPlayerLooper? + + private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer } + init() { autoplayAllowed = .init(initialValue: false) super.init(frame: .zero) @@ -66,18 +76,8 @@ private final class VideoBlockLegacyView: BlockView { NotificationCenter.default.removeObserver(self) } - private let disposePool = AutodisposePool() - private let avPlayer = AVQueuePlayer() - private var playerLooper: AVPlayerLooper? - - private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer } - - override static var layerClass: AnyClass { AVPlayerLayer.self } - func onVisibleBoundsChanged(from _: CGRect, to _: CGRect) {} - let effectiveBackgroundColor: UIColor? = nil - private func configurePlayer(with playerItem: AVPlayerItem) { avPlayer.removeAllItems() playerLooper = AVPlayerLooper(player: avPlayer, templateItem: playerItem) diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/AnimatingGradientView.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/AnimatingGradientView.swift index 58c1c003e..6545829a4 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/AnimatingGradientView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/AnimatingGradientView.swift @@ -3,14 +3,15 @@ import UIKit import VGSL public final class AnimatingGradientView: UIView { + public private(set) var isAnimationActive = false + private let gradientView: LinearGradientView + private var willEnterForegroundNotificationObserver: AnyObject! + private var gradientLayer: CALayer { gradientView.layer } - public private(set) var isAnimationActive = false - private var willEnterForegroundNotificationObserver: AnyObject! - public init(color: Color = Color(white: 1, alpha: 0.8)) { let clear = color.withAlphaComponent(0) gradientView = LinearGradientView(.init( @@ -39,29 +40,23 @@ public final class AnimatingGradientView: UIView { NotificationCenter.default.removeObserver(willEnterForegroundNotificationObserver as Any) } + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + public override func layoutSubviews() { super.layoutSubviews() gradientView.bounds = bounds gradientView.center = bounds.center } - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - public override func willMove(toWindow newWindow: UIWindow?) { if newWindow != nil { restartAnimationIfNeeded() } } - private func restartAnimationIfNeeded() { - if isAnimationActive { - startAnimating() - } - } - public func startAnimating() { gradientLayer.removeAnimation(forKey: animationKey) let animation = makeAnimation() @@ -73,6 +68,13 @@ public final class AnimatingGradientView: UIView { gradientLayer.removeAnimation(forKey: animationKey) isAnimationActive = false } + + private func restartAnimationIfNeeded() { + if isAnimationActive { + startAnimating() + } + } + } private let animationKey = "AnimatingGradientViewAnimation" diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/Background+UIViewRendering.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/Background+UIViewRendering.swift index dc1571913..1b0aefd5c 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/Background+UIViewRendering.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/Background+UIViewRendering.swift @@ -39,6 +39,9 @@ private final class BackgroundView: BlockView, VisibleBoundsTrackingLeaf { } } + private var model: Background! + private weak var observer: ElementStateObserver? + var effectiveBackgroundColor: UIColor? { innerView?.backgroundColor } override func layoutSubviews() { @@ -47,9 +50,6 @@ private final class BackgroundView: BlockView, VisibleBoundsTrackingLeaf { innerView?.frame = bounds } - private var model: Background! - private weak var observer: ElementStateObserver? - func configure( model: Background, observer: ElementStateObserver?, diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/BlockHostingView.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/BlockHostingView.swift index 5420d984b..538477560 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/BlockHostingView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/BlockHostingView.swift @@ -2,8 +2,6 @@ import UIKit public final class BlockHostingView: UIView { - private var contentView: BlockView! - public var block: UIViewRenderable! { didSet { contentView = block.reuse(contentView, superview: self) @@ -11,6 +9,8 @@ public final class BlockHostingView: UIView { } } + private var contentView: BlockView! + public override func layoutSubviews() { super.layoutSubviews() contentView.frame = bounds diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/BoxShadowView.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/BoxShadowView.swift index 377b3b001..c0929547c 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/BoxShadowView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/BoxShadowView.swift @@ -21,6 +21,15 @@ public final class BoxShadowView: UIView { fatalError("init(coder:) has not been implemented") } + public override func layoutSubviews() { + super.layoutSubviews() + innerShadowLayer.frame = bounds + let path = UIBezierPath(rect: innerShadowLayer.bounds.insetBy(dx: -30, dy: -30)) + let cutout = UIBezierPath(rect: innerShadowLayer.bounds).reversing() + path.append(cutout) + innerShadowLayer.shadowPath = path.cgPath + } + private func makeInnerShadowLayer() -> CALayer { let layer = CALayer() layer.masksToBounds = true @@ -31,13 +40,5 @@ public final class BoxShadowView: UIView { return layer } - public override func layoutSubviews() { - super.layoutSubviews() - innerShadowLayer.frame = bounds - let path = UIBezierPath(rect: innerShadowLayer.bounds.insetBy(dx: -30, dy: -30)) - let cutout = UIBezierPath(rect: innerShadowLayer.bounds).reversing() - path.append(cutout) - innerShadowLayer.shadowPath = path.cgPath - } } #endif diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/ContextMenuDelegate.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/ContextMenuDelegate.swift index ae48e5941..a3f240147 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/ContextMenuDelegate.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/ContextMenuDelegate.swift @@ -5,6 +5,7 @@ import VGSL final class ContextMenuDelegate: NSObject, UIContextMenuInteractionDelegate { private let contextMenu: ContextMenu private weak var view: UIView? + public init( contextMenu: ContextMenu, view: UIView? @@ -25,13 +26,6 @@ final class ContextMenuDelegate: NSObject, UIContextMenuInteractionDelegate { } } - private func makeVC() -> UIViewController? { - let viewController = UIViewController(nibName: nil, bundle: nil) - viewController.view = contextMenu.preview.makeBlockView() - viewController.preferredContentSize = contextMenu.preview.intrinsicSize - return viewController - } - public func contextMenuInteraction( _: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration _: UIContextMenuConfiguration @@ -51,6 +45,14 @@ final class ContextMenuDelegate: NSObject, UIContextMenuInteractionDelegate { ) return UITargetedPreview(view: UIView(), parameters: previewParameters, target: previewTarget) } + + private func makeVC() -> UIViewController? { + let viewController = UIViewController(nibName: nil, bundle: nil) + viewController.view = contextMenu.preview.makeBlockView() + viewController.preferredContentSize = contextMenu.preview.intrinsicSize + return viewController + } + } extension ContextMenu { diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/GalleryView.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/GalleryView.swift index 29b6fd00f..397cfd3a3 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/GalleryView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/GalleryView.swift @@ -13,6 +13,18 @@ public final class GalleryView: BlockView { case firstLayoutPerformed } + public var layoutReporter: LayoutReporter? + public weak var observer: ElementStateObserver? { + didSet { + dataSource.observer = observer + } + } + + public weak var updatesDelegate: GalleryViewModelDelegate? + public weak var visibilityDelegate: GalleryVisibilityDelegate? + + private(set) var state: GalleryViewState! + private let collectionView: VisibleBoundsTrackingCollectionView private let collectionViewLayout: GenericCollectionViewLayout private let dataSource = GalleryDataSource() @@ -29,15 +41,11 @@ public final class GalleryView: BlockView { } } - public var layoutReporter: LayoutReporter? private var model: GalleryViewModel! private var layout: GalleryViewLayouting! - private(set) var state: GalleryViewState! private var layoutFactory: LayoutFactory! private var deferredStateSetting = DeferredStateSetting.idle private var scrollStartOffset: CGFloat = 0 - public var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { [collectionView] } - private weak var overscrollDelegate: ScrollDelegate? { didSet { if let previousDelegate = oldValue { @@ -56,11 +64,7 @@ public final class GalleryView: BlockView { } } - public weak var observer: ElementStateObserver? { - didSet { - dataSource.observer = observer - } - } + public var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { [collectionView] } public var path: UIElementPath { model.path @@ -83,6 +87,60 @@ public final class GalleryView: BlockView { public var effectiveBackgroundColor: UIColor? { collectionView.backgroundColor } + public override init(frame: CGRect) { + collectionViewLayout = GenericCollectionViewLayout() + collectionView = VisibleBoundsTrackingCollectionView( + frame: frame, + collectionViewLayout: collectionViewLayout + ) + collectionView.alwaysBounceVertical = false + collectionView.clipsToBounds = false + collectionView.backgroundColor = .clear + collectionView.scrollsToTop = false + collectionView.dataSource = dataSource + + collectionView.disableContentInsetAdjustmentBehavior() + + super.init(frame: frame) + (collectionView as UIScrollView).delegate = compoundScrollDelegate + compoundScrollDelegate.add(self) + addSubview(collectionView) + } + + @available(*, unavailable) + public required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let superResult = super.hitTest(point, with: event) + if model.areEmptySpaceTouchesEnabled { + return superResult + } else { + return superResult === collectionView ? nil : superResult + } + } + + public override func layoutSubviews() { + guard !bounds.isEmpty else { + return + } + layoutReporter?.willLayoutSubviews() + collectionView.frame = bounds + + if let model, layout?.isEqual(to: model, boundsSize: bounds.size) != true { + updateLayout(to: model) + setState(stateWithScrollRange, notifyingObservers: true) + } + if case let .pending(state) = deferredStateSetting { + collectionView.performWithDetachedDelegate { + updateContentOffset(to: state.contentPosition, animated: false) + } + } + deferredStateSetting = .idle + layoutReporter?.didLayoutSubviews() + } + public func configure( model: GalleryViewModel, state: GalleryViewState, @@ -198,63 +256,6 @@ public final class GalleryView: BlockView { } } - public weak var updatesDelegate: GalleryViewModelDelegate? - public weak var visibilityDelegate: GalleryVisibilityDelegate? - - public override init(frame: CGRect) { - collectionViewLayout = GenericCollectionViewLayout() - collectionView = VisibleBoundsTrackingCollectionView( - frame: frame, - collectionViewLayout: collectionViewLayout - ) - collectionView.alwaysBounceVertical = false - collectionView.clipsToBounds = false - collectionView.backgroundColor = .clear - collectionView.scrollsToTop = false - collectionView.dataSource = dataSource - - collectionView.disableContentInsetAdjustmentBehavior() - - super.init(frame: frame) - (collectionView as UIScrollView).delegate = compoundScrollDelegate - compoundScrollDelegate.add(self) - addSubview(collectionView) - } - - @available(*, unavailable) - public required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let superResult = super.hitTest(point, with: event) - if model.areEmptySpaceTouchesEnabled { - return superResult - } else { - return superResult === collectionView ? nil : superResult - } - } - - public override func layoutSubviews() { - guard !bounds.isEmpty else { - return - } - layoutReporter?.willLayoutSubviews() - collectionView.frame = bounds - - if let model, layout?.isEqual(to: model, boundsSize: bounds.size) != true { - updateLayout(to: model) - setState(stateWithScrollRange, notifyingObservers: true) - } - if case let .pending(state) = deferredStateSetting { - collectionView.performWithDetachedDelegate { - updateContentOffset(to: state.contentPosition, animated: false) - } - } - deferredStateSetting = .idle - layoutReporter?.didLayoutSubviews() - } - private func updateContentOffset( to contentPosition: GalleryViewState.Position, animated: Bool diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/NinePatchImageView.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/NinePatchImageView.swift index 2b6f34a24..0f0581c02 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/NinePatchImageView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/NinePatchImageView.swift @@ -3,8 +3,6 @@ import UIKit import VGSL public final class NinePatchImageView: UIView, RemoteImageViewContentProtocol { - private var image: UIImage? - public var capInsets: UIEdgeInsets = .zero { didSet { updateLayout() @@ -15,6 +13,8 @@ public final class NinePatchImageView: UIView, RemoteImageViewContentProtocol { public var imageRedrawingStyle: ImageRedrawingStyle? public var imageContentMode = ImageContentMode.default + private var image: UIImage? + init() { super.init(frame: .zero) isOpaque = false diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/SegmentedProgressView.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/SegmentedProgressView.swift index 07c94c027..f717269ae 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/SegmentedProgressView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/SegmentedProgressView.swift @@ -31,6 +31,9 @@ public final class SegmentedProgressView: UIView { } } + private var segmentLayers: [SegmentLayer] = [] + private var isReloadSegmentLayersNeeded = true + public init(fillColor: Color, segmentBackgroundColor: Color) { self.fillColor = fillColor self.segmentBackgroundColor = segmentBackgroundColor @@ -42,9 +45,6 @@ public final class SegmentedProgressView: UIView { fatalError("init(coder:) has not been implemented") } - private var segmentLayers: [SegmentLayer] = [] - private var isReloadSegmentLayersNeeded = true - public override func layoutSubviews() { super.layoutSubviews() diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabContentsView.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabContentsView.swift index ad6007a9c..4f2d9ac5a 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabContentsView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabContentsView.swift @@ -7,6 +7,16 @@ final class TabContentsView: BlockView { static let animationDuration: TimeInterval = 0.2 } + var delegate: TabContentsViewDelegate? + weak var updatesDelegate: TabbedPagesViewModelDelegate? + private(set) var selectedPageIndex: CGFloat = 0 { + didSet { + updateContentOffset() + } + } + + private(set) var model: TabContentsViewModel! + private var collectionView: VisibleBoundsTrackingCollectionView! private var listViews: [UIView] = [] private var footerView: UIView? { @@ -27,11 +37,6 @@ final class TabContentsView: BlockView { } } - var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { collectionView.asArray() } - var effectiveBackgroundColor: UIColor? { backgroundView?.effectiveBackgroundColor } - - var delegate: TabContentsViewDelegate? - weak var updatesDelegate: TabbedPagesViewModelDelegate? private weak var overscrollDelegate: ScrollDelegate? { didSet { dataSource.overscrollDelegate = overscrollDelegate @@ -44,21 +49,6 @@ final class TabContentsView: BlockView { } } - private(set) var selectedPageIndex: CGFloat = 0 { - didSet { - updateContentOffset() - } - } - - private var selectedPagePath: UIElementPath { - let index = clamp( - Int(selectedPageIndex.rounded()), - min: 0, - max: max(0, model.pages.count - 1) - ) - return model.pages.element(at: index)!.path - } - private var layout: TabContentsViewLayout! { didSet { collectionViewLayout.layout = layout.map { @@ -75,7 +65,6 @@ final class TabContentsView: BlockView { } } - private(set) var model: TabContentsViewModel! private weak var observer: ElementStateObserver? { didSet { dataSource.observer = observer @@ -84,6 +73,76 @@ final class TabContentsView: BlockView { private let cellRegistrator = CollectionCellRegistrator() + private let dataSource = GenericCollectionViewDataSource() + private let collectionViewLayout = GenericCollectionViewLayout() + + private var scrolledPageIndex: CGFloat = 0 + + var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { collectionView.asArray() } + var effectiveBackgroundColor: UIColor? { backgroundView?.effectiveBackgroundColor } + + private var selectedPagePath: UIElementPath { + let index = clamp( + Int(selectedPageIndex.rounded()), + min: 0, + max: max(0, model.pages.count - 1) + ) + return model.pages.element(at: index)!.path + } + + private var selectedPageContentOffset: CGPoint { + CGPoint(x: collectionView.bounds.width * selectedPageIndex, y: 0) + } + + init() { + collectionView = VisibleBoundsTrackingCollectionView( + frame: .zero, + collectionViewLayout: collectionViewLayout + ) + collectionView.dataSource = dataSource + collectionView.backgroundColor = .clear + collectionView.scrollsToTop = false + collectionView.showsHorizontalScrollIndicator = false + collectionView.showsVerticalScrollIndicator = false + collectionView.isPagingEnabled = true + + collectionView.disableContentInsetAdjustmentBehavior() + + super.init(frame: .zero) + addSubview(collectionView) + collectionView.delegate = self + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + if collectionView.frame != bounds { + collectionView.frame = bounds + updateContentOffset(animated: false) + } + + let newLayout = TabContentsViewLayout( + pages: model.pages.map(\.block), + footer: model.footer, + size: collectionView.bounds.size, + pagesInsets: model.contentInsets.horizontal + ) + guard newLayout != layout else { + return + } + + layout = newLayout + collectionView.setNeedsLayout() + + footerView?.frame = layout.footerFrame + backgroundView?.frame = bounds + } + func configure( model: TabContentsViewModel, state: TabViewState, @@ -146,35 +205,6 @@ final class TabContentsView: BlockView { } } - private let dataSource = GenericCollectionViewDataSource() - private let collectionViewLayout = GenericCollectionViewLayout() - - private var scrolledPageIndex: CGFloat = 0 - - init() { - collectionView = VisibleBoundsTrackingCollectionView( - frame: .zero, - collectionViewLayout: collectionViewLayout - ) - collectionView.dataSource = dataSource - collectionView.backgroundColor = .clear - collectionView.scrollsToTop = false - collectionView.showsHorizontalScrollIndicator = false - collectionView.showsVerticalScrollIndicator = false - collectionView.isPagingEnabled = true - - collectionView.disableContentInsetAdjustmentBehavior() - - super.init(frame: .zero) - addSubview(collectionView) - collectionView.delegate = self - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - func updateRelativeOffset(_ offset: CGFloat) { relativeContentOffset = offset updateSelectedPageIndexIfNeeded(relativeContentOffset) @@ -193,35 +223,6 @@ final class TabContentsView: BlockView { collectionView.setContentOffset(selectedPageContentOffset, animated: animated) } - private var selectedPageContentOffset: CGPoint { - CGPoint(x: collectionView.bounds.width * selectedPageIndex, y: 0) - } - - override func layoutSubviews() { - super.layoutSubviews() - - if collectionView.frame != bounds { - collectionView.frame = bounds - updateContentOffset(animated: false) - } - - let newLayout = TabContentsViewLayout( - pages: model.pages.map(\.block), - footer: model.footer, - size: collectionView.bounds.size, - pagesInsets: model.contentInsets.horizontal - ) - guard newLayout != layout else { - return - } - - layout = newLayout - collectionView.setNeedsLayout() - - footerView?.frame = layout.footerFrame - backgroundView?.frame = bounds - } - private func updateSelectedPageIndexFromRelativeContentOffset( isIntermediate: Bool ) { diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListSelectionDataSourceImpl.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListSelectionDataSourceImpl.swift index 0706e51d6..118f67416 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListSelectionDataSourceImpl.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListSelectionDataSourceImpl.swift @@ -6,15 +6,15 @@ import VGSL final class TabListSelectionDataSourceImpl: TabListSelectionDataSource { private let listModel: TabListViewModel + var numberOfTabs: Int { + listModel.tabTitles.count + } + init(listModel: TabListViewModel) { precondition(!listModel.tabTitles.isEmpty) self.listModel = listModel } - var numberOfTabs: Int { - listModel.tabTitles.count - } - func modelForItemSelection(_ selection: CGFloat) -> TabTitlesViewModel { let items = listModel.tabs.enumerated().map { offset, tab -> TabTitleViewModel in let distanceToSelected = abs(CGFloat(offset) - selection) diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListView.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListView.swift index 63997dbad..517aaa5a5 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListView.swift @@ -3,20 +3,7 @@ import UIKit import VGSL final class TabListView: UIView { - private let collectionView: UICollectionView - private let delegate: TabListViewDelegate - private let collectionViewLayout = TabsCollectionViewFlowLayout() - - private var selectedItemBackground: UIView! - private var setInitialModel = false var isMovingToSelectedItem = false - - private var dataSource: TabListViewDataSource! { - didSet { - collectionView.dataSource = dataSource - } - } - var model: TabTitlesViewModel! { didSet { if oldValue?.items != model.items || oldValue?.tabTitleDelimiter != model.tabTitleDelimiter { @@ -40,6 +27,19 @@ final class TabListView: UIView { } } + private let collectionView: UICollectionView + private let delegate: TabListViewDelegate + private let collectionViewLayout = TabsCollectionViewFlowLayout() + + private var selectedItemBackground: UIView! + private var setInitialModel = false + + private var dataSource: TabListViewDataSource! { + didSet { + collectionView.dataSource = dataSource + } + } + private var lastLayout: ( bounds: CGRect, contentOffset: CGPoint, diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListViewDataSource.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListViewDataSource.swift index ff7e65c0a..3a5d13ab3 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListViewDataSource.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListViewDataSource.swift @@ -58,8 +58,6 @@ final class TabListViewDataSource: NSObject, UICollectionViewDataSource { final class TabListItemCell: UICollectionViewCell { static let reuseID = "TabListItemCell" - private var textLabel: Label! - var model: TabTitleViewModel! { didSet { if textLabel.attributedText !== model.text { @@ -72,6 +70,8 @@ final class TabListItemCell: UICollectionViewCell { } } + private var textLabel: Label! + override init(frame: CGRect) { super.init(frame: frame) textLabel = makeTextLabel() @@ -83,12 +83,6 @@ final class TabListItemCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } - private func makeTextLabel() -> Label { - let label = Label(frame: .zero) - label.textAlignment = .center - return label - } - override func layoutSubviews() { super.layoutSubviews() textLabel.frame = bounds.inset(by: model.insets) @@ -101,5 +95,12 @@ final class TabListItemCell: UICollectionViewCell { class func sizeForModel(_ model: TabTitleViewModel) -> CGSize { model.totalSize.ceiled() } + + private func makeTextLabel() -> Label { + let label = Label(frame: .zero) + label.textAlignment = .center + return label + } + } #endif diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListViewDelegate.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListViewDelegate.swift index 9cf61ace2..f71489d70 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListViewDelegate.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabListViewDelegate.swift @@ -3,17 +3,13 @@ import UIKit import VGSL final class TabListViewDelegate: NSObject, UICollectionViewDelegateFlowLayout { - private let collectionView: UICollectionView - public var layoutDirection: UserInterfaceLayoutDirection = .leftToRight - - private var itemSizes: [CGSize] = [] { - didSet { - if itemSizes != oldValue { - collectionView.collectionViewLayout.invalidateLayout() - } - } + struct Layout { + let contentOffset: CGPoint + let pillOriginX: CGFloat } + public var layoutDirection: UserInterfaceLayoutDirection = .leftToRight + var tabs: [TabTitleViewModel] = [] { didSet { itemSizes = tabs.map { TabListItemCell.sizeForModel($0) } @@ -31,6 +27,16 @@ final class TabListViewDelegate: NSObject, UICollectionViewDelegateFlowLayout { weak var selectionDelegate: TabListViewDelegateTabSelection? weak var overscrollDelegate: ScrollDelegate? + private let collectionView: UICollectionView + + private var itemSizes: [CGSize] = [] { + didSet { + if itemSizes != oldValue { + collectionView.collectionViewLayout.invalidateLayout() + } + } + } + private var scrollInfo: (startOffset: CGFloat, itemPath: UIElementPath)? init(collectionView: UICollectionView) { @@ -45,11 +51,6 @@ final class TabListViewDelegate: NSObject, UICollectionViewDelegateFlowLayout { itemSizes[indexPath.item] } - struct Layout { - let contentOffset: CGPoint - let pillOriginX: CGFloat - } - func collectionView( _ collectionView: UICollectionView, layoutForItemSelection itemSelection: CGFloat, diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabTitleDelimiterReusableView.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabTitleDelimiterReusableView.swift index 1be3351b3..1d17e871f 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabTitleDelimiterReusableView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabTitleDelimiterReusableView.swift @@ -3,6 +3,7 @@ import UIKit final class TabTitleDelimiterReusableView: UICollectionReusableView { static let reuseID = "TabTitleDelimiterReusableView" + private let delimiterImageView = UIImageView() override init(frame: CGRect) { @@ -15,11 +16,6 @@ final class TabTitleDelimiterReusableView: UICollectionReusableView { setupView() } - private func setupView() { - addSubview(delimiterImageView) - delimiterImageView.contentMode = .scaleAspectFit - } - override func layoutSubviews() { super.layoutSubviews() delimiterImageView.frame = bounds @@ -28,5 +24,11 @@ final class TabTitleDelimiterReusableView: UICollectionReusableView { func configure(with image: UIImage?) { delimiterImageView.image = image } + + private func setupView() { + addSubview(delimiterImageView) + delimiterImageView.contentMode = .scaleAspectFit + } + } #endif diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabbedPagesView.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabbedPagesView.swift index e7eff3a56..210b3cbc9 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabbedPagesView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabbedPagesView.swift @@ -3,6 +3,9 @@ import UIKit import VGSL public final class TabbedPagesView: BlockView, VisibleBoundsTrackingContainer { + public let visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] + public var layoutReporter: LayoutReporter? + private var selectionWireframe: TabSelectionWireframe? private let tabListView = TabListView() @@ -23,10 +26,44 @@ public final class TabbedPagesView: BlockView, VisibleBoundsTrackingContainer { private var tabDataSource: TabListSelectionDataSource! private var layout: TabViewLayout! - public let visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] public var effectiveBackgroundColor: UIColor? { tabContentsView.effectiveBackgroundColor } - public var layoutReporter: LayoutReporter? + + public weak var updatesDelegate: TabbedPagesViewModelDelegate? { + get { tabContentsView.updatesDelegate } + set { tabContentsView.updatesDelegate = newValue } + } + + @available(*, unavailable) + public required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override init(frame: CGRect) { + visibleBoundsTrackingSubviews = [tabContentsView] + super.init(frame: frame) + attachViews() + } + + public override func layoutSubviews() { + let newLayout = TabViewLayout( + model: model, + selectedPageIndex: tabContentsView.selectedPageIndex, + size: bounds.size + ) + + guard newLayout != layout else { + return + } + layoutReporter?.willLayoutSubviews() + + layout = newLayout + tabListView.frame = layout.listFrame + tabContentsView.frame = layout.contentsFrame + separatorView?.frame = layout.separatorFrame + + layoutReporter?.didLayoutSubviews() + } public func configure( model: TabViewModel, @@ -60,46 +97,10 @@ public final class TabbedPagesView: BlockView, VisibleBoundsTrackingContainer { setNeedsLayout() } - public weak var updatesDelegate: TabbedPagesViewModelDelegate? { - get { tabContentsView.updatesDelegate } - set { tabContentsView.updatesDelegate = newValue } - } - - override init(frame: CGRect) { - visibleBoundsTrackingSubviews = [tabContentsView] - super.init(frame: frame) - attachViews() - } - private func attachViews() { addSubviews([tabListView, tabContentsView]) } - @available(*, unavailable) - public required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func layoutSubviews() { - let newLayout = TabViewLayout( - model: model, - selectedPageIndex: tabContentsView.selectedPageIndex, - size: bounds.size - ) - - guard newLayout != layout else { - return - } - layoutReporter?.willLayoutSubviews() - - layout = newLayout - tabListView.frame = layout.listFrame - tabContentsView.frame = layout.contentsFrame - separatorView?.frame = layout.separatorFrame - - layoutReporter?.didLayoutSubviews() - } - private func configureSeparatorView() { guard let separatorStyle = model.separatorStyle else { separatorView?.removeFromSuperview() diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabsCollectionViewFlowLayout.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabsCollectionViewFlowLayout.swift index 58da5c73f..d5d626afe 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabsCollectionViewFlowLayout.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/TabbedPages/TabsCollectionViewFlowLayout.swift @@ -5,6 +5,10 @@ import VGSL final class TabsCollectionViewFlowLayout: UICollectionViewFlowLayout { static let delimiterKind = "TabDelimiter" + override var flipsHorizontallyInOppositeLayoutDirection: Bool { + true + } + var tabTitleDelimiter: TabTitleDelimiterStyle? { didSet { if tabTitleDelimiter != oldValue { @@ -26,10 +30,6 @@ final class TabsCollectionViewFlowLayout: UICollectionViewFlowLayout { fatalError("init(coder:) has not been implemented") } - override var flipsHorizontallyInOppositeLayoutDirection: Bool { - true - } - override func prepare() { super.prepare() diff --git a/client/ios/LayoutKit/LayoutKit/UI/Views/VisibleBoundsTrackingCollectionView.swift b/client/ios/LayoutKit/LayoutKit/UI/Views/VisibleBoundsTrackingCollectionView.swift index 3ad84e06d..22f378ad9 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Views/VisibleBoundsTrackingCollectionView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Views/VisibleBoundsTrackingCollectionView.swift @@ -4,12 +4,6 @@ import VGSL public final class VisibleBoundsTrackingCollectionView: NoContentTouchDelaysCollectionView, VisibleBoundsTrackingContainer { - private var previousVisibleBounds = CGRect.zero - private var currentVisibleBounds = CGRect.zero - public var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { - subviews.compactMap { $0 as? VisibleBoundsTrackingView } - } - public override var bounds: CGRect { didSet { if bounds.size == oldValue.size { @@ -23,6 +17,14 @@ public final class VisibleBoundsTrackingCollectionView: NoContentTouchDelaysColl previousVisibleBounds = currentVisibleBounds } } + + private var previousVisibleBounds = CGRect.zero + private var currentVisibleBounds = CGRect.zero + + public var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { + subviews.compactMap { $0 as? VisibleBoundsTrackingView } + } + } extension VisibleBoundsTrackingCollectionView: VisibleBoundsTracking { diff --git a/client/ios/LayoutKit/LayoutKit/ViewModels/GalleryViewLayout.swift b/client/ios/LayoutKit/LayoutKit/ViewModels/GalleryViewLayout.swift index c62f4f376..fce797132 100644 --- a/client/ios/LayoutKit/LayoutKit/ViewModels/GalleryViewLayout.swift +++ b/client/ios/LayoutKit/LayoutKit/ViewModels/GalleryViewLayout.swift @@ -53,6 +53,10 @@ public struct GalleryViewLayout: GalleryViewLayouting, Equatable { self.boundsSize = boundsSize ?? contentSize } + public static func ==(_ lhs: GalleryViewLayout, _ rhs: GalleryViewLayout) -> Bool { + lhs.isEqual(to: rhs.model, boundsSize: rhs.boundsSize) + } + public func contentOffset(pageIndex: CGFloat) -> CGFloat { let integralIndex = Int(pageIndex) guard blockPages.indices.contains(integralIndex) else { @@ -82,9 +86,6 @@ public struct GalleryViewLayout: GalleryViewLayouting, Equatable { self.model == model && self.boundsSize == boundsSize } - public static func ==(_ lhs: GalleryViewLayout, _ rhs: GalleryViewLayout) -> Bool { - lhs.isEqual(to: rhs.model, boundsSize: rhs.boundsSize) - } } extension GalleryViewModel { diff --git a/client/ios/LayoutKit/LayoutKit/ViewModels/GalleryViewModel.swift b/client/ios/LayoutKit/LayoutKit/ViewModels/GalleryViewModel.swift index 9191cc821..679d2a03d 100644 --- a/client/ios/LayoutKit/LayoutKit/ViewModels/GalleryViewModel.swift +++ b/client/ios/LayoutKit/LayoutKit/ViewModels/GalleryViewModel.swift @@ -55,6 +55,14 @@ public struct GalleryViewModel: Equatable { public let scrollbar: Scrollbar public let transformation: ElementsTransformation? + var itemsCountWithoutInfinite: Int { + items.count - infiniteCorrection * 2 + } + + var infiniteCorrection: Int { + infiniteScroll && items.count > 0 ? bufferSize : 0 + } + public init( blocks: [Block], layoutDirection: UserInterfaceLayoutDirection = .leftToRight, @@ -150,13 +158,6 @@ public struct GalleryViewModel: Equatable { ) } - var itemsCountWithoutInfinite: Int { - items.count - infiniteCorrection * 2 - } - - var infiniteCorrection: Int { - infiniteScroll && items.count > 0 ? bufferSize : 0 - } } extension GalleryViewModel.Item { diff --git a/client/ios/LayoutKit/LayoutKit/ViewModels/InsetMode.swift b/client/ios/LayoutKit/LayoutKit/ViewModels/InsetMode.swift index 955678996..1fb097b2a 100644 --- a/client/ios/LayoutKit/LayoutKit/ViewModels/InsetMode.swift +++ b/client/ios/LayoutKit/LayoutKit/ViewModels/InsetMode.swift @@ -3,6 +3,9 @@ import Foundation import VGSL public enum InsetMode: Equatable { + case fixed(values: SideInsets) + case resizable(params: Resizable) + public struct Resizable: Equatable { public let minValue: CGFloat public let maxViewportSize: CGFloat @@ -16,8 +19,6 @@ public enum InsetMode: Equatable { } } - case fixed(values: SideInsets) - case resizable(params: Resizable) } extension InsetMode { diff --git a/client/ios/LayoutKit/LayoutKit/ViewModels/PagerViewLayout.swift b/client/ios/LayoutKit/LayoutKit/ViewModels/PagerViewLayout.swift index 278100eb4..bcb8a2fb4 100644 --- a/client/ios/LayoutKit/LayoutKit/ViewModels/PagerViewLayout.swift +++ b/client/ios/LayoutKit/LayoutKit/ViewModels/PagerViewLayout.swift @@ -59,6 +59,10 @@ public struct PagerViewLayout: GalleryViewLayouting, Equatable { self.boundsSize = boundsSize } + public static func ==(_ lhs: PagerViewLayout, _ rhs: PagerViewLayout) -> Bool { + lhs.isEqual(to: rhs.model, boundsSize: rhs.boundsSize) + } + public func contentOffset(pageIndex: CGFloat) -> CGFloat { let integralIndex = Int(pageIndex) @@ -84,9 +88,6 @@ public struct PagerViewLayout: GalleryViewLayouting, Equatable { self.model == model && self.boundsSize == boundsSize } - public static func ==(_ lhs: PagerViewLayout, _ rhs: PagerViewLayout) -> Bool { - lhs.isEqual(to: rhs.model, boundsSize: rhs.boundsSize) - } } extension GalleryViewModel { diff --git a/client/ios/LayoutKit/LayoutKit/ViewModels/PagerViewState.swift b/client/ios/LayoutKit/LayoutKit/ViewModels/PagerViewState.swift index 76c5bcfb9..40d5905c7 100644 --- a/client/ios/LayoutKit/LayoutKit/ViewModels/PagerViewState.swift +++ b/client/ios/LayoutKit/LayoutKit/ViewModels/PagerViewState.swift @@ -2,6 +2,12 @@ import CoreGraphics import VGSL public struct PagerViewState: ElementState, Equatable { + public static let `default` = Self( + numberOfPages: 0, + currentPage: 0, + animated: true + ) + public let numberOfPages: Int public let currentPage: CGFloat public let animated: Bool @@ -19,11 +25,6 @@ public struct PagerViewState: ElementState, Equatable { self.animated = true } - public static let `default` = Self( - numberOfPages: 0, - currentPage: 0, - animated: true - ) } extension PagerViewState { diff --git a/client/ios/LayoutKit/LayoutKit/ViewModels/TabbedPages/TabContentsViewModel.swift b/client/ios/LayoutKit/LayoutKit/ViewModels/TabbedPages/TabContentsViewModel.swift index 691551e91..191aa7b50 100644 --- a/client/ios/LayoutKit/LayoutKit/ViewModels/TabbedPages/TabContentsViewModel.swift +++ b/client/ios/LayoutKit/LayoutKit/ViewModels/TabbedPages/TabContentsViewModel.swift @@ -39,6 +39,16 @@ public struct TabContentsViewModel: Equatable { try checkConstraints() } + public static func ==(lhs: TabContentsViewModel, rhs: TabContentsViewModel) -> Bool { + lhs.path == rhs.path + && lhs.pages == rhs.pages + && lhs.pagesHeight == rhs.pagesHeight + && lhs.contentInsets == rhs.contentInsets + && lhs.background == rhs.background + && lhs.footer == rhs.footer + && lhs.scrollingEnabled == rhs.scrollingEnabled + } + private func checkConstraints() throws { if pages.isEmpty { throw BlockError("Tab error: no children provided") @@ -49,15 +59,6 @@ public struct TabContentsViewModel: Equatable { } } - public static func ==(lhs: TabContentsViewModel, rhs: TabContentsViewModel) -> Bool { - lhs.path == rhs.path - && lhs.pages == rhs.pages - && lhs.pagesHeight == rhs.pagesHeight - && lhs.contentInsets == rhs.contentInsets - && lhs.background == rhs.background - && lhs.footer == rhs.footer - && lhs.scrollingEnabled == rhs.scrollingEnabled - } } extension TabContentsViewModel: CustomDebugStringConvertible { diff --git a/client/ios/LayoutKit/LayoutKit/ViewModels/TabbedPages/TabViewState.swift b/client/ios/LayoutKit/LayoutKit/ViewModels/TabbedPages/TabViewState.swift index ff9d12aed..39a2f4037 100644 --- a/client/ios/LayoutKit/LayoutKit/ViewModels/TabbedPages/TabViewState.swift +++ b/client/ios/LayoutKit/LayoutKit/ViewModels/TabbedPages/TabViewState.swift @@ -3,11 +3,11 @@ import Foundation import VGSL public struct TabViewState: ElementState, Equatable { + public static let `default` = TabViewState(selectedPageIndex: 0, countOfPages: 0) + public let selectedPageIndex: CGFloat public let countOfPages: Int - public static let `default` = TabViewState(selectedPageIndex: 0, countOfPages: 0) - public init(selectedPageIndex: CGFloat, countOfPages: Int) { self.selectedPageIndex = selectedPageIndex self.countOfPages = countOfPages diff --git a/client/ios/LayoutKit/LayoutKit/ViewModels/VisibilityActionPerformer/VisibilityActionPerformer.swift b/client/ios/LayoutKit/LayoutKit/ViewModels/VisibilityActionPerformer/VisibilityActionPerformer.swift index fbacbb6bd..39ebbcd12 100644 --- a/client/ios/LayoutKit/LayoutKit/ViewModels/VisibilityActionPerformer/VisibilityActionPerformer.swift +++ b/client/ios/LayoutKit/LayoutKit/ViewModels/VisibilityActionPerformer/VisibilityActionPerformer.swift @@ -31,6 +31,10 @@ final class VisibilityActionPerformer { self.timerScheduler = timerScheduler } + deinit { + visibilityTimer?.invalidate() + } + func onVisibleBoundsChanged( visibleAreaPercentageBefore: Int, visibleAreaPercentageAfter: Int @@ -78,7 +82,4 @@ final class VisibilityActionPerformer { visibilityTimer = nil } - deinit { - visibilityTimer?.invalidate() - } } diff --git a/client/ios/LayoutKit/LayoutKitTests/Blocks/BlockWithFixedWrapContent.swift b/client/ios/LayoutKit/LayoutKitTests/Blocks/BlockWithFixedWrapContent.swift index 45b115328..93c93f833 100644 --- a/client/ios/LayoutKit/LayoutKitTests/Blocks/BlockWithFixedWrapContent.swift +++ b/client/ios/LayoutKit/LayoutKitTests/Blocks/BlockWithFixedWrapContent.swift @@ -6,6 +6,14 @@ final class BlockWithFixedWrapContent: BlockWithTraits { var width: CGFloat var height: CGFloat + var widthTrait: LayoutTrait + + var heightTrait: LayoutTrait + + var intrinsicContentWidth: CGFloat { + width + } + init( width: CGFloat = 0, height: CGFloat = 0, @@ -26,14 +34,6 @@ final class BlockWithFixedWrapContent: BlockWithTraits { ) } - var widthTrait: LayoutTrait - - var heightTrait: LayoutTrait - - var intrinsicContentWidth: CGFloat { - width - } - func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { height } diff --git a/client/ios/LayoutKit/LayoutKitTests/Blocks/GridBlockTests.swift b/client/ios/LayoutKit/LayoutKitTests/Blocks/GridBlockTests.swift index 3f70c8698..c817c3ef4 100644 --- a/client/ios/LayoutKit/LayoutKitTests/Blocks/GridBlockTests.swift +++ b/client/ios/LayoutKit/LayoutKitTests/Blocks/GridBlockTests.swift @@ -82,43 +82,6 @@ final class GridBlockTests: XCTestCase { assertIntrinsicContentSize(ofBlock: grid, equals: minSize) } - private func makeGrid( - contentSize: CGSize, - widthTrait: LayoutTrait, - heightTrait: LayoutTrait - ) -> GridBlock { - let itemBlock = EmptyBlock( - widthTrait: .fixed(contentSize.width), - heightTrait: .fixed(contentSize.height) - ) - - return try! GridBlock( - widthTrait: widthTrait, - heightTrait: heightTrait, - contentAlignment: .default, - items: [ - .init( - weight: GridBlock.Item.Weight(), - contents: itemBlock - ), - ], - columnCount: 1 - ) - } - - private func assertIntrinsicContentSize( - ofBlock block: GridBlock, - equals: CGSize - ) { - let width = block.intrinsicContentWidth - let height = block.intrinsicContentHeight(forWidth: width) - - XCTAssertEqual( - CGSize(width: width, height: height), - equals - ) - } - func test_KeepWeightWhenUpdateState() { struct TestState: ElementState {} @@ -159,15 +122,75 @@ final class GridBlockTests: XCTestCase { XCTAssertTrue(newBlock == block) } + + private func makeGrid( + contentSize: CGSize, + widthTrait: LayoutTrait, + heightTrait: LayoutTrait + ) -> GridBlock { + let itemBlock = EmptyBlock( + widthTrait: .fixed(contentSize.width), + heightTrait: .fixed(contentSize.height) + ) + + return try! GridBlock( + widthTrait: widthTrait, + heightTrait: heightTrait, + contentAlignment: .default, + items: [ + .init( + weight: GridBlock.Item.Weight(), + contents: itemBlock + ), + ], + columnCount: 1 + ) + } + + private func assertIntrinsicContentSize( + ofBlock block: GridBlock, + equals: CGSize + ) { + let width = block.intrinsicContentWidth + let height = block.intrinsicContentHeight(forWidth: width) + + XCTAssertEqual( + CGSize(width: width, height: height), + equals + ) + } + } private final class AlwaysRecreatedTestBlock: BlockWithTraits { + enum AlwaysRecreatedTestBlockState: ElementState, Equatable { + case state1 + } + + private final class AlwaysRecreatedTestBlockView: BlockView, VisibleBoundsTrackingContainer { + var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] = [] + + var effectiveBackgroundColor: UIColor? + + func onVisibleBoundsChanged(from _: CGRect, to _: CGRect) {} + + } + var widthTrait: LayoutTrait var heightTrait: LayoutTrait let state: AlwaysRecreatedTestBlockState var debugDescription = "" + var intrinsicContentWidth: CGFloat { + switch widthTrait { + case let .fixed(value): + value + case .intrinsic, .weighted: + 0 + } + } + init( widthTrait: LayoutTrait, @@ -179,6 +202,10 @@ private final class AlwaysRecreatedTestBlock: BlockWithTraits { self.state = state } + static func makeBlockView() -> BlockView { + AlwaysRecreatedTestBlockView() + } + func configureBlockView( _: BlockView, observer _: ElementStateObserver? = nil, @@ -186,15 +213,6 @@ private final class AlwaysRecreatedTestBlock: BlockWithTraits { renderingDelegate _: RenderingDelegate? = nil ) {} - var intrinsicContentWidth: CGFloat { - switch widthTrait { - case let .fixed(value): - value - case .intrinsic, .weighted: - 0 - } - } - func intrinsicContentHeight( forWidth _: CGFloat ) -> CGFloat { @@ -214,10 +232,6 @@ private final class AlwaysRecreatedTestBlock: BlockWithTraits { return self.state == otherBlock.state } - static func makeBlockView() -> BlockView { - AlwaysRecreatedTestBlockView() - } - func canConfigureBlockView(_ view: BlockView) -> Bool { view is AlwaysRecreatedTestBlockView } @@ -232,17 +246,6 @@ private final class AlwaysRecreatedTestBlock: BlockWithTraits { ) } - enum AlwaysRecreatedTestBlockState: ElementState, Equatable { - case state1 - } - - private final class AlwaysRecreatedTestBlockView: BlockView, VisibleBoundsTrackingContainer { - var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] = [] - - func onVisibleBoundsChanged(from _: CGRect, to _: CGRect) {} - - var effectiveBackgroundColor: UIColor? - } } extension AlwaysRecreatedTestBlock: LayoutCachingDefaultImpl {} diff --git a/client/ios/LayoutKit/LayoutKitTests/UI/Blocks/Array+UIViewRenderableTests.swift b/client/ios/LayoutKit/LayoutKitTests/UI/Blocks/Array+UIViewRenderableTests.swift index 3e715bfaf..233bf95e6 100644 --- a/client/ios/LayoutKit/LayoutKitTests/UI/Blocks/Array+UIViewRenderableTests.swift +++ b/client/ios/LayoutKit/LayoutKitTests/UI/Blocks/Array+UIViewRenderableTests.swift @@ -202,6 +202,14 @@ private class TestBlock: BlockWithTraits { let path: UIElementPath? + var debugDescription = "" + + init(_ path: UIElementPath? = nil) { + self.path = path + } + + static func makeBlockView() -> BlockView { TestView() } + func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat { 0.0 } @@ -214,8 +222,6 @@ private class TestBlock: BlockWithTraits { return other.valuableContent == valuableContent } - var debugDescription = "" - func getImageHolders() -> [VGSLUI.ImageHolder] { [] } @@ -232,8 +238,6 @@ private class TestBlock: BlockWithTraits { self } - static func makeBlockView() -> BlockView { TestView() } - func isBestViewForReuse(_ view: BlockView) -> Bool { if let path, path == ((view as? TestView)?.block as? TestBlock)?.path { true @@ -255,17 +259,17 @@ private class TestBlock: BlockWithTraits { (view as! TestView).block = self } - init(_ path: UIElementPath? = nil) { - self.path = path - } } private final class TestView: BlockView, VisibleBoundsTrackingLeaf { var block: TestBlock? + var effectiveBackgroundColor: UIColor? { backgroundColor } } private final class OtherTestBlock: UIViewRenderable { + init() {} + static func makeBlockView() -> BlockView { OtherTestView() } func canConfigureBlockView(_ view: BlockView) -> Bool { @@ -279,7 +283,6 @@ private final class OtherTestBlock: UIViewRenderable { renderingDelegate _: RenderingDelegate? ) {} - init() {} } private final class OtherTestView: BlockView, VisibleBoundsTrackingLeaf { diff --git a/client/ios/LayoutKit/LayoutKitTests/UI/Views/TextSelectionTests.swift b/client/ios/LayoutKit/LayoutKitTests/UI/Views/TextSelectionTests.swift index 027d5b780..d21d95a50 100644 --- a/client/ios/LayoutKit/LayoutKitTests/UI/Views/TextSelectionTests.swift +++ b/client/ios/LayoutKit/LayoutKitTests/UI/Views/TextSelectionTests.swift @@ -3,6 +3,53 @@ import VGSL import XCTest final class TextSelectionTests: XCTestCase { + private let rect = CGRect(x: 0, y: 0, width: 100, height: 100) + + private lazy var context: CGContext = { + let width = Int(rect.width) + let height = Int(rect.height) + let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) + let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue + + return CGContext( + data: nil, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: width * 4, + space: colorSpace!, + bitmapInfo: bitmapInfo + )! + }() + + private let text = NSAttributedString(string: "Hello world!") + + private lazy var point1: TextSelection.Point = .init( + point: CGPoint(x: 5, y: 10), + viewBounds: rect + ) + + private lazy var point2: TextSelection.Point = .init( + point: CGPoint(x: 45, y: 10), + viewBounds: rect + ) + + private lazy var words: [String] = text.string.components(separatedBy: " ") + + private lazy var model: TextSelection.TextModel = { + let textLayout: AttributedStringLayout = text.drawAndGetLayout( + inContext: context, + verticalPosition: .top, + rect: rect, + textInsets: .zero + ) + + return TextSelection.TextModel( + layout: textLayout, + text: text + ) + }() + func testInitCreatesValidRangeLowerBound() { let results: [(value: TextSelection.Point, result: Int)] = [ ( @@ -99,8 +146,6 @@ final class TextSelectionTests: XCTestCase { } } - private let rect = CGRect(x: 0, y: 0, width: 100, height: 100) - private func makeSelection(point: TextSelection.Point) -> TextSelection { TextSelection( textModel: model, @@ -109,53 +154,9 @@ final class TextSelectionTests: XCTestCase { )! } - private lazy var context: CGContext = { - let width = Int(rect.width) - let height = Int(rect.height) - let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) - let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue - - return CGContext( - data: nil, - width: width, - height: height, - bitsPerComponent: 8, - bytesPerRow: width * 4, - space: colorSpace!, - bitmapInfo: bitmapInfo - )! - }() - - private let text = NSAttributedString(string: "Hello world!") - - private lazy var point1: TextSelection.Point = .init( - point: CGPoint(x: 5, y: 10), - viewBounds: rect - ) - - private lazy var point2: TextSelection.Point = .init( - point: CGPoint(x: 45, y: 10), - viewBounds: rect - ) - - private lazy var words: [String] = text.string.components(separatedBy: " ") - private func range(of word: String) -> NSRange { let text = text.string as NSString return text.range(of: word) } - private lazy var model: TextSelection.TextModel = { - let textLayout: AttributedStringLayout = text.drawAndGetLayout( - inContext: context, - verticalPosition: .top, - rect: rect, - textInsets: .zero - ) - - return TextSelection.TextModel( - layout: textLayout, - text: text - ) - }() } diff --git a/client/ios/LayoutKit/LayoutKitTests/Utils.swift b/client/ios/LayoutKit/LayoutKitTests/Utils.swift index 2158768a6..4f9c7689d 100644 --- a/client/ios/LayoutKit/LayoutKitTests/Utils.swift +++ b/client/ios/LayoutKit/LayoutKitTests/Utils.swift @@ -4,9 +4,11 @@ import XCTest public final class TestTimer: TimerType { public let timeInterval: TimeInterval + public var fireDate: Date + public private(set) var isValid = true + private let block: () -> Void private let repeating: Bool - public var fireDate: Date public init( fireDate: Date = .distantFuture, @@ -20,8 +22,6 @@ public final class TestTimer: TimerType { self.repeating = repeating } - public private(set) var isValid = true - public func fire() { precondition(isValid) block() diff --git a/client/ios/LayoutKit/LayoutKitTests/ViewModels/GalleryViewLayoutTests.swift b/client/ios/LayoutKit/LayoutKitTests/ViewModels/GalleryViewLayoutTests.swift index cc011bf69..f2d30b19c 100644 --- a/client/ios/LayoutKit/LayoutKitTests/ViewModels/GalleryViewLayoutTests.swift +++ b/client/ios/LayoutKit/LayoutKitTests/ViewModels/GalleryViewLayoutTests.swift @@ -332,6 +332,46 @@ final class GalleryViewLayoutTests: XCTestCase { } private enum Blocks { + static let smallCount = 2 + static let bigCount = 2 + static let resizableCount = 2 + static let totalCount = bigCount + smallCount + resizableCount + static let blocksCount = smallCount + bigCount + resizableCount + static let smallSize = CGFloat(64) + static let bigSize = CGFloat(128) + static let resizableSize = CGFloat(1) + static let gapSize = CGFloat(1) + static let bigAxialSize = bigSize * CGFloat(bigCount) + static let smallAxialSize = smallSize * CGFloat(smallCount) + static let resizableAxialSize = resizableSize * CGFloat(resizableCount) + static let gapAxialSize = gapSize * CGFloat(blocksCount + 1) + static let framesAxialSize = bigAxialSize + smallAxialSize + resizableAxialSize + gapSize * + CGFloat(max(blocksCount - 1, 0)) + static let contentAxialSize = bigAxialSize + smallAxialSize + resizableAxialSize + gapAxialSize + static let contentAxialSizeWithoutSideGaps = contentAxialSize - 2 * gapSize + static let contentCrossMaxSize = max(bigSize, smallSize) + static let horizontalCenterModel = makeModel() + static let horizontalLeadingModel = makeModel(crossAlignment: .leading) + static let horizontalTrailingModel = makeModel(crossAlignment: .trailing) + static let verticalCenterModel = makeModel(direction: .vertical) + static let verticalLeadingModel = makeModel( + direction: .vertical, + crossAlignment: .leading + ) + static let verticalTrailingModel = makeModel( + direction: .vertical, + crossAlignment: .trailing + ) + static let fixedPagingModel = makeModel(scrollMode: .fixedPaging(pageSize: boundsSize.width)) + static let autoPagingModel = makeModel(scrollMode: .autoPaging(inertionEnabled: true)) + static let fixedInsets = SideInsets(leading: 4, trailing: 10) + static let resizableInsets = InsetMode.Resizable(minValue: 10, maxViewportSize: 14) + static let horizontalResizableModel = makeModel(metrics: .resizableAxial) + static let verticalResizableModel = makeModel( + direction: .vertical, + metrics: .resizableAxial + ) + private static let horizontallyResizable: Block = SeparatorBlock(direction: .horizontal) private static let verticallyResizable: Block = SeparatorBlock(direction: .vertical) @@ -372,45 +412,6 @@ private enum Blocks { ) } - static let smallCount = 2 - static let bigCount = 2 - static let resizableCount = 2 - static let totalCount = bigCount + smallCount + resizableCount - static let blocksCount = smallCount + bigCount + resizableCount - static let smallSize = CGFloat(64) - static let bigSize = CGFloat(128) - static let resizableSize = CGFloat(1) - static let gapSize = CGFloat(1) - static let bigAxialSize = bigSize * CGFloat(bigCount) - static let smallAxialSize = smallSize * CGFloat(smallCount) - static let resizableAxialSize = resizableSize * CGFloat(resizableCount) - static let gapAxialSize = gapSize * CGFloat(blocksCount + 1) - static let framesAxialSize = bigAxialSize + smallAxialSize + resizableAxialSize + gapSize * - CGFloat(max(blocksCount - 1, 0)) - static let contentAxialSize = bigAxialSize + smallAxialSize + resizableAxialSize + gapAxialSize - static let contentAxialSizeWithoutSideGaps = contentAxialSize - 2 * gapSize - static let contentCrossMaxSize = max(bigSize, smallSize) - static let horizontalCenterModel = makeModel() - static let horizontalLeadingModel = makeModel(crossAlignment: .leading) - static let horizontalTrailingModel = makeModel(crossAlignment: .trailing) - static let verticalCenterModel = makeModel(direction: .vertical) - static let verticalLeadingModel = makeModel( - direction: .vertical, - crossAlignment: .leading - ) - static let verticalTrailingModel = makeModel( - direction: .vertical, - crossAlignment: .trailing - ) - static let fixedPagingModel = makeModel(scrollMode: .fixedPaging(pageSize: boundsSize.width)) - static let autoPagingModel = makeModel(scrollMode: .autoPaging(inertionEnabled: true)) - static let fixedInsets = SideInsets(leading: 4, trailing: 10) - static let resizableInsets = InsetMode.Resizable(minValue: 10, maxViewportSize: 14) - static let horizontalResizableModel = makeModel(metrics: .resizableAxial) - static let verticalResizableModel = makeModel( - direction: .vertical, - metrics: .resizableAxial - ) } private let boundsSize = CGSize(width: 64, height: 64) diff --git a/client/ios/LayoutKit/LayoutKitTests/ViewModels/TabbedPages/TabContentsViewModelTests.swift b/client/ios/LayoutKit/LayoutKitTests/ViewModels/TabbedPages/TabContentsViewModelTests.swift index ccd0b2540..23280259d 100644 --- a/client/ios/LayoutKit/LayoutKitTests/ViewModels/TabbedPages/TabContentsViewModelTests.swift +++ b/client/ios/LayoutKit/LayoutKitTests/ViewModels/TabbedPages/TabContentsViewModelTests.swift @@ -3,11 +3,12 @@ import Foundation import XCTest final class TabContentsViewModelTests: XCTestCase { + func test_WhenCreatingFromEmptyTabs_ThrowsMissingChildrenError() { + XCTAssertThrowsError(try makeMe(pages: []), BlockError("Tab error: no children provided")) + } + private func makeMe(pages: [TabPageViewModel]) throws -> TabContentsViewModel { try TabContentsViewModel(pages: pages, pagesHeight: .default) } - func test_WhenCreatingFromEmptyTabs_ThrowsMissingChildrenError() { - XCTAssertThrowsError(try makeMe(pages: []), BlockError("Tab error: no children provided")) - } } diff --git a/client/ios/LayoutKitPlayground/GenericViewBlock.playground/Contents.swift b/client/ios/LayoutKitPlayground/GenericViewBlock.playground/Contents.swift index c533ac030..44652927a 100644 --- a/client/ios/LayoutKitPlayground/GenericViewBlock.playground/Contents.swift +++ b/client/ios/LayoutKitPlayground/GenericViewBlock.playground/Contents.swift @@ -29,6 +29,12 @@ private final class SampleView: UIView { fatalError("init(coder:) has not been implemented") } + override func layoutSubviews() { + super.layoutSubviews() + label.center = bounds.center + gradientLayer.frame = bounds + } + private func setupView(text: String = "") { label.text = text label.sizeToFit() @@ -36,11 +42,6 @@ private final class SampleView: UIView { self.addSubview(label) } - override func layoutSubviews() { - super.layoutSubviews() - label.center = bounds.center - gradientLayer.frame = bounds - } } private func createBlock() -> Block { diff --git a/client/ios/LayoutKitSnapshotTests/ReferenceSet.swift b/client/ios/LayoutKitSnapshotTests/ReferenceSet.swift index 35aacb20b..09b5fcf93 100644 --- a/client/ios/LayoutKitSnapshotTests/ReferenceSet.swift +++ b/client/ios/LayoutKitSnapshotTests/ReferenceSet.swift @@ -12,9 +12,10 @@ enum ReferenceSet { } private struct PlistContents: Decodable { - let referenceSnapshotsPath: String - enum CodingKeys: String, CodingKey { case referenceSnapshotsPath = "REFERENCE_SNAPSHOTS_PATH" } + + let referenceSnapshotsPath: String + } diff --git a/client/ios/Samples/SwiftUIIntegration/DivKitSample/SampleDivCustomBlockFactory.swift b/client/ios/Samples/SwiftUIIntegration/DivKitSample/SampleDivCustomBlockFactory.swift index 4e13c9b95..48ba9ec7d 100644 --- a/client/ios/Samples/SwiftUIIntegration/DivKitSample/SampleDivCustomBlockFactory.swift +++ b/client/ios/Samples/SwiftUIIntegration/DivKitSample/SampleDivCustomBlockFactory.swift @@ -48,6 +48,12 @@ private final class SampleView: UIView { setupView() } + override func layoutSubviews() { + super.layoutSubviews() + label.center = bounds.center + gradientLayer.frame = bounds + } + private func setupView(text: String = "") { label.text = text label.sizeToFit() @@ -55,9 +61,4 @@ private final class SampleView: UIView { self.addSubview(label) } - override func layoutSubviews() { - super.layoutSubviews() - label.center = bounds.center - gradientLayer.frame = bounds - } } diff --git a/client/ios/Samples/UIKitIntegration/DivKitSample/SampleDivCustomBlockFactory.swift b/client/ios/Samples/UIKitIntegration/DivKitSample/SampleDivCustomBlockFactory.swift index 4e13c9b95..48ba9ec7d 100644 --- a/client/ios/Samples/UIKitIntegration/DivKitSample/SampleDivCustomBlockFactory.swift +++ b/client/ios/Samples/UIKitIntegration/DivKitSample/SampleDivCustomBlockFactory.swift @@ -48,6 +48,12 @@ private final class SampleView: UIView { setupView() } + override func layoutSubviews() { + super.layoutSubviews() + label.center = bounds.center + gradientLayer.frame = bounds + } + private func setupView(text: String = "") { label.text = text label.sizeToFit() @@ -55,9 +61,4 @@ private final class SampleView: UIView { self.addSubview(label) } - override func layoutSubviews() { - super.layoutSubviews() - label.center = bounds.center - gradientLayer.frame = bounds - } } diff --git a/client/ios/Serialization/DeserializationError.swift b/client/ios/Serialization/DeserializationError.swift index 7a34bd1e3..fd015bbb8 100644 --- a/client/ios/Serialization/DeserializationError.swift +++ b/client/ios/Serialization/DeserializationError.swift @@ -17,21 +17,19 @@ public indirect enum DeserializationError: Error, CustomStringConvertible { case composite(error: DerivedError, causes: NonEmptyArray) case unexpectedError(message: String) - public var description: String { - getDescription(path: []) + public enum DerivedError: Error, CustomStringConvertible { + case invalidValue(result: Any?, from: Any?) + + public var description: String { + switch self { + case let .invalidValue(result, value): + "Invalid value: '\(dbgStrLimited(result))', from: '\(dbgStrLimited(value))'" + } + } } - private func getDescription(path: [String]) -> String { - switch self { - case let .composite(error, causes): - "[\(path.joined(separator: "/"))]: \(error)," + - " caused by " + - "\(causes.map { $0.getDescription(path: path) }.joined(separator: "; "))" - case let .nestedObjectError(field, error): - error.getDescription(path: path + [field]) - default: - "[\(path.joined(separator: "/"))]: \(errorMessage)" - } + public var description: String { + getDescription(path: []) } public var errorMessage: String { @@ -69,26 +67,6 @@ public indirect enum DeserializationError: Error, CustomStringConvertible { getUserInfo([]) } - private func getUserInfo(_ path: [String]) -> [String: String] { - var userInfo: [String: String] = [:] - - switch self { - case let .nestedObjectError(field, error): - return error.getUserInfo(path + [field]) - case .composite: - userInfo["root_causes"] = - "\(rootCauses.map { $0.getDescription(path: path) }.joined(separator: "; "))" - default: - break - } - - return userInfo + [ - "subkind": subkind, - "path": path.joined(separator: "/"), - "description": errorMessage, - ] - } - public var rootCauses: [DeserializationError] { switch self { case let .composite(_, causes): @@ -118,16 +96,39 @@ public indirect enum DeserializationError: Error, CustomStringConvertible { } } - public enum DerivedError: Error, CustomStringConvertible { - case invalidValue(result: Any?, from: Any?) - - public var description: String { - switch self { - case let .invalidValue(result, value): - "Invalid value: '\(dbgStrLimited(result))', from: '\(dbgStrLimited(value))'" - } + private func getDescription(path: [String]) -> String { + switch self { + case let .composite(error, causes): + "[\(path.joined(separator: "/"))]: \(error)," + + " caused by " + + "\(causes.map { $0.getDescription(path: path) }.joined(separator: "; "))" + case let .nestedObjectError(field, error): + error.getDescription(path: path + [field]) + default: + "[\(path.joined(separator: "/"))]: \(errorMessage)" } } + + private func getUserInfo(_ path: [String]) -> [String: String] { + var userInfo: [String: String] = [:] + + switch self { + case let .nestedObjectError(field, error): + return error.getUserInfo(path + [field]) + case .composite: + userInfo["root_causes"] = + "\(rootCauses.map { $0.getDescription(path: path) }.joined(separator: "; "))" + default: + break + } + + return userInfo + [ + "subkind": subkind, + "path": path.joined(separator: "/"), + "description": errorMessage, + ] + } + } fileprivate func dbgStrLimited(_ val: (some Any)?, limit: UInt = 100) -> String {