ReadViewSDK/Sources/RDEpubReaderView/ReaderView/Paging/RDEpubReaderPreloadController.swift
shenlei d7fcda345d refactor: rename RDReaderView -> RDEpubReaderView, update pod config and docs
- Rename source module from RDReaderView to RDEpubReaderView
- Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/
- Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec
- Update Podfile, demo project, and CocoaPods config for new pod name
- Delete old RDReaderView pod support files from ReadViewDemo/Pods
- Add new RDEpubReaderView pod support files
- Update documentation (API ref, architecture, UML, conventions, etc.)
- Add FixedLayoutRotationTests
- Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
2026-07-10 19:44:53 +09:00

367 lines
14 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()
}
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 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 {
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 {
contentView = contentViewProvider(targetPage, nil) ?? UIView()
RDEpubReaderTapDebug.log(
"PreloadController.prime",
"cache MISS page=\(targetPage) created=\(RDEpubReaderTapDebug.describe(contentView))"
)
}
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 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 {
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.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()
}
}