105 lines
4.3 KiB
Swift
105 lines
4.3 KiB
Swift
import UIKit
|
||
|
||
/// 试读墙支持。
|
||
///
|
||
/// 设计:`pageCountOfReaderView` 仍返回原始内容页数(内容页显示的总页数不受影响),
|
||
/// 仅 `numberOfPages(in:)` 在启用试读墙时 +1;试读墙页固定位于最后一个可读页之后。
|
||
/// 两种形态统一覆盖:
|
||
/// - readoor 独立试读 epub(整本都是试读章节):readableChapterCount 传 nil,墙追加在全书末尾;
|
||
/// - 整本 epub + 按章限制:readableChapterCount 传 N,超出章节页不展示(仅 bookPageMap 路径)。
|
||
extension RDEPUBReaderController {
|
||
|
||
/// 试读墙视图(首次访问经 delegate 获取并缓存)
|
||
var trialWallView: UIView? {
|
||
if let cached = cachedTrialWallView {
|
||
return cached
|
||
}
|
||
guard configuration.trialPolicy != nil else { return nil }
|
||
let view = delegate?.epubReaderTrialWallView(self)
|
||
cachedTrialWallView = view
|
||
return view
|
||
}
|
||
|
||
/// 是否启用试读墙:配置了试读策略且宿主提供了墙视图
|
||
var isTrialWallEnabled: Bool {
|
||
configuration.trialPolicy != nil && trialWallView != nil
|
||
}
|
||
|
||
/// 原始可读内容页数(不含试读墙页)。
|
||
/// readableChapterCount 指定且存在 bookPageMap 时按章截断,否则为全部内容页。
|
||
var readableContentPageCount: Int {
|
||
let rawCount = pageCountOfReaderView(readerView: readerView)
|
||
guard let policy = configuration.trialPolicy,
|
||
let readableChapterCount = policy.readableChapterCount,
|
||
readableChapterCount > 0,
|
||
let pageMap = readerContext.bookPageMap else {
|
||
return rawCount
|
||
}
|
||
// 第一个不可读章节(spineIndex >= readableChapterCount)的起始绝对页 = 可读页数
|
||
let firstLockedStart = pageMap.entries
|
||
.first { $0.spineIndex >= readableChapterCount }?
|
||
.absolutePageStart
|
||
return firstLockedStart ?? rawCount
|
||
}
|
||
|
||
/// 试读墙页的绝对索引(0-based);未启用时为 nil
|
||
var trialWallPageIndex: Int? {
|
||
guard isTrialWallEnabled else { return nil }
|
||
return readableContentPageCount
|
||
}
|
||
|
||
/// 供 RDEpubReaderView 使用的有效页数:启用试读墙时为可读页数 + 1(墙页)
|
||
var effectiveNumberOfPages: Int {
|
||
guard isTrialWallEnabled else {
|
||
return pageCountOfReaderView(readerView: readerView)
|
||
}
|
||
return readableContentPageCount + 1
|
||
}
|
||
|
||
func isReadableTableOfContentsLocation(_ location: RDEPUBLocation) -> Bool {
|
||
guard let policy = configuration.trialPolicy,
|
||
let readableChapterCount = policy.readableChapterCount else {
|
||
return true
|
||
}
|
||
guard let spineIndex = readerContext.normalizedSpineIndex(for: location) else {
|
||
return true
|
||
}
|
||
return spineIndex < readableChapterCount
|
||
}
|
||
|
||
/// 构建试读墙页承载视图(铺满一页,居中放置宿主提供的墙视图)
|
||
func makeTrialWallPageView(reusableView: UIView?) -> UIView {
|
||
let container = (reusableView as? RDEPUBTrialWallContainerView) ?? RDEPUBTrialWallContainerView()
|
||
container.setWallView(trialWallView)
|
||
return container
|
||
}
|
||
}
|
||
|
||
/// 试读墙页容器:让宿主墙视图铺满整页
|
||
final class RDEPUBTrialWallContainerView: UIView, RDEpubReaderCachePolicyProviding {
|
||
|
||
/// 宿主只提供一份试读墙 View。预加载会同时构建多个页容器,若缓存该容器会
|
||
/// 把同一 View 从实际可见页移走,最终表现为横竖翻页的试读墙白屏。
|
||
var shouldAvoidReaderPageCaching: Bool { true }
|
||
|
||
private weak var wallView: UIView?
|
||
|
||
func setWallView(_ view: UIView?) {
|
||
// 即使 View 对象相同,只要它已被其他复用容器接管,也必须重新挂回当前页。
|
||
if wallView === view, view?.superview === self {
|
||
return
|
||
}
|
||
wallView?.removeFromSuperview()
|
||
guard let view else { return }
|
||
view.translatesAutoresizingMaskIntoConstraints = false
|
||
addSubview(view)
|
||
NSLayoutConstraint.activate([
|
||
view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||
view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||
view.topAnchor.constraint(equalTo: topAnchor),
|
||
view.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||
])
|
||
wallView = view
|
||
}
|
||
}
|