mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Use lazy pop to work around swipe gesture issues with iOS 17
This commit is contained in:
parent
d1eca8b6fd
commit
dfaf61cf8c
11 changed files with 406 additions and 221 deletions
|
|
@ -82,7 +82,7 @@ struct BriefingView: View {
|
|||
var body: some View {
|
||||
ZStack { // Using ZStack so .task can be used on if/else body
|
||||
if let item = viewModel.item {
|
||||
WebReaderContainerView(item: item)
|
||||
WebReaderContainerView(item: item, pop: {})
|
||||
}
|
||||
}
|
||||
.task {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,57 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
// SlideAnimatedTransitioning.swift
|
||||
// SwipeRightToPopController
|
||||
//
|
||||
// Created by Jackson Harper on 9/11/23.
|
||||
// Created by Warif Akhand Rishi on 2/19/16.
|
||||
// Copyright © 2016 Warif Akhand Rishi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class SlideAnimatedTransitioning: NSObject {}
|
||||
|
||||
extension SlideAnimatedTransitioning: UIViewControllerAnimatedTransitioning {
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
let containerView = transitionContext.containerView
|
||||
let fromView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!.view
|
||||
let toView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!.view
|
||||
|
||||
let width = containerView.frame.width
|
||||
|
||||
var offsetLeft = fromView?.frame
|
||||
offsetLeft?.origin.x = width
|
||||
|
||||
var offscreenRight = toView?.frame
|
||||
offscreenRight?.origin.x = -width / 3.33
|
||||
|
||||
toView?.frame = offscreenRight!
|
||||
|
||||
fromView?.layer.shadowRadius = 5.0
|
||||
fromView?.layer.shadowOpacity = 1.0
|
||||
toView?.layer.opacity = 0.9
|
||||
|
||||
containerView.insertSubview(toView!, belowSubview: fromView!)
|
||||
|
||||
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveLinear, animations: {
|
||||
toView?.frame = (fromView?.frame)!
|
||||
fromView?.frame = offsetLeft!
|
||||
|
||||
toView?.layer.opacity = 1.0
|
||||
fromView?.layer.shadowOpacity = 0.1
|
||||
|
||||
}, completion: { _ in
|
||||
toView?.layer.opacity = 1.0
|
||||
toView?.layer.shadowOpacity = 0
|
||||
fromView?.layer.opacity = 1.0
|
||||
fromView?.layer.shadowOpacity = 0
|
||||
|
||||
// when cancelling or completing the animation, ios simulator seems to sometimes flash black backgrounds during the animation. on devices, this doesn't seem to happen though.
|
||||
// containerView.backgroundColor = [UIColor whiteColor];
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
})
|
||||
}
|
||||
|
||||
func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
0.3
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ struct FeedCardNavigationLink: View {
|
|||
isPDF: item.isPDF
|
||||
), label: {
|
||||
LibraryItemCard(item: item, viewer: dataService.currentViewer)
|
||||
.padding(10)
|
||||
.padding(.top, 15)
|
||||
})
|
||||
.onAppear {
|
||||
Task { await viewModel.itemAppeared(item: item, dataService: dataService) }
|
||||
|
|
@ -71,8 +71,8 @@ struct GridCardNavigationLink: View {
|
|||
if isContextMenuOpen {
|
||||
isContextMenuOpen = false
|
||||
} else {
|
||||
viewModel.selectedItem = item
|
||||
viewModel.linkIsActive = true
|
||||
// viewModel.selectedItem = item
|
||||
// viewModel.linkIsActive = true
|
||||
}
|
||||
} label: {
|
||||
NavigationLink(destination: EmptyView()) {
|
||||
|
|
|
|||
|
|
@ -20,49 +20,38 @@ struct LibraryFeatureCardNavigationLink: View {
|
|||
@State var showFeatureActions = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Button {
|
||||
NavigationLink(destination: EmptyView()) {
|
||||
LibraryFeatureCard(item: item, viewer: dataService.currentViewer)
|
||||
}
|
||||
.confirmationDialog("", isPresented: $showFeatureActions) {
|
||||
if FeaturedItemFilter(rawValue: viewModel.featureFilter) == .pinned {
|
||||
Button("Unpin", action: {
|
||||
viewModel.unpinItem(dataService: dataService, item: item)
|
||||
})
|
||||
}
|
||||
Button("Pin", action: {
|
||||
viewModel.pinItem(dataService: dataService, item: item)
|
||||
})
|
||||
Button("Archive", action: {
|
||||
viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: true)
|
||||
})
|
||||
Button("Remove", action: {
|
||||
viewModel.removeLink(dataService: dataService, objectID: item.objectID)
|
||||
})
|
||||
if FeaturedItemFilter(rawValue: viewModel.featureFilter) != .pinned {
|
||||
Button("Mark Read", action: {
|
||||
viewModel.markRead(dataService: dataService, item: item)
|
||||
})
|
||||
Button("Mark Unread", action: {
|
||||
viewModel.markUnread(dataService: dataService, item: item)
|
||||
})
|
||||
}
|
||||
Button("Dismiss", role: .cancel, action: {
|
||||
showFeatureActions = false
|
||||
viewModel.selectedItem = item
|
||||
viewModel.linkIsActive = true
|
||||
} label: {
|
||||
NavigationLink(destination: EmptyView()) {
|
||||
EmptyView()
|
||||
}
|
||||
.opacity(0)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
LibraryFeatureCard(item: item, viewer: dataService.currentViewer)
|
||||
}
|
||||
.confirmationDialog("", isPresented: $showFeatureActions) {
|
||||
if FeaturedItemFilter(rawValue: viewModel.featureFilter) == .pinned {
|
||||
Button("Unpin", action: {
|
||||
viewModel.unpinItem(dataService: dataService, item: item)
|
||||
})
|
||||
}
|
||||
Button("Pin", action: {
|
||||
viewModel.pinItem(dataService: dataService, item: item)
|
||||
})
|
||||
Button("Archive", action: {
|
||||
viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: true)
|
||||
})
|
||||
Button("Remove", action: {
|
||||
viewModel.removeLink(dataService: dataService, objectID: item.objectID)
|
||||
})
|
||||
if FeaturedItemFilter(rawValue: viewModel.featureFilter) != .pinned {
|
||||
Button("Mark Read", action: {
|
||||
viewModel.markRead(dataService: dataService, item: item)
|
||||
})
|
||||
Button("Mark Unread", action: {
|
||||
viewModel.markUnread(dataService: dataService, item: item)
|
||||
})
|
||||
}
|
||||
Button("Dismiss", role: .cancel, action: {
|
||||
showFeatureActions = false
|
||||
})
|
||||
}
|
||||
.delayedGesture(LongPressGesture().onEnded { _ in
|
||||
showFeatureActions = true
|
||||
})
|
||||
}
|
||||
.delayedGesture(LongPressGesture().onEnded { _ in
|
||||
showFeatureActions = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,121 +44,110 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// if let linkRequest = viewModel.linkRequest {
|
||||
// NavigationLink(
|
||||
// destination: WebReaderLoadingContainer(requestID: linkRequest.serverID),
|
||||
// tag: linkRequest,
|
||||
// selection: $viewModel.linkRequest
|
||||
// ) {
|
||||
// EmptyView()
|
||||
// }
|
||||
// }
|
||||
HomeFeedView(
|
||||
listTitle: $listTitle,
|
||||
isListScrolled: $isListScrolled,
|
||||
prefersListLayout: $prefersListLayout,
|
||||
viewModel: viewModel
|
||||
)
|
||||
.refreshable {
|
||||
loadItems(isRefresh: true)
|
||||
HomeFeedView(
|
||||
listTitle: $listTitle,
|
||||
isListScrolled: $isListScrolled,
|
||||
prefersListLayout: $prefersListLayout,
|
||||
viewModel: viewModel
|
||||
)
|
||||
.refreshable {
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.onChange(of: viewModel.searchTerm) { _ in
|
||||
// Maybe we should debounce this, but
|
||||
// it feels like it works ok without
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.onChange(of: viewModel.selectedLabels) { _ in
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.onChange(of: viewModel.negatedLabels) { _ in
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.onChange(of: viewModel.appliedFilter) { _ in
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.onChange(of: viewModel.appliedSort) { _ in
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.sheet(item: $viewModel.itemUnderLabelEdit) { item in
|
||||
ApplyLabelsView(mode: .item(item), onSave: nil)
|
||||
}
|
||||
.sheet(item: $viewModel.itemUnderTitleEdit) { item in
|
||||
LinkedItemMetadataEditView(item: item)
|
||||
}
|
||||
.sheet(item: $viewModel.itemForHighlightsView) { item in
|
||||
NotebookView(itemObjectID: item.objectID, hasHighlightMutations: $hasHighlightMutations)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showFiltersModal) {
|
||||
NavigationView {
|
||||
FilterSelectorView(viewModel: viewModel)
|
||||
}
|
||||
.onChange(of: viewModel.searchTerm) { _ in
|
||||
// Maybe we should debounce this, but
|
||||
// it feels like it works ok without
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.onChange(of: viewModel.selectedLabels) { _ in
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.onChange(of: viewModel.negatedLabels) { _ in
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.onChange(of: viewModel.appliedFilter) { _ in
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.onChange(of: viewModel.appliedSort) { _ in
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.sheet(item: $viewModel.itemUnderLabelEdit) { item in
|
||||
ApplyLabelsView(mode: .item(item), onSave: nil)
|
||||
}
|
||||
.sheet(item: $viewModel.itemUnderTitleEdit) { item in
|
||||
LinkedItemMetadataEditView(item: item)
|
||||
}
|
||||
.sheet(item: $viewModel.itemForHighlightsView) { item in
|
||||
NotebookView(itemObjectID: item.objectID, hasHighlightMutations: $hasHighlightMutations)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showFiltersModal) {
|
||||
NavigationView {
|
||||
FilterSelectorView(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .barLeading) {
|
||||
VStack(alignment: .leading) {
|
||||
let title = (LinkedItemFilter(rawValue: viewModel.appliedFilter) ?? LinkedItemFilter.inbox).displayName
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .barLeading) {
|
||||
VStack(alignment: .leading) {
|
||||
let title = (LinkedItemFilter(rawValue: viewModel.appliedFilter) ?? LinkedItemFilter.inbox).displayName
|
||||
|
||||
Text(title)
|
||||
.font(Font.system(size: isListScrolled ? 10 : 18, weight: .semibold))
|
||||
Text(title)
|
||||
.font(Font.system(size: isListScrolled ? 10 : 18, weight: .semibold))
|
||||
|
||||
if isListScrolled {
|
||||
Text(listTitle)
|
||||
.font(Font.system(size: 15, weight: .regular))
|
||||
.foregroundColor(Color.appGrayText)
|
||||
}
|
||||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
Button("", action: {})
|
||||
.disabled(true)
|
||||
.overlay {
|
||||
if viewModel.isLoading, !prefersListLayout, enableGrid {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: UIDevice.isIPhone ? .barLeading : .barTrailing) {
|
||||
if enableGrid {
|
||||
Button(
|
||||
action: { prefersListLayout.toggle() },
|
||||
label: {
|
||||
Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet")
|
||||
}
|
||||
)
|
||||
} else {
|
||||
EmptyView()
|
||||
if isListScrolled {
|
||||
Text(listTitle)
|
||||
.font(Font.system(size: 15, weight: .regular))
|
||||
.foregroundColor(Color.appGrayText)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
Button("", action: {})
|
||||
.disabled(true)
|
||||
.overlay {
|
||||
if viewModel.isLoading, !prefersListLayout, enableGrid {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: UIDevice.isIPhone ? .barLeading : .barTrailing) {
|
||||
if enableGrid {
|
||||
Button(
|
||||
action: { searchPresented = true },
|
||||
action: { prefersListLayout.toggle() },
|
||||
label: {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.resizable()
|
||||
.frame(width: 18, height: 18)
|
||||
.padding(.vertical)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet")
|
||||
}
|
||||
)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
if UIDevice.isIPhone {
|
||||
Menu(content: {
|
||||
Button(action: { settingsPresented = true }, label: {
|
||||
Label(LocalText.genericProfile, systemImage: "person.circle")
|
||||
})
|
||||
Button(action: { addLinkPresented = true }, label: {
|
||||
Label("Add Link", systemImage: "plus.circle")
|
||||
})
|
||||
}, label: {
|
||||
Image.utilityMenu
|
||||
})
|
||||
}
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
Button(
|
||||
action: { searchPresented = true },
|
||||
label: {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.resizable()
|
||||
.frame(width: 18, height: 18)
|
||||
.padding(.vertical)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
)
|
||||
}
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
if UIDevice.isIPhone {
|
||||
Menu(content: {
|
||||
Button(action: { settingsPresented = true }, label: {
|
||||
Label(LocalText.genericProfile, systemImage: "person.circle")
|
||||
})
|
||||
Button(action: { addLinkPresented = true }, label: {
|
||||
Label("Add Link", systemImage: "plus.circle")
|
||||
})
|
||||
}, label: {
|
||||
Image.utilityMenu
|
||||
})
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -170,13 +159,8 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
guard let objectID = dataService.persist(jsonArticle: jsonArticle) else { return }
|
||||
guard let linkedItem = dataService.viewContext.object(with: objectID) as? LinkedItem else { return }
|
||||
viewModel.pushFeedItem(item: linkedItem)
|
||||
viewModel.selectedItem = linkedItem
|
||||
viewModel.linkIsActive = true
|
||||
}
|
||||
.onReceive(NSNotification.pushReaderItemPublisher) { notification in
|
||||
if let objectID = notification.userInfo?["objectID"] as? NSManagedObjectID {
|
||||
viewModel.handleReaderItemNotification(objectID: objectID, dataService: dataService)
|
||||
}
|
||||
// viewModel.selectedItem = linkedItem
|
||||
// viewModel.linkIsActive = true
|
||||
}
|
||||
.onOpenURL { url in
|
||||
viewModel.linkRequest = nil
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import Views
|
|||
@Published var appliedSort = LinkedItemSort.newest.rawValue
|
||||
|
||||
@Published var selectedLinkItem: NSManagedObjectID? // used by mac app only
|
||||
@Published var selectedItem: LinkedItem?
|
||||
// @Published var selectedItem: LinkedItem?
|
||||
@Published var linkIsActive = false
|
||||
|
||||
@Published var showLabelsSheet = false
|
||||
|
|
@ -79,35 +79,6 @@ import Views
|
|||
}
|
||||
}
|
||||
|
||||
func handleReaderItemNotification(objectID: NSManagedObjectID, dataService: DataService) {
|
||||
// Pop the current selected item if needed
|
||||
if selectedItem != nil, selectedItem?.objectID != objectID {
|
||||
// Temporarily disable animation to avoid excessive animations
|
||||
#if os(iOS)
|
||||
UIView.setAnimationsEnabled(false)
|
||||
#endif
|
||||
|
||||
linkIsActive = false
|
||||
selectedItem = nil
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
|
||||
self.selectedLinkItem = objectID
|
||||
self.selectedItem = dataService.viewContext.object(with: objectID) as? LinkedItem
|
||||
self.linkIsActive = true
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
|
||||
#if os(iOS)
|
||||
UIView.setAnimationsEnabled(true)
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
selectedLinkItem = objectID
|
||||
selectedItem = dataService.viewContext.object(with: objectID) as? LinkedItem
|
||||
linkIsActive = true
|
||||
}
|
||||
}
|
||||
|
||||
func itemAppeared(item: LinkedItem, dataService: DataService) async {
|
||||
if isLoading { return }
|
||||
let itemIndex = items.firstIndex(where: { $0.id == item.id })
|
||||
|
|
|
|||
|
|
@ -75,25 +75,29 @@ struct LinkItemDetailView: View {
|
|||
|
||||
@StateObject private var viewModel = LinkItemDetailViewModel()
|
||||
|
||||
@State var isEnabled = true
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
init(linkedItemObjectID: NSManagedObjectID, isPDF: Bool) {
|
||||
self.linkedItemObjectID = linkedItemObjectID
|
||||
self.isPDF = isPDF
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack { // Using ZStack so .task can be used on if/else body
|
||||
ZStack {
|
||||
if isPDF {
|
||||
pdfContainerView
|
||||
} else if let item = viewModel.item {
|
||||
WebReaderContainerView(item: item)
|
||||
WebReaderContainerView(item: item, pop: { dismiss() })
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await viewModel.loadItem(linkedItemObjectID: linkedItemObjectID, dataService: dataService)
|
||||
}
|
||||
#if os(iOS)
|
||||
.navigationBarHidden(true)
|
||||
#endif
|
||||
.navigationBarHidden(true)
|
||||
.lazyPop(pop: {
|
||||
dismiss()
|
||||
}, isEnabled: $isEnabled)
|
||||
}
|
||||
|
||||
@ViewBuilder private var pdfContainerView: some View {
|
||||
|
|
@ -114,16 +118,29 @@ struct LinkItemDetailView: View {
|
|||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
// Enable swipe to go back behavior if nav bar is hidden
|
||||
extension UINavigationController: UIGestureRecognizerDelegate {
|
||||
override open func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
interactivePopGestureRecognizer?.delegate = self
|
||||
}
|
||||
|
||||
public func gestureRecognizerShouldBegin(_: UIGestureRecognizer) -> Bool {
|
||||
viewControllers.count > 1
|
||||
}
|
||||
}
|
||||
#endif
|
||||
//
|
||||
// #if os(iOS)
|
||||
// // Enable swipe to go back behavior if nav bar is hidden
|
||||
// extension UINavigationController: UIGestureRecognizerDelegate, UINavigationControllerDelegate {
|
||||
// override open func viewDidLoad() {
|
||||
// super.viewDidLoad()
|
||||
// print("INIT: ", viewControllers)
|
||||
// delegate = self
|
||||
// interactivePopGestureRecognizer?.delegate = nil
|
||||
// }
|
||||
////
|
||||
//// public func gestureRecognizerShouldBegin(_ gesture: UIGestureRecognizer) -> Bool {
|
||||
//// print("SHOULD BEGIN: ", viewControllers, gesture)
|
||||
//// return viewControllers.count > 1
|
||||
//// }
|
||||
////
|
||||
//// public func navigationController(_ navigationController: UINavigationController, willShow _: UIViewController, animated _: Bool) {
|
||||
//// navigationController.transitionCoordinator?.notifyWhenInteractionChanges { context in
|
||||
//// print("DID SHOW CONTEXT: ", context.percentComplete, navigationController.viewControllers)
|
||||
//// navigationController.interactivePopGestureRecognizer?.delegate = nil
|
||||
//// navigationController.popToRootViewController(animated: false)
|
||||
//// // interactivePopGestureRecognizer.
|
||||
//// }
|
||||
//// }
|
||||
// }
|
||||
// #endif
|
||||
|
|
|
|||
|
|
@ -1,8 +1,181 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
// SwipeRightToPopViewController.swift
|
||||
// SwipeRightToPopController
|
||||
//
|
||||
// Created by Jackson Harper on 9/11/23.
|
||||
// Created by Warif Akhand Rishi on 2/19/16.
|
||||
// Copyright © 2016 Warif Akhand Rishi. All rights reserved.
|
||||
//
|
||||
// Modified by Joseph Hinkle on 12/1/19.
|
||||
// Modified version allows use in SwiftUI by subclassing UIHostingController.
|
||||
// Copyright © 2019 Joseph Hinkle. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
private func < <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (lll?, rrr?):
|
||||
return lll < rrr
|
||||
case (nil, _?):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func > <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (lll?, rrr?):
|
||||
return lll > rrr
|
||||
default:
|
||||
return rhs < lhs
|
||||
}
|
||||
}
|
||||
|
||||
class SwipeRightToPopViewController<Content>: UIHostingController<Content>, UINavigationControllerDelegate where Content: View {
|
||||
fileprivate var pop: (() -> Void)?
|
||||
fileprivate var lazyPopContent: LazyPop<Content>?
|
||||
private var percentDrivenInteractiveTransition: UIPercentDrivenInteractiveTransition?
|
||||
private var panGestureRecognizer: UIPanGestureRecognizer!
|
||||
private var parentNavigationControllerToUse: UINavigationController?
|
||||
private var gestureAdded = false
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
// You need to add gesture events after every subview layout to protect against weird edge cases
|
||||
// One notable edgecase is if you are in a splitview in landscape. In this case, there will be
|
||||
// no nav controller with 2 vcs, so our addGesture will fail. After rotating back to portrait,
|
||||
// the splitview will combine into one view with the details pushed on top. So only then would
|
||||
// would the addGesture find a parent nav controller with 2 view controllers. I don't know if
|
||||
// there are other edge cases, but running addGesture on every viewDidLayoutSubviews seems safe.
|
||||
addGesture()
|
||||
}
|
||||
|
||||
public func addGesture() {
|
||||
if !gestureAdded {
|
||||
// attempt to find a parent navigationController
|
||||
var currentVc: UIViewController = self
|
||||
while true {
|
||||
if currentVc.navigationController != nil,
|
||||
currentVc.navigationController?.viewControllers.count > 1
|
||||
{
|
||||
parentNavigationControllerToUse = currentVc.navigationController
|
||||
break
|
||||
}
|
||||
guard let parent = currentVc.parent else {
|
||||
return
|
||||
}
|
||||
currentVc = parent
|
||||
}
|
||||
guard parentNavigationControllerToUse?.viewControllers.count > 1 else {
|
||||
return
|
||||
}
|
||||
|
||||
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(SwipeRightToPopViewController.handlePanGesture(_:)))
|
||||
view.addGestureRecognizer(panGestureRecognizer)
|
||||
gestureAdded = true
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
|
||||
// if the parentNavigationControllerToUse has a width value, use that because it's more accurate. Otherwise use this view's width as a backup
|
||||
let total = parentNavigationControllerToUse?.view.frame.width ?? view.frame.width
|
||||
let percent = max(panGesture.translation(in: view).x, 0) / total
|
||||
|
||||
switch panGesture.state {
|
||||
case .began:
|
||||
if lazyPopContent?.isEnabled == true {
|
||||
parentNavigationControllerToUse?.delegate = self
|
||||
if let pop = self.pop {
|
||||
pop()
|
||||
}
|
||||
}
|
||||
|
||||
case .changed:
|
||||
if let percentDrivenInteractiveTransition = percentDrivenInteractiveTransition {
|
||||
percentDrivenInteractiveTransition.update(percent)
|
||||
}
|
||||
|
||||
case .ended:
|
||||
let velocity = panGesture.velocity(in: view).x
|
||||
|
||||
// Continue if drag more than 50% of screen width or velocity is higher than 100
|
||||
if percent > 0.5 || velocity > 100 {
|
||||
percentDrivenInteractiveTransition?.finish()
|
||||
} else {
|
||||
percentDrivenInteractiveTransition?.cancel()
|
||||
}
|
||||
|
||||
case .cancelled, .failed:
|
||||
percentDrivenInteractiveTransition?.cancel()
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func didReceiveMemoryWarning() {
|
||||
super.didReceiveMemoryWarning()
|
||||
}
|
||||
|
||||
func navigationController(_: UINavigationController,
|
||||
animationControllerFor _: UINavigationController.Operation,
|
||||
from _: UIViewController,
|
||||
to _: UIViewController) -> UIViewControllerAnimatedTransitioning?
|
||||
{
|
||||
SlideAnimatedTransitioning()
|
||||
}
|
||||
|
||||
func navigationController(_: UINavigationController,
|
||||
interactionControllerFor _: UIViewControllerAnimatedTransitioning)
|
||||
-> UIViewControllerInteractiveTransitioning?
|
||||
{
|
||||
parentNavigationControllerToUse?.delegate = nil
|
||||
// navigationController.delegate = nil
|
||||
|
||||
if panGestureRecognizer.state == .began {
|
||||
percentDrivenInteractiveTransition = UIPercentDrivenInteractiveTransition()
|
||||
percentDrivenInteractiveTransition?.completionCurve = .easeOut
|
||||
} else {
|
||||
percentDrivenInteractiveTransition = nil
|
||||
}
|
||||
|
||||
return percentDrivenInteractiveTransition
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Lazy Pop SwiftUI Component
|
||||
//
|
||||
// Created by Joseph Hinkle on 12/1/19.
|
||||
// Copyright © 2019 Joseph Hinkle. All rights reserved.
|
||||
//
|
||||
|
||||
private struct LazyPop<Content: View>: UIViewControllerRepresentable {
|
||||
let rootView: Content
|
||||
let pop: () -> Void
|
||||
@Binding var isEnabled: Bool
|
||||
|
||||
init(_ rootView: Content, pop: @escaping () -> Void, isEnabled: (Binding<Bool>)? = nil) {
|
||||
self.rootView = rootView
|
||||
self.pop = pop
|
||||
self._isEnabled = isEnabled ?? Binding<Bool>(get: { true }, set: { _ in })
|
||||
}
|
||||
|
||||
func makeUIViewController(context _: Context) -> UIViewController {
|
||||
let vc = SwipeRightToPopViewController(rootView: rootView)
|
||||
vc.pop = pop
|
||||
vc.lazyPopContent = self
|
||||
return vc
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewController, context _: Context) {
|
||||
if let host = uiViewController as? UIHostingController<Content> {
|
||||
host.rootView = rootView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func lazyPop(pop: @escaping () -> Void, isEnabled: (Binding<Bool>)? = nil) -> some View {
|
||||
LazyPop(self, pop: pop, isEnabled: isEnabled)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import WebKit
|
|||
// swiftlint:disable file_length type_body_length
|
||||
struct WebReaderContainerView: View {
|
||||
let item: LinkedItem
|
||||
let pop: () -> Void
|
||||
|
||||
@State private var showPreferencesPopover = false
|
||||
@State private var showPreferencesFormsheet = false
|
||||
|
|
@ -40,9 +41,9 @@ struct WebReaderContainerView: View {
|
|||
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@EnvironmentObject var audioController: AudioController
|
||||
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
|
||||
@Environment(\.openURL) var openURL
|
||||
@StateObject var viewModel = WebReaderViewModel()
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
func webViewActionHandler(message: WKScriptMessage, replyHandler: WKScriptMessageReplyHandler?) {
|
||||
if let replyHandler = replyHandler {
|
||||
|
|
@ -271,7 +272,9 @@ struct WebReaderContainerView: View {
|
|||
HStack(alignment: .center, spacing: 10) {
|
||||
#if os(iOS)
|
||||
Button(
|
||||
action: { self.presentationMode.wrappedValue.dismiss() },
|
||||
action: {
|
||||
pop()
|
||||
},
|
||||
label: {
|
||||
Image.chevronRight
|
||||
.padding(.horizontal, 10)
|
||||
|
|
@ -620,7 +623,7 @@ struct WebReaderContainerView: View {
|
|||
func archive() {
|
||||
dataService.archiveLink(objectID: item.objectID, archived: !item.isArchived)
|
||||
#if os(iOS)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
pop()
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -651,7 +654,7 @@ struct WebReaderContainerView: View {
|
|||
removeLibraryItemAction(dataService: dataService, objectID: item.objectID)
|
||||
#if os(iOS)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
pop()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,9 +53,8 @@ public struct WebReaderLoadingContainer: View {
|
|||
}
|
||||
#endif
|
||||
} else {
|
||||
WebReaderContainerView(item: item)
|
||||
WebReaderContainerView(item: item, pop: {})
|
||||
#if os(iOS)
|
||||
.navigationBarHidden(true)
|
||||
.navigationViewStyle(.stack)
|
||||
#endif
|
||||
.accentColor(.appGrayTextContrast)
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ public struct LibraryItemCard: View {
|
|||
public var body: some View {
|
||||
VStack {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
articleInfo
|
||||
imageBox
|
||||
articleInfo
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
|
|
@ -156,12 +156,12 @@ public struct LibraryItemCard: View {
|
|||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 40, height: 40)
|
||||
.frame(width: 74 * 0.666, height: 74)
|
||||
.cornerRadius(5)
|
||||
.padding(.top, 2)
|
||||
} else {
|
||||
Color.systemBackground
|
||||
.frame(width: 40, height: 40)
|
||||
.frame(width: 74 * 0.666, height: 74)
|
||||
.cornerRadius(5)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue