安全区域改为从 key window 动态获取
- 新增 RDEPUBSafeArea:参考 GKNavigationBarSwift 的 keyWindow 三级查找与临时 window 兜底 - 移除 reflowableContentInsets 中 top/bottom 40 的固定默认值,改按设备真实安全区生成 - 各取安全区处的 ?? .zero 兜底改为回退到 key window 值 - DEBUG 构建下为文本可读区域绘制红色边框,方便查看排版范围 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ca408c20ab
commit
d0efe3f2cc
3006
ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj
generated
3006
ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj
generated
File diff suppressed because it is too large
Load Diff
@ -18,7 +18,7 @@ public struct RDEPUBNavigatorLayoutContext: Equatable {
|
|||||||
pagesPerScreen: Int = 1,
|
pagesPerScreen: Int = 1,
|
||||||
safeAreaInsets: UIEdgeInsets = .zero,
|
safeAreaInsets: UIEdgeInsets = .zero,
|
||||||
userInterfaceIdiom: UIUserInterfaceIdiom = .phone,
|
userInterfaceIdiom: UIUserInterfaceIdiom = .phone,
|
||||||
reflowableContentInsets: UIEdgeInsets = UIEdgeInsets(top: 40, left: 16, bottom: 40, right: 16)
|
reflowableContentInsets: UIEdgeInsets = RDEPUBSafeArea.defaultReflowableContentInsets()
|
||||||
) {
|
) {
|
||||||
self.containerSize = containerSize
|
self.containerSize = containerSize
|
||||||
self.pagesPerScreen = max(1, pagesPerScreen)
|
self.pagesPerScreen = max(1, pagesPerScreen)
|
||||||
|
|||||||
67
Sources/RDReaderView/EPUBCore/RDEPUBSafeArea.swift
Normal file
67
Sources/RDReaderView/EPUBCore/RDEPUBSafeArea.swift
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// Device-level safe area lookup modeled after GKNavigationBarSwift.
|
||||||
|
/// Reads the real safe area from the key window so correct values are
|
||||||
|
/// available even before a view has been laid out in the hierarchy.
|
||||||
|
/// Must be called on the main thread.
|
||||||
|
public enum RDEPUBSafeArea {
|
||||||
|
|
||||||
|
/// Minimum text margins applied when the device safe area on an edge is
|
||||||
|
/// smaller (e.g. no notch / no home indicator). These are aesthetic
|
||||||
|
/// paddings, not approximations of the safe area itself.
|
||||||
|
public static let minimumVerticalTextMargin: CGFloat = 20
|
||||||
|
|
||||||
|
public static let minimumHorizontalTextMargin: CGFloat = 16
|
||||||
|
|
||||||
|
public static func keyWindow() -> UIWindow? {
|
||||||
|
let scenes = UIApplication.shared.connectedScenes
|
||||||
|
.compactMap { $0 as? UIWindowScene }
|
||||||
|
if let window = scenes
|
||||||
|
.filter({ $0.activationState == .foregroundActive })
|
||||||
|
.flatMap({ $0.windows })
|
||||||
|
.first(where: { $0.isKeyWindow }) {
|
||||||
|
return window
|
||||||
|
}
|
||||||
|
if let window = scenes
|
||||||
|
.flatMap({ $0.windows })
|
||||||
|
.first(where: { $0.isKeyWindow }) {
|
||||||
|
return window
|
||||||
|
}
|
||||||
|
return UIApplication.shared.delegate?.window ?? nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func insets() -> UIEdgeInsets {
|
||||||
|
if let window = keyWindow() {
|
||||||
|
return window.safeAreaInsets
|
||||||
|
}
|
||||||
|
// No key window yet (early launch): create a detached window to read
|
||||||
|
// the device safe area, same fallback as GKNavigationBarSwift.
|
||||||
|
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||||
|
if window.safeAreaInsets.bottom <= 0 {
|
||||||
|
window.rootViewController = UIViewController()
|
||||||
|
}
|
||||||
|
return window.safeAreaInsets
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prefers insets measured from a view already installed in the hierarchy;
|
||||||
|
/// falls back to the key-window insets when the view is not laid out yet
|
||||||
|
/// and reports .zero.
|
||||||
|
public static func resolve(_ measuredInsets: UIEdgeInsets?) -> UIEdgeInsets {
|
||||||
|
if let measuredInsets, measuredInsets != .zero {
|
||||||
|
return measuredInsets
|
||||||
|
}
|
||||||
|
return insets()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default reflowable content insets derived from the live device safe
|
||||||
|
/// area instead of hard-coded heights.
|
||||||
|
public static func defaultReflowableContentInsets() -> UIEdgeInsets {
|
||||||
|
let safe = insets()
|
||||||
|
return UIEdgeInsets(
|
||||||
|
top: max(safe.top, minimumVerticalTextMargin),
|
||||||
|
left: max(safe.left, minimumHorizontalTextMargin),
|
||||||
|
bottom: max(safe.bottom, minimumVerticalTextMargin),
|
||||||
|
right: max(safe.right, minimumHorizontalTextMargin)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -154,7 +154,7 @@ public final class RDURLReaderController: UIViewController {
|
|||||||
let bookTitle = bookURL.deletingPathExtension().lastPathComponent
|
let bookTitle = bookURL.deletingPathExtension().lastPathComponent
|
||||||
let pageSize = currentTextPageSize()
|
let pageSize = currentTextPageSize()
|
||||||
let renderStyle = currentTextRenderStyle()
|
let renderStyle = currentTextRenderStyle()
|
||||||
let safeInsets = view.safeAreaInsets
|
let safeInsets = RDEPUBSafeArea.resolve(view.safeAreaInsets)
|
||||||
let edgeInsets = UIEdgeInsets(
|
let edgeInsets = UIEdgeInsets(
|
||||||
top: max(epubConfiguration.reflowableContentInsets.top, safeInsets.top),
|
top: max(epubConfiguration.reflowableContentInsets.top, safeInsets.top),
|
||||||
left: max(epubConfiguration.reflowableContentInsets.left, safeInsets.left),
|
left: max(epubConfiguration.reflowableContentInsets.left, safeInsets.left),
|
||||||
|
|||||||
@ -163,7 +163,7 @@ final class RDEPUBReaderContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func currentPreferences() -> RDEPUBPreferences {
|
func currentPreferences() -> RDEPUBPreferences {
|
||||||
let safeInsets = controller?.view.safeAreaInsets ?? .zero
|
let safeInsets = RDEPUBSafeArea.resolve(controller?.view.safeAreaInsets)
|
||||||
return configuration.makePreferences(safeAreaInsets: safeInsets)
|
return configuration.makePreferences(safeAreaInsets: safeInsets)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,7 +47,7 @@ final class RDEPUBReaderEnvironment {
|
|||||||
return RDEPUBNavigatorLayoutContext(
|
return RDEPUBNavigatorLayoutContext(
|
||||||
containerSize: resolvedSize,
|
containerSize: resolvedSize,
|
||||||
pagesPerScreen: readerView?.pagesPerScreen ?? 1,
|
pagesPerScreen: readerView?.pagesPerScreen ?? 1,
|
||||||
safeAreaInsets: controller?.view.safeAreaInsets ?? .zero,
|
safeAreaInsets: RDEPUBSafeArea.resolve(controller?.view.safeAreaInsets),
|
||||||
userInterfaceIdiom: controller?.traitCollection.userInterfaceIdiom ?? .phone,
|
userInterfaceIdiom: controller?.traitCollection.userInterfaceIdiom ?? .phone,
|
||||||
reflowableContentInsets: configuration.reflowableContentInsets
|
reflowableContentInsets: configuration.reflowableContentInsets
|
||||||
)
|
)
|
||||||
@ -68,7 +68,7 @@ final class RDEPUBReaderEnvironment {
|
|||||||
configuration: RDEPUBReaderConfiguration,
|
configuration: RDEPUBReaderConfiguration,
|
||||||
pageSize: CGSize
|
pageSize: CGSize
|
||||||
) -> RDEPUBTextLayoutConfig {
|
) -> RDEPUBTextLayoutConfig {
|
||||||
let safeAreaInsets = controller?.view.safeAreaInsets ?? .zero
|
let safeAreaInsets = RDEPUBSafeArea.resolve(controller?.view.safeAreaInsets)
|
||||||
return RDEPUBTextLayoutConfig(
|
return RDEPUBTextLayoutConfig(
|
||||||
frameWidth: max(pageSize.width, 1),
|
frameWidth: max(pageSize.width, 1),
|
||||||
frameHeight: max(pageSize.height, 1),
|
frameHeight: max(pageSize.height, 1),
|
||||||
|
|||||||
@ -109,7 +109,7 @@ public struct RDEPUBReaderConfiguration: Equatable {
|
|||||||
showsTableOfContents: Bool = true,
|
showsTableOfContents: Bool = true,
|
||||||
allowsHighlights: Bool = true,
|
allowsHighlights: Bool = true,
|
||||||
showsSettingsPanel: Bool = true,
|
showsSettingsPanel: Bool = true,
|
||||||
reflowableContentInsets: UIEdgeInsets = UIEdgeInsets(top: 40, left: 16, bottom: 40, right: 16),
|
reflowableContentInsets: UIEdgeInsets = RDEPUBSafeArea.defaultReflowableContentInsets(),
|
||||||
fixedContentInset: UIEdgeInsets = .zero,
|
fixedContentInset: UIEdgeInsets = .zero,
|
||||||
theme: RDEPUBReaderTheme = .light,
|
theme: RDEPUBReaderTheme = .light,
|
||||||
darkImageAdjustmentEnabled: Bool = true,
|
darkImageAdjustmentEnabled: Bool = true,
|
||||||
@ -172,11 +172,13 @@ extension RDEPUBReaderConfiguration {
|
|||||||
func makePreferences(safeAreaInsets: UIEdgeInsets = .zero) -> RDEPUBPreferences {
|
func makePreferences(safeAreaInsets: UIEdgeInsets = .zero) -> RDEPUBPreferences {
|
||||||
// Use the larger of reflowableContentInsets and safeAreaInsets for each edge
|
// Use the larger of reflowableContentInsets and safeAreaInsets for each edge
|
||||||
// to prevent content from being hidden under Dynamic Island / home indicator.
|
// to prevent content from being hidden under Dynamic Island / home indicator.
|
||||||
|
// Falls back to the key-window safe area when the caller has no laid-out view.
|
||||||
|
let resolvedSafeAreaInsets = RDEPUBSafeArea.resolve(safeAreaInsets)
|
||||||
let safeInsets = UIEdgeInsets(
|
let safeInsets = UIEdgeInsets(
|
||||||
top: max(reflowableContentInsets.top, safeAreaInsets.top),
|
top: max(reflowableContentInsets.top, resolvedSafeAreaInsets.top),
|
||||||
left: max(reflowableContentInsets.left, safeAreaInsets.left),
|
left: max(reflowableContentInsets.left, resolvedSafeAreaInsets.left),
|
||||||
bottom: max(reflowableContentInsets.bottom, safeAreaInsets.bottom),
|
bottom: max(reflowableContentInsets.bottom, resolvedSafeAreaInsets.bottom),
|
||||||
right: max(reflowableContentInsets.right, safeAreaInsets.right)
|
right: max(reflowableContentInsets.right, resolvedSafeAreaInsets.right)
|
||||||
)
|
)
|
||||||
return RDEPUBPreferences(
|
return RDEPUBPreferences(
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
|
|||||||
@ -126,6 +126,19 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
|||||||
return spinner
|
return spinner
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
/// Debug-only outline of the readable content area (bounds inset by contentInsets).
|
||||||
|
private let debugContentAreaBorderView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.isUserInteractionEnabled = false
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.layer.borderColor = UIColor.systemRed.withAlphaComponent(0.6).cgColor
|
||||||
|
view.layer.borderWidth = 1
|
||||||
|
view.accessibilityIdentifier = "epub.reader.debug.contentAreaBorder"
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
#endif
|
||||||
|
|
||||||
private lazy var longPressGestureRecognizer: UILongPressGestureRecognizer = {
|
private lazy var longPressGestureRecognizer: UILongPressGestureRecognizer = {
|
||||||
let gesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
|
let gesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
|
||||||
gesture.minimumPressDuration = 0.5
|
gesture.minimumPressDuration = 0.5
|
||||||
@ -243,6 +256,9 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
|||||||
addSubview(pageNumberLabel)
|
addSubview(pageNumberLabel)
|
||||||
addSubview(loadingSpinner)
|
addSubview(loadingSpinner)
|
||||||
addSubview(selectionLoupeView)
|
addSubview(selectionLoupeView)
|
||||||
|
#if DEBUG
|
||||||
|
addSubview(debugContentAreaBorderView)
|
||||||
|
#endif
|
||||||
addGestureRecognizer(longPressGestureRecognizer)
|
addGestureRecognizer(longPressGestureRecognizer)
|
||||||
addGestureRecognizer(panGestureRecognizer)
|
addGestureRecognizer(panGestureRecognizer)
|
||||||
addGestureRecognizer(tapGestureRecognizer)
|
addGestureRecognizer(tapGestureRecognizer)
|
||||||
@ -338,6 +354,9 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
|||||||
coverImageView.frame = bounds.inset(by: contentInsets)
|
coverImageView.frame = bounds.inset(by: contentInsets)
|
||||||
|
|
||||||
let contentRect = bounds.inset(by: contentInsets)
|
let contentRect = bounds.inset(by: contentInsets)
|
||||||
|
#if DEBUG
|
||||||
|
debugContentAreaBorderView.frame = contentRect
|
||||||
|
#endif
|
||||||
let labelSize = pageNumberLabel.sizeThatFits(
|
let labelSize = pageNumberLabel.sizeThatFits(
|
||||||
CGSize(width: contentRect.width, height: Self.pageNumberReservedHeight)
|
CGSize(width: contentRect.width, height: Self.pageNumberReservedHeight)
|
||||||
)
|
)
|
||||||
@ -374,7 +393,7 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
|||||||
selectionController.clearSelection(renderView: coreTextRenderView)
|
selectionController.clearSelection(renderView: coreTextRenderView)
|
||||||
contentInsets = Self.safeContentInsets(
|
contentInsets = Self.safeContentInsets(
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
safeAreaInsets: safeAreaInsets
|
safeAreaInsets: RDEPUBSafeArea.resolve(safeAreaInsets)
|
||||||
)
|
)
|
||||||
backgroundColor = configuration.theme.contentBackgroundColor
|
backgroundColor = configuration.theme.contentBackgroundColor
|
||||||
pageNumberLabel.textColor = configuration.theme.contentTextColor
|
pageNumberLabel.textColor = configuration.theme.contentTextColor
|
||||||
@ -459,7 +478,7 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
|||||||
selectionController.clearSelection(renderView: coreTextRenderView)
|
selectionController.clearSelection(renderView: coreTextRenderView)
|
||||||
contentInsets = Self.safeContentInsets(
|
contentInsets = Self.safeContentInsets(
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
safeAreaInsets: safeAreaInsets
|
safeAreaInsets: RDEPUBSafeArea.resolve(safeAreaInsets)
|
||||||
)
|
)
|
||||||
backgroundColor = configuration.theme.contentBackgroundColor
|
backgroundColor = configuration.theme.contentBackgroundColor
|
||||||
pageNumberLabel.textColor = configuration.theme.contentTextColor
|
pageNumberLabel.textColor = configuration.theme.contentTextColor
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user