update swiftformat, add organize rule

commit_hash:0f7ba811b37db6d2a9fc05efe5f94502feda4856
This commit is contained in:
babaevmm
2025-08-04 16:24:48 +03:00
parent 5b23b82131
commit 956c055cd8
210 changed files with 3773 additions and 3579 deletions
+6
View File
@@ -21,3 +21,9 @@
--wraparguments before-first
--wrapcollections before-first
--wrapparameters before-first
--rules organizeDeclarations
# options for organizeDeclarations
--organization-mode type
--mark-categories false
+1 -1
View File
@@ -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:
+10 -7
View File
@@ -46,6 +46,7 @@ final class ValueAnimator<I: ValueInterpolator>: Animator {
}
let id: String
private var configuration: Configuration
private let animationBlock: (AnimatedType) -> Void
private let valueInterpolator: I
@@ -135,6 +136,14 @@ final class ValueAnimator<I: ValueInterpolator>: 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<I: ValueInterpolator>: 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() {}
}
@@ -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() }
}
}
}
@@ -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 {
@@ -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")!
+4 -4
View File
@@ -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()
}
@@ -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<Int>(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
+4 -4
View File
@@ -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
}
+2 -2
View File
@@ -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<T>(action: () throws -> T) throws -> T {
+18 -16
View File
@@ -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
+56 -55
View File
@@ -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<ChangeEvent>()
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<ChangeEvent>()
private(set) var isInputFocused = false
var stateUpdates: Signal<ChangeEvent> {
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 {
+3 -2
View File
@@ -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()
}
+4 -4
View File
@@ -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.
///
+8 -7
View File
@@ -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 }
}
}
@@ -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<CalcExpression.Symbol> {
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 var first: UnicodeScalar? {
@@ -147,12 +147,10 @@ fileprivate struct UnicodeScalarView {
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..<endIndex]
}
}
extension String {
@@ -1,8 +1,6 @@
import Foundation
final class CustomFunction: SimpleFunction {
var signature: FunctionSignature
struct Signature: Hashable {
var name: String
var arguments: [DivEvaluableType]
@@ -13,7 +11,10 @@ final class CustomFunction: SimpleFunction {
let type: DivEvaluableType
}
var signature: FunctionSignature
let name: String
private let arguments: [Argument]
private let body: String
@@ -3,14 +3,14 @@ import VGSL
@_spi(Internal)
public final class DivFunctionsStorage {
let outerStorage: DivFunctionsStorage?
private var functions: [CustomFunction.Signature: CustomFunction] = [:]
private var storages: [UIElementPath: DivFunctionsStorage] = [:]
private let reporter: DivReporter
private let lock = AllocatedUnfairLock()
let outerStorage: DivFunctionsStorage?
init(
outerStorage: DivFunctionsStorage? = DivFunctionsStorage(outerStorage: nil),
reporter: DivReporter = DefaultDivReporter()
@@ -1,6 +1,27 @@
import Foundation
public struct ExpressionError: LocalizedError, CustomStringConvertible {
public let description: String
let message: String
public var errorDescription: String? {
description
}
init(
_ message: String,
expression: String? = nil
) {
self.message = message
description = if let expression {
"\(message) Expression: \(expression)"
} else {
message
}
}
static func integerOverflow() -> 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
}
}
@@ -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<Any> = makeLink(expression) {
return resolveAnyLink(link)
@@ -34,17 +34,17 @@ extension SimpleFunction {
struct NoMatchingSignatureError: Error {}
struct ConstantFunction<R>: 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<R>: 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<R>: SimpleFunction {
}
struct FunctionUnary<T1, R>: SimpleFunction {
private let impl: (T1) throws -> R
let signature = FunctionSignature(
arguments: [
.init(type: T1.self),
@@ -97,6 +95,8 @@ struct FunctionUnary<T1, R>: 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<T1, R>: SimpleFunction {
}
struct FunctionBinary<T1, T2, R>: SimpleFunction {
private let impl: (T1, T2, ExpressionContext) throws -> R
let signature = FunctionSignature(
arguments: [
.init(type: T1.self),
@@ -118,6 +116,8 @@ struct FunctionBinary<T1, T2, R>: 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<T1, T2, R>: SimpleFunction {
}
struct FunctionTernary<T1, T2, T3, R>: SimpleFunction {
private let impl: (T1, T2, T3) throws -> R
let signature = FunctionSignature(
arguments: [
.init(type: T1.self),
@@ -148,6 +146,8 @@ struct FunctionTernary<T1, T2, T3, R>: 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<T1, T2, T3, R>: SimpleFunction {
}
struct FunctionQuaternary<T1, T2, T3, T4, R>: SimpleFunction {
private let impl: (T1, T2, T3, T4) throws -> R
let signature = FunctionSignature(
arguments: [
.init(type: T1.self),
@@ -175,6 +173,8 @@ struct FunctionQuaternary<T1, T2, T3, T4, R>: 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<T1, T2, T3, T4, R>: SimpleFunction {
}
struct FunctionVarUnary<T1, R>: SimpleFunction {
private let impl: ([T1]) throws -> R
let signature = FunctionSignature(
arguments: [
.init(type: T1.self, vararg: true),
@@ -200,6 +198,8 @@ struct FunctionVarUnary<T1, R>: 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<T1, R>: SimpleFunction {
}
struct FunctionVarBinary<T1, T2, R>: SimpleFunction {
private let impl: (T1, [T2]) throws -> R
let signature = FunctionSignature(
arguments: [
.init(type: T1.self),
@@ -221,6 +219,8 @@ struct FunctionVarBinary<T1, T2, R>: 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<T1, T2, R>: SimpleFunction {
}
struct FunctionVarTernary<T1, T2, T3, R>: SimpleFunction {
private let impl: (T1, T2, [T3]) throws -> R
let signature = FunctionSignature(
arguments: [
.init(type: T1.self),
@@ -246,6 +244,8 @@ struct FunctionVarTernary<T1, T2, T3, R>: 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<T1, T2, T3, R>: SimpleFunction {
struct OverloadedFunction: Function {
let functions: [SimpleFunction]
private let makeError: ([Any]) -> Error
init(functions: [SimpleFunction], makeError: (([Any]) -> Error)? = nil) {
@@ -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 {
@@ -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\"")
)
}
}
}
@@ -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 {
@@ -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(
+42 -41
View File
@@ -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<String>) {
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<String>) {
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 {
@@ -3,8 +3,17 @@ import VGSL
public final class DivPersistentValuesStorage {
static let storageFileName = "divkit.values_storage"
private let timestampProvider: Variable<Milliseconds>
private let storage = Property<StoredValues>(
fileName: storageFileName,
initialValue: StoredValues(items: [:]),
onError: { error in
DivKitLogger.error("Failed to create storage: \(error)")
}
)
public init(
timestampProvider: Variable<Milliseconds> = Variable {
Date().timeIntervalSince1970.milliseconds
@@ -14,14 +23,6 @@ public final class DivPersistentValuesStorage {
removeOutdatedStoredValues()
}
private let storage = Property<StoredValues>(
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(
@@ -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]
@@ -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?
@@ -11,11 +11,15 @@ public final class DivVariableStorage {
}
let initialPath: UIElementPath?
let changeEvents: Signal<ChangeEvent>
private let outerStorage: DivVariableStorage?
private var _values = DivVariables()
private let lock = AllocatedUnfairLock()
private let changeEventsPipe = SignalPipe<ChangeEvent>()
/// 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<ChangeEvent>()
let changeEvents: Signal<ChangeEvent>
/// 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)
@@ -12,28 +12,6 @@ public enum DivVariableValue: Hashable {
case dict(DivDictionary)
case array(DivArray)
@inlinable
public func typedValue<T>() -> 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>() -> 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
}
}
}
@@ -17,10 +17,6 @@ public final class DivVariablesStorage {
public let kind: Kind
init(_ kind: Kind) {
self.kind = kind
}
public var changedVariables: Set<DivVariableName> {
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<ChangeEvent>
private let globalStorage: DivVariableStorage
private var localStorages: [UIElementPath: DivVariableStorage] = [:]
private let lock = AllocatedUnfairLock()
private let changeEventsPipe = SignalPipe<ChangeEvent>()
public let changeEvents: Signal<ChangeEvent>
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<T>(
path: UIElementPath,
name: DivVariableName
) -> T? {
lock.withLock {
getNearestStorage(path).getValue(name)
}
}
public func getVariableValue<T>(
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<T>(
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 {
+99 -99
View File
@@ -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
+2 -2
View File
@@ -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
+88 -87
View File
@@ -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 {
+15 -13
View File
@@ -14,10 +14,12 @@ public final class DivViewPreloader {
public let estimatedSize: DivViewSize
}
private(set) var setSourceTask: Task<Void, Error>?
private var blockProviders = [DivCardID: DivBlockProvider]()
private let divKitComponents: DivKitComponents
private let changeEventsPipe = SignalPipe<DivViewSizeChange>()
private(set) var setSourceTask: Task<Void, Error>?
var changeEvents: Signal<DivViewSizeChange> {
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)
}
+6 -6
View File
@@ -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:
@@ -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 {}
@@ -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)"
}
@@ -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)"
}
}
@@ -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
@@ -4,15 +4,27 @@ import LayoutKit
import VGSL
public final class RiveAnimationBlock: BlockWithTraits {
let animationHolder: AnimationHolder
let animatableView: Lazy<AsyncSourceAnimatableView>
public let widthTrait: LayoutTrait
public let heightTrait: LayoutTrait
let animationHolder: AnimationHolder
let animatableView: Lazy<AsyncSourceAnimatableView>
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<AsyncSourceAnimatableView>,
@@ -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):
@@ -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) {
@@ -4,6 +4,7 @@ import VGSL
public final class ImageExtensionHandler: DivExtensionHandler {
public let id: String
private let image: ImageHolder?
public init(
@@ -5,6 +5,7 @@ import VGSL
public final class ImageThemeExtensionHandler: DivExtensionHandler {
public let id = extensionID
private let theme: Variable<Theme>
public init(theme: Variable<Theme>) {
@@ -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) {
@@ -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
}
@@ -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,
@@ -6,6 +6,7 @@ import VGSL
public final class ShimmerImagePreviewExtension: DivExtensionHandler {
public let id: String = extensionID
private let effectBeginTime = CACurrentMediaTime()
public init() {}
@@ -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
@@ -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 {
@@ -37,6 +37,7 @@ struct RiveDivCustomData {
}
static let divCustomType = "rive_animation"
let url: URL
let fit: Fit
let alignment: Alignment
@@ -3,13 +3,14 @@ import SwiftUI
@main
struct DivKitPlaygroundApp: App {
init() {
DivKitLogger.isEnabled = true
}
var body: some Scene {
WindowGroup {
MainView()
}
}
init() {
DivKitLogger.isEnabled = true
}
}
@@ -5,16 +5,17 @@ import VGSL
typealias LogError = (String) -> Void
final class MetadataCaptureSession: NSObject, AVCaptureMetadataOutputObjectsDelegate {
let result: ObservableProperty<String>
private let captureSession = AVCaptureSession()
private let previewLayer: AVCaptureVideoPreviewLayer
let result: ObservableProperty<String>
private let logError: LogError
var layer: CALayer { previewLayer }
private var isInitialized = false
var layer: CALayer { previewLayer }
init(
result: ObservableProperty<String>,
logError: @escaping LogError
@@ -2,10 +2,10 @@ import UIKit
import VGSL
final class ScannerViewController: UIViewController {
private let captureSession: Lazy<MetadataCaptureSession>
let result = ObservableProperty(initialValue: "")
private let captureSession: Lazy<MetadataCaptureSession>
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
@@ -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<RegressionTestModel>].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<RegressionTestModel>].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 {
@@ -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<Destination>: View where Destination: View {
@@ -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
@@ -4,6 +4,7 @@ import SwiftUI
final class DivViewProvider {
let jsonProvider: PlaygroundJsonProvider
private let divKitComponents: DivKitComponents
private let layoutDirection: UIUserInterfaceLayoutDirection
@@ -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 {
@@ -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
@@ -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
@@ -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"
@@ -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<AnyCancellable>()
@@ -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)
@@ -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 = [
@@ -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
}
@@ -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] = [:],
@@ -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 = [
@@ -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 {
@@ -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 {
@@ -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)
}
+12 -9
View File
@@ -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 {
@@ -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)
}
}
+3 -3
View File
@@ -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.")
+66 -63
View File
@@ -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 }
}
}
@@ -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"
}
}
+11 -10
View File
@@ -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
}
}
@@ -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 {
@@ -10,12 +10,13 @@ public class MaskedInputViewModel {
case clearRange(range: Range<String.Index>)
}
@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(
@@ -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))
}
@@ -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 {
@@ -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<Bool>
private let allowEmpty: Bool
private let validator: (String) -> Bool
public init(
isValid: Binding<Bool>,
allowEmpty: Bool,
+2 -1
View File
@@ -2,9 +2,10 @@ import Foundation
import VGSL
public struct Binding<T: Equatable>: Equatable {
private let name: String
@Property public var value: T
private let name: String
public init(
name: String,
value: Property<T>
@@ -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]
@@ -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
@@ -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
)
}
}
@@ -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
@@ -6,14 +6,21 @@ public final class AspectBlock<Content: Block>: 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<Content: Block>: 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))
}
@@ -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(
@@ -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 {
@@ -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
@@ -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 &&
@@ -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,
@@ -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(
@@ -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,
@@ -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()
@@ -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))
@@ -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
@@ -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):

Some files were not shown because too many files have changed in this diff Show More