Files
ReadViewSDK/Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderController+Trial.swift
T
2026-07-11 08:16:56 +08:00

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
}
}