diff --git a/Sources/KarrotListKit/Extension/UIView+TraitCollection.swift .swift b/Sources/KarrotListKit/Extension/UIView+TraitCollection.swift .swift new file mode 100644 index 0000000..3131128 --- /dev/null +++ b/Sources/KarrotListKit/Extension/UIView+TraitCollection.swift .swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2025 Danggeun Market Inc. +// + +import UIKit + +extension UIView { + + func shouldInvalidateContentSize( + previousTraitCollection: UITraitCollection? + ) -> Bool { + if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory { + return true + } + + if traitCollection.legibilityWeight != previousTraitCollection?.legibilityWeight { + return true + } + + if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass || + traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass { + return true + } + + return false + } +} diff --git a/Sources/KarrotListKit/FeatureFlag/DefaultFeatureFlagProvider.swift b/Sources/KarrotListKit/FeatureFlag/DefaultFeatureFlagProvider.swift new file mode 100644 index 0000000..df7435c --- /dev/null +++ b/Sources/KarrotListKit/FeatureFlag/DefaultFeatureFlagProvider.swift @@ -0,0 +1,12 @@ +// +// Copyright (c) 2025 Danggeun Market Inc. +// + +import Foundation + +final class DefaultFeatureFlagProvider: FeatureFlagProviding { + + func featureFlags() -> [FeatureFlagItem] { + [] + } +} diff --git a/Sources/KarrotListKit/FeatureFlag/FeatureFlagItem.swift b/Sources/KarrotListKit/FeatureFlag/FeatureFlagItem.swift new file mode 100644 index 0000000..f5cd58e --- /dev/null +++ b/Sources/KarrotListKit/FeatureFlag/FeatureFlagItem.swift @@ -0,0 +1,28 @@ +// +// Copyright (c) 2025 Danggeun Market Inc. +// + +import Foundation + +/// Representing a feature flag item. +public struct FeatureFlagItem { + + /// The type of the feature flag. + public let type: FeatureFlagType + + /// A Boolean value indicating whether the feature flag is enabled. + public let isEnabled: Bool + + /// Initializes a new `FeatureFlagItem`. + /// + /// - Parameters: + /// - type: The type of the feature flag. + /// - isEnabled: A Boolean value indicating whether the feature flag is enabled. + public init( + type: FeatureFlagType, + isEnabled: Bool + ) { + self.type = type + self.isEnabled = isEnabled + } +} diff --git a/Sources/KarrotListKit/FeatureFlag/FeatureFlagProviding.swift b/Sources/KarrotListKit/FeatureFlag/FeatureFlagProviding.swift new file mode 100644 index 0000000..c8591d0 --- /dev/null +++ b/Sources/KarrotListKit/FeatureFlag/FeatureFlagProviding.swift @@ -0,0 +1,23 @@ +// +// Copyright (c) 2025 Danggeun Market Inc. +// + +import Foundation + +/// A protocol for providing feature flags. +public protocol FeatureFlagProviding { + + /// Returns an array of feature flags. + /// + /// - Returns: An array of `FeatureFlagItem`. + func featureFlags() -> [FeatureFlagItem] +} + +extension FeatureFlagProviding { + + func isEnabled(for type: FeatureFlagType) -> Bool { + featureFlags() + .first(where: { $0.type == type })? + .isEnabled ?? false + } +} diff --git a/Sources/KarrotListKit/FeatureFlag/FeatureFlagType.swift b/Sources/KarrotListKit/FeatureFlag/FeatureFlagType.swift new file mode 100644 index 0000000..880ae92 --- /dev/null +++ b/Sources/KarrotListKit/FeatureFlag/FeatureFlagType.swift @@ -0,0 +1,13 @@ +// +// Copyright (c) 2025 Danggeun Market Inc. +// + +import Foundation + +/// Define the feature flags +public enum FeatureFlagType: Equatable { + + /// Improve scrolling performance using calculated view size. + /// You can find more information at https://developer.apple.com/documentation/uikit/building-high-performance-lists-and-collection-views + case usesCachedViewSize +} diff --git a/Sources/KarrotListKit/FeatureFlag/KarrotListKitFeatureFlag.swift b/Sources/KarrotListKit/FeatureFlag/KarrotListKitFeatureFlag.swift new file mode 100644 index 0000000..791c625 --- /dev/null +++ b/Sources/KarrotListKit/FeatureFlag/KarrotListKitFeatureFlag.swift @@ -0,0 +1,15 @@ +// +// Copyright (c) 2025 Danggeun Market Inc. +// + +import Foundation + +/// An interface for injecting a feature flag provider. +public enum KarrotListKitFeatureFlag { + + /// The feature flag provider used by `KarrotListKit`. + /// + /// By default, this is set to `DefaultFeatureFlagProvider`. + /// You can replace it with a custom provider to change the feature flag behavior. + public static var provider: FeatureFlagProviding = DefaultFeatureFlagProvider() +} diff --git a/Sources/KarrotListKit/View/UICollectionComponentReusableView.swift b/Sources/KarrotListKit/View/UICollectionComponentReusableView.swift index d6a8369..e855cfa 100644 --- a/Sources/KarrotListKit/View/UICollectionComponentReusableView.swift +++ b/Sources/KarrotListKit/View/UICollectionComponentReusableView.swift @@ -14,6 +14,8 @@ final class UICollectionComponentReusableView: UICollectionReusableView, Compone var onSizeChanged: ((CGSize) -> Void)? + private var previousBounds: CGSize = .zero + // MARK: - Initializing @available(*, unavailable) @@ -29,6 +31,30 @@ final class UICollectionComponentReusableView: UICollectionReusableView, Compone // MARK: - Override Methods + public override func traitCollectionDidChange( + _ previousTraitCollection: UITraitCollection? + ) { + super.traitCollectionDidChange(previousTraitCollection) + + if shouldInvalidateContentSize( + previousTraitCollection: previousTraitCollection + ) { + previousBounds = .zero + } + } + + public override func prepareForReuse() { + super.prepareForReuse() + + previousBounds = .zero + } + + public override func layoutSubviews() { + super.layoutSubviews() + + previousBounds = bounds.size + } + override func preferredLayoutAttributesFitting( _ layoutAttributes: UICollectionViewLayoutAttributes ) -> UICollectionViewLayoutAttributes { @@ -38,6 +64,11 @@ final class UICollectionComponentReusableView: UICollectionReusableView, Compone return attributes } + if KarrotListKitFeatureFlag.provider.isEnabled(for: .usesCachedViewSize), + previousBounds == attributes.size { + return attributes + } + let size = renderedContent.sizeThatFits(bounds.size) if renderedComponent != nil { diff --git a/Sources/KarrotListKit/View/UICollectionViewComponentCell.swift b/Sources/KarrotListKit/View/UICollectionViewComponentCell.swift index 10840da..03f08ba 100644 --- a/Sources/KarrotListKit/View/UICollectionViewComponentCell.swift +++ b/Sources/KarrotListKit/View/UICollectionViewComponentCell.swift @@ -17,6 +17,8 @@ public final class UICollectionViewComponentCell: UICollectionViewCell, Componen var onSizeChanged: ((CGSize) -> Void)? + private var previousBounds: CGSize = .zero + // MARK: - Initializing @available(*, unavailable) @@ -37,12 +39,31 @@ public final class UICollectionViewComponentCell: UICollectionViewCell, Componen // MARK: - Override Methods + public override func traitCollectionDidChange( + _ previousTraitCollection: UITraitCollection? + ) { + super.traitCollectionDidChange(previousTraitCollection) + + if shouldInvalidateContentSize( + previousTraitCollection: previousTraitCollection + ) { + previousBounds = .zero + } + } + public override func prepareForReuse() { super.prepareForReuse() + previousBounds = .zero cancellables?.forEach { $0.cancel() } } + public override func layoutSubviews() { + super.layoutSubviews() + + previousBounds = bounds.size + } + public override func preferredLayoutAttributesFitting( _ layoutAttributes: UICollectionViewLayoutAttributes ) -> UICollectionViewLayoutAttributes { @@ -52,6 +73,11 @@ public final class UICollectionViewComponentCell: UICollectionViewCell, Componen return attributes } + if KarrotListKitFeatureFlag.provider.isEnabled(for: .usesCachedViewSize), + previousBounds == attributes.size { + return attributes + } + let size = renderedContent.sizeThatFits(contentView.bounds.size) if renderedComponent != nil { diff --git a/Tests/KarrotListKitTests/FeatureFlagProviderTests.swift b/Tests/KarrotListKitTests/FeatureFlagProviderTests.swift new file mode 100644 index 0000000..c98c602 --- /dev/null +++ b/Tests/KarrotListKitTests/FeatureFlagProviderTests.swift @@ -0,0 +1,46 @@ +// +// Copyright (c) 2025 Danggeun Market Inc. +// + +import Foundation +import XCTest + +@testable import KarrotListKit + +final class FeatureFlagProviderTests: XCTestCase { + + final class FeatureFlagProviderStub: FeatureFlagProviding { + + var featureFlagsStub: [FeatureFlagItem] = [] + + func featureFlags() -> [FeatureFlagItem] { + featureFlagsStub + } + } + + func test_default_featureFlags_is_empty() { + // given + let sut = KarrotListKitFeatureFlag.provider + + // when + let featureFlags = sut.featureFlags() + + // then + XCTAssertTrue(featureFlags.isEmpty) + } + + func test_usesCachedViewSize_isEnabled() { + [true, false].forEach { flag in + // given + let provider = FeatureFlagProviderStub() + provider.featureFlagsStub = [.init(type: .usesCachedViewSize, isEnabled: flag)] + KarrotListKitFeatureFlag.provider = provider + + // when + let isEnabled = KarrotListKitFeatureFlag.provider.isEnabled(for: .usesCachedViewSize) + + // then + XCTAssertEqual(isEnabled, flag) + } + } +}