ReadViewSDK/Sources/RDEpubReaderView/ReaderView/Paging/RDEpubReaderPreloadController.swift

396 lines
16 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import UIKit
protocol RDEpubReaderCachePolicyProviding {
var shouldAvoidReaderPageCaching: Bool { get }
}
///
/// WKWebView dealloc
/// configure
protocol RDEpubReaderPageResourceReleasing {
func releaseResources()
}
///
///
protocol RDEpubReaderPageVisibilityHandling: AnyObject {
func readerPageVisibilityDidChange(isVisible: Bool)
}
final class RDEpubReaderPreloadController {
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: RDEpubReaderView.DisplayType
let isLandscape: Bool
let pagesPerScreen: Int
let boundsSize: CGSize
let landscapeDualPageEnabled: Bool
let coverPageIndex: Int?
let totalPages: Int
let spreadResolver: RDEpubReaderSpreadResolver
}
private struct CacheSignature: Equatable {
let displayType: RDEpubReaderView.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) {
RDEpubReaderTapDebug.log(
"PreloadController.invalidate",
"clearing pageCurlCached=\(pageCurlCachedViews.keys.sorted()) preloaded=\(preloadedPageViews.keys.sorted())"
)
pageCurlCachedViews.values.forEach { discard($0) }
preloadedPageViews.values.forEach { discard($0) }
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
RDEpubReaderTapDebug.log(
"PreloadController.pageViewForDisplay",
"cache HIT page=\(pageNum) view=\(RDEpubReaderTapDebug.describe(reusableView))"
)
} else {
view = contentViewProvider(pageNum, nil) ?? UIView()
RDEpubReaderTapDebug.log(
"PreloadController.pageViewForDisplay",
"cache MISS page=\(pageNum) created=\(RDEpubReaderTapDebug.describe(view))"
)
}
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()
RDEpubReaderTapDebug.log(
"PreloadController.takePreloadedView",
"cache \(preloaded == nil ? "MISS" : "HIT") page=\(pageNum) view=\(RDEpubReaderTapDebug.describe(preloaded))"
)
return preloaded
}
func updatePageVisibility(visibleViewIdentifiers: Set<ObjectIdentifier>) {
let cachedViews = pageCurlCachedViews.merging(preloadedPageViews) { current, _ in current }
var notifiedViews = Set<ObjectIdentifier>()
// Cache entries are never the authority for activation: UIPageViewController
// can create two views for the same page number. Keep every non-winning
// instance suspended; the reader activates only its actually attached view.
for view in cachedViews.values {
let identifier = ObjectIdentifier(view)
guard !visibleViewIdentifiers.contains(identifier),
notifiedViews.insert(identifier).inserted else { continue }
(view as? RDEpubReaderPageVisibilityHandling)?
.readerPageVisibilityDidChange(isVisible: false)
}
}
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(spreadPageNumbers(startingAt: pageNum, environment: environment))
trimCachedPageViews(keeping: keepSet)
guard !targets.isEmpty else { return }
ensureHostView(in: parentView)
preloadHostView.frame = parentView.bounds
for targetPage in targets {
if let liveCached = pageCurlCachedViews[targetPage],
liveCached.superview != nil,
liveCached.superview !== preloadHostView {
//
//
//
RDEpubReaderTapDebug.log(
"PreloadController.prime",
"skip live page=\(targetPage) view=\(RDEpubReaderTapDebug.describe(liveCached))"
)
continue
}
let contentView: UIView
if let existing = preloadedPageViews[targetPage], existing.superview === preloadHostView {
contentView = existing
RDEpubReaderTapDebug.log(
"PreloadController.prime",
"cache HIT(preloaded) page=\(targetPage) view=\(RDEpubReaderTapDebug.describe(existing))"
)
} else if let cached = pageCurlCachedViews.removeValue(forKey: targetPage), cached.superview == nil {
contentView = cached
RDEpubReaderTapDebug.log(
"PreloadController.prime",
"cache HIT(pageCurl) page=\(targetPage) view=\(RDEpubReaderTapDebug.describe(cached))"
)
} else {
guard let providedView = contentViewProvider(targetPage, nil) else {
//
//
preloadedPageViews.removeValue(forKey: targetPage)
pageCurlCachedViews.removeValue(forKey: targetPage)
continue
}
contentView = providedView
RDEpubReaderTapDebug.log(
"PreloadController.prime",
"cache MISS page=\(targetPage) created=\(RDEpubReaderTapDebug.describe(contentView))"
)
}
(contentView as? RDEpubReaderPageVisibilityHandling)?
.readerPageVisibilityDidChange(isVisible: false)
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 != RDEpubReaderView.blankPageNum
&& pageNum != RDEpubReaderView.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? RDEpubReaderCachePolicyProviding {
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 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 {
discard(value)
}
return keep
}
preloadedPageViews = preloadedPageViews.filter { key, value in
let keep = pageNumbers.contains(key)
if !keep {
discard(value)
}
return keep
}
}
private func discard(_ view: UIView) {
// 宿
// VC / cell UIPageViewController
//
// deinit
let isHeldOutsidePreloadHost = view.superview != nil && view.superview !== preloadHostView
(view as? RDEpubReaderPageVisibilityHandling)?
.readerPageVisibilityDidChange(isVisible: false)
view.removeFromSuperview()
if !isHeldOutsidePreloadHost {
(view as? RDEpubReaderPageResourceReleasing)?.releaseResources()
}
}
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()
}
}