Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 2 additions & 143 deletions TiebaPure/Core/UI/InteractiveNavigationPop.swift
Original file line number Diff line number Diff line change
@@ -1,48 +1,8 @@
import SwiftUI
import UIKit

/// Documents the system navigation gesture used by each supported OS family.
///
/// iOS 26 adds `UINavigationController.interactiveContentPopGestureRecognizer`,
/// which recognizes an interactive pop across the navigation controller's
/// content. Earlier systems only provide the leading-edge
/// `interactivePopGestureRecognizer`.
enum NavigationBackGesturePolicy {
enum Mode: Equatable {
case content
case edge
}

static func mode(systemMajorVersion: Int) -> Mode {
systemMajorVersion >= 26 ? .content : .edge
}

static var currentMode: Mode {
mode(systemMajorVersion: ProcessInfo.processInfo.operatingSystemVersion.majorVersion)
}
}

enum NativeEdgePopGestureActivationPolicy {
static func shouldEnable(
requestedEnabled: Bool,
mode: NavigationBackGesturePolicy.Mode,
isVisible: Bool,
isAttachedToWindow: Bool,
stackDepth: Int,
hasActiveTransition: Bool
) -> Bool {
requestedEnabled
&& mode == .edge
&& isVisible
&& isAttachedToWindow
&& stackDepth > 1
&& hasActiveTransition == false
}
}

enum NavigationPopGestureControlHostingPolicy {
static func requiresController(systemMajorVersion: Int, isEnabled: Bool) -> Bool {
_ = systemMajorVersion
static func requiresController(isEnabled: Bool) -> Bool {
// A normal NavigationStack must keep complete ownership of its native
// recognizers. Hosting a controller merely to force-enable the edge
// recognizer raced SwiftUI's push transition on iOS 16-18, especially
Expand Down Expand Up @@ -72,7 +32,6 @@ extension View {
/// afterwards.
func fullScreenInteractiveNavigationPop(isEnabled: Bool = true) -> some View {
let hostsController = NavigationPopGestureControlHostingPolicy.requiresController(
systemMajorVersion: ProcessInfo.processInfo.operatingSystemVersion.majorVersion,
isEnabled: isEnabled
)
let exposesDiagnostics = NavigationPopGestureDiagnostics.isEnabled
Expand Down Expand Up @@ -122,9 +81,6 @@ private struct NativeNavigationPopGestureControl: UIViewControllerRepresentable
@MainActor
final class Controller: UIViewController {
private var requestedEnabled = true
private var isVisible = false
private var scheduledUpdateGeneration = 0
private var transitionRetryGeneration: Int?
private weak var controlledNavigationController: UINavigationController?
private var previousEdgeGestureState: Bool?
private var previousContentGestureState: Bool?
Expand All @@ -140,22 +96,15 @@ private struct NativeNavigationPopGestureControl: UIViewControllerRepresentable

func setPopGesturesEnabled(_ isEnabled: Bool) {
requestedEnabled = isEnabled
scheduledUpdateGeneration &+= 1
applyRequestedState()
scheduleRequestedStateUpdateIfNeeded()
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
isVisible = true
applyRequestedState()
scheduleRequestedStateUpdateIfNeeded()
}

override func viewWillDisappear(_ animated: Bool) {
isVisible = false
scheduledUpdateGeneration &+= 1
transitionRetryGeneration = nil
restorePopGesturesIfNeeded()
super.viewWillDisappear(animated)
}
Expand Down Expand Up @@ -197,41 +146,6 @@ private struct NativeNavigationPopGestureControl: UIViewControllerRepresentable
updateDiagnostics(using: navigationController)
}

private func enableNativeEdgePopIfNeeded() {
guard let navigationController else {
updateDiagnostics(using: nil)
return
}
guard requestedEnabled,
isVisible,
viewIfLoaded?.window != nil,
NavigationBackGesturePolicy.currentMode == .edge,
navigationController.viewControllers.count > 1 else {
updateDiagnostics(using: navigationController)
return
}
if let transitionCoordinator = navigationController.transitionCoordinator {
updateDiagnostics(using: navigationController)
scheduleEdgeActivationAfterTransition(
transitionCoordinator,
navigationController: navigationController
)
return
}
guard NativeEdgePopGestureActivationPolicy.shouldEnable(
requestedEnabled: requestedEnabled,
mode: NavigationBackGesturePolicy.currentMode,
isVisible: isVisible,
isAttachedToWindow: viewIfLoaded?.window != nil,
stackDepth: navigationController.viewControllers.count,
hasActiveTransition: false
) else {
return
}
navigationController.interactivePopGestureRecognizer?.isEnabled = true
updateDiagnostics(using: navigationController)
}

private func updateDiagnostics(using navigationController: UINavigationController?) {
#if DEBUG
guard NavigationPopGestureDiagnostics.isEnabled else { return }
Expand All @@ -243,66 +157,11 @@ private struct NativeNavigationPopGestureControl: UIViewControllerRepresentable
"enabled=\(edgeGesture?.isEnabled ?? false)",
"shouldBegin=\(delegateAllowsBegin ?? false)",
"depth=\(navigationController?.viewControllers.count ?? 0)",
"visible=\(isVisible)",
"visible=\(viewIfLoaded?.window != nil)",
"attached=\(viewIfLoaded?.window != nil)"
].joined(separator: ",")
#endif
}

private func scheduleEdgeActivationAfterTransition(
_ transitionCoordinator: UIViewControllerTransitionCoordinator,
navigationController: UINavigationController
) {
let generation = scheduledUpdateGeneration
guard transitionRetryGeneration != generation else { return }
transitionRetryGeneration = generation
let accepted = transitionCoordinator.animate(alongsideTransition: nil) { [weak self, weak navigationController] _ in
DispatchQueue.main.async {
guard let self else { return }
if self.transitionRetryGeneration == generation {
self.transitionRetryGeneration = nil
}
guard let navigationController,
self.requestedEnabled,
self.isVisible,
self.scheduledUpdateGeneration == generation,
self.viewIfLoaded?.window != nil,
self.navigationController === navigationController else {
return
}
guard NativeEdgePopGestureActivationPolicy.shouldEnable(
requestedEnabled: true,
mode: NavigationBackGesturePolicy.currentMode,
isVisible: self.isVisible,
isAttachedToWindow: self.viewIfLoaded?.window != nil,
stackDepth: navigationController.viewControllers.count,
hasActiveTransition: navigationController.transitionCoordinator != nil
) else {
return
}
navigationController.interactivePopGestureRecognizer?.isEnabled = true
self.updateDiagnostics(using: navigationController)
}
}
if accepted == false {
transitionRetryGeneration = nil
}
}

private func scheduleRequestedStateUpdateIfNeeded() {
guard requestedEnabled, isVisible else { return }
let generation = scheduledUpdateGeneration
DispatchQueue.main.async { [weak self] in
guard let self,
self.requestedEnabled,
self.isVisible,
self.scheduledUpdateGeneration == generation,
self.viewIfLoaded?.window != nil else {
return
}
self.applyRequestedState()
}
}
}
}

Expand Down
49 changes: 44 additions & 5 deletions TiebaPure/Domain/Models/ReadingPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,7 @@ final class ReaderFontStore: ObservableObject {
let fileName = "\(prepared.sha256).\(prepared.fileExtension)"
let destinationURL = directoryURL.appendingPathComponent(fileName, isDirectory: false)
let destinationExisted = fileManager.fileExists(atPath: destinationURL.path)
let fileManager = self.fileManager
let displayName: String
do {
displayName = try await Task.detached(priority: .userInitiated) {
Expand All @@ -731,7 +732,7 @@ final class ReaderFontStore: ObservableObject {
throw ReaderFontStoreError.invalidFont
}
try prepared.data.write(to: destinationURL, options: [.atomic])
try FileManager.default.setAttributes(
try fileManager.setAttributes(
Self.protectedFileAttributes,
ofItemAtPath: destinationURL.path
)
Expand All @@ -743,6 +744,7 @@ final class ReaderFontStore: ObservableObject {
}
guard registerFont(at: destinationURL, postScriptName: prepared.postScriptName),
UIFont(name: prepared.postScriptName, size: 17) != nil else {
CTFontManagerUnregisterFontsForURL(destinationURL as CFURL, .process, nil)
try? fileManager.removeItem(at: destinationURL)
throw ReaderFontStoreError.registrationFailed
}
Expand All @@ -763,6 +765,7 @@ final class ReaderFontStore: ObservableObject {
persistenceError = nil
return imported
} catch {
CTFontManagerUnregisterFontsForURL(destinationURL as CFURL, .process, nil)
if destinationExisted == false { try? fileManager.removeItem(at: destinationURL) }
persistenceError = error.localizedDescription
throw error
Expand Down Expand Up @@ -795,12 +798,48 @@ final class ReaderFontStore: ObservableObject {
let validated = await Task.detached(priority: .utility) {
candidates.filter { Self.isStoredFontValid($0, directoryURL: directoryURL) }
}.value
entries = validated.filter { font in
entries = validated.compactMap { font in
let fileURL = directoryURL.appendingPathComponent(font.fileName, isDirectory: false)
return registerFont(at: fileURL, postScriptName: font.postScriptName)
&& UIFont(name: font.postScriptName, size: 17) != nil
guard registerFont(at: fileURL, postScriptName: font.postScriptName),
UIFont(name: font.postScriptName, size: 17) != nil else {
CTFontManagerUnregisterFontsForURL(fileURL as CFURL, .process, nil)
return nil
}
return font
}
let loadedEntries = entries
let catalogFile = self.catalogFile
let fileManager = self.fileManager
do {
try await Task.detached(priority: .utility) {
if loadedEntries != candidates {
try catalogFile?.replace(loadedEntries)
}
let referenced = Set(loadedEntries.map(\.fileName))
let children = try fileManager.contentsOfDirectory(
at: directoryURL,
includingPropertiesForKeys: [
.isRegularFileKey,
.isSymbolicLinkKey
],
options: []
)
for child in children {
let fileExtension = child.pathExtension.lowercased()
guard ["ttf", "otf"].contains(fileExtension),
referenced.contains(child.lastPathComponent) == false else { continue }
let values = try child.resourceValues(forKeys: [
.isRegularFileKey,
.isSymbolicLinkKey
])
guard values.isRegularFile == true, values.isSymbolicLink != true else { continue }
try fileManager.removeItem(at: child)
}
}.value
} catch {
persistenceError = error.localizedDescription
}
if entries.count != candidates.count {
if loadedEntries.count != candidates.count, persistenceError == nil {
persistenceError = ReaderFontStoreError.registrationFailed.localizedDescription
}
isReady = true
Expand Down
Loading
Loading