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