308 lines
11 KiB
Swift
308 lines
11 KiB
Swift
|
|
import UIKit
|
|
|
|
protocol RDReaderCachePolicyProviding {
|
|
var shouldAvoidReaderPageCaching: Bool { get }
|
|
}
|
|
|
|
final class RDReaderPreloadController {
|
|
|
|
var radius: Int = 2
|
|
|
|
private let preloadHostView = UIView()
|
|
|
|
private var preloadedPageViews: [Int: UIView] = [:]
|
|
|
|
private var pageCurlCachedViews: [Int: UIView] = [:]
|
|
|
|
private var cacheSignature: CacheSignature?
|
|
|
|
struct Environment {
|
|
|
|
let displayType: RDReaderView.DisplayType
|
|
|
|
let isLandscape: Bool
|
|
|
|
let pagesPerScreen: Int
|
|
|
|
let boundsSize: CGSize
|
|
|
|
let landscapeDualPageEnabled: Bool
|
|
|
|
let coverPageIndex: Int?
|
|
|
|
let totalPages: Int
|
|
|
|
let spreadResolver: RDReaderSpreadResolver
|
|
}
|
|
|
|
private struct CacheSignature: Equatable {
|
|
|
|
let displayType: RDReaderView.DisplayType
|
|
|
|
let isLandscape: Bool
|
|
|
|
let pagesPerScreen: Int
|
|
|
|
let boundsSize: CGSize
|
|
}
|
|
|
|
func setHostFrame(_ frame: CGRect) {
|
|
preloadHostView.frame = frame
|
|
}
|
|
|
|
func ensureHostView(in parentView: UIView) {
|
|
guard preloadHostView.superview == nil else { return }
|
|
preloadHostView.isHidden = true
|
|
preloadHostView.isUserInteractionEnabled = false
|
|
preloadHostView.clipsToBounds = true
|
|
preloadHostView.frame = parentView.bounds
|
|
preloadHostView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
parentView.insertSubview(preloadHostView, at: 0)
|
|
}
|
|
|
|
func initializeSignature(_ environment: Environment) {
|
|
cacheSignature = currentCacheSignature(environment)
|
|
}
|
|
|
|
func invalidate(environment: Environment) {
|
|
pageCurlCachedViews.values.forEach { $0.removeFromSuperview() }
|
|
preloadedPageViews.values.forEach { $0.removeFromSuperview() }
|
|
pageCurlCachedViews.removeAll()
|
|
preloadedPageViews.removeAll()
|
|
cacheSignature = currentCacheSignature(environment)
|
|
}
|
|
|
|
func pageViewForDisplay(
|
|
pageNum: Int,
|
|
environment: Environment,
|
|
contentViewProvider: (Int, UIView?) -> UIView?
|
|
) -> UIView {
|
|
let view: UIView
|
|
if let reusableView = detachedReusablePageView(for: pageNum) {
|
|
view = reusableView
|
|
} else {
|
|
view = contentViewProvider(pageNum, nil) ?? UIView()
|
|
}
|
|
if shouldCache(view: view, for: pageNum, environment: environment) {
|
|
pageCurlCachedViews[pageNum] = view
|
|
} else {
|
|
pageCurlCachedViews.removeValue(forKey: pageNum)
|
|
}
|
|
return view
|
|
}
|
|
|
|
func takePreloadedView(for pageNum: Int) -> UIView? {
|
|
let preloaded = preloadedPageViews.removeValue(forKey: pageNum)
|
|
preloaded?.removeFromSuperview()
|
|
return preloaded
|
|
}
|
|
|
|
func prime(
|
|
around pageNum: Int,
|
|
preferredForward: Bool? = nil,
|
|
parentView: UIView,
|
|
environment: Environment,
|
|
contentViewProvider: (Int, UIView?) -> UIView?
|
|
) {
|
|
refreshCacheSignatureIfNeeded(environment)
|
|
let targets = forecastTargets(around: pageNum, preferredForward: preferredForward, environment: environment)
|
|
let keepSet = Set(targets).union(visiblePageNumbers(for: pageNum, environment: environment))
|
|
trimCachedPageViews(keeping: keepSet)
|
|
guard !targets.isEmpty else { return }
|
|
|
|
ensureHostView(in: parentView)
|
|
preloadHostView.frame = parentView.bounds
|
|
|
|
for targetPage in targets {
|
|
let contentView: UIView
|
|
if let existing = preloadedPageViews[targetPage], existing.superview === preloadHostView {
|
|
contentView = existing
|
|
} else if let cached = pageCurlCachedViews.removeValue(forKey: targetPage), cached.superview == nil {
|
|
contentView = cached
|
|
} else {
|
|
contentView = contentViewProvider(targetPage, nil) ?? UIView()
|
|
}
|
|
let shouldCacheContentView = shouldCache(view: contentView, for: targetPage, environment: environment)
|
|
if shouldCacheContentView {
|
|
preloadedPageViews[targetPage] = contentView
|
|
} else {
|
|
preloadedPageViews.removeValue(forKey: targetPage)
|
|
}
|
|
if contentView.superview !== preloadHostView {
|
|
contentView.removeFromSuperview()
|
|
preloadHostView.addSubview(contentView)
|
|
}
|
|
contentView.frame = preloadHostView.bounds
|
|
if !shouldCacheContentView {
|
|
contentView.removeFromSuperview()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func currentCacheSignature(_ environment: Environment) -> CacheSignature {
|
|
CacheSignature(
|
|
displayType: environment.displayType,
|
|
isLandscape: environment.isLandscape,
|
|
pagesPerScreen: environment.pagesPerScreen,
|
|
boundsSize: environment.boundsSize
|
|
)
|
|
}
|
|
|
|
private func refreshCacheSignatureIfNeeded(_ environment: Environment) {
|
|
let signature = currentCacheSignature(environment)
|
|
if cacheSignature != signature {
|
|
invalidate(environment: environment)
|
|
} else if cacheSignature == nil {
|
|
cacheSignature = signature
|
|
}
|
|
}
|
|
|
|
private func shouldCachePage(_ pageNum: Int, environment: Environment) -> Bool {
|
|
pageNum >= 0
|
|
&& pageNum != RDReaderView.blankPageNum
|
|
&& pageNum != RDReaderView.blankEndPageNum
|
|
&& pageNum < environment.totalPages
|
|
}
|
|
|
|
private func shouldCache(view: UIView, for pageNum: Int, environment: Environment) -> Bool {
|
|
guard shouldCachePage(pageNum, environment: environment) else { return false }
|
|
if let policyProvider = view as? RDReaderCachePolicyProviding {
|
|
return !policyProvider.shouldAvoidReaderPageCaching
|
|
}
|
|
return true
|
|
}
|
|
|
|
private func isDualPage(environment: Environment) -> Bool {
|
|
environment.landscapeDualPageEnabled
|
|
&& environment.isLandscape
|
|
&& environment.displayType != .verticalScroll
|
|
}
|
|
|
|
private func spreadStart(for pageNum: Int, environment: Environment) -> Int? {
|
|
guard shouldCachePage(pageNum, environment: environment) else { return nil }
|
|
guard isDualPage(environment: environment) else { return pageNum }
|
|
return environment.spreadResolver.dualPagePair(
|
|
for: pageNum,
|
|
totalPages: environment.totalPages,
|
|
coverPageIndex: environment.coverPageIndex
|
|
).left
|
|
}
|
|
|
|
private func spreadPageNumbers(startingAt pageNum: Int, environment: Environment) -> Set<Int> {
|
|
guard shouldCachePage(pageNum, environment: environment) else { return [] }
|
|
guard isDualPage(environment: environment) else { return [pageNum] }
|
|
let pair = environment.spreadResolver.dualPagePair(
|
|
for: pageNum,
|
|
totalPages: environment.totalPages,
|
|
coverPageIndex: environment.coverPageIndex
|
|
)
|
|
var pages: Set<Int> = [pair.left]
|
|
if let right = pair.right, shouldCachePage(right, environment: environment) {
|
|
pages.insert(right)
|
|
}
|
|
return pages
|
|
}
|
|
|
|
private func adjacentSpreadStart(from pageNum: Int, forward: Bool, environment: Environment) -> Int? {
|
|
guard environment.totalPages > 0,
|
|
let spreadStart = spreadStart(for: pageNum, environment: environment) else {
|
|
return nil
|
|
}
|
|
|
|
guard isDualPage(environment: environment) else {
|
|
let target = forward ? spreadStart + 1 : spreadStart - 1
|
|
return shouldCachePage(target, environment: environment) ? target : nil
|
|
}
|
|
|
|
if forward {
|
|
return environment.spreadResolver.adjacentDualPage(
|
|
from: spreadStart,
|
|
totalPages: environment.totalPages,
|
|
coverPageIndex: environment.coverPageIndex,
|
|
forward: true
|
|
)
|
|
}
|
|
|
|
let pair = environment.spreadResolver.dualPagePair(
|
|
for: spreadStart,
|
|
totalPages: environment.totalPages,
|
|
coverPageIndex: environment.coverPageIndex
|
|
)
|
|
let prevEnd = pair.left - 1
|
|
guard prevEnd >= 0 else { return nil }
|
|
return environment.spreadResolver.dualPagePair(
|
|
for: prevEnd,
|
|
totalPages: environment.totalPages,
|
|
coverPageIndex: environment.coverPageIndex
|
|
).left
|
|
}
|
|
|
|
private func visiblePageNumbers(for anchorPage: Int, environment: Environment) -> Set<Int> {
|
|
spreadPageNumbers(startingAt: anchorPage, environment: environment)
|
|
}
|
|
|
|
private func detachedReusablePageView(for pageNum: Int) -> UIView? {
|
|
if let preloaded = preloadedPageViews.removeValue(forKey: pageNum) {
|
|
preloaded.removeFromSuperview()
|
|
return preloaded
|
|
}
|
|
|
|
guard let cached = pageCurlCachedViews[pageNum], cached.superview == nil else {
|
|
return nil
|
|
}
|
|
|
|
pageCurlCachedViews.removeValue(forKey: pageNum)
|
|
return cached
|
|
}
|
|
|
|
private func trimCachedPageViews(keeping pageNumbers: Set<Int>) {
|
|
pageCurlCachedViews = pageCurlCachedViews.filter { key, value in
|
|
let keep = pageNumbers.contains(key)
|
|
if !keep {
|
|
value.removeFromSuperview()
|
|
}
|
|
return keep
|
|
}
|
|
|
|
preloadedPageViews = preloadedPageViews.filter { key, value in
|
|
let keep = pageNumbers.contains(key)
|
|
if !keep {
|
|
value.removeFromSuperview()
|
|
}
|
|
return keep
|
|
}
|
|
}
|
|
|
|
private func forecastTargets(around pageNum: Int, preferredForward: Bool?, environment: Environment) -> [Int] {
|
|
guard environment.totalPages > 0,
|
|
shouldCachePage(pageNum, environment: environment) else {
|
|
return []
|
|
}
|
|
|
|
var targets = Set<Int>()
|
|
|
|
var previousAnchor = pageNum
|
|
for _ in 0..<max(radius, 0) {
|
|
guard let previous = adjacentSpreadStart(from: previousAnchor, forward: false, environment: environment) else { break }
|
|
targets.formUnion(spreadPageNumbers(startingAt: previous, environment: environment))
|
|
previousAnchor = previous
|
|
}
|
|
|
|
var nextAnchor = pageNum
|
|
for _ in 0..<max(radius, 0) {
|
|
guard let next = adjacentSpreadStart(from: nextAnchor, forward: true, environment: environment) else { break }
|
|
targets.formUnion(spreadPageNumbers(startingAt: next, environment: environment))
|
|
nextAnchor = next
|
|
}
|
|
|
|
if let preferredForward,
|
|
let edgeAnchor = preferredForward ? adjacentSpreadStart(from: nextAnchor, forward: true, environment: environment)
|
|
: adjacentSpreadStart(from: previousAnchor, forward: false, environment: environment) {
|
|
targets.formUnion(spreadPageNumbers(startingAt: edgeAnchor, environment: environment))
|
|
}
|
|
|
|
return targets.sorted()
|
|
}
|
|
}
|