ReadViewSDK/Sources/RDEpubReaderView/EPUBCore/RDEPUBSafeArea.swift

72 lines
2.8 KiB
Swift

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
/// Compact top/bottom padding used by a landscape two-page spread. This
/// applies only beyond the actual safe area, which is always preserved.
public static let landscapeVerticalTextMargin: CGFloat = 12
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)
)
}
}