-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
242 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
Sources/UIComponent/Core/Model/Component/CachingItem.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// | ||
// CachingItem.swift | ||
// UIComponent | ||
// | ||
// Created by Luke Zhao on 11/8/24. | ||
// | ||
|
||
/// A Component that caches data item. | ||
/// | ||
/// It is useful when you want to cache item that is expensive to generate | ||
/// or if you want to share the same item across multiple reload. | ||
/// | ||
/// The item will be released when the hosting view is released | ||
/// | ||
/// `itemGenerator` will be called to generate the item if it's not in the view's cache. | ||
/// `componentBuilder` will be called to build the component with the item. | ||
public struct CachingItem<T, C: Component>: Component { | ||
/// The key to identify the item in the cache. | ||
public let key: String | ||
|
||
/// The closure that generates the item. Will only be called if the item is not in the cache. | ||
public let itemGenerator: () -> T | ||
|
||
/// The closure that builds the component with the item | ||
public let componentBuilder: (T) -> C | ||
|
||
@Environment(\.hostingView) private var hostingView | ||
|
||
/// Initializes a new `CachingItem` with the provided key, item generator, and component builder. | ||
/// - Parameters: | ||
/// - key: The key to identify the item in the cache. | ||
/// - itemGenerator: The closure that generates the item. Will only be called if the item is not in the cache. | ||
/// - componentBuilder: The closure that builds the component | ||
public init(key: String, itemGenerator: @escaping () -> T, componentBuilder: @escaping (T) -> C) { | ||
self.key = key | ||
self.itemGenerator = itemGenerator | ||
self.componentBuilder = componentBuilder | ||
} | ||
|
||
/// Component protocol method | ||
public func layout(_ constraint: Constraint) -> C.R { | ||
let data: T = hostingView?.componentEngine.loadCachingData(id: key, generator: itemGenerator) ?? itemGenerator() | ||
return componentBuilder(data).layout(constraint) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
// | ||
// File.swift | ||
// UIComponent | ||
// | ||
// Created by Luke Zhao on 11/8/24. | ||
// | ||
|
||
import Testing | ||
@testable import UIComponent | ||
import UIKit | ||
|
||
@Suite("Data Caching") | ||
@MainActor | ||
struct DataCachingTest { | ||
|
||
class DataHolder<T> { | ||
var data: T | ||
init(data: T) { | ||
self.data = data | ||
} | ||
} | ||
|
||
@Test func testCachingData() throws { | ||
var view: UIView! = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500)) | ||
var callCount = 0 | ||
weak var holder: DataHolder<String>? | ||
view.componentEngine.component = CachingItem(key: "1", itemGenerator: { | ||
callCount += 1 | ||
let h = DataHolder(data: "1") | ||
holder = h | ||
return h | ||
}, componentBuilder: { (data: DataHolder<String>) in | ||
Text(data.data) | ||
}) | ||
view.componentEngine.reloadData() | ||
#expect(callCount == 1) | ||
#expect(view.subviews.count == 1) | ||
let existingLabel = view.subviews.first as? UILabel | ||
#expect(existingLabel?.text == "1") | ||
#expect(holder != nil) | ||
|
||
view.componentEngine.component = CachingItem(key: "1", itemGenerator: { | ||
callCount += 1 | ||
let h = DataHolder(data: "1") | ||
holder = h | ||
return h | ||
}, componentBuilder: { (data: DataHolder<String>) in | ||
Text(data.data) | ||
}) | ||
view.componentEngine.reloadData() | ||
#expect(callCount == 1) | ||
#expect(holder != nil) | ||
|
||
view.componentEngine.component = Text("2") | ||
view.componentEngine.reloadData() | ||
#expect(callCount == 1) | ||
#expect(holder != nil) | ||
|
||
// data should be released | ||
view = nil | ||
#expect(callCount == 1) | ||
#expect(holder == nil) | ||
} | ||
|
||
@Test func testCachingDataWithWrappedView() throws { | ||
let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500)) | ||
var callCount = 0 | ||
view.componentEngine.component = CachingItem(key: "1", itemGenerator: { | ||
callCount += 1 | ||
return "1" | ||
}, componentBuilder: { | ||
Text($0) | ||
}).view() | ||
view.componentEngine.reloadData() | ||
#expect(callCount == 1) | ||
#expect(view.subviews.count == 1) | ||
|
||
view.componentEngine.component = CachingItem(key: "1", itemGenerator: { | ||
callCount += 1 | ||
return "1" | ||
}, componentBuilder: { | ||
Text($0) | ||
}).view() | ||
view.componentEngine.reloadData() | ||
#expect(callCount == 1) | ||
} | ||
|
||
@Test func testCachingDataScrolling() throws { | ||
let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500)) | ||
var callCount = 0 | ||
|
||
view.componentEngine.component = CachingItem(key: "1", itemGenerator: { | ||
callCount += 1 | ||
return "1" | ||
}, componentBuilder: { | ||
Text($0) | ||
}).view() | ||
view.componentEngine.reloadData() | ||
#expect(callCount == 1) | ||
#expect(view.subviews.count == 1) | ||
|
||
view.componentEngine.component = VStack { | ||
Space(height: 5000) | ||
CachingItem(key: "1", itemGenerator: { | ||
callCount += 1 | ||
return "1" | ||
}, componentBuilder: { | ||
Text($0) | ||
}).view() | ||
} | ||
view.componentEngine.reloadData() | ||
#expect(view.subviews.count == 0) | ||
#expect(callCount == 1) | ||
|
||
view.bounds.origin = CGPoint(x: 0, y: 5000) | ||
view.componentEngine.reloadData() | ||
#expect(view.subviews.count == 1) | ||
#expect(callCount == 1) | ||
} | ||
|
||
@Test func testCachingDataWithFixedSizeScrolling() throws { | ||
let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500)) | ||
var callCount = 0 | ||
view.componentEngine.component = VStack { | ||
Space(height: 5000) | ||
CachingItem(key: "1", itemGenerator: { | ||
callCount += 1 | ||
return "1" | ||
}, componentBuilder: { | ||
Text($0) | ||
}).size(width: 100, height: 100) | ||
} | ||
view.componentEngine.reloadData() | ||
#expect(view.subviews.count == 0) | ||
#expect(callCount == 0) | ||
|
||
view.componentEngine.component = CachingItem(key: "1", itemGenerator: { | ||
callCount += 1 | ||
return "1" | ||
}, componentBuilder: { | ||
Text($0) | ||
}).size(width: 100, height: 100) | ||
view.componentEngine.reloadData() | ||
#expect(callCount == 1) | ||
#expect(view.subviews.count == 1) | ||
|
||
view.componentEngine.component = VStack { | ||
Space(height: 5000) | ||
CachingItem(key: "1", itemGenerator: { | ||
callCount += 1 | ||
return "1" | ||
}, componentBuilder: { | ||
Text($0) | ||
}).size(width: 100, height: 100) | ||
} | ||
view.componentEngine.reloadData() | ||
#expect(view.subviews.count == 0) | ||
#expect(callCount == 1) | ||
|
||
view.bounds.origin = CGPoint(x: 0, y: 5000) | ||
view.componentEngine.reloadData() | ||
#expect(view.subviews.count == 1) | ||
#expect(callCount == 1) | ||
} | ||
|
||
@Test func testCachingDataWithDifferentType() throws { | ||
let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500)) | ||
var callCount = 0 | ||
view.componentEngine.component = CachingItem(key: "1", itemGenerator: { | ||
callCount += 1 | ||
return 1 | ||
}, componentBuilder: { | ||
Text("\($0)") | ||
}) | ||
view.componentEngine.reloadData() | ||
#expect(callCount == 1) | ||
#expect(view.subviews.count == 1) | ||
|
||
view.componentEngine.component = CachingItem(key: "1", itemGenerator: { | ||
callCount += 1 | ||
return "1" | ||
}, componentBuilder: { | ||
Text($0) | ||
}) | ||
view.componentEngine.reloadData() | ||
#expect(callCount == 2) | ||
} | ||
} |