diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cbcecf1..27560496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +- Added `CacheCleaner` which exposes a method to force Listable's static caches to be cleared. + ### Removed ### Changed diff --git a/CacheClearer.swift b/CacheClearer.swift new file mode 100644 index 00000000..6f43b259 --- /dev/null +++ b/CacheClearer.swift @@ -0,0 +1,19 @@ +import Foundation + +@_spi(CacheManagement) +public struct CacheClearer { + + /// Clears all static caches that are in use. + /// + /// Listable leverages static caching to improve performance however there are situations in which + /// this can cause object lifetimes to be extended unexpectedly, especially in cases where cached + /// views reference other objects. + /// + /// - WARNING: Clearing these caches can have global performance implications. This method + /// should be invoked sparingley and only after other workarounds to manage object lifetimes have failed. + @_spi(CacheManagement) + public static func clearStaticCaches() { + ListProperties.headerFooterMeasurementCache.removeAllObjects() + ListProperties.itemMeasurementCache.removeAllObjects() + } +} diff --git a/CacheClearerTests.swift b/CacheClearerTests.swift new file mode 100644 index 00000000..449befe7 --- /dev/null +++ b/CacheClearerTests.swift @@ -0,0 +1,19 @@ +import XCTest +@testable @_spi(CacheManagement) import ListableUI + +class CacheClearerTests: XCTestCase { + + func test_clearStaticCaches() { + + ListProperties.headerFooterMeasurementCache.push(UIView(), with: .identifier(for: UIView.self)) + ListProperties.itemMeasurementCache.push(UIView(), with: .identifier(for: UIView.self)) + + XCTAssertEqual(ListProperties.headerFooterMeasurementCache.cachedViewCount, 1) + XCTAssertEqual(ListProperties.itemMeasurementCache.cachedViewCount, 1) + + CacheClearer.clearStaticCaches() + + XCTAssertEqual(ListProperties.headerFooterMeasurementCache.cachedViewCount, 0) + XCTAssertEqual(ListProperties.itemMeasurementCache.cachedViewCount, 0) + } +} diff --git a/ListableUI/Sources/Internal/ReusableViewCache.swift b/ListableUI/Sources/Internal/ReusableViewCache.swift index cf0f4108..144bf350 100644 --- a/ListableUI/Sources/Internal/ReusableViewCache.swift +++ b/ListableUI/Sources/Internal/ReusableViewCache.swift @@ -11,7 +11,11 @@ import Foundation final class ReusableViewCache { private var views : [String:[AnyObject]] = [:] - + + var cachedViewCount : Int { + return self.views.values.reduce(0) { $0 + $1.count } + } + init() {} func count(for reuseIdentifier : ReuseIdentifier) -> Int @@ -61,4 +65,8 @@ final class ReusableViewCache return result } } + + func removeAllObjects() { + self.views = [:] + } } diff --git a/ListableUI/Sources/Layout/ListLayout/ListLayout+Layout.swift b/ListableUI/Sources/Layout/ListLayout/ListLayout+Layout.swift index 57fc3185..6996d9a4 100644 --- a/ListableUI/Sources/Layout/ListLayout/ListLayout+Layout.swift +++ b/ListableUI/Sources/Layout/ListLayout/ListLayout+Layout.swift @@ -11,8 +11,8 @@ import UIKit extension ListProperties { - private static let headerFooterMeasurementCache = ReusableViewCache() - private static let itemMeasurementCache = ReusableViewCache() + static let headerFooterMeasurementCache = ReusableViewCache() + static let itemMeasurementCache = ReusableViewCache() /// **Note**: For testing or measuring content sizes only. /// diff --git a/ListableUI/Sources/ListView/ListView+ContentSize.swift b/ListableUI/Sources/ListView/ListView+ContentSize.swift index f48d69b0..13fd5111 100644 --- a/ListableUI/Sources/ListView/ListView+ContentSize.swift +++ b/ListableUI/Sources/ListView/ListView+ContentSize.swift @@ -14,9 +14,6 @@ extension ListView // MARK: Measuring Lists // - static let headerFooterMeasurementCache = ReusableViewCache() - static let itemMeasurementCache = ReusableViewCache() - public static let defaultContentSizeItemLimit = 50 /// diff --git a/ListableUI/Sources/ListView/ListView.Delegate.swift b/ListableUI/Sources/ListView/ListView.Delegate.swift index 66a39143..d96b30a5 100644 --- a/ListableUI/Sources/ListView/ListView.Delegate.swift +++ b/ListableUI/Sources/ListView/ListView.Delegate.swift @@ -16,11 +16,6 @@ extension ListView unowned var presentationState : PresentationState! unowned var layoutManager : LayoutManager! - private let itemMeasurementCache = ReusableViewCache() - private let headerFooterMeasurementCache = ReusableViewCache() - - private let headerFooterViewCache = ReusableViewCache() - // MARK: UICollectionViewDelegate func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool diff --git a/ListableUI/Tests/Internal/ReusableViewCacheTests.swift b/ListableUI/Tests/Internal/ReusableViewCacheTests.swift index bf8a8c1f..b6b29b0c 100644 --- a/ListableUI/Tests/Internal/ReusableViewCacheTests.swift +++ b/ListableUI/Tests/Internal/ReusableViewCacheTests.swift @@ -168,6 +168,36 @@ class ReusableViewCacheTests: XCTestCase XCTAssertEqual(result, "result") } } + + func test_removeAllObjects() { + self.testcase("empty") { + let cache = ReusableViewCache() + + XCTAssertEqual(cache.cachedViewCount, 0) + + cache.removeAllObjects() + + XCTAssertEqual(cache.cachedViewCount, 0) + } + + self.testcase("non-empty") { + let cache = ReusableViewCache() + + let view1 = TestView1() + view1.identifier = "pushed_1" + cache.push(view1, with: ReuseIdentifier.identifier(for: TestView1.self)) + + let view2 = TestView1() + view2.identifier = "pushed_2" + cache.push(view2, with: ReuseIdentifier.identifier(for: TestView1.self)) + + XCTAssertEqual(cache.cachedViewCount, 2) + + cache.removeAllObjects() + + XCTAssertEqual(cache.cachedViewCount, 0) + } + } }