#1803 (add interactively swipe right anywhere on the screen to return to the feed list)

- Added third-party code to support a “lazy pop”.
- Fixed some issues with that code.
This commit is contained in:
David Sinclair 2023-07-21 11:50:47 -06:00
parent 8e98ae51d5
commit 43ff718cd6
4 changed files with 266 additions and 0 deletions

View file

@ -144,6 +144,9 @@ struct FeedDetailGridView: View {
})
}
.background(Color.themed([0xE0E0E0, 0xFFF8CA, 0x363636, 0x101010]))
.if(cache.isGrid) { view in
view.lazyPop()
}
}
@ViewBuilder

View file

@ -0,0 +1,70 @@
//
// SlideAnimatedTransitioning.swift
// SwipeRightToPopController
//
// Created by Warif Akhand Rishi on 2/19/16.
// Copyright © 2016 Warif Akhand Rishi. All rights reserved.
//
import UIKit
class SlideAnimatedTransitioning: NSObject {
}
extension SlideAnimatedTransitioning: UIViewControllerAnimatedTransitioning {
// NOTE: includes DJS modifications to fix some issues.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard let fromView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!.view,
let toView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!.view else {
return
}
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.origin.x = fromView.frame.origin.x
toView.frame.size.width = fromView.frame.size.width
fromView.frame = offsetLeft
toView.layer.opacity = 1.0
fromView.layer.shadowOpacity = 0.1
}, completion: { finished 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 transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
}

View file

@ -0,0 +1,173 @@
//
// SwipeRightToPopViewController.swift
// SwipeRightToPopController
//
// 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 SwiftUI
fileprivate func < <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (l?, r?):
return l < r
case (nil, _?):
return true
default:
return false
}
}
fileprivate func > <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (l?, r?):
return l > r
default:
return rhs < lhs
}
}
class SwipeRightToPopViewController<Content>: UIHostingController<Content>, UINavigationControllerDelegate where Content : View {
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(_:)))
self.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
_ = parentNavigationControllerToUse?.popViewController(animated: true)
}
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(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SlideAnimatedTransitioning()
}
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: 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.
//
fileprivate struct LazyPop<Content: View>: UIViewControllerRepresentable {
let rootView: Content
@Binding var isEnabled: Bool
init(_ rootView: Content, isEnabled: (Binding<Bool>)? = nil) {
self.rootView = rootView
self._isEnabled = isEnabled ?? Binding<Bool>(get: { return true }, set: { _ in })
}
func makeUIViewController(context: Context) -> UIViewController {
let vc = SwipeRightToPopViewController(rootView: rootView)
vc.lazyPopContent = self
return vc
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
if let host = uiViewController as? UIHostingController<Content> {
host.rootView = rootView
}
}
}
extension View {
public func lazyPop(isEnabled: (Binding<Bool>)? = nil) -> some View {
return LazyPop(self, isEnabled: isEnabled)
}
}

View file

@ -844,6 +844,10 @@
17F39EBA26478538004B46D1 /* content_preview_small@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17F39EB726478538004B46D1 /* content_preview_small@2x.png */; };
17F39EBB26478538004B46D1 /* content_preview_large@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17F39EB826478538004B46D1 /* content_preview_large@2x.png */; };
17FB51D723AC81C500F5D5BF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 17FB51D923AC81C500F5D5BF /* InfoPlist.strings */; };
17FBFA982A6AEA4100149651 /* SlideAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FBFA962A6AEA4100149651 /* SlideAnimatedTransitioning.swift */; };
17FBFA992A6AEA4100149651 /* SlideAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FBFA962A6AEA4100149651 /* SlideAnimatedTransitioning.swift */; };
17FBFA9A2A6AEA4100149651 /* SwipeRightToPopViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FBFA972A6AEA4100149651 /* SwipeRightToPopViewController.swift */; };
17FBFA9B2A6AEA4100149651 /* SwipeRightToPopViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FBFA972A6AEA4100149651 /* SwipeRightToPopViewController.swift */; };
17FF4F7D27DA9645000526E6 /* ShareAddSiteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FF4F7C27DA9645000526E6 /* ShareAddSiteCell.swift */; };
17FF4F7E27DAAA6E000526E6 /* ak-icon-allstories.png in Resources */ = {isa = PBXBuildFile; fileRef = FF85BF7216D6A972002D334D /* ak-icon-allstories.png */; };
17FF4F7F27DAAA78000526E6 /* g_icn_folder.png in Resources */ = {isa = PBXBuildFile; fileRef = FFDD847216E887D3000AA0A2 /* g_icn_folder.png */; };
@ -1603,6 +1607,8 @@
17F39EB726478538004B46D1 /* content_preview_small@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "content_preview_small@2x.png"; sourceTree = "<group>"; };
17F39EB826478538004B46D1 /* content_preview_large@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "content_preview_large@2x.png"; sourceTree = "<group>"; };
17FB51D823AC81C500F5D5BF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
17FBFA962A6AEA4100149651 /* SlideAnimatedTransitioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlideAnimatedTransitioning.swift; sourceTree = "<group>"; };
17FBFA972A6AEA4100149651 /* SwipeRightToPopViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwipeRightToPopViewController.swift; sourceTree = "<group>"; };
17FF4F7C27DA9645000526E6 /* ShareAddSiteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAddSiteCell.swift; sourceTree = "<group>"; };
1D30AB110D05D00D00671497 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
1D3623240D0F684500981E51 /* NewsBlurAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = NewsBlurAppDelegate.h; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; };
@ -2489,6 +2495,15 @@
path = "Old Widget Extension";
sourceTree = "<group>";
};
17FBFA952A6AEA4100149651 /* Lazy Pop */ = {
isa = PBXGroup;
children = (
17FBFA962A6AEA4100149651 /* SlideAnimatedTransitioning.swift */,
17FBFA972A6AEA4100149651 /* SwipeRightToPopViewController.swift */,
);
path = "Lazy Pop";
sourceTree = SOURCE_ROOT;
};
19C28FACFE9D520D11CA2CBB /* Products */ = {
isa = PBXGroup;
children = (
@ -2536,6 +2551,7 @@
FF22FE5316E53ADC0046165A /* Underscore */,
FF2924CE1E93293F00FCFA63 /* PINCache */,
FF34FD2A1E9D93CB0062F8ED /* InAppSettingsKit */,
17FBFA952A6AEA4100149651 /* Lazy Pop */,
43A4C3E615B0099B008787B5 /* NewsBlur_Prefix.pch */,
179DD9CC23DFD20E007BFD21 /* BridgingHeader.h */,
43A4C3B915B00966008787B5 /* ABTableViewCell.h */,
@ -4863,6 +4879,7 @@
175792372930605500490924 /* FeedTableCell.m in Sources */,
175792382930605500490924 /* StoryPagesViewController.swift in Sources */,
175792392930605500490924 /* NewsBlurAppDelegate.m in Sources */,
17FBFA9B2A6AEA4100149651 /* SwipeRightToPopViewController.swift in Sources */,
1757923A2930605500490924 /* FeedChooserTitleView.m in Sources */,
1757923B2930605500490924 /* FeedsObjCViewController.m in Sources */,
1757923C2930605500490924 /* NBURLCache.m in Sources */,
@ -4887,6 +4904,7 @@
1785524D2A21693300A8CD92 /* FeedDetailLoadingView.swift in Sources */,
175792502930605500490924 /* IASKSwitch.m in Sources */,
175792512930605500490924 /* NBBarButtonItem.m in Sources */,
17FBFA992A6AEA4100149651 /* SlideAnimatedTransitioning.swift in Sources */,
175792522930605500490924 /* MoveSiteViewController.m in Sources */,
175792532930605500490924 /* FirstTimeUserViewController.m in Sources */,
1785524A2A1F115800A8CD92 /* FeedDetailTableCell.m in Sources */,
@ -5045,6 +5063,7 @@
43F44B1C159D8DBC00F48F8A /* FeedTableCell.m in Sources */,
171B6FFD25C4C7C8008638A9 /* StoryPagesViewController.swift in Sources */,
1D3623260D0F684500981E51 /* NewsBlurAppDelegate.m in Sources */,
17FBFA9A2A6AEA4100149651 /* SwipeRightToPopViewController.swift in Sources */,
17432C861C5343C0003F8FD6 /* FeedChooserTitleView.m in Sources */,
28D7ACF80DDB3853001CB0EB /* FeedsObjCViewController.m in Sources */,
FFFF683D19D628000081904A /* NBURLCache.m in Sources */,
@ -5069,6 +5088,7 @@
1785524C2A21693300A8CD92 /* FeedDetailLoadingView.swift in Sources */,
FF34FD6D1E9D93CB0062F8ED /* IASKSwitch.m in Sources */,
FF9B8BB217F2351A0036A41C /* NBBarButtonItem.m in Sources */,
17FBFA982A6AEA4100149651 /* SlideAnimatedTransitioning.swift in Sources */,
FF2D8CE514893BC000057B80 /* MoveSiteViewController.m in Sources */,
433323CD158968ED0025064D /* FirstTimeUserViewController.m in Sources */,
178552492A1F115800A8CD92 /* FeedDetailTableCell.m in Sources */,