import UIKit final class RDEPUBReaderViewportMonitor { private unowned let context: RDEPUBReaderContext private var lastAppliedViewportSignature: RDEPUBViewportSignature? private var pendingViewportChangeReason: RDEPUBViewportChangeReason? private var pendingPresentationRestoreLocation: RDEPUBLocation? private var isWaitingForViewportTransitionCompletion = false init(context: RDEPUBReaderContext) { self.context = context } private var controller: RDEPUBReaderController? { context.controller } func viewDidLayoutSubviews() { guard let controller else { return } guard let viewportSignature = currentViewportSignature() else { return } if !controller.didStartInitialLoad { lastAppliedViewportSignature = viewportSignature controller.startInitialLoadIfNeeded() return } guard controller.publication != nil || controller.isExternalTextBook else { lastAppliedViewportSignature = viewportSignature return } guard !isWaitingForViewportTransitionCompletion else { return } handleViewportChangeIfNeeded(reason: .viewLayout, viewportSignature: viewportSignature) } func viewWillTransition(with coordinator: UIViewControllerTransitionCoordinator) { guard let controller else { return } guard controller.didStartInitialLoad else { return } pendingPresentationRestoreLocation = controller.currentVisibleLocation() ?? controller.persistenceLocation() isWaitingForViewportTransitionCompletion = true coordinator.animate(alongsideTransition: nil) { [weak self] _ in guard let self, let controller = self.controller else { return } self.isWaitingForViewportTransitionCompletion = false controller.view.layoutIfNeeded() self.handleViewportChangeIfNeeded(reason: .orientationTransition) } } func resetForReload() { lastAppliedViewportSignature = currentViewportSignature() pendingViewportChangeReason = nil pendingPresentationRestoreLocation = nil isWaitingForViewportTransitionCompletion = false } func consumePendingPresentationRestoreLocation() -> RDEPUBLocation? { defer { pendingPresentationRestoreLocation = nil } return pendingPresentationRestoreLocation } func capturePendingPresentationRestoreLocation() { guard let controller else { return } pendingPresentationRestoreLocation = controller.currentVisibleLocation() ?? controller.persistenceLocation() } func processPendingChangeAfterPagination() { guard let pendingReason = pendingViewportChangeReason else { return } pendingViewportChangeReason = nil DispatchQueue.main.async { [weak self] in self?.handleViewportChangeIfNeeded(reason: pendingReason) } } func currentViewportSignature() -> RDEPUBViewportSignature? { guard let controller else { return nil } let containerSize = controller.readerView.bounds.size == .zero ? controller.view.bounds.size : controller.readerView.bounds.size guard containerSize.width > 0, containerSize.height > 0 else { return nil } let insets = controller.view.safeAreaInsets return RDEPUBViewportSignature( width: containerSize.width, height: containerSize.height, safeTop: insets.top, safeLeft: insets.left, safeBottom: insets.bottom, safeRight: insets.right ) } func handleViewportChangeIfNeeded( reason: RDEPUBViewportChangeReason, viewportSignature: RDEPUBViewportSignature? = nil ) { guard let controller else { return } guard controller.didStartInitialLoad, let signature = viewportSignature ?? currentViewportSignature() else { return } if controller.isRepaginating { pendingViewportChangeReason = reason return } if let lastAppliedViewportSignature, !signature.differsSignificantly(from: lastAppliedViewportSignature) { return } lastAppliedViewportSignature = signature if controller.isExternalTextBook { controller.rebuildExternalTextBook() return } guard controller.publication != nil else { return } controller.repaginatePreservingCurrentLocation() } }