From b4ef008cd8424bea39b3537c0c15412402df8662 Mon Sep 17 00:00:00 2001 From: Jaxtyn Date: Fri, 24 Jan 2025 16:18:48 +0900 Subject: [PATCH 1/2] =?UTF-8?q?`usesCachedViewSize`=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=B4=20FeatureFlag=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=9D=84=20=EC=A7=80=EC=9B=90=ED=95=B4=EC=9A=94.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DefaultFeatureFlagProvider.swift | 12 +++++ .../FeatureFlag/FeatureFlagItem.swift | 28 +++++++++++ .../FeatureFlag/FeatureFlagProviding.swift | 23 ++++++++++ .../FeatureFlag/FeatureFlagType.swift | 13 ++++++ .../KarrotListKitFeatureFlag.swift | 15 ++++++ .../FeatureFlagProviderTests.swift | 46 +++++++++++++++++++ 6 files changed, 137 insertions(+) create mode 100644 Sources/KarrotListKit/FeatureFlag/DefaultFeatureFlagProvider.swift create mode 100644 Sources/KarrotListKit/FeatureFlag/FeatureFlagItem.swift create mode 100644 Sources/KarrotListKit/FeatureFlag/FeatureFlagProviding.swift create mode 100644 Sources/KarrotListKit/FeatureFlag/FeatureFlagType.swift create mode 100644 Sources/KarrotListKit/FeatureFlag/KarrotListKitFeatureFlag.swift create mode 100644 Tests/KarrotListKitTests/FeatureFlagProviderTests.swift 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/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) + } + } +} From b597f56b24b82390d2d6269613d4782e49ff9535 Mon Sep 17 00:00:00 2001 From: Jaxtyn Date: Fri, 24 Jan 2025 16:19:39 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EC=82=AC=EC=9D=B4=EC=A6=88=20=EC=BA=90?= =?UTF-8?q?=EC=8B=B1=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20`usesCachedViewSize`=20=ED=94=BC=EC=B3=90=ED=94=8C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8=20=EC=97=B0=EB=8F=99=ED=95=B4=EC=9A=94.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UIView+TraitCollection.swift .swift | 27 ++++++++++++++++ .../UICollectionComponentReusableView.swift | 31 +++++++++++++++++++ .../View/UICollectionViewComponentCell.swift | 26 ++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 Sources/KarrotListKit/Extension/UIView+TraitCollection.swift .swift 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/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 {