From d56d50116d9d8fd34cdc8bebf075f97f4256417e Mon Sep 17 00:00:00 2001 From: babaevmm Date: Mon, 2 Jun 2025 11:29:08 +0300 Subject: [PATCH] supported hover and press actions commit_hash:dfd0403da40ba5f5d7de502611b918ad8d37d98e --- .../DivAction/DivActionExtensions.swift | 4 ++ .../DivAction/DivActionsHolder.swift | 40 ++++++------ .../Decorations/Block+Decorations.swift | 20 ++++++ .../Blocks/Decorations/DecoratingBlock.swift | 24 +++++++ ...ecoratingBlock+UIViewRenderableBlock.swift | 65 ++++++++++++++++++- schema/div-actionable.json | 12 ++-- test_data/regression_test_data/index.json | 1 + 7 files changed, 139 insertions(+), 27 deletions(-) diff --git a/client/ios/DivKit/Extensions/DivAction/DivActionExtensions.swift b/client/ios/DivKit/Extensions/DivAction/DivActionExtensions.swift index e3cdf988a..2a69791f5 100644 --- a/client/ios/DivKit/Extensions/DivAction/DivActionExtensions.swift +++ b/client/ios/DivKit/Extensions/DivAction/DivActionExtensions.swift @@ -63,4 +63,8 @@ extension [DivAction] { public func uiActions(context: DivBlockModelingContext) -> [UserInterfaceAction] { compactMap { $0.uiAction(context: context) } } + + func nonEmptyUIActions(context: DivBlockModelingContext) -> NonEmptyArray? { + NonEmptyArray(uiActions(context: context)) + } } diff --git a/client/ios/DivKit/Extensions/DivAction/DivActionsHolder.swift b/client/ios/DivKit/Extensions/DivAction/DivActionsHolder.swift index 2dd77e04f..6cecd06dd 100644 --- a/client/ios/DivKit/Extensions/DivAction/DivActionsHolder.swift +++ b/client/ios/DivKit/Extensions/DivAction/DivActionsHolder.swift @@ -7,6 +7,10 @@ protocol DivActionsHolder { var actionAnimation: DivAnimation { get } var doubletapActions: [DivAction]? { get } var longtapActions: [DivAction]? { get } + var pressStartActions: [DivAction]? { get } + var pressEndActions: [DivAction]? { get } + var hoverStartActions: [DivAction]? { get } + var hoverEndActions: [DivAction]? { get } func resolveCaptureFocusOnAction(_ resolver: ExpressionResolver) -> Bool } @@ -31,24 +35,6 @@ extension DivActionsHolder { return NonEmptyArray(allActions) } - - fileprivate func makeDoubleTapActions( - context: DivBlockModelingContext - ) -> NonEmptyArray? { - if let actions = doubletapActions?.uiActions(context: context) { - return NonEmptyArray(actions) - } - return nil - } - - fileprivate func makeLongTapActions( - context: DivBlockModelingContext - ) -> LongTapActions? { - if let actions = longtapActions?.uiActions(context: context) { - return NonEmptyArray(actions).map(LongTapActions.actions) - } - return nil - } } extension Block { @@ -62,9 +48,17 @@ extension Block { } let actions = actionsHolder.makeActions(context: context) - let doubletapActions = actionsHolder.makeDoubleTapActions(context: context) - let longtapActions = actionsHolder.makeLongTapActions(context: context) - if actions == nil, doubletapActions == nil, longtapActions == nil { + let doubletapActions = actionsHolder.doubletapActions?.nonEmptyUIActions(context: context) + let longtapActions = actionsHolder.longtapActions?.nonEmptyUIActions(context: context) + .map(LongTapActions.actions) + let pressStartActions = actionsHolder.pressStartActions?.nonEmptyUIActions(context: context) + let pressEndActions = actionsHolder.pressEndActions?.nonEmptyUIActions(context: context) + let hoverStartActions = actionsHolder.hoverStartActions?.nonEmptyUIActions(context: context) + let hoverEndActions = actionsHolder.hoverEndActions?.nonEmptyUIActions(context: context) + + if actions == nil, doubletapActions == nil, longtapActions == nil, + pressStartActions == nil, pressEndActions == nil, + hoverStartActions == nil, hoverEndActions == nil { return self } @@ -75,6 +69,10 @@ extension Block { .resolveActionAnimation(context.expressionResolver), doubleTapActions: doubletapActions, longTapActions: longtapActions, + pressStartActions: pressStartActions, + pressEndActions: pressEndActions, + hoverStartActions: hoverStartActions, + hoverEndActions: hoverEndActions, path: context.path, captureFocusOnAction: actionsHolder.resolveCaptureFocusOnAction(context.expressionResolver) ) diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/Block+Decorations.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/Block+Decorations.swift index c4e30011b..8a6861680 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/Block+Decorations.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/Block+Decorations.swift @@ -22,6 +22,10 @@ extension Block { actionAnimation: ActionAnimation? = nil, doubleTapActions: NonEmptyArray? = nil, longTapActions: LongTapActions? = nil, + pressStartActions: NonEmptyArray? = nil, + pressEndActions: NonEmptyArray? = nil, + hoverStartActions: NonEmptyArray? = nil, + hoverEndActions: NonEmptyArray? = nil, analyticsURL: URL? = nil, visibilityParams: VisibilityParams? = nil, tooltips: [BlockTooltip]? = nil, @@ -70,6 +74,10 @@ extension Block { actionAnimation: actionAnimation ?? block.actionAnimation, doubleTapActions: doubleTapActions ?? block.doubleTapActions, longTapActions: longTapActions ?? block.longTapActions, + pressStartActions: pressStartActions ?? block.pressStartActions, + pressEndActions: pressEndActions ?? block.pressEndActions, + hoverStartActions: hoverStartActions ?? block.hoverStartActions, + hoverEndActions: hoverEndActions ?? block.hoverEndActions, analyticsURL: (analyticsURL ?? block.analyticsURL) as URL?, boundary: boundary ?? block.boundary, border: (border ?? block.border) as BlockBorder?, @@ -99,6 +107,10 @@ extension Block { actionAnimation: actionAnimation, doubleTapActions: doubleTapActions, longTapActions: longTapActions, + pressStartActions: pressStartActions, + pressEndActions: pressEndActions, + hoverStartActions: hoverStartActions, + hoverEndActions: hoverEndActions, analyticsURL: analyticsURL, boundary: boundary ?? DecoratingBlock.defaultBoundary, border: border, @@ -134,6 +146,10 @@ extension Block { actionAnimation: ActionAnimation? = nil, doubleTapActions: NonEmptyArray? = nil, longTapActions: LongTapActions? = nil, + pressStartActions: NonEmptyArray? = nil, + pressEndActions: NonEmptyArray? = nil, + hoverStartActions: NonEmptyArray? = nil, + hoverEndActions: NonEmptyArray? = nil, analyticsURL: URL? = nil, shadow: BlockShadow? = nil, visibilityParams: VisibilityParams? = nil, @@ -157,6 +173,10 @@ extension Block { actionAnimation: actionAnimation, doubleTapActions: doubleTapActions, longTapActions: longTapActions, + pressStartActions: pressStartActions, + pressEndActions: pressEndActions, + hoverStartActions: hoverStartActions, + hoverEndActions: hoverEndActions, analyticsURL: analyticsURL, visibilityParams: visibilityParams, tooltips: tooltips, diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/DecoratingBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/DecoratingBlock.swift index 38dca22b3..cb22bda77 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/DecoratingBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/DecoratingBlock.swift @@ -16,6 +16,10 @@ final class DecoratingBlock: WrapperBlock { let actionAnimation: ActionAnimation? let doubleTapActions: NonEmptyArray? let longTapActions: LongTapActions? + let pressStartActions: NonEmptyArray? + let pressEndActions: NonEmptyArray? + let hoverStartActions: NonEmptyArray? + let hoverEndActions: NonEmptyArray? let analyticsURL: URL? let boundary: BoundaryTrait let border: BlockBorder? @@ -39,6 +43,10 @@ final class DecoratingBlock: WrapperBlock { actionAnimation: ActionAnimation? = nil, doubleTapActions: NonEmptyArray? = nil, longTapActions: LongTapActions? = nil, + pressStartActions: NonEmptyArray? = nil, + pressEndActions: NonEmptyArray? = nil, + hoverStartActions: NonEmptyArray? = nil, + hoverEndActions: NonEmptyArray? = nil, analyticsURL: URL? = nil, boundary: BoundaryTrait = DecoratingBlock.defaultBoundary, border: BlockBorder? = nil, @@ -61,6 +69,10 @@ final class DecoratingBlock: WrapperBlock { self.actionAnimation = actionAnimation self.doubleTapActions = doubleTapActions self.longTapActions = longTapActions + self.pressStartActions = pressStartActions + self.pressEndActions = pressEndActions + self.hoverStartActions = hoverStartActions + self.hoverEndActions = hoverEndActions self.analyticsURL = analyticsURL self.boundary = boundary self.border = border @@ -118,6 +130,10 @@ final class DecoratingBlock: WrapperBlock { && actions == other.actions && longTapActions == other.longTapActions && doubleTapActions == other.doubleTapActions + && pressStartActions == other.pressStartActions + && pressEndActions == other.pressEndActions + && hoverStartActions == other.hoverStartActions + && hoverEndActions == other.hoverEndActions && actionAnimation == other.actionAnimation && analyticsURL == other.analyticsURL && boundary == other.boundary @@ -154,6 +170,10 @@ extension DecoratingBlock { actionAnimation: ActionAnimation? = nil, doubleTapActions: NonEmptyArray? = nil, longTapActions: LongTapActions? = nil, + pressStartActions: NonEmptyArray? = nil, + pressEndActions: NonEmptyArray? = nil, + hoverStartActions: NonEmptyArray? = nil, + hoverEndActions: NonEmptyArray? = nil, analyticsURL: URL?? = nil, boundary: BoundaryTrait? = nil, border: BlockBorder?? = nil, @@ -177,6 +197,10 @@ extension DecoratingBlock { actionAnimation: actionAnimation ?? self.actionAnimation, doubleTapActions: doubleTapActions ?? self.doubleTapActions, longTapActions: longTapActions ?? self.longTapActions, + pressStartActions: pressStartActions ?? self.pressStartActions, + pressEndActions: pressEndActions ?? self.pressEndActions, + hoverStartActions: hoverStartActions ?? self.hoverStartActions, + hoverEndActions: hoverEndActions ?? self.hoverEndActions, analyticsURL: analyticsURL ?? self.analyticsURL, boundary: boundary ?? self.boundary, border: border ?? self.border, diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift index 902587ead..c825517bc 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift @@ -37,6 +37,10 @@ extension DecoratingBlock { actionAnimation: actionAnimation, doubleTapActions: doubleTapActions, longTapActions: longTapActions, + pressStartActions: pressStartActions, + pressEndActions: pressEndActions, + hoverStartActions: hoverStartActions, + hoverEndActions: hoverEndActions, analyticsURL: analyticsURL, boundary: boundary, border: border, @@ -113,6 +117,10 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT let actionAnimation: ActionAnimation? let doubleTapActions: NonEmptyArray? let longTapActions: LongTapActions? + let pressStartActions: NonEmptyArray? + let pressEndActions: NonEmptyArray? + let hoverStartActions: NonEmptyArray? + let hoverEndActions: NonEmptyArray? let analyticsURL: URL? let boundary: BoundaryTrait let border: BlockBorder? @@ -143,6 +151,22 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT var shouldHandleDoubleTap: Bool { doubleTapActions != nil } + + var shouldHandlePress: Bool { + pressStartActions != nil || pressEndActions != nil + } + + var shouldHandleHover: Bool { + hoverStartActions != nil || hoverEndActions != nil + } + + var shouldHandleAnyAction: Bool { + shouldHandleTap || + shouldHandleLongTap || + shouldHandleDoubleTap || + shouldHandlePress || + shouldHandleHover + } } fileprivate var model: Model! @@ -183,6 +207,13 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT } } + private var hoverRecognizer: UIHoverGestureRecognizer? { + didSet { + oldValue.flatMap(removeGestureRecognizer(_:)) + hoverRecognizer.flatMap(addGestureRecognizer(_:)) + } + } + override var isHighlighted: Bool { didSet { guard oldValue != isHighlighted else { @@ -239,10 +270,11 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT } private func configureRecognizers() { - guard model.shouldHandleTap else { + guard model.shouldHandleAnyAction else { tapRecognizer?.isEnabled = false doubleTapRecognizer?.isEnabled = false longPressRecognizer?.isEnabled = false + hoverRecognizer?.isEnabled = false return } @@ -265,9 +297,17 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT ) } + 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() { @@ -315,7 +355,7 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) if result === self { - return model.shouldHandleTap ? self : nil + return model.shouldHandleAnyAction ? self : nil } else { return result } @@ -365,6 +405,16 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT updateVoiceOverFocus() } + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + model.pressStartActions?.asArray().perform(sendingFrom: self) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + model.pressEndActions?.asArray().perform(sendingFrom: self) + } + func configure( model: Model, observer: ElementStateObserver?, @@ -549,6 +599,17 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT } } + @objc private func handleHover(recognizer: UIHoverGestureRecognizer) { + switch recognizer.state { + case .began: + model.hoverStartActions?.asArray().perform(sendingFrom: self) + case .ended, .cancelled, .failed: + model.hoverEndActions?.asArray().perform(sendingFrom: self) + default: + break + } + } + deinit { if model?.tooltips.isEmpty == false { renderingDelegate?.tooltipAnchorViewRemoved(anchorView: self) diff --git a/schema/div-actionable.json b/schema/div-actionable.json index f04f866a7..1948d2dea 100644 --- a/schema/div-actionable.json +++ b/schema/div-actionable.json @@ -43,7 +43,8 @@ "$description": "translations.json#/div_actionable_press_start_actions", "platforms": [ "web", - "android" + "android", + "ios" ] }, "press_end_actions": { @@ -54,7 +55,8 @@ "$description": "translations.json#/div_actionable_press_end_actions", "platforms": [ "web", - "android" + "android", + "ios" ] }, "hover_start_actions": { @@ -65,7 +67,8 @@ "$description": "translations.json#/div_actionable_hover_start_actions", "platforms": [ "web", - "android" + "android", + "ios" ] }, "hover_end_actions": { @@ -76,7 +79,8 @@ "$description": "translations.json#/div_actionable_hover_end_actions", "platforms": [ "web", - "android" + "android", + "ios" ] }, "action_animation": { diff --git a/test_data/regression_test_data/index.json b/test_data/regression_test_data/index.json index dea46552e..6bea05539 100644 --- a/test_data/regression_test_data/index.json +++ b/test_data/regression_test_data/index.json @@ -135,6 +135,7 @@ ], "platforms": [ "android", + "ios", "web" ], "file": "actions/hover-and-press.json"