diff --git a/.mapping.json b/.mapping.json index d84e9d00..e327e65c 100644 --- a/.mapping.json +++ b/.mapping.json @@ -907,6 +907,7 @@ "Specs/DivKit/30.15.0/DivKit.podspec":"divkit/public-ios/Specs/DivKit/30.15.0/DivKit.podspec", "Specs/DivKit/30.16.0/DivKit.podspec":"divkit/public-ios/Specs/DivKit/30.16.0/DivKit.podspec", "Specs/DivKit/30.17.0/DivKit.podspec":"divkit/public-ios/Specs/DivKit/30.17.0/DivKit.podspec", + "Specs/DivKit/30.18.0/DivKit.podspec":"divkit/public-ios/Specs/DivKit/30.18.0/DivKit.podspec", "Specs/DivKit/30.2.0/DivKit.podspec":"divkit/public-ios/Specs/DivKit/30.2.0/DivKit.podspec", "Specs/DivKit/30.3.0/DivKit.podspec":"divkit/public-ios/Specs/DivKit/30.3.0/DivKit.podspec", "Specs/DivKit/30.4.0/DivKit.podspec":"divkit/public-ios/Specs/DivKit/30.4.0/DivKit.podspec", @@ -976,6 +977,7 @@ "Specs/DivKitExtensions/30.15.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/30.15.0/DivKitExtensions.podspec", "Specs/DivKitExtensions/30.16.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/30.16.0/DivKitExtensions.podspec", "Specs/DivKitExtensions/30.17.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/30.17.0/DivKitExtensions.podspec", + "Specs/DivKitExtensions/30.18.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/30.18.0/DivKitExtensions.podspec", "Specs/DivKitExtensions/30.2.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/30.2.0/DivKitExtensions.podspec", "Specs/DivKitExtensions/30.3.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/30.3.0/DivKitExtensions.podspec", "Specs/DivKitExtensions/30.4.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/30.4.0/DivKitExtensions.podspec", @@ -1027,6 +1029,7 @@ "Specs/DivKit_LayoutKit/30.15.0/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/30.15.0/DivKit_LayoutKit.podspec", "Specs/DivKit_LayoutKit/30.16.0/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/30.16.0/DivKit_LayoutKit.podspec", "Specs/DivKit_LayoutKit/30.17.0/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/30.17.0/DivKit_LayoutKit.podspec", + "Specs/DivKit_LayoutKit/30.18.0/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/30.18.0/DivKit_LayoutKit.podspec", "Specs/DivKit_LayoutKit/30.2.0/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/30.2.0/DivKit_LayoutKit.podspec", "Specs/DivKit_LayoutKit/30.3.0/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/30.3.0/DivKit_LayoutKit.podspec", "Specs/DivKit_LayoutKit/30.4.0/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/30.4.0/DivKit_LayoutKit.podspec", @@ -1078,6 +1081,7 @@ "Specs/DivKit_LayoutKitInterface/30.15.0/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/30.15.0/DivKit_LayoutKitInterface.podspec", "Specs/DivKit_LayoutKitInterface/30.16.0/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/30.16.0/DivKit_LayoutKitInterface.podspec", "Specs/DivKit_LayoutKitInterface/30.17.0/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/30.17.0/DivKit_LayoutKitInterface.podspec", + "Specs/DivKit_LayoutKitInterface/30.18.0/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/30.18.0/DivKit_LayoutKitInterface.podspec", "Specs/DivKit_LayoutKitInterface/30.2.0/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/30.2.0/DivKit_LayoutKitInterface.podspec", "Specs/DivKit_LayoutKitInterface/30.3.0/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/30.3.0/DivKit_LayoutKitInterface.podspec", "Specs/DivKit_LayoutKitInterface/30.4.0/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/30.4.0/DivKit_LayoutKitInterface.podspec", @@ -1129,6 +1133,7 @@ "Specs/DivKit_Serialization/30.15.0/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/30.15.0/DivKit_Serialization.podspec", "Specs/DivKit_Serialization/30.16.0/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/30.16.0/DivKit_Serialization.podspec", "Specs/DivKit_Serialization/30.17.0/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/30.17.0/DivKit_Serialization.podspec", + "Specs/DivKit_Serialization/30.18.0/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/30.18.0/DivKit_Serialization.podspec", "Specs/DivKit_Serialization/30.2.0/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/30.2.0/DivKit_Serialization.podspec", "Specs/DivKit_Serialization/30.3.0/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/30.3.0/DivKit_Serialization.podspec", "Specs/DivKit_Serialization/30.4.0/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/30.4.0/DivKit_Serialization.podspec", diff --git a/DivKit/Actions/FocusElementActionHandler.swift b/DivKit/Actions/FocusElementActionHandler.swift index a3b15434..95483c4c 100644 --- a/DivKit/Actions/FocusElementActionHandler.swift +++ b/DivKit/Actions/FocusElementActionHandler.swift @@ -9,10 +9,13 @@ final class FocusElementActionHandler { if let previousCard = context.blockStateStorage.getFocusedElement()?.cardId { context.updateCard(.state(previousCard)) } + let cardId = context.path.cardId + let element = IdAndCardId(id: elementId, cardId: cardId) + context.blockStateStorage.setFocused( isFocused: true, - element: IdAndCardId(id: elementId, cardId: cardId) + element: element ) context.updateCard(.state(cardId)) } diff --git a/DivKit/DivBlockStateStorage.swift b/DivKit/DivBlockStateStorage.swift index 3d6b3ba2..54a399e5 100644 --- a/DivKit/DivBlockStateStorage.swift +++ b/DivKit/DivBlockStateStorage.swift @@ -24,10 +24,16 @@ public final class DivBlockStateStorage { public let state: ElementState } + private enum FocusedElement: Equatable { + case none + case pathFocused(UIElementPath) + case idFocused(IdAndCardId) + } + public private(set) var states: BlocksState private var statesById: [IdAndCardId: ElementState] = [:] - private var focusedElement: UIElementPath? - private var focusedElementById: IdAndCardId? + + private var focusedElement: FocusedElement = .none private let lock = AllocatedUnfairLock() private let stateUpdatesPipe = SignalPipe() @@ -81,65 +87,69 @@ public final class DivBlockStateStorage { public func setFocused(isFocused: Bool, element: IdAndCardId) { lock.withLock { - if isFocused { - focusedElement = nil - focusedElementById = element - } else if self.isFocusedInternal(element: element) { - focusedElement = nil - focusedElementById = nil - } + focusedElement = isFocused ? .idFocused(element) : removeFocus(from: element) } } public func setFocused(isFocused: Bool, path: UIElementPath) { lock.withLock { - if isFocused { - focusedElement = path - focusedElementById = nil - } else if self.isFocusedInternal(path: path) { - focusedElement = nil - focusedElementById = nil - } + focusedElement = isFocused ? .pathFocused(path) : .none } } public func clearFocus() { lock.withLock { - focusedElement = nil - focusedElementById = nil + focusedElement = .none } } public func isFocused(element: IdAndCardId) -> Bool { lock.withLock { - isFocusedInternal(element: element) + isFocusedInternal(checkedElement: .idFocused(element)) } } public func isFocused(path: UIElementPath) -> Bool { lock.withLock { - isFocusedInternal(path: path) + isFocusedInternal(checkedElement: .pathFocused(path)) } } - private func isFocusedInternal(element: IdAndCardId) -> Bool { - getFocusedElement() == element + private func isFocusedInternal(checkedElement: FocusedElement) -> Bool { + switch (focusedElement, checkedElement) { + case (.none, _), (_, .none): + false + case let (.pathFocused(focusedPath), .pathFocused(checkedPath)): + focusedPath == checkedPath + case let (.idFocused(focusedId), .idFocused(checkedId)): + focusedId == checkedId + case let (.pathFocused(focusedPath), .idFocused(checkedId)): + IdAndCardId(path: focusedPath) == checkedId + case let (.idFocused(focusedId), .pathFocused(checkedPath)): + focusedId == IdAndCardId(path: checkedPath) + } } - private func isFocusedInternal(path: UIElementPath) -> Bool { - focusedElement == path || focusedElementById == IdAndCardId(path: path) + private func removeFocus(from element: IdAndCardId) -> FocusedElement { + isFocusedInternal(checkedElement: FocusedElement.idFocused(element)) ? .none : focusedElement } public func getFocusedElement() -> IdAndCardId? { - focusedElementById ?? focusedElement.map(IdAndCardId.init) + switch focusedElement { + case .none: + nil + case let .pathFocused(focusedPath): + IdAndCardId(path: focusedPath) + case let .idFocused(focusedId): + focusedId + } } public func reset() { lock.withLock { states = [:] statesById = [:] - focusedElement = nil - focusedElementById = nil + focusedElement = .none } } @@ -148,8 +158,7 @@ public final class DivBlockStateStorage { states = states.filter { $0.key.root != cardId.rawValue } statesById = statesById.filter { $0.key.cardId != cardId } if getFocusedElement()?.cardId == cardId { - focusedElement = nil - focusedElementById = nil + focusedElement = .none } } } diff --git a/DivKit/DivFlagsInfo.swift b/DivKit/DivFlagsInfo.swift index 57439381..c2986137 100644 --- a/DivKit/DivFlagsInfo.swift +++ b/DivKit/DivFlagsInfo.swift @@ -9,11 +9,19 @@ public struct DivFlagsInfo { /// Enable experimental image loading optimization public let imageLoadingOptimizationEnabled: Bool + /// Experimental tint/blur effect renderer + public let imageBlurPreferMetal: Bool + public let imageTintPreferMetal: Bool + /// Creates an instance of `DivFlagsInfo`. public init( - imageLoadingOptimizationEnabled: Bool = true + imageLoadingOptimizationEnabled: Bool = true, + imageBlurPreferMetal: Bool = true, + imageTintPreferMetal: Bool = true ) { self.imageLoadingOptimizationEnabled = imageLoadingOptimizationEnabled + self.imageBlurPreferMetal = imageBlurPreferMetal + self.imageTintPreferMetal = imageTintPreferMetal } /// The default instance of `DivFlagsInfo`. diff --git a/DivKit/DivKitInfo.swift b/DivKit/DivKitInfo.swift index 8dd5713f..b1765fd6 100644 --- a/DivKit/DivKitInfo.swift +++ b/DivKit/DivKitInfo.swift @@ -1,3 +1,3 @@ public enum DivKitInfo { - public static let version = "30.17.0" + public static let version = "30.18.0" } diff --git a/DivKit/Extensions/DivImage/DivImageExtensions.swift b/DivKit/Extensions/DivImage/DivImageExtensions.swift index 4020e7c9..204e7716 100644 --- a/DivKit/Extensions/DivImage/DivImageExtensions.swift +++ b/DivKit/Extensions/DivImage/DivImageExtensions.swift @@ -37,7 +37,9 @@ extension DivImage: DivBlockModeling, DivImageProtocol { tintColor: resolveTintColor(expressionResolver), tintMode: resolveTintMode(expressionResolver).tintMode, effects: resolveEffects(expressionResolver), - appearanceAnimation: appearanceAnimation?.resolve(expressionResolver) + appearanceAnimation: appearanceAnimation?.resolve(expressionResolver), + blurUsingMetal: context.flagsInfo.imageBlurPreferMetal, + tintUsingMetal: context.flagsInfo.imageTintPreferMetal ) } } diff --git a/DivKit/Extensions/DivInputExtensions.swift b/DivKit/Extensions/DivInputExtensions.swift index b24769fa..500a9492 100644 --- a/DivKit/Extensions/DivInputExtensions.swift +++ b/DivKit/Extensions/DivInputExtensions.swift @@ -76,7 +76,8 @@ extension DivInput: DivBlockModeling { textAlignmentHorizontal: resolveTextAlignmentHorizontal(expressionResolver).textAlignment, textAlignmentVertical: resolveTextAlignmentVertical(expressionResolver).textAlignment, paddings: paddings?.resolve(context), - isEnabled: resolveIsEnabled(expressionResolver) + isEnabled: resolveIsEnabled(expressionResolver), + maxLength: resolveMaxLength(expressionResolver) ) } diff --git a/DivKit/Extensions/DivStateExtensions.swift b/DivKit/Extensions/DivStateExtensions.swift index 4fb7948a..692a0200 100644 --- a/DivKit/Extensions/DivStateExtensions.swift +++ b/DivKit/Extensions/DivStateExtensions.swift @@ -52,7 +52,8 @@ extension DivState: DivBlockModeling { let previousState = getPreviousState(stateManagerItem: stateManagerItem), previousState.stateId != activeStateId, let previousDiv = previousState.div { - context.triggersStorage?.disableTriggers(path: context.parentPath + id + previousState.stateId) + context.triggersStorage? + .disableTriggers(path: context.parentPath + id + previousState.stateId) context.triggersStorage?.enableTriggers(path: context.parentPath + id + activeStateId) // state changed -> drop visibility cache for all children @@ -94,12 +95,14 @@ extension DivState: DivBlockModeling { let stateAlignment = activeStateDiv? .resolveAlignment(context, defaultAlignment: defaultStateAlignment) ?? defaultStateAlignment - - return LayeredBlock( - widthTrait: resolveContentWidthTrait(context), - heightTrait: resolveContentHeightTrait(context), - children: [ - LayeredBlock.Child( + let child: LayeredBlock.Child = { + if animationOut == nil, animationIn == nil { + return LayeredBlock.Child( + content: activeBlock, + alignment: stateAlignment + ) + } else { + return LayeredBlock.Child( content: TransitioningBlock( from: previousBlock, to: activeBlock, @@ -107,8 +110,14 @@ extension DivState: DivBlockModeling { animationIn: animationIn ), alignment: stateAlignment - ), - ] + ) + } + }() + + return LayeredBlock( + widthTrait: resolveContentWidthTrait(context), + heightTrait: resolveContentHeightTrait(context), + children: [child] ).addingStateBlock( ids: stateManager.getVisibleIds(statePath: activeStatePath) ) diff --git a/DivKit/Templates/Field.swift b/DivKit/Templates/Field.swift index b1ec9510..f766b3ba 100644 --- a/DivKit/Templates/Field.swift +++ b/DivKit/Templates/Field.swift @@ -122,6 +122,13 @@ extension Field where T: ValidSerializationValue { } extension Field where T: RawRepresentable, T.RawValue: ValidSerializationValue { + @inlinable + func resolveValue( + context: TemplatesContext + ) -> DeserializationResult { + resolveValue(context: context, transform: T.init(rawValue:)) + } + @inlinable func resolveOptionalValue( context: TemplatesContext, diff --git a/LayoutKit/LayoutKit/Blocks/DetachableAnimationBlock.swift b/LayoutKit/LayoutKit/Blocks/DetachableAnimationBlock.swift index e3017211..10d67701 100644 --- a/LayoutKit/LayoutKit/Blocks/DetachableAnimationBlock.swift +++ b/LayoutKit/LayoutKit/Blocks/DetachableAnimationBlock.swift @@ -16,7 +16,7 @@ public struct ChangeBoundsTransition: Equatable { } } -public final class DetachableAnimationBlock: WrapperBlock, LayoutCachingDefaultImpl { +public final class DetachableAnimationBlock: WrapperBlock, LayoutCachingDefaultImpl, Identifiable { public let child: Block public let id: String public let animationIn: [TransitioningAnimation]? @@ -74,7 +74,9 @@ extension DetachableAnimationBlock: CustomDebugStringConvertible { extension DetachableAnimationBlock: ElementStateUpdating { public func updated(withStates _: BlocksState) throws -> Self { - Self( + guard animationIn != nil else { return self } + + return Self( child: child, id: id, animationIn: nil, diff --git a/LayoutKit/LayoutKit/Blocks/ImageBlock+Copy.swift b/LayoutKit/LayoutKit/Blocks/ImageBlock+Copy.swift index 5b11151e..736dace1 100644 --- a/LayoutKit/LayoutKit/Blocks/ImageBlock+Copy.swift +++ b/LayoutKit/LayoutKit/Blocks/ImageBlock+Copy.swift @@ -10,7 +10,9 @@ extension ImageBlock { tintColor: tintColor, tintMode: tintMode, effects: effects, - accessibilityElement: accessibilityElement + accessibilityElement: accessibilityElement, + blurUsingMetal: blurUsingMetal, + tintUsingMetal: tintUsingMetal ) } } diff --git a/LayoutKit/LayoutKit/Blocks/ImageBlock.swift b/LayoutKit/LayoutKit/Blocks/ImageBlock.swift index 42a9d18d..d9fc1535 100644 --- a/LayoutKit/LayoutKit/Blocks/ImageBlock.swift +++ b/LayoutKit/LayoutKit/Blocks/ImageBlock.swift @@ -13,6 +13,8 @@ public final class ImageBlock: ImageBaseBlock { public let effects: [ImageEffect] public let accessibilityElement: AccessibilityElement? public let appearanceAnimation: TransitioningAnimation? + public let blurUsingMetal: Bool? + public let tintUsingMetal: Bool? public init( imageHolder: ImageHolder, @@ -23,7 +25,9 @@ public final class ImageBlock: ImageBaseBlock { tintMode: TintMode, effects: [ImageEffect] = [], accessibilityElement: AccessibilityElement? = nil, - appearanceAnimation: TransitioningAnimation? = nil + appearanceAnimation: TransitioningAnimation? = nil, + blurUsingMetal: Bool? = nil, + tintUsingMetal: Bool? = nil ) { self.imageHolder = imageHolder self.widthTrait = widthTrait @@ -34,6 +38,8 @@ public final class ImageBlock: ImageBaseBlock { self.effects = effects self.accessibilityElement = accessibilityElement self.appearanceAnimation = appearanceAnimation + self.blurUsingMetal = blurUsingMetal + self.tintUsingMetal = tintUsingMetal } public convenience init( @@ -45,7 +51,9 @@ public final class ImageBlock: ImageBaseBlock { tintMode: TintMode = .sourceIn, effects: [ImageEffect] = [], accessibilityElement: AccessibilityElement? = nil, - appearanceAnimation: TransitioningAnimation? = nil + appearanceAnimation: TransitioningAnimation? = nil, + blurUsingMetal: Bool? = nil, + tintUsingMetal: Bool? = nil ) { self.init( imageHolder: imageHolder, @@ -56,7 +64,9 @@ public final class ImageBlock: ImageBaseBlock { tintMode: tintMode, effects: effects, accessibilityElement: accessibilityElement, - appearanceAnimation: appearanceAnimation + appearanceAnimation: appearanceAnimation, + blurUsingMetal: blurUsingMetal, + tintUsingMetal: tintUsingMetal ) } @@ -68,7 +78,9 @@ public final class ImageBlock: ImageBaseBlock { tintMode: TintMode = .sourceIn, effects: [ImageEffect] = [], accessibilityElement: AccessibilityElement? = nil, - appearanceAnimation: TransitioningAnimation? = nil + appearanceAnimation: TransitioningAnimation? = nil, + blurUsingMetal: Bool? = nil, + tintUsingMetal: Bool? = nil ) { self.init( imageHolder: imageHolder, @@ -79,7 +91,9 @@ public final class ImageBlock: ImageBaseBlock { tintMode: tintMode, effects: effects, accessibilityElement: accessibilityElement, - appearanceAnimation: appearanceAnimation + appearanceAnimation: appearanceAnimation, + blurUsingMetal: blurUsingMetal, + tintUsingMetal: tintUsingMetal ) } diff --git a/LayoutKit/LayoutKit/Blocks/Slider/SliderBlock.swift b/LayoutKit/LayoutKit/Blocks/Slider/SliderBlock.swift index 0bda885a..f41355a1 100644 --- a/LayoutKit/LayoutKit/Blocks/Slider/SliderBlock.swift +++ b/LayoutKit/LayoutKit/Blocks/Slider/SliderBlock.swift @@ -47,7 +47,11 @@ public final class SliderBlock: BlockWithTraits { } public func equals(_ other: Block) -> Bool { - (other as? SliderBlock)?.sliderModel == sliderModel + guard let otherSlider = (other as? SliderBlock) else { return false } + + return otherSlider.sliderModel == sliderModel && + widthTrait == otherSlider.widthTrait && + heightTrait == otherSlider.heightTrait } public func getImageHolders() -> [ImageHolder] { diff --git a/LayoutKit/LayoutKit/Blocks/TextInputBlock.swift b/LayoutKit/LayoutKit/Blocks/TextInputBlock.swift index 34a85bd8..40bcc43c 100644 --- a/LayoutKit/LayoutKit/Blocks/TextInputBlock.swift +++ b/LayoutKit/LayoutKit/Blocks/TextInputBlock.swift @@ -84,6 +84,7 @@ public final class TextInputBlock: BlockWithTraits { public let layoutDirection: UserInterfaceLayoutDirection public let paddings: EdgeInsets? public let isEnabled: Bool + public let maxLength: Int? public init( widthTrait: LayoutTrait = .resizable, @@ -110,7 +111,8 @@ public final class TextInputBlock: BlockWithTraits { textAlignmentHorizontal: TextAlignmentHorizontal = .start, textAlignmentVertical: TextAlignmentVertical = .center, paddings: EdgeInsets? = nil, - isEnabled: Bool = true + isEnabled: Bool = true, + maxLength: Int? = nil ) { self.widthTrait = widthTrait self.heightTrait = heightTrait @@ -137,6 +139,7 @@ public final class TextInputBlock: BlockWithTraits { self.textAlignmentVertical = textAlignmentVertical self.paddings = paddings self.isEnabled = isEnabled + self.maxLength = maxLength } public var intrinsicContentWidth: CGFloat { diff --git a/LayoutKit/LayoutKit/UI/Blocks/DetachableAnimationBlock+UIViewRenderableBlock.swift b/LayoutKit/LayoutKit/UI/Blocks/DetachableAnimationBlock+UIViewRenderableBlock.swift index 66731c08..2050f9b8 100644 --- a/LayoutKit/LayoutKit/UI/Blocks/DetachableAnimationBlock+UIViewRenderableBlock.swift +++ b/LayoutKit/LayoutKit/UI/Blocks/DetachableAnimationBlock+UIViewRenderableBlock.swift @@ -34,12 +34,20 @@ extension DetachableAnimationBlock { final class DetachableAnimationBlockView: BlockView { private var childView: BlockView? { didSet { - if let childView { + guard childView !== oldValue else { return } + + switch (oldValue, childView) { + case (.none, .none): + break + case let (.some(oldValue), .some(childView)): addSubview(childView) - animatedView = nil - } else { animatedView = oldValue - oldValue?.removeFromSuperview() + oldValue.removeFromSuperview() + case let (.some(oldValue), .none): + animatedView = oldValue + oldValue.removeFromSuperview() + case let (.none, .some(childView)): + addSubview(childView) } } } @@ -54,15 +62,6 @@ final class DetachableAnimationBlockView: BlockView { var effectiveBackgroundColor: UIColor? { childView?.effectiveBackgroundColor } - init() { - super.init(frame: .zero) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - func configure( child: Block, animationIn: [TransitioningAnimation]?, @@ -108,8 +107,13 @@ final class DetachableAnimationBlockView: BlockView { let animationChange else { return } + let finishFrame = convertFrame(to: container) + + guard finishFrame != startFrame else { return } + self.childView = nil + let blockSize = CGSize( width: startFrame.width, height: child?.intrinsicContentHeight(forWidth: startFrame.width) ?? .zero @@ -121,6 +125,7 @@ final class DetachableAnimationBlockView: BlockView { animationContainer.frame = startFrame animationContainer.clipsToBounds = true animationContainer.addSubview(childView) + container.addSubview(animationContainer) container.layoutIfNeeded() @@ -138,11 +143,10 @@ final class DetachableAnimationBlockView: BlockView { container.layoutIfNeeded() }, completion: { [weak self] _ in - guard animationContainer.superview == container else { - return + if animationContainer.superview == container { + animationContainer.removeFromSuperview() + self?.childView = childView } - animationContainer.removeFromSuperview() - self?.childView = childView } ) } @@ -172,17 +176,18 @@ final class DetachableAnimationBlockView: BlockView { self.childView = nil let minDelay = animationIn.sortedChronologically().first?.delay.value ?? 0 + let item = DispatchWorkItem { [weak self] in container.addSubview(childView) childView.setInitialParamsAndAnimate( animations: animationIn.withDelay(-minDelay), completion: { [weak self] in + self?.queuedAnimation = nil - guard childView.superview == container else { - return + if childView.superview == container { + childView.frame = originalFrame + self?.childView = childView } - childView.frame = originalFrame - self?.childView = childView } ) } diff --git a/LayoutKit/LayoutKit/UI/Blocks/ImageBlock+UIViewRenderableBlock.swift b/LayoutKit/LayoutKit/UI/Blocks/ImageBlock+UIViewRenderableBlock.swift index 83324b88..d1e76b5c 100644 --- a/LayoutKit/LayoutKit/UI/Blocks/ImageBlock+UIViewRenderableBlock.swift +++ b/LayoutKit/LayoutKit/UI/Blocks/ImageBlock+UIViewRenderableBlock.swift @@ -14,9 +14,9 @@ extension ImageBlock { ) { let remoteImageViewContainer = view as! RemoteImageViewContainer var contentView = remoteImageViewContainer.contentView - if !effects.isEmpty || tintMode != .sourceIn, !contentView - .isKind(of: MetalImageView.self) { - contentView = MetalImageView() + let targetType = suitableTypeOfImageView() + if !contentView.isKind(of: targetType) { + contentView = targetType.init() } contentView.appearanceAnimation = appearanceAnimation?.cast() contentView.imageContentMode = contentMode @@ -37,6 +37,31 @@ extension ImageBlock { public func canConfigureBlockView(_ view: BlockView) -> Bool { view is RemoteImageViewContainer } + + private func suitableTypeOfImageView() -> any RemoteImageViewContentProtocol.Type { + if tintMode != .sourceIn { + return MetalImageView.self + } + let blurEffectUsingMetal = blurUsingMetal ?? true + let tintEffectUsingMetal = tintUsingMetal ?? true + let (hasBlur, hasTint) = usedEffects() + let useMetal = (blurEffectUsingMetal && hasBlur || tintEffectUsingMetal && hasTint) + return useMetal ? MetalImageView.self : RemoteImageView.self + } + + private func usedEffects() -> (blur: Bool, tint: Bool) { + var hasBlur = false + var hasTint = false + for effect in effects { + switch effect { + case .blur: + hasBlur = true + case .tint: + hasTint = true + } + } + return (blur: hasBlur, tint: hasTint) + } } extension RemoteImageViewContainer: BlockViewProtocol, VisibleBoundsTrackingLeaf { diff --git a/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift b/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift index f1e51a00..8d293131 100644 --- a/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift +++ b/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift @@ -22,7 +22,8 @@ extension StateBlock { ids: Set(ids.map { BlockViewID(rawValue: $0) }), observer: observer, overscrollDelegate: overscrollDelegate, - renderingDelegate: renderingDelegate + renderingDelegate: renderingDelegate, + parentBlock: self ) } } @@ -109,24 +110,27 @@ private final class StateBlockView: BlockView { private var childView: BlockView? private var stateId: String? - var effectiveBackgroundColor: UIColor? { childView?.effectiveBackgroundColor } - - init() { - super.init(frame: .zero) - } + private var parentBlock: StateBlock? - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + var effectiveBackgroundColor: UIColor? { childView?.effectiveBackgroundColor } func configure( child: Block, ids: Set, observer: ElementStateObserver?, overscrollDelegate: ScrollDelegate?, - renderingDelegate: RenderingDelegate? + renderingDelegate: RenderingDelegate?, + parentBlock: StateBlock ) { + defer { + self.parentBlock = parentBlock + } + + if let oldParentChild = self.parentBlock?.child, + child.self.equals(oldParentChild) { + return // The child block hasn't changed, stop configuring + } + // remove views with unfinished animations for subview in subviews { if subview !== childView { @@ -158,19 +162,27 @@ private final class StateBlockView: BlockView { ) let viewsToAdd = subviewStorage.getViewsToAdd() + if viewsToAdd.isEmpty, viewsToTransition.isEmpty { setNeedsLayout() } else { forceLayout() - for view in viewsToAdd { - view.addWithAnimation(in: self) - } + changeBoundsWithAnimation(viewsToTransition) + addWithAnimations(viewsToAdd) + } + } + + private func addWithAnimations(_ views: [DetachableAnimationBlockView]) { + for view in views { + view.addWithAnimation(in: self) + } + } - for (id, frame) in viewsToTransition { - if let view = subviewStorage.getView(id) { - view.changeBoundsWithAnimation(in: self, startFrame: frame) - } + private func changeBoundsWithAnimation(_ views: [SubviewStorage.FrameWithID]) { + for (id, frame) in views { + if let view = subviewStorage.getView(id) { + view.changeBoundsWithAnimation(in: self, startFrame: frame) } } } diff --git a/LayoutKit/LayoutKit/UI/Blocks/TextInputBlock+UIViewRenderableBlock.swift b/LayoutKit/LayoutKit/UI/Blocks/TextInputBlock+UIViewRenderableBlock.swift index 5f1b973f..7b858ebb 100644 --- a/LayoutKit/LayoutKit/UI/Blocks/TextInputBlock+UIViewRenderableBlock.swift +++ b/LayoutKit/LayoutKit/UI/Blocks/TextInputBlock+UIViewRenderableBlock.swift @@ -37,6 +37,7 @@ extension TextInputBlock { inputView.setPath(path) inputView.setObserver(observer) inputView.setIsEnabled(isEnabled) + inputView.setMaxLength(maxLength) inputView.paddings = paddings ?? .zero } @@ -72,6 +73,8 @@ private final class TextInputBlockView: BlockView, VisibleBoundsTrackingLeaf { private var textAlignmentVertical: TextInputBlock.TextAlignmentVertical = .center private var isInputFocused = false private var keyboardHeight: CGFloat? + private var maxLength: Int? + var paddings: EdgeInsets = .zero var effectiveBackgroundColor: UIColor? { backgroundColor } @@ -293,6 +296,10 @@ private final class TextInputBlockView: BlockView, VisibleBoundsTrackingLeaf { setNeedsLayout() } + func setMaxLength(_ value: Int?) { + maxLength = value + } + private func updateHintVisibility() { hintView.isHidden = !currentText.isEmpty } @@ -556,6 +563,12 @@ extension TextInputBlockView { filter(currentText + text) } } + + if let maxLength, text != "" { + let updatedText = currentText.replacingCharactersInRange(range, withString: text) + return updatedText.result.count <= maxLength + } + return true } } @@ -682,7 +695,7 @@ extension TextInputBlockView { extension UIView { fileprivate func allSuperviewsAreVisible() -> Bool { - var inspectedView = self as UIView? + var inspectedView: UIView? = self while let currentView = inspectedView { guard !currentView.isHidden, currentView.alpha > 0 else { return false } inspectedView = currentView.superview diff --git a/Specs/DivKit/30.18.0/DivKit.podspec b/Specs/DivKit/30.18.0/DivKit.podspec new file mode 100644 index 00000000..9cccb0b0 --- /dev/null +++ b/Specs/DivKit/30.18.0/DivKit.podspec @@ -0,0 +1,24 @@ +Pod::Spec.new do |s| + s.name = 'DivKit' + s.version = '30.18.0' + s.summary = 'DivKit framework' + s.description = 'DivKit is a backend-driven UI framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5.9' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '13.0' } + + s.dependency 'DivKit_LayoutKit', s.version.to_s + s.dependency 'DivKit_Serialization', s.version.to_s + s.dependency 'VGSL', '~> 6.0' + + s.source_files = [ + 'DivKit/**/*' + ] +end diff --git a/Specs/DivKitExtensions/30.18.0/DivKitExtensions.podspec b/Specs/DivKitExtensions/30.18.0/DivKitExtensions.podspec new file mode 100644 index 00000000..44077ce9 --- /dev/null +++ b/Specs/DivKitExtensions/30.18.0/DivKitExtensions.podspec @@ -0,0 +1,22 @@ +Pod::Spec.new do |s| + s.name = 'DivKitExtensions' + s.version = '30.18.0' + s.summary = 'DivKit framework extensions' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5.9' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '13.0' } + + s.dependency 'DivKit', s.version.to_s + + s.source_files = [ + 'DivKitExtensions/**/*' + ] +end diff --git a/Specs/DivKit_LayoutKit/30.18.0/DivKit_LayoutKit.podspec b/Specs/DivKit_LayoutKit/30.18.0/DivKit_LayoutKit.podspec new file mode 100644 index 00000000..eb07dfe2 --- /dev/null +++ b/Specs/DivKit_LayoutKit/30.18.0/DivKit_LayoutKit.podspec @@ -0,0 +1,24 @@ +Pod::Spec.new do |s| + s.name = 'DivKit_LayoutKit' + s.module_name = 'LayoutKit' + s.version = '30.18.0' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5.9' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '13.0' } + + s.dependency 'DivKit_LayoutKitInterface', s.version.to_s + s.dependency 'VGSL', '~> 6.0' + + s.source_files = [ + 'LayoutKit/LayoutKit/**/*' + ] +end diff --git a/Specs/DivKit_LayoutKitInterface/30.18.0/DivKit_LayoutKitInterface.podspec b/Specs/DivKit_LayoutKitInterface/30.18.0/DivKit_LayoutKitInterface.podspec new file mode 100644 index 00000000..82622613 --- /dev/null +++ b/Specs/DivKit_LayoutKitInterface/30.18.0/DivKit_LayoutKitInterface.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'DivKit_LayoutKitInterface' + s.module_name = 'LayoutKitInterface' + s.version = '30.18.0' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5.9' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '13.0' } + + s.dependency 'VGSL', '~> 6.0' + + s.source_files = [ + 'LayoutKit/Interface/**/*' + ] +end diff --git a/Specs/DivKit_Serialization/30.18.0/DivKit_Serialization.podspec b/Specs/DivKit_Serialization/30.18.0/DivKit_Serialization.podspec new file mode 100644 index 00000000..fb41adc3 --- /dev/null +++ b/Specs/DivKit_Serialization/30.18.0/DivKit_Serialization.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'DivKit_Serialization' + s.module_name = 'Serialization' + s.version = '30.18.0' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5.9' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '13.0' } + + s.dependency 'VGSL', '~> 6.0' + + s.source_files = [ + 'Serialization/**/*' + ] +end