Warning for too small clickable element

commit_hash:de018d18bbd539f7a35949cbf0a3a18942a17d56
This commit is contained in:
shakhavn
2024-12-19 19:22:09 +03:00
parent e01a5e4364
commit 607deeb857
15 changed files with 366 additions and 74 deletions
+3
View File
@@ -11803,6 +11803,9 @@
"client/ios/DivKit/Animators/ProgressInterpolator.swift":"divkit/public/client/ios/DivKit/Animators/ProgressInterpolator.swift",
"client/ios/DivKit/Animators/ValueInterpolator.swift":"divkit/public/client/ios/DivKit/Animators/ValueInterpolator.swift",
"client/ios/DivKit/Debug/Block+DebugInfo.swift":"divkit/public/client/ios/DivKit/Debug/Block+DebugInfo.swift",
"client/ios/DivKit/Debug/DebugBlock+UIViewRenderableBlock.swift":"divkit/public/client/ios/DivKit/Debug/DebugBlock+UIViewRenderableBlock.swift",
"client/ios/DivKit/Debug/DebugBlock.swift":"divkit/public/client/ios/DivKit/Debug/DebugBlock.swift",
"client/ios/DivKit/Debug/DebugErrorCollector.swift":"divkit/public/client/ios/DivKit/Debug/DebugErrorCollector.swift",
"client/ios/DivKit/Debug/DebugParams.swift":"divkit/public/client/ios/DivKit/Debug/DebugParams.swift",
"client/ios/DivKit/Debug/ErrorListView.swift":"divkit/public/client/ios/DivKit/Debug/ErrorListView.swift",
"client/ios/DivKit/DivAccessibilityElementsStorage.swift":"divkit/public/client/ios/DivKit/DivAccessibilityElementsStorage.swift",
+7 -61
View File
@@ -7,77 +7,23 @@ import VGSL
extension Block {
func addingDebugInfo(context: DivBlockModelingContext) -> Block {
let debugParams = context.debugParams
guard debugParams.isDebugInfoEnabled else {
guard debugParams.isDebugInfoEnabled,
let debugErrorCollector = context.debugErrorCollector else {
return self
}
let errors = context.errorsStorage.errors
let errorsCount = errors.count
guard errorsCount > 0 else {
return self
}
let counterText = errorsCount > maxCount
? "\(maxCount)+"
: "\(errorsCount)"
let typo = Typo(font: context.fontProvider.font(size: 14))
.with(color: .white)
let action = UserInterfaceAction(
payload: .url(DebugInfoBlock.showOverlayURL),
path: context.parentPath + "div_errors_indicator",
accessibilityElement: nil
)
let counter = TextBlock(
widthTrait: .intrinsic,
text: counterText.with(typo: typo)
)
let indicator = counter
.addingVerticalGaps(errorsButtonCounterGaps)
.addingHorizontalGaps(calculateCounterHorizontalGaps(counter: counter))
.addingDecorations(
boundary: .clipCorner(radius: 10),
backgroundColor: .red
)
.addingEdgeInsets(debugParams.errorCounterInsets)
.addingEdgeGaps(2)
.addingAccessibilityID(withTraits: (
"divLayoutErrorCounter",
.button
))
.addingDecorations(action: action)
let debugInfoBlock = DebugInfoBlock(
child: indicator,
showDebugInfo: {
#if os(iOS)
debugParams.showDebugInfo(ErrorListView(errors: errors.map(\.prettyMessage)))
#else
return
#endif
}
let debugBlock = DebugBlock(
errorCollector: debugErrorCollector,
showDebugInfo: debugParams.showDebugInfo
)
let block = LayeredBlock(
widthTrait: calculatedWidthTrait,
heightTrait: calculatedHeightTrait,
children: [self, debugInfoBlock]
verticalChildrenAlignment: .center,
children: [self, debugBlock]
)
return block
}
}
private func calculateCounterHorizontalGaps(counter: Block) -> CGFloat {
let additionalGap = (counter.intrinsicSize.height - counter.intrinsicSize.width) / 2
return max(
errorsButtonCounterGaps,
errorsButtonCounterGaps + additionalGap
)
}
private let errorsButtonCounterGaps: CGFloat = 4
private let maxCount = 9999
@@ -0,0 +1,97 @@
import UIKit
import LayoutKit
import VGSL
extension DebugBlock: UIViewRenderable {
static func makeBlockView() -> any LayoutKit.BlockView {
DebugBlockView()
}
func canConfigureBlockView(_ view: any LayoutKit.BlockView) -> Bool {
view is DebugBlockView
}
func configureBlockView(
_ view: BlockView,
observer _: ElementStateObserver?,
overscrollDelegate _: ScrollDelegate?,
renderingDelegate _: RenderingDelegate?
) {
(view as? DebugBlockView)?.configure(
errorCollector: errorCollector,
showDebugInfo: showDebugInfo
)
}
}
private final class DebugBlockView: BlockView, VisibleBoundsTrackingContainer {
let effectiveBackgroundColor: UIColor? = nil
let visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] = []
private var showDebugInfo: ((ViewType) -> Void)?
private var errorCollector: DebugErrorCollector?
private let disposePool = AutodisposePool()
private let errorsLabel: UILabel = {
let label = UILabel(frame: CGRect(origin: .zero, size: CGSize(squareDimension: 50.0)))
label.text = "0"
label.numberOfLines = 1
label.font = .systemFont(ofSize: 14)
label.textColor = .white
label.textAlignment = .center
label.backgroundColor = .red
label.isHidden = true
return label
}()
init() {
super.init(frame: .zero)
addSubview(errorsLabel)
accessibilityIdentifier = "divLayoutErrorCounter"
accessibilityTraits = .button
clipsToBounds = true
addGestureRecognizer(UITapGestureRecognizer(
target: self,
action: #selector(handleTapGesture(_:))
))
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(
errorCollector: DebugErrorCollector,
showDebugInfo: @escaping (ViewType) -> Void
) {
self.showDebugInfo = showDebugInfo
if self.errorCollector !== errorCollector {
self.errorCollector = errorCollector
errorCollector.observableErrorCount.currentAndNewValues.addObserver { [weak self] _ in
self?.updateCountLabel()
}.dispose(in: disposePool)
}
}
override func layoutSubviews() {
super.layoutSubviews()
errorsLabel.frame = bounds
layer.cornerRadius = bounds.size.height / 2
}
private func updateCountLabel() {
let errorsCount = errorCollector?.totalErrorCount ?? 0
errorsLabel.isHidden = errorsCount == 0
errorsLabel.text = "\(min(maxCount, errorsCount))"
}
@objc func handleTapGesture(_: UITapGestureRecognizer) {
guard let showDebugInfo, let errorCollector, errorCollector.totalErrorCount > 0 else { return }
showDebugInfo(ErrorListView(errors: errorCollector.errorList))
}
}
private let showOverlayURL = URL(string: "debugInfo://show")!
private let maxCount = 9999
+48
View File
@@ -0,0 +1,48 @@
import Foundation
import LayoutKit
import VGSL
final class DebugBlock: BlockWithTraits, LayoutCachingDefaultImpl {
let widthTrait = LayoutTrait.fixed(buttonSize)
let heightTrait = LayoutTrait.fixed(buttonSize)
var intrinsicContentWidth: CGFloat {
widthTrait.fixedValue ?? 0.0
}
func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat {
heightTrait.fixedValue ?? 0.0
}
let errorCollector: DebugErrorCollector
let showDebugInfo: (ViewType) -> Void
init(
errorCollector: DebugErrorCollector,
showDebugInfo: @escaping (ViewType) -> Void
) {
self.errorCollector = errorCollector
self.showDebugInfo = showDebugInfo
}
func equals(_ other: any LayoutKit.Block) -> Bool {
if self === other { return true }
guard let other = other as? DebugBlock else { return false }
return errorCollector === other.errorCollector
}
var debugDescription: String {
"DebugBlock errors: \(errorCollector.debugDescription))"
}
func getImageHolders() -> [any VGSLUI.ImageHolder] {
[]
}
func updated(withStates _: LayoutKit.BlocksState) throws -> Self {
self
}
}
private let buttonSize = 50.0
@@ -0,0 +1,50 @@
import VGSLFundamentals
final class DebugErrorCollector: DivReporter {
private let wrappedDivReporter: DivReporter
var errorStorage: DivErrorsStorage
private(set) var layoutErrors = [DivError]()
let observableErrorCount = ObservableProperty<Int>(initialValue: 0)
init(
wrappedDivReporter: DivReporter,
errorStorage: DivErrorsStorage
) {
self.wrappedDivReporter = wrappedDivReporter
self.errorStorage = errorStorage
}
func reportError(cardId: DivCardID, error: DivError) {
wrappedDivReporter.reportError(cardId: cardId, error: error)
guard !hasError(error) else { return }
layoutErrors.append(error)
observableErrorCount.value += 1
}
func reportAction(cardId: DivCardID, info: DivActionInfo) {
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
&& $0.message == error.message
&& $0.path == error.path
&& $0.level == error.level
}
}
}
@@ -30,6 +30,7 @@ public struct DivBlockModelingContext {
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?
@@ -102,6 +103,7 @@ public struct DivBlockModelingContext {
scheduler: scheduler,
parentScrollView: parentScrollView,
errorsStorage: errorsStorage,
debugErrorCollector: nil,
layoutDirection: layoutDirection,
variableTracker: variableTracker,
persistentValuesStorage: persistentValuesStorage,
@@ -136,6 +138,7 @@ public struct DivBlockModelingContext {
scheduler: Scheduling?,
parentScrollView: ScrollView?,
errorsStorage: DivErrorsStorage?,
debugErrorCollector: DebugErrorCollector?,
layoutDirection: UserInterfaceLayoutDirection,
variableTracker: DivVariableTracker?,
persistentValuesStorage: DivPersistentValuesStorage?,
@@ -165,6 +168,7 @@ public struct DivBlockModelingContext {
self.scheduler = scheduler ?? TimerScheduler()
self.parentScrollView = parentScrollView
let errorsStorage = errorsStorage ?? DivErrorsStorage(errors: [])
self.debugErrorCollector = debugErrorCollector
self.errorsStorage = errorsStorage
self.layoutDirection = layoutDirection
self.variableTracker = variableTracker
+27
View File
@@ -13,6 +13,7 @@ public protocol DivError: CustomStringConvertible {
public enum DivErrorKind: String {
case deserialization
case blockModeling = "divModeling"
case layout
case expression
case unknown
}
@@ -105,6 +106,32 @@ struct DivBlockModelingWarning: DivError {
}
}
struct DivLayoutError: DivError {
public let kind = DivErrorKind.layout
public let level = DivErrorLevel.error
public let message: String
public let path: UIElementPath
init(_ message: String, path: UIElementPath) {
self.message = message
self.path = path
DivKitLogger.error(description)
}
}
struct DivLayoutWarning: DivError {
public let kind = DivErrorKind.layout
public let level = DivErrorLevel.warning
public let message: String
public let path: UIElementPath
init(_ message: String, path: UIElementPath) {
self.message = message
self.path = path
DivKitLogger.warning(description)
}
}
struct DivExpressionError: Error, DivError {
public let kind = DivErrorKind.expression
public let message: String
+76 -1
View File
@@ -50,6 +50,7 @@ public final class DivKitComponents {
private let variableTracker = DivVariableTracker()
private let idToPath = IdToPath()
private let animatorController = DivAnimatorController()
private var debugErrorCollectors = [DivCardID: DebugErrorCollector]()
/// 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.
@@ -231,6 +232,7 @@ public final class DivKitComponents {
tooltipManager.reset()
idToPath.reset()
animatorController.reset()
debugErrorCollectors = [:]
}
public func reset(cardId: DivCardID) {
@@ -244,6 +246,7 @@ public final class DivKitComponents {
timerStorage.reset(cardId: cardId)
idToPath.reset(cardId: cardId)
animatorController.reset(cardId: cardId)
debugErrorCollectors[cardId] = nil
}
/// When using DivView, use DivData.resolve to avoid adding variables twice.
@@ -313,6 +316,7 @@ public final class DivKitComponents {
? DivStateManager()
: stateManagement.getStateManagerForCard(cardId: cardId)
let errorsStorage = DivErrorsStorage(errors: [])
return DivBlockModelingContext(
viewId: viewId,
cardLogId: nil,
@@ -337,7 +341,12 @@ public final class DivKitComponents {
debugParams: debugParams,
scheduler: nil,
parentScrollView: parentScrollView,
errorsStorage: nil,
errorsStorage: errorsStorage,
debugErrorCollector: debugErrorCollector(
for: cardId,
debugParams: debugParams,
errorsStorage: errorsStorage
),
layoutDirection: layoutDirection,
variableTracker: variableTracker,
persistentValuesStorage: persistentValuesStorage,
@@ -379,6 +388,28 @@ public final class DivKitComponents {
updateAggregator.forceUpdate()
}
@_spi(Internal)
public func renderingDelegate(for cardId: DivCardID) -> RenderingDelegate {
ErrorsReportingRenderingDelegate(
wrappedRenderingDelegate: tooltipManager,
cardId: cardId,
divReporter: debugErrorCollectors[cardId]
)
}
private func debugErrorCollector(
for cardId: DivCardID,
debugParams: DebugParams,
errorsStorage: DivErrorsStorage
) -> DebugErrorCollector? {
guard debugParams.isDebugInfoEnabled else { return nil }
let collector = debugErrorCollectors.getOrCreate(cardId, factory: {
DebugErrorCollector(wrappedDivReporter: reporter, errorStorage: errorsStorage)
})
collector.errorStorage = errorsStorage
return collector
}
private func onVariablesChanged(event: DivVariablesStorage.ChangeEvent) {
switch event.kind {
case let .global(variables):
@@ -397,3 +428,47 @@ let defaultPlayerFactory: PlayerFactory? = DefaultPlayerFactory()
#else
let defaultPlayerFactory: PlayerFactory? = nil
#endif
private final class ErrorsReportingRenderingDelegate: RenderingDelegate {
private let wrappedRenderingDelegate: RenderingDelegate
private let cardId: DivCardID
private let divReporter: DivReporter?
init(
wrappedRenderingDelegate: RenderingDelegate,
cardId: DivCardID,
divReporter: DivReporter?
) {
self.wrappedRenderingDelegate = wrappedRenderingDelegate
self.cardId = cardId
self.divReporter = divReporter
}
func reportRenderingError(message: String, isWarning: Bool, path: UIElementPath) {
wrappedRenderingDelegate.reportRenderingError(
message: message,
isWarning: isWarning,
path: path
)
guard let divReporter else { return }
let error: DivError = if isWarning {
DivLayoutWarning(message, path: path)
}
else {
DivLayoutError(message, path: path)
}
divReporter.reportError(cardId: cardId, error: error)
}
func mapView(_ view: any LayoutKit.BlockView, to id: LayoutKit.BlockViewID) {
wrappedRenderingDelegate.mapView(view, to: id)
}
func tooltipAnchorViewAdded(anchorView: any LayoutKit.TooltipAnchorView) {
wrappedRenderingDelegate.tooltipAnchorViewAdded(anchorView: anchorView)
}
func tooltipAnchorViewRemoved(anchorView: any LayoutKit.TooltipAnchorView) {
wrappedRenderingDelegate.tooltipAnchorViewRemoved(anchorView: anchorView)
}
}
@@ -72,7 +72,8 @@ extension Block {
actionAnimation: actionsHolder.actionAnimation
.resolveActionAnimation(context.expressionResolver),
doubleTapActions: doubletapActions,
longTapActions: longtapActions
longTapActions: longtapActions,
path: context.parentPath
)
}
}
+8 -2
View File
@@ -180,17 +180,23 @@ public final class DivView: VisibleBoundsTrackingView {
}
private func update(block: Block) {
let renderingDelegate: RenderingDelegate?
if blockProvider?.id != nil, let divCardId = blockProvider?.cardId {
renderingDelegate = divKitComponents.renderingDelegate(for: divCardId)
} else {
renderingDelegate = nil
}
if let blockView, block.canConfigureBlockView(blockView) {
block.configureBlockView(
blockView,
observer: self,
overscrollDelegate: nil,
renderingDelegate: divKitComponents.tooltipManager
renderingDelegate: renderingDelegate
)
} else {
blockView = block.makeBlockView(
observer: self,
renderingDelegate: divKitComponents.tooltipManager
renderingDelegate: renderingDelegate
)
}
invalidateIntrinsicContentSize()
@@ -28,7 +28,8 @@ extension Block {
tooltips: [BlockTooltip]? = nil,
forceWrapping: Bool,
accessibilityElement: AccessibilityElement? = nil,
reuseId: String?
reuseId: String?,
path: UIElementPath?
) -> Block {
let hasAlpha = alpha != nil && alpha?.isApproximatelyEqualTo(1) != true
let anythingToApplyExceptBoundary =
@@ -72,7 +73,8 @@ extension Block {
visibilityParams: visibilityParams ?? block.visibilityParams,
tooltips: [tooltips, block.tooltips].compactMap { $0 }.flatMap { $0 },
accessibilityElement: accessibilityElement,
reuseId: reuseId
reuseId: reuseId,
path: path
)
}
@@ -97,7 +99,8 @@ extension Block {
visibilityParams: visibilityParams,
tooltips: tooltips ?? [],
accessibilityElement: accessibilityElement,
reuseId: reuseId
reuseId: reuseId,
path: path
)
}
@@ -126,7 +129,8 @@ extension Block {
tooltips: [BlockTooltip]? = nil,
forceWrapping: Bool = false,
accessibilityElement: AccessibilityElement? = nil,
reuseId: String? = nil
reuseId: String? = nil,
path: UIElementPath? = nil
) -> Block {
let decoratedBlock = applyDecoratingBlockProperties(
boundary: boundary,
@@ -144,7 +148,8 @@ extension Block {
tooltips: tooltips,
forceWrapping: forceWrapping,
accessibilityElement: accessibilityElement,
reuseId: reuseId
reuseId: reuseId,
path: path
)
return decoratedBlock.shaded(with: shadow)
}
@@ -26,6 +26,7 @@ final class DecoratingBlock: WrapperBlock {
let tooltips: [BlockTooltip]
let accessibilityElement: AccessibilityElement?
let reuseId: String
let path: UIElementPath?
init(
child: Block,
@@ -44,7 +45,8 @@ final class DecoratingBlock: WrapperBlock {
visibilityParams: VisibilityParams? = nil,
tooltips: [BlockTooltip] = [],
accessibilityElement: AccessibilityElement? = nil,
reuseId: String? = nil
reuseId: String? = nil,
path: UIElementPath? = nil
) {
self.child = child
self.backgroundColor = backgroundColor
@@ -63,6 +65,7 @@ final class DecoratingBlock: WrapperBlock {
self.tooltips = tooltips
self.accessibilityElement = accessibilityElement
self.reuseId = reuseId ?? DecoratingBlock.defaultReuseId
self.path = path
}
var intrinsicContentWidth: CGFloat {
@@ -148,7 +151,8 @@ extension DecoratingBlock {
visibilityParams: VisibilityParams? = nil,
tooltips: [BlockTooltip]? = nil,
accessibilityElement: AccessibilityElement? = nil,
reuseId: String? = nil
reuseId: String? = nil,
path: UIElementPath? = nil
) -> DecoratingBlock {
DecoratingBlock(
child: child ?? self.child,
@@ -167,7 +171,8 @@ extension DecoratingBlock {
visibilityParams: visibilityParams ?? self.visibilityParams,
tooltips: tooltips ?? self.tooltips,
accessibilityElement: accessibilityElement ?? self.accessibilityElement,
reuseId: reuseId ?? self.reuseId
reuseId: reuseId ?? self.reuseId,
path: path ?? self.path
)
}
}
@@ -48,7 +48,8 @@ extension DecoratingBlock {
visibilityParams: visibilityParams,
tooltips: tooltips,
accessibility: accessibilityElement,
reuseId: reuseId
reuseId: reuseId,
path: path
)
view.configure(
model: model,
@@ -122,6 +123,7 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT
let tooltips: [BlockTooltip]
let accessibility: AccessibilityElement?
let reuseId: String?
let path: UIElementPath?
var hasResponsiveUI: Bool {
actions.hasPayload || longTapActions.hasPayload || doubleTapActions.hasPayload
@@ -257,6 +259,18 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT
doubleTapRecognizer?.isEnabled = model.shouldHandleDoubleTap
longPressRecognizer?.isEnabled = model.shouldHandleLongTap
}
private func checkTouchableArea() {
guard tapRecognizer != nil || doubleTapRecognizer != nil || longPressRecognizer != nil else { return }
guard !bounds.size.isApproximatelyEqualTo(.zero) 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") }
@@ -321,6 +335,8 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT
}
blurView?.frame = bounds
checkTouchableArea()
guard let view = childView else { return }
@@ -7,6 +7,7 @@ public protocol RenderingDelegate: AnyObject {
func mapView(_ view: BlockView, to id: BlockViewID)
func tooltipAnchorViewAdded(anchorView: TooltipAnchorView)
func tooltipAnchorViewRemoved(anchorView: TooltipAnchorView)
func reportRenderingError(message: String, isWarning: Bool, path: UIElementPath)
}
public typealias BlockViewID = Tagged<BlockViewProtocol, String>
@@ -18,6 +19,10 @@ public protocol DivViewMetaProviding: AnyObject {
public protocol TooltipAnchorView: ViewType {
var tooltips: [BlockTooltip] { get }
}
extension RenderingDelegate {
public func reportRenderingError(message: String, isWarning: Bool, path: UIElementPath) { }
}
#else
public protocol RenderingDelegate {}
public typealias BlockViewID = Tagged<AnyObject, String>
@@ -68,6 +68,10 @@ private final class SubviewStorage: RenderingDelegate {
func tooltipAnchorViewRemoved(anchorView: TooltipAnchorView) {
wrappedRenderingDelegate?.tooltipAnchorViewRemoved(anchorView: anchorView)
}
func reportRenderingError(message: String, isWarning: Bool, path: UIElementPath) {
wrappedRenderingDelegate?.reportRenderingError(message: message, isWarning: isWarning, path: path)
}
func getView(_ id: BlockViewID) -> DetachableAnimationBlockView? {
views.first { $0.id == id }?.view