LPLazyDisplayStackScrollView는 on-demand subviews render를 지원합니다. Note) addArrangedSubview 하려는 LPLazyDisplayView의 estimatedHeight(추측 높이)값을 대략적으로 정확히 설정해주지 않으면, LPLazyDisplayStackScrollView가 좋은 성능을 내기 힘듭니다.
// Example)
class ViewController: UIViewController {
private let contentView = LPLazyDisplayStackScrollView()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(self.contentView)
self.contentView.snp.makeConstraints {
$0.top.equalTo(self.view.safeAreaLayoutGuide)
$0.left.right.bottom.equalToSuperview()
}
let item0 = ItemView().then {
$0.backgroundColor = .gray
$0.label.text = "0"
$0.tag = 0
}
let item1 = ItemView().then {
$0.backgroundColor = .gray
$0.label.text = "1"
$0.tag = 1
}
let item2 = ItemView().then {
$0.backgroundColor = .gray
$0.label.text = "2"
$0.tag = 3
}
let item5 = ItemView().then {
$0.backgroundColor = .gray
$0.label.text = "3"
$0.tag = 5
}
self.contentView.addArrangedSubview(item0)
self.contentView.addArrangedSubview(item1)
self.contentView.addArrangedSubview(item2)
self.contentView.addArrangedSubview(item5)
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [unowned self] in
self.contentView.insertArrangedSubview(ItemView().then {
$0.backgroundColor = .gray
$0.label.text = "11"
$0.tag = 11
}, at: 1)
}
}
}class LPLazyDisplayStackScrollView: UIView {
// 위 아래, 허용
var distance: CGFloat = 100
private(set) var lazyDisplayViews: [LPLazyDisplayView] = []
private lazy var scrollView = UIScrollView()
private let contentStack = UIStackView().then {
$0.axis = .vertical
$0.distribution = .fill
$0.alignment = .fill
}
private var scrollViewBoundsToken: NSKeyValueObservation?
private var scrollViewContentSizeToken: NSKeyValueObservation?
deinit {
self.scrollViewBoundsToken?.invalidate()
self.scrollViewContentSizeToken?.invalidate()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
self.scrollViewBoundsToken = self.scrollView.observe(\UIScrollView.bounds, options: [.new]) { [weak self] scrollView, change in
guard let self = self else { return }
self.displayPendingViewsIfNeeded()
}
self.scrollViewContentSizeToken = self.scrollView.observe(\UIScrollView.contentSize, options: [.new]) { [weak self] scrollView, change in
guard let self = self else { return }
self.displayPendingViewsIfNeeded()
}
self.addSubview(self.scrollView)
self.scrollView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
self.scrollView.addSubview(self.contentStack)
self.contentStack.snp.makeConstraints {
$0.edges.equalToSuperview()
$0.width.equalToSuperview()
}
}
func addArrangedSubview(_ view: LPLazyDisplayView) {
self.lazyDisplayViews.append(view)
self.contentStack.addArrangedSubview(view)
self.contentStack.setNeedsLayout()
self.contentStack.layoutIfNeeded()
}
func insertArrangedSubview(_ view: LPLazyDisplayView, at index: Int) {
self.lazyDisplayViews.insert(view, at: index)
self.contentStack.insertArrangedSubview(view, at: index)
self.scrollView.setNeedsLayout()
self.scrollView.layoutIfNeeded()
}
func removeArrangedSubview(_ view: LPLazyDisplayView) {
let index = self.lazyDisplayViews.firstIndex { $0 === view }
if let index = index {
self.lazyDisplayViews.remove(at: index)
self.contentStack.removeArrangedSubview(view)
view.removeFromSuperview()
self.contentStack.setNeedsLayout()
self.contentStack.layoutIfNeeded()
}
}
func removeAllArrangedSubviews() {
self.lazyDisplayViews.removeAll()
for subview in self.contentStack.arrangedSubviews {
self.contentStack.removeArrangedSubview(subview)
subview.removeFromSuperview()
}
self.contentStack.setNeedsLayout()
self.contentStack.layoutIfNeeded()
}
private func displayPendingViewsIfNeeded() {
let topPendingRect = CGRect(
origin: .init(x: self.scrollView.contentOffset.x, y: self.scrollView.contentOffset.y - self.distance),
size: self.scrollView.frame.size
)
let bottomPendingRect = CGRect(
origin: .init(x: self.scrollView.contentOffset.x, y: self.scrollView.contentOffset.y + self.distance),
size: self.scrollView.frame.size
)
for lazyDisplayView in self.lazyDisplayViews {
if lazyDisplayView.isPending {
if topPendingRect.intersects(lazyDisplayView.frame) || bottomPendingRect.intersects(lazyDisplayView.frame) {
lazyDisplayView.isPending = false
lazyDisplayView.display()
}
}
}
}
}
class LPLazyDisplayView: UIView {
fileprivate(set) var isPending: Bool = true
private(set) var estimatedHeight: CGFloat = 50
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
self.snp.makeConstraints {
$0.height.equalTo(self.estimatedHeight)
}
}
// Override 해서 사용하세요.
func display() {
// 기본 estimated height 설정 삭제
self.snp.remakeConstraints { _ in }
}
func setEstimatedHeight(_ height: CGFloat) {
self.estimatedHeight = height
self.snp.updateConstraints {
$0.height.equalTo(height)
}
}
}