diff --git a/CHANGELOG.md b/CHANGELOG.md index e599f00..2e7d46e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Rewrote accessibility code to avoid posting notifications, which causes poor Voice Over performance and odd focus bugs +- Rewrote `ItemViewReuseManager` to perform fewer set operations, improving CPU usage by ~15% when scrolling quickly on an iPhone XR ## [v2.0.0](https://github.com/airbnb/HorizonCalendar/compare/v1.16.0...v2.0.0) - 2023-12-19 diff --git a/Sources/Internal/ItemViewReuseManager.swift b/Sources/Internal/ItemViewReuseManager.swift index e471039..f0bf643 100644 --- a/Sources/Internal/ItemViewReuseManager.swift +++ b/Sources/Internal/ItemViewReuseManager.swift @@ -23,139 +23,68 @@ final class ItemViewReuseManager { // MARK: Internal - func viewsForVisibleItems( - _ visibleItems: Set, - recycleUnusedViews: Bool, - viewHandler: ( - ItemView, - VisibleItem, - _ previousBackingVisibleItem: VisibleItem?, - _ isReusedViewSameAsPreviousView: Bool) - -> Void) + func reusedViewContexts( + visibleItems: Set, + reuseUnusedViews: Bool) + -> [ReusedViewContext] { - var visibleItemsDifferencesItemViewDifferentiators = [ - _CalendarItemViewDifferentiator: Set - ]() - - // For each reuse ID, track the difference between the new set of visible items and the previous - // set of visible items. The remaining previous visible items after subtracting the current - // visible items are the previously visible items that aren't currently visible, and are - // therefore free to be reused. + var contexts = [ReusedViewContext]() + + var previousViewsForVisibleItems = viewsForVisibleItems + viewsForVisibleItems.removeAll(keepingCapacity: true) + for visibleItem in visibleItems { - let differentiator = visibleItem.calendarItemModel._itemViewDifferentiator + let viewDifferentiator = visibleItem.calendarItemModel._itemViewDifferentiator - var visibleItemsDifference: Set - if let difference = visibleItemsDifferencesItemViewDifferentiators[differentiator] { - visibleItemsDifference = difference - } else if - let previouslyVisibleItems = visibleItemsForItemViewDifferentiators[differentiator] + let context: ReusedViewContext = + if let view = previousViewsForVisibleItems.removeValue(forKey: visibleItem) { - visibleItemsDifference = previouslyVisibleItems.subtracting(visibleItems) + ReusedViewContext( + view: view, + visibleItem: visibleItem, + isViewReused: true, + isReusedViewSameAsPreviousView: true) + } else if !(unusedViewsForViewDifferentiators[viewDifferentiator]?.isEmpty ?? true) { + ReusedViewContext( + view: unusedViewsForViewDifferentiators[viewDifferentiator]!.remove(at: 0), + visibleItem: visibleItem, + isViewReused: true, + isReusedViewSameAsPreviousView: false) } else { - visibleItemsDifference = [] + ReusedViewContext( + view: ItemView(initialCalendarItemModel: visibleItem.calendarItemModel), + visibleItem: visibleItem, + isViewReused: false, + isReusedViewSameAsPreviousView: false) } - let context = reusedViewContext( - for: visibleItem, - recycleUnusedViews: recycleUnusedViews, - unusedPreviouslyVisibleItems: &visibleItemsDifference) - viewHandler( - context.view, - visibleItem, - context.previousBackingVisibleItem, - context.isReusedViewSameAsPreviousView) - - visibleItemsDifferencesItemViewDifferentiators[differentiator] = visibleItemsDifference - } - } - - // MARK: Private - - private var visibleItemsForItemViewDifferentiators = [ - _CalendarItemViewDifferentiator: Set - ]() - private var viewsForVisibleItems = [VisibleItem: ItemView]() - - private func reusedViewContext( - for visibleItem: VisibleItem, - recycleUnusedViews: Bool, - unusedPreviouslyVisibleItems: inout Set) - -> ReusedViewContext - { - let differentiator = visibleItem.calendarItemModel._itemViewDifferentiator - - let view: ItemView - let previousBackingVisibleItem: VisibleItem? - let isReusedViewSameAsPreviousView: Bool - - if let previouslyVisibleItems = visibleItemsForItemViewDifferentiators[differentiator] { - if previouslyVisibleItems.contains(visibleItem) { - // New visible item was also an old visible item, so we can just use the same view again. - - guard let previousView = viewsForVisibleItems[visibleItem] else { - preconditionFailure(""" - `viewsForVisibleItems` must have a key for every member in - `visibleItemsForItemViewDifferentiators`'s values. - """) - } + contexts.append(context) - view = previousView - previousBackingVisibleItem = visibleItem - isReusedViewSameAsPreviousView = true + viewsForVisibleItems[visibleItem] = context.view + } - visibleItemsForItemViewDifferentiators[differentiator]?.remove(visibleItem) - viewsForVisibleItems.removeValue(forKey: visibleItem) - } else { - if recycleUnusedViews, let previouslyVisibleItem = unusedPreviouslyVisibleItems.first { - // An unused, previously-visible item is available, so reuse it. - - guard let previousView = viewsForVisibleItems[previouslyVisibleItem] else { - preconditionFailure(""" - `viewsForVisibleItems` must have a key for every member in - `visibleItemsForItemViewDifferentiators`'s values. - """) - } - - view = previousView - previousBackingVisibleItem = previouslyVisibleItem - isReusedViewSameAsPreviousView = false - - unusedPreviouslyVisibleItems.remove(previouslyVisibleItem) - - visibleItemsForItemViewDifferentiators[differentiator]?.remove(previouslyVisibleItem) - viewsForVisibleItems.removeValue(forKey: previouslyVisibleItem) - } else { - // No previously-visible item is available for reuse (or view recycling is disabled), so - // create a new view. - view = ItemView(initialCalendarItemModel: visibleItem.calendarItemModel) - previousBackingVisibleItem = nil - isReusedViewSameAsPreviousView = false - } + if reuseUnusedViews { + for (visibleItem, unusedView) in previousViewsForVisibleItems { + let viewDifferentiator = visibleItem.calendarItemModel._itemViewDifferentiator + unusedViewsForViewDifferentiators[viewDifferentiator, default: .init()].append(unusedView) } - } else { - // No previously-visible item is available for reuse, so create a new view. - view = ItemView(initialCalendarItemModel: visibleItem.calendarItemModel) - previousBackingVisibleItem = nil - isReusedViewSameAsPreviousView = false } - let newVisibleItems = visibleItemsForItemViewDifferentiators[differentiator] ?? [] - visibleItemsForItemViewDifferentiators[differentiator] = newVisibleItems - visibleItemsForItemViewDifferentiators[differentiator]?.insert(visibleItem) - viewsForVisibleItems[visibleItem] = view - - return ReusedViewContext( - view: view, - previousBackingVisibleItem: previousBackingVisibleItem, - isReusedViewSameAsPreviousView: isReusedViewSameAsPreviousView) + return contexts } + // MARK: Private + + private var viewsForVisibleItems = [VisibleItem: ItemView]() + private var unusedViewsForViewDifferentiators = [_CalendarItemViewDifferentiator: [ItemView]]() + } // MARK: - ReusedViewContext -private struct ReusedViewContext { +struct ReusedViewContext { let view: ItemView - let previousBackingVisibleItem: VisibleItem? + let visibleItem: VisibleItem + let isViewReused: Bool let isReusedViewSameAsPreviousView: Bool } diff --git a/Sources/Public/AnyCalendarItemModel.swift b/Sources/Public/AnyCalendarItemModel.swift index 7583318..0249d57 100644 --- a/Sources/Public/AnyCalendarItemModel.swift +++ b/Sources/Public/AnyCalendarItemModel.swift @@ -54,7 +54,6 @@ public protocol AnyCalendarItemModel { /// /// - Note: There is no reason to create an instance of this enum from your feature code; it should only be invoked internally. public struct _CalendarItemViewDifferentiator: Hashable { - let viewRepresentableTypeDescription: String - let viewTypeDescription: String + let viewType: ObjectIdentifier let invariantViewProperties: AnyHashable } diff --git a/Sources/Public/CalendarItemModel.swift b/Sources/Public/CalendarItemModel.swift index d965e40..3ab6cba 100644 --- a/Sources/Public/CalendarItemModel.swift +++ b/Sources/Public/CalendarItemModel.swift @@ -46,8 +46,7 @@ public struct CalendarItemModel: AnyCalendarItemModel where content: ViewRepresentable.Content) { _itemViewDifferentiator = _CalendarItemViewDifferentiator( - viewRepresentableTypeDescription: String(reflecting: ViewRepresentable.self), - viewTypeDescription: String(reflecting: ViewRepresentable.ViewType.self), + viewType: ObjectIdentifier(ViewRepresentable.self), invariantViewProperties: invariantViewProperties) self.invariantViewProperties = invariantViewProperties @@ -115,8 +114,7 @@ extension CalendarItemModel where ViewRepresentable.Content == Never { /// and `font`, assuming none of those values change in response to `content` updates. public init(invariantViewProperties: ViewRepresentable.InvariantViewProperties) { _itemViewDifferentiator = _CalendarItemViewDifferentiator( - viewRepresentableTypeDescription: String(reflecting: ViewRepresentable.self), - viewTypeDescription: String(reflecting: ViewRepresentable.ViewType.self), + viewType: ObjectIdentifier(ViewRepresentable.self), invariantViewProperties: invariantViewProperties) self.invariantViewProperties = invariantViewProperties diff --git a/Sources/Public/CalendarView.swift b/Sources/Public/CalendarView.swift index fdaf821..a94eb26 100644 --- a/Sources/Public/CalendarView.swift +++ b/Sources/Public/CalendarView.swift @@ -773,29 +773,30 @@ public final class CalendarView: UIView { var viewsToHideForVisibleItems = visibleViewsForVisibleItems visibleViewsForVisibleItems.removeAll(keepingCapacity: true) - reuseManager.viewsForVisibleItems( - visibleItems, - recycleUnusedViews: !UIAccessibility.isVoiceOverRunning, - viewHandler: { view, visibleItem, previousBackingVisibleItem, isReusedViewSameAsPreviousView in - UIView.conditionallyPerformWithoutAnimation(when: !isReusedViewSameAsPreviousView) { - if view.superview == nil { - let insertionIndex = subviewInsertionIndexTracker.insertionIndex( - forSubviewWithCorrespondingItemType: visibleItem.itemType) - scrollView.insertSubview(view, at: insertionIndex) - } - - view.isHidden = false - - configureView(view, with: visibleItem) + let contexts = reuseManager.reusedViewContexts( + visibleItems: visibleItems, + reuseUnusedViews: !UIAccessibility.isVoiceOverRunning) + + for context in contexts { + UIView.conditionallyPerformWithoutAnimation(when: !context.isReusedViewSameAsPreviousView) { + if context.view.superview == nil { + let insertionIndex = subviewInsertionIndexTracker.insertionIndex( + forSubviewWithCorrespondingItemType: context.visibleItem.itemType) + scrollView.insertSubview(context.view, at: insertionIndex) } - visibleViewsForVisibleItems[visibleItem] = view + context.view.isHidden = false - if let previousBackingVisibleItem { - // Don't hide views that were reused - viewsToHideForVisibleItems.removeValue(forKey: previousBackingVisibleItem) - } - }) + configureView(context.view, with: context.visibleItem) + } + + visibleViewsForVisibleItems[context.visibleItem] = context.view + + if context.isViewReused { + // Don't hide views that were reused + viewsToHideForVisibleItems.removeValue(forKey: context.visibleItem) + } + } // Hide any old views that weren't reused. This is faster than adding / removing subviews. // If VoiceOver is running, we remove the view to save memory (since views aren't reused). diff --git a/Tests/ItemViewReuseManagerTests.swift b/Tests/ItemViewReuseManagerTests.swift index bef1c74..50f61f2 100644 --- a/Tests/ItemViewReuseManagerTests.swift +++ b/Tests/ItemViewReuseManagerTests.swift @@ -56,17 +56,17 @@ final class ItemViewReuseManagerTests: XCTestCase { frame: .zero), ] - reuseManager.viewsForVisibleItems( - visibleItems, - recycleUnusedViews: true, - viewHandler: { _, _, previousBackingItem, isReusedViewSameAsPreviousView in - XCTAssert( - previousBackingItem == nil, - "Previous backing item should be nil since there are no views to reuse.") - XCTAssert( - !isReusedViewSameAsPreviousView, - "isReusedViewSameAsPreviousView should be false when no view was reused.") - }) + let contexts = reuseManager.reusedViewContexts( + visibleItems: visibleItems, + reuseUnusedViews: true) + for context in contexts { + XCTAssert( + !context.isViewReused, + "isViewReused should be false since there are no views to reuse.") + XCTAssert( + !context.isReusedViewSameAsPreviousView, + "isReusedViewSameAsPreviousView should be false when no view was reused.") + } } func testReusingIdenticalViews() { @@ -102,26 +102,25 @@ final class ItemViewReuseManagerTests: XCTestCase { let subsequentVisibleItems = initialVisibleItems // Populate the reuse manager with the initial visible items - reuseManager.viewsForVisibleItems( - initialVisibleItems, - recycleUnusedViews: true, - viewHandler: { _, _, _, _ in }) + let _ = reuseManager.reusedViewContexts( + visibleItems: initialVisibleItems, + reuseUnusedViews: true) // Ensure all views are reused by using the exact same previous views - reuseManager.viewsForVisibleItems( - subsequentVisibleItems, - recycleUnusedViews: true, - viewHandler: { _, item, previousBackingItem, isReusedViewSameAsPreviousView in - XCTAssert( - item == previousBackingItem, - """ - Expected the new item to be identical to the previous backing item, since the subsequent - visible items are identical to the initial visible items. - """) - XCTAssert( - isReusedViewSameAsPreviousView, - "isReusedViewSameAsPreviousView should be true when the same view was reused.") - }) + let contexts = reuseManager.reusedViewContexts( + visibleItems: subsequentVisibleItems, + reuseUnusedViews: true) + for context in contexts { + XCTAssert( + context.isViewReused, + """ + Expected every view to be reused, since the subsequent visible items are identical to the + initial visible items. + """) + XCTAssert( + context.isReusedViewSameAsPreviousView, + "isReusedViewSameAsPreviousView should be true when the same view was reused.") + } } func testReusingAllViews() { @@ -184,23 +183,22 @@ final class ItemViewReuseManagerTests: XCTestCase { ] // Populate the reuse manager with the initial visible items - reuseManager.viewsForVisibleItems( - initialVisibleItems, - recycleUnusedViews: true, - viewHandler: { _, _, _, _ in }) + let _ = reuseManager.reusedViewContexts( + visibleItems: initialVisibleItems, + reuseUnusedViews: true) + + // Allow the reuse manager to figure out which items are reusable + let _ = reuseManager.reusedViewContexts( + visibleItems: [], + reuseUnusedViews: true) // Ensure all views are reused given the subsequent visible items - reuseManager.viewsForVisibleItems( - subsequentVisibleItems, - recycleUnusedViews: true, - viewHandler: { _, item, previousBackingItem, _ in - XCTAssert( - item.calendarItemModel._itemViewDifferentiator == previousBackingItem?.calendarItemModel._itemViewDifferentiator, - """ - Expected the new item to have the same view differentiator as the previous backing item, - since it was reused. - """) - }) + let contexts = reuseManager.reusedViewContexts( + visibleItems: subsequentVisibleItems, + reuseUnusedViews: true) + for context in contexts { + XCTAssert(context.isViewReused, "Expected every view to be reused") + } } func testReusingSomeViews() { @@ -279,41 +277,42 @@ final class ItemViewReuseManagerTests: XCTestCase { ] // Populate the reuse manager with the initial visible items - reuseManager.viewsForVisibleItems( - initialVisibleItems, - recycleUnusedViews: true, - viewHandler: { _, _, _, _ in }) + let _ = reuseManager.reusedViewContexts( + visibleItems: initialVisibleItems, + reuseUnusedViews: true) + + // Allow the reuse manager to figure out which items are reusable + let _ = reuseManager.reusedViewContexts( + visibleItems: [], + reuseUnusedViews: true) // Ensure the correct subset of views are reused given the subsequent visible items - reuseManager.viewsForVisibleItems( - subsequentVisibleItems, - recycleUnusedViews: true, - viewHandler: { _, item, previousBackingItem, isReusedViewSameAsPreviousView in - guard let itemModel = item.calendarItemModel as? MockCalendarItemModel else { - preconditionFailure( - "Failed to convert the calendar item model to an instance of MockCalendarItemModel.") - } - - switch itemModel { - case .variant1, .variant3: - XCTAssert( - item.calendarItemModel._itemViewDifferentiator == previousBackingItem?.calendarItemModel._itemViewDifferentiator, - """ - Expected the new item to have the same reuse identifier as the previous backing item, - since it was reused. - """) - XCTAssert( - !isReusedViewSameAsPreviousView, - "isReusedViewSameAsPreviousView should be false when a different view was reused.") - default: - XCTAssert( - previousBackingItem == nil, - "Previous backing item should be nil since there are no views to reuse.") - XCTAssert( - !isReusedViewSameAsPreviousView, - "isReusedViewSameAsPreviousView should be false when a different view was reused.") - } - }) + let contexts = reuseManager.reusedViewContexts( + visibleItems: subsequentVisibleItems, + reuseUnusedViews: true) + for context in contexts { + guard let itemModel = context.visibleItem.calendarItemModel as? MockCalendarItemModel else { + preconditionFailure( + "Failed to convert the calendar item model to an instance of MockCalendarItemModel.") + } + + switch itemModel { + case .variant1, .variant3: + XCTAssert( + context.isViewReused, + "isViewReused should be true since it was reused.") + XCTAssert( + !context.isReusedViewSameAsPreviousView, + "isReusedViewSameAsPreviousView should be false when a different view was reused.") + default: + XCTAssert( + !context.isViewReused, + "isViewReused should be false since there are no views to reuse.") + XCTAssert( + !context.isReusedViewSameAsPreviousView, + "isReusedViewSameAsPreviousView should be false when a different view was reused.") + } + } } func testDepletingAvailableReusableViews() { @@ -421,26 +420,31 @@ final class ItemViewReuseManagerTests: XCTestCase { ] // Populate the reuse manager with the initial visible items - reuseManager.viewsForVisibleItems( - initialVisibleItems, - recycleUnusedViews: true, - viewHandler: { _, _, _, _ in }) + let _ = reuseManager.reusedViewContexts( + visibleItems: initialVisibleItems, + reuseUnusedViews: true) + + // Allow the reuse manager to figure out which items are reusable + let _ = reuseManager.reusedViewContexts( + visibleItems: [], + reuseUnusedViews: true) // Ensure the correct subset of views are reused given the subsequent visible items var reuseCountsForDifferentiators = [_CalendarItemViewDifferentiator: Int]() var newViewCountsForDifferentiators = [_CalendarItemViewDifferentiator: Int]() - reuseManager.viewsForVisibleItems( - subsequentVisibleItems, - recycleUnusedViews: true, - viewHandler: { _, item, previousBackingItem, _ in - if previousBackingItem != nil { - let reuseCount = (reuseCountsForDifferentiators[item.calendarItemModel._itemViewDifferentiator] ?? 0) + 1 - reuseCountsForDifferentiators[item.calendarItemModel._itemViewDifferentiator] = reuseCount - } else { - let newViewCount = (newViewCountsForDifferentiators[item.calendarItemModel._itemViewDifferentiator] ?? 0) + 1 - newViewCountsForDifferentiators[item.calendarItemModel._itemViewDifferentiator] = newViewCount - } - }) + let contexts = reuseManager.reusedViewContexts( + visibleItems: subsequentVisibleItems, + reuseUnusedViews: true) + for context in contexts { + let item = context.visibleItem + if context.isViewReused { + let reuseCount = (reuseCountsForDifferentiators[item.calendarItemModel._itemViewDifferentiator] ?? 0) + 1 + reuseCountsForDifferentiators[item.calendarItemModel._itemViewDifferentiator] = reuseCount + } else { + let newViewCount = (newViewCountsForDifferentiators[item.calendarItemModel._itemViewDifferentiator] ?? 0) + 1 + newViewCountsForDifferentiators[item.calendarItemModel._itemViewDifferentiator] = newViewCount + } + } let expectedReuseCountsForDifferentiators: [_CalendarItemViewDifferentiator: Int] = [ MockCalendarItemModel.variant0._itemViewDifferentiator: 2, @@ -537,38 +541,37 @@ final class ItemViewReuseManagerTests: XCTestCase { ] // Populate the reuse manager with the initial visible items - reuseManager.viewsForVisibleItems( - initialVisibleItems, - recycleUnusedViews: false, - viewHandler: { _, _, _, _ in }) + let _ = reuseManager.reusedViewContexts( + visibleItems: initialVisibleItems, + reuseUnusedViews: false) // Ensure the correct subset of views are reused given the subsequent visible items - reuseManager.viewsForVisibleItems( - subsequentVisibleItems, - recycleUnusedViews: false, - viewHandler: { _, item, previousBackingItem, isReusedViewSameAsPreviousView in - guard let itemModel = item.calendarItemModel as? MockCalendarItemModel else { - preconditionFailure( - "Failed to convert the calendar item model to an instance of MockCalendarItemModel.") - } - - switch itemModel { - case .variant1, .variant3: - XCTAssert( - previousBackingItem == nil, - "Previous backing item should be nil since view recycling is disabled.") - XCTAssert( - !isReusedViewSameAsPreviousView, - "isReusedViewSameAsPreviousView should be false when a different view was reused.") - default: - XCTAssert( - previousBackingItem == nil, - "Previous backing item should be nil since there are no views to reuse.") - XCTAssert( - !isReusedViewSameAsPreviousView, - "isReusedViewSameAsPreviousView should be false when a different view was reused.") - } - }) + let contexts = reuseManager.reusedViewContexts( + visibleItems: subsequentVisibleItems, + reuseUnusedViews: false) + for context in contexts { + guard let itemModel = context.visibleItem.calendarItemModel as? MockCalendarItemModel else { + preconditionFailure( + "Failed to convert the calendar item model to an instance of MockCalendarItemModel.") + } + + switch itemModel { + case .variant1, .variant3: + XCTAssert( + !context.isViewReused, + "isViewReused should be false since view recycling is disabled.") + XCTAssert( + !context.isReusedViewSameAsPreviousView, + "isReusedViewSameAsPreviousView should be false when a different view was reused.") + default: + XCTAssert( + !context.isViewReused, + "isViewReused should be false since there are no views to reuse.") + XCTAssert( + !context.isReusedViewSameAsPreviousView, + "isReusedViewSameAsPreviousView should be false when a different view was reused.") + } + } } // MARK: Private @@ -585,13 +588,11 @@ private struct MockCalendarItemModel: AnyCalendarItemModel, Equatable { // MARK: Lifecycle init( - viewRepresentableTypeDescription: String, - viewTypeDescription: String, + viewType: ObjectIdentifier, invariantViewProperties: AnyHashable) { _itemViewDifferentiator = _CalendarItemViewDifferentiator( - viewRepresentableTypeDescription: viewRepresentableTypeDescription, - viewTypeDescription: viewTypeDescription, + viewType: viewType, invariantViewProperties: invariantViewProperties) } @@ -613,28 +614,22 @@ private struct MockCalendarItemModel: AnyCalendarItemModel, Equatable { } static let variant0 = MockCalendarItemModel( - viewRepresentableTypeDescription: "ViewRepresentingA", - viewTypeDescription: "UIView", + viewType: ObjectIdentifier(UIView.self), invariantViewProperties: InvariantViewProperties(font: .systemFont(ofSize: 12), color: .white)) static let variant1 = MockCalendarItemModel( - viewRepresentableTypeDescription: "ViewRepresentingA", - viewTypeDescription: "UIView", + viewType: ObjectIdentifier(UIView.self), invariantViewProperties: InvariantViewProperties(font: .systemFont(ofSize: 14), color: .white)) static let variant2 = MockCalendarItemModel( - viewRepresentableTypeDescription: "ViewRepresentingA", - viewTypeDescription: "UIView", + viewType: ObjectIdentifier(UIView.self), invariantViewProperties: InvariantViewProperties(font: .systemFont(ofSize: 14), color: .black)) static let variant3 = MockCalendarItemModel( - viewRepresentableTypeDescription: "LabelRepresentingA", - viewTypeDescription: "UILabel", + viewType: ObjectIdentifier(UILabel.self), invariantViewProperties: InvariantLabelPropertiesA(font: .systemFont(ofSize: 14), color: .red)) static let variant4 = MockCalendarItemModel( - viewRepresentableTypeDescription: "LabelRepresentingB", - viewTypeDescription: "UILabel", + viewType: ObjectIdentifier(UILabel.self), invariantViewProperties: InvariantLabelPropertiesB(font: .systemFont(ofSize: 16), color: .red)) static let variant5 = MockCalendarItemModel( - viewRepresentableTypeDescription: "LabelRepresentingB", - viewTypeDescription: "UILabel", + viewType: ObjectIdentifier(UILabel.self), invariantViewProperties: InvariantLabelPropertiesB(font: .systemFont(ofSize: 16), color: .blue)) var _itemViewDifferentiator: _CalendarItemViewDifferentiator