ReadViewSDK/Sources/RDEpubReaderView/EPUBUI/ReaderController/RDEPUBReaderChromeCoordinator.swift

314 lines
12 KiB
Swift

import UIKit
final class RDEPUBReaderChromeCoordinator: NSObject, UIAdaptivePresentationControllerDelegate {
private unowned let context: RDEPUBReaderContext
private var settingsTransitionDelegate: RDEPUBReaderSettingsTransitionDelegate?
init(context: RDEPUBReaderContext) {
self.context = context
}
private var controller: RDEPUBReaderController? {
context.controller
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
context.runtime?.settingsPanelDidDisappear()
}
func makeTopToolView() -> RDEPUBReaderTopToolViewProviding {
let toolView: RDEPUBReaderTopToolViewProviding =
context.controller?.dependencies.makeTopToolView?() ?? RDEPUBReaderTopToolView()
toolView.onBack = { [weak self] in
self?.handleBackAction()
}
toolView.onSearch = { [weak self] in
self?.toggleSearchBar()
}
toolView.onToggleBookmark = { [weak self] in
_ = self?.context.runtime?.toggleBookmark()
}
return toolView
}
func makeBottomToolView() -> RDEPUBReaderBottomToolViewProviding {
let toolView: RDEPUBReaderBottomToolViewProviding =
context.controller?.dependencies.makeBottomToolView?() ?? RDEPUBReaderBottomToolView()
toolView.onShowTableOfContents = { [weak self] in
self?.presentTableOfContents()
}
toolView.onShowBookmarks = { [weak self] in
self?.context.runtime?.presentBookmarksManager()
}
toolView.onShowHighlights = { [weak self] in
self?.context.runtime?.presentHighlightsManager()
}
toolView.onAddHighlight = { [weak self] in
self?.context.runtime?.presentAnnotationCreation()
}
toolView.onShowSettings = { [weak self] in
self?.presentSettings()
}
return toolView
}
func updateReaderChrome() {
guard let controller else { return }
let uiState = makeUIState()
applyUIState(uiState)
updateSearchBar()
}
func makeUIState() -> RDEPUBReaderUIState {
guard let controller else { return .empty }
return RDEPUBReaderUIState(
canToggleBookmark: controller.currentBookIdentifier != nil,
hasBookmarkAtCurrentLocation: hasBookmarkAtCurrentLocation(),
canShowBookmarks: !controller.activeBookmarks.isEmpty,
canAddHighlight: controller.configuration.allowsHighlights && context.selectionState.hasSelection,
canShowHighlights: controller.configuration.allowsHighlights && !controller.activeHighlights.isEmpty,
showsTableOfContents: controller.configuration.showsTableOfContents,
allowsHighlights: controller.configuration.allowsHighlights,
showsSettingsPanel: controller.configuration.showsSettingsPanel
)
}
func applyUIState(_ state: RDEPUBReaderUIState) {
guard let controller else { return }
controller.topToolView.apply(theme: controller.configuration.theme)
controller.topToolView.setTitle(
controller.title
?? controller.parser?.metadata.title
?? controller.epubURL.deletingPathExtension().lastPathComponent
)
controller.topToolView.setBookmarkEnabled(state.canToggleBookmark)
controller.topToolView.setBookmarkSelected(state.hasBookmarkAtCurrentLocation)
controller.bottomToolView.apply(theme: controller.configuration.theme)
controller.bottomToolView.updateVisibility(
showsTableOfContents: state.showsTableOfContents,
allowsHighlights: state.allowsHighlights,
showsSettingsPanel: state.showsSettingsPanel
)
controller.bottomToolView.setBookmarksEnabled(state.canShowBookmarks)
controller.bottomToolView.setAddHighlightEnabled(state.canAddHighlight)
controller.bottomToolView.setHighlightsEnabled(state.canShowHighlights)
}
private func hasBookmarkAtCurrentLocation() -> Bool {
guard let controller else { return false }
return context.runtime?.annotationCoordinator.currentBookmark() != nil
}
func presentSettings() {
guard let controller else { return }
guard controller.configuration.showsSettingsPanel else { return }
context.runtime?.settingsPanelWillAppear()
let settingsController = RDEPUBReaderSettingsViewController(
configuration: controller.configuration,
brightness: controller.currentBrightness
)
settingsController.onBrightnessChange = { [weak controller] brightness in
controller?.setScreenBrightness(brightness)
}
settingsController.onFontSizeChange = { [weak controller] fontSize in
controller?.updateConfiguration { $0.fontSize = fontSize }
}
settingsController.onFontChoiceChange = { [weak controller] fontChoice in
controller?.updateConfiguration { $0.fontChoice = fontChoice }
}
settingsController.onLineHeightChange = { [weak controller] lineHeightMultiple in
controller?.updateConfiguration { $0.lineHeightMultiple = lineHeightMultiple }
}
settingsController.onDisplayTypeChange = { [weak controller] displayType in
controller?.updateConfiguration { $0.displayType = displayType }
}
settingsController.onThemeChange = { [weak controller] theme in
controller?.updateConfiguration { $0.theme = theme }
}
settingsController.onDismiss = { [weak self] in
self?.context.runtime?.settingsPanelDidDisappear()
}
let navigationController = UINavigationController(rootViewController: settingsController)
let transitionDelegate = RDEPUBReaderSettingsTransitionDelegate()
settingsTransitionDelegate = transitionDelegate
navigationController.setNavigationBarHidden(true, animated: false)
navigationController.view.backgroundColor = .clear
navigationController.view.isOpaque = false
navigationController.modalPresentationStyle = .custom
navigationController.transitioningDelegate = transitionDelegate
controller.present(navigationController, animated: true)
}
func presentTableOfContents() {
guard let controller else { return }
guard controller.configuration.showsTableOfContents else { return }
let items = controller.flattenedTableOfContentsItems(
from: controller.publication?.tableOfContents ?? [],
includePageNumbers: false
)
guard !items.isEmpty else { return }
let chapterController = RDEPUBReaderChapterListController(
items: items,
currentItem: controller.currentTableOfContentsItem,
theme: controller.configuration.theme
)
chapterController.onSelectItem = { [weak controller] item in
guard let controller else { return }
chapterController.dismiss(animated: true) {
_ = controller.go(toTableOfContentsItem: item, animated: true)
}
}
let navigationController = UINavigationController(rootViewController: chapterController)
navigationController.modalPresentationStyle = .pageSheet
controller.present(navigationController, animated: true)
}
func toggleSearchBar() {
guard let controller else { return }
if controller.isSearchBarVisible {
controller.hideSearchBar()
} else {
controller.showSearchBar()
}
}
func updateSearchBar() {
guard let controller else { return }
controller.searchBarView.apply(theme: controller.configuration.theme)
if let searchState = controller.searchState {
if let index = searchState.currentMatchIndex {
controller.searchBarView.updateMatchCount(current: index + 1, total: searchState.matches.count)
} else if searchState.matches.isEmpty {
controller.searchBarView.showNoResults()
}
}
}
func handleBackAction() {
guard let controller else { return }
close(controller)
}
private func close(_ controller: UIViewController) {
let target = closestDismissTarget(from: controller)
if let navigationController = target.navigationController,
navigationController.viewControllers.first !== target {
navigationController.popViewController(animated: true)
return
}
if let navigationController = target.navigationController,
navigationController.presentingViewController != nil {
navigationController.dismiss(animated: true)
return
}
if target.presentingViewController != nil {
target.dismiss(animated: true)
return
}
controller.dismiss(animated: true)
}
private func closestDismissTarget(from controller: UIViewController) -> UIViewController {
var candidate: UIViewController = controller
var current = controller.parent
while let parent = current {
if let navigationController = parent.navigationController,
navigationController.viewControllers.contains(parent) {
return parent
}
if parent.presentingViewController != nil || parent.navigationController?.presentingViewController != nil {
return parent
}
candidate = parent
current = parent.parent
}
return candidate
}
}
private final class RDEPUBReaderSettingsTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {
func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController
) -> UIPresentationController? {
RDEPUBReaderSettingsPresentationController(
presentedViewController: presented,
presenting: presenting ?? source
)
}
}
private final class RDEPUBReaderSettingsPresentationController: UIPresentationController {
private let dimmingView = UIControl()
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerView else { return .zero }
let bounds = containerView.bounds
let isLandscape = bounds.width > bounds.height
if isLandscape {
let width = min(390, max(300, bounds.width * 0.32))
let height = min(340, bounds.height - 32)
return CGRect(
x: bounds.maxX - width - 16,
y: bounds.midY - height / 2,
width: width,
height: height
)
}
let height = min(350, max(280, bounds.height * 0.40))
return CGRect(x: 0, y: bounds.maxY - height, width: bounds.width, height: height)
}
override func presentationTransitionWillBegin() {
guard let containerView else { return }
dimmingView.backgroundColor = UIColor.black.withAlphaComponent(0.18)
dimmingView.alpha = 0
dimmingView.addTarget(self, action: #selector(dismissPresentedController), for: .touchUpInside)
dimmingView.frame = containerView.bounds
containerView.insertSubview(dimmingView, at: 0)
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 1
})
}
override func dismissalTransitionWillBegin() {
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 0
})
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
dimmingView.frame = containerView?.bounds ?? .zero
presentedView?.frame = frameOfPresentedViewInContainerView
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
super.dismissalTransitionDidEnd(completed)
if completed { dimmingView.removeFromSuperview() }
}
@objc private func dismissPresentedController() {
if let navigationController = presentedViewController as? UINavigationController,
let settingsController = navigationController.topViewController as? RDEPUBReaderSettingsViewController {
settingsController.notifyDismissalIfNeeded()
}
presentedViewController.dismiss(animated: true)
}
}