I recently implemented into my code a way to scroll to the top of UIViewController by tapping an icon in UITabBar twice. The code is located inside my UITabBarController code. It works great, however the unfortunate side effect I've found is that every time I open up a page on my app, it's now at the top of UIViewController instead of the place where I last left off. I'm sure there's an error somewhere in this code
import UIKit
class TabViewController: UITabBarController, UITabBarControllerDelegate {
var pressedCount: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
// Do any additional setup after loading the view.
}
#IBAction func unwindToMain(segue: UIStoryboardSegue) {}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.isNavigationBarHidden = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
pressedCount += 1
if pressedCount > 1 {
scrollToTop()
} else {
//do something for first press
}
print("Selected item")
}
// UITabBarControllerDelegate
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
print("Selected view controller")
}
func tabBarController(_ TabViewController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let viewControllers = viewControllers else { return false }
if viewController == viewControllers[selectedIndex] {
if let nav = viewController as? UINavigationController {
guard let topController = nav.viewControllers.last else { return true }
if !topController.isScrolledToTop {
topController.scrollToTop()
return false
} else {
nav.popViewController(animated: true)
}
return true
}
}
return true
}
}
extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: view)
}
var isScrolledToTop: Bool {
if self is UITableViewController {
return (self as! UITableViewController).tableView.contentOffset.y == 0
}
for subView in view.subviews {
if let scrollView = subView as? UIScrollView {
return (scrollView.contentOffset.y == 0)
}
}
return true
}
}
Problem is, that when your ViewController disappear, it still has pressedCount property set to 2
So to viewWillAppear add this line to reset this:
pressedCount = 0
also fix if statement in tabBar didSelect item to reset pressedCount every time user presses tabBar item twice
if pressedCount > 1 {
scrollToTop()
pressedCount = 0
} else {
//do something for first press
}
Related
It uses navigation that allows you to go back even if you swipe the middle of the screen as shown in the code here.
In this case, if a SwiftUI view exists in the viewControllers of the navigation, the SwiftUI view will not accept the action if you cancel the return operation in the middle.
Is there a solution to this?
https://gist.github.com/policante/33a2d11b0501052d230c1f213dcacde3
class BaseNavigationController: UINavigationController, UINavigationControllerDelegate {
var interactivePopTransition: UIPercentDrivenInteractiveTransition!
override func viewDidLoad() {
self.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
addPanGesture(viewController)
}
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if operation == .pop {
return NavigationPopTransition()
}else{
return nil
}
}
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
if animationController.isKind(of: NavigationPopTransition.self) {
return interactivePopTransition
}else{
return nil
}
}
private func addPanGesture(_ viewController: UIViewController){
let popRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanRecognizer(recognizer:)))
viewController.view.addGestureRecognizer(popRecognizer)
}
#objc func handlePanRecognizer(recognizer: UIPanGestureRecognizer){
var progress = recognizer.translation(in: self.view).x / self.view.bounds.size.width
progress = min(1, max(0, progress))
print(progress)
if recognizer.state == .began {
self.interactivePopTransition = UIPercentDrivenInteractiveTransition()
self.popViewController(animated: true)
}else if recognizer.state == .changed {
interactivePopTransition.update(progress)
}else if recognizer.state == .ended || recognizer.state == .cancelled {
if progress > 0.2 {
interactivePopTransition.finish()
}else{
interactivePopTransition.cancel()
}
interactivePopTransition = nil
}
}
}
struct SampleView: View {
var body: some View {
NavigationLink(
destination: SampleView(),
label: {
Text("LINK")
})
}
}
let view: UIHostingController = UIHostingController(rootView: SampleView())
let nav = BaseNavigationController[enter image description here][1](rootViewController: view)
present(nav, animated: true, completion: nil)
Reproduced gif
In SwiftUI, whenever the navigation bar is hidden, the swipe to go back gesture is disabled as well.
Is there any way to hide the navigation bar while preserving the swipe back gesture in SwiftUI? I've already had a custom "Back" button, but still need the gesture.
I've seen some solutions for UIKit, but still don't know how to do it in SwiftUI
Here is the code to try yourself:
import SwiftUI
struct RootView: View {
var body: some View {
NavigationView {
NavigationLink(destination: SecondView()) {
Text("Go to second view")
}
}
}
}
struct SecondView: View {
var body: some View{
Text("As you can see, swipe to go back will not work")
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
Any suggestions or solutions are greatly appreciated
This should work by just extending UINavigationController.
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
It is even easier than what Nick Bellucci answered.
Here is the simplest working solution:
extension UINavigationController {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = nil
}
}
When using the UINavigationController extension you might encounter a bug that blocks your navigation after you start swiping the screen and let it go without navigating back. Adding .navigationViewStyle(StackNavigationViewStyle()) to NavigationView does fix this issue.
If you need different view styles based on device, this extension helps:
extension View {
public func currentDeviceNavigationViewStyle() -> AnyView {
if UIDevice.current.userInterfaceIdiom == .pad {
return AnyView(self.navigationViewStyle(DefaultNavigationViewStyle()))
} else {
return AnyView(self.navigationViewStyle(StackNavigationViewStyle()))
}
}
}
I looked around documentation and other sources about this issue and found nothing. There are only a few solutions, based on using UIKit and UIViewControllerRepresentable. I tried to combine solutions from this question and I saved swipe back gesture even while replacing back button with other view. The code is still dirty a little, but I think that is the start point to go further (totally hide navigation bar, for example). So, here is how ContentView looks like:
import SwiftUI
struct ContentView: View {
var body: some View {
SwipeBackNavController {
SwipeBackNavigationLink(destination: DetailViewWithCustomBackButton()) {
Text("Main view")
}
.navigationBarTitle("Standard SwiftUI nav view")
}
.edgesIgnoringSafeArea(.top)
}
}
// MARK: detail view with custom back button
struct DetailViewWithCustomBackButton: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("detail")
.navigationBarItems(leading: Button(action: {
self.dismissView()
}) {
HStack {
Image(systemName: "return")
Text("Back")
}
})
.navigationBarTitle("Detailed view")
}
private func dismissView() {
presentationMode.wrappedValue.dismiss()
}
}
Here is realization of SwipeBackNavController and SwipeBackNavigationLink which mimic NavigationView and NavigationLink. They are just wrappers for SwipeNavigationController's work. The last one is a subclass of UINavigationController, which can be customized for your needs:
import UIKit
import SwiftUI
struct SwipeBackNavController<Content: View>: UIViewControllerRepresentable {
let content: Content
public init(#ViewBuilder content: #escaping () -> Content) {
self.content = content()
}
func makeUIViewController(context: Context) -> SwipeNavigationController {
let hostingController = UIHostingController(rootView: content)
let swipeBackNavController = SwipeNavigationController(rootViewController: hostingController)
return swipeBackNavController
}
func updateUIViewController(_ pageViewController: SwipeNavigationController, context: Context) {
}
}
struct SwipeBackNavigationLink<Destination: View, Label:View>: View {
var destination: Destination
var label: () -> Label
public init(destination: Destination, #ViewBuilder label: #escaping () -> Label) {
self.destination = destination
self.label = label
}
var body: some View {
Button(action: {
guard let window = UIApplication.shared.windows.first else { return }
guard let swipeBackNavController = window.rootViewController?.children.first as? SwipeNavigationController else { return }
swipeBackNavController.pushSwipeBackView(DetailViewWithCustomBackButton())
}, label: label)
}
}
final class SwipeNavigationController: UINavigationController {
// MARK: - Lifecycle
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
delegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
// This needs to be in here, not in init
interactivePopGestureRecognizer?.delegate = self
}
deinit {
delegate = nil
interactivePopGestureRecognizer?.delegate = nil
}
// MARK: - Overrides
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
duringPushAnimation = true
setNavigationBarHidden(true, animated: false)
super.pushViewController(viewController, animated: animated)
}
var duringPushAnimation = false
// MARK: - Custom Functions
func pushSwipeBackView<Content>(_ content: Content) where Content: View {
let hostingController = SwipeBackHostingController(rootView: content)
self.delegate = hostingController
self.pushViewController(hostingController, animated: true)
}
}
// MARK: - UINavigationControllerDelegate
extension SwipeNavigationController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.duringPushAnimation = false
}
}
// MARK: - UIGestureRecognizerDelegate
extension SwipeNavigationController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == interactivePopGestureRecognizer else {
return true // default value
}
// Disable pop gesture in two situations:
// 1) when the pop animation is in progress
// 2) when user swipes quickly a couple of times and animations don't have time to be performed
let result = viewControllers.count > 1 && duringPushAnimation == false
return result
}
}
// MARK: Hosting controller
class SwipeBackHostingController<Content: View>: UIHostingController<Content>, UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.duringPushAnimation = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.delegate = nil
}
}
This realization provides to save custom back button and swipe back gesture for now. I still don't like some moments, like how SwipeBackNavigationLink pushes view, so later I'll try to continue research.
I'm trying to make my UITabBarController scroll to the top of the page when pressed twice. I have tried several times to get this to work with no luck. As of now, I only have one class for the UITabBarController on the storyboard that is linked to the code. Am I supposed to link UITabBar as well? Here's my code that I've attempted so far.
import UIKit
class TabViewController: UITabBarController, UITabBarControllerDelegate {
var pressedCount: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
// Do any additional setup after loading the view.
}
#IBAction func unwindToMain(segue: UIStoryboardSegue) {}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.isNavigationBarHidden = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
print("Selected item")
}
// UITabBarControllerDelegate
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
print("Selected view controller")
}
func tabBarController(_ TabViewController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let viewControllers = viewControllers else { return false }
if viewController == viewControllers[selectedIndex] {
if let nav = viewController as? UINavigationController {
guard let topController = nav.viewControllers.last else { return true }
if !topController.isScrolledToTop {
topController.scrollToTop()
return false
} else {
nav.popViewController(animated: true)
}
return true
}
}
return true
}
}
extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: view)
}
var isScrolledToTop: Bool {
if self is UITableViewController {
return (self as! UITableViewController).tableView.contentOffset.y == 0
}
for subView in view.subviews {
if let scrollView = subView as? UIScrollView {
return (scrollView.contentOffset.y == 0)
}
}
return true
}
}
For example:
var pressedCount: Int = 0
func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
pressedCount += 1
if pressedCount > 1 {
scrollToTop(view: self.view)
} else {
//do something for first press
}
}
Customize the UITabBar and then use the notification time to make the outside world respond!Or record the number of clicks through the UITabBarController agent and then notify the outside world to respond!
I integrated pageviewcontroller using storyboard. i have 4 view. how to stop after scroll at third view. how to disable after scoll?
This my code. how to stop after scroll in ThirdViewController.
import UIKit
class PageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
var pageControl = UIPageControl()
// MARK: UIPageViewControllerDataSource
lazy var orderedViewControllers: [UIViewController] = {
return [self.newVc(viewController: "sbRed"),self.newVc(viewController: "sbBlue"),self.newVc(viewController: "sbOrange"),self.newVc(viewController: "sbGray")]
}()
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.delegate = self
// This sets up the first view that will show up on our page control
if let firstViewController = orderedViewControllers.first {
setViewControllers([firstViewController],
direction: .forward,
animated: true,
completion: nil)
}
configurePageControl()
// Do any additional setup after loading the view.
}
func configurePageControl() {
// The total number of pages that are available is based on how many available colors we have.
pageControl = UIPageControl(frame: CGRect(x: 0,y: UIScreen.main.bounds.maxY - 50,width: UIScreen.main.bounds.width,height: 50))
self.pageControl.numberOfPages = orderedViewControllers.count
self.pageControl.currentPage = 0
self.pageControl.tintColor = UIColor.black
self.pageControl.pageIndicatorTintColor = UIColor.white
self.pageControl.currentPageIndicatorTintColor = UIColor.black
self.view.addSubview(pageControl)
}
func newVc(viewController: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: viewController)
}
// MARK: Delegate methords
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
let pageContentViewController = pageViewController.viewControllers![0]
self.pageControl.currentPage = orderedViewControllers.index(of: pageContentViewController)!
}
// MARK: Data source functions.
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
// User is on the first view controller and swiped left to loop to
// the last view controller.
guard previousIndex >= 0 else {
return orderedViewControllers.last
// Uncommment the line below, remove the line above if you don't want the page control to loop.
// return nil
}
guard orderedViewControllers.count > previousIndex else {
return nil
}
return orderedViewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.index(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
let orderedViewControllersCount = orderedViewControllers.count
// User is on the last view controller and swiped right to loop to
// the first view controller.
guard orderedViewControllersCount != nextIndex else {
return orderedViewControllers.first
// Uncommment the line below, remove the line above if you don't want the page control to loop.
// return nil
}
guard orderedViewControllersCount > nextIndex else {
return nil
}
return orderedViewControllers[nextIndex]
}
}
import UIKit
class ThirdViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
You need to use UiPagerViewDatasource methods :
a) func pageViewController(UIPageViewController, viewControllerBefore: UIViewController)
b) func pageViewController(UIPageViewController, viewControllerAfter: UIViewController)
Example:
func pageViewController(UIPageViewController, viewControllerBefore: UIViewController){
if viewController == controllerA {
return controllerB;
}
else if viewController == controllerB {
return controllerC;
}
else ifviewController == controllerC {
return nil;
}}
and
func pageViewController(UIPageViewController, viewControllerAfter: UIViewController){
if (viewController == controllerC) {
return controllerB;
}
else if(viewController == controllerB) {
return controllerA;
}
else {
return nil;
}}
Sets the view controllers to be displayed you need to call :
pager.setViewControllers([selectingViewController]?, direction: UIPageViewControllerNavigationDirectionForward, animated: false, completion:nil)
I want to retrieve the value from the scrollViewDidScroll function at my view controller. It gives the right value back in the console so that's nice.
My view controller:
import UIKit
extension UIPageViewController: UIScrollViewDelegate {
public override func viewDidLoad() {
super.viewDidLoad()
for subView in view.subviews {
if subView is UIScrollView {
(subView as! UIScrollView).delegate = self
}
}
}
public func scrollViewDidScroll(scrollView: UIScrollView) {
let point = scrollView.contentOffset
var percentComplete: CGFloat
percentComplete = fabs(point.x - view.frame.size.width)/view.frame.size.width
print("percentComplete: ", percentComplete)
}
}
class StepsDetailViewController: UIViewController {
var pageIndex: Int!
override func viewDidLoad()
{
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Where the view controller came from:
import UIKit
class PageControl: UIPageViewController, UIPageViewControllerDataSource {
var pageViewController: UIPageViewController!
var pageTitles: [[String]]!
override func viewDidLoad(){
super.viewDidLoad()
testC().getRecent() { result, error in
self.pageTitles = result;
self.pageViewController = self
self.pageViewController.dataSource = self
let startVC = self.viewControllerAtIndex(0) as StepsDetailViewController
dispatch_async(dispatch_get_main_queue(), {
let viewControllers = NSArray(object: startVC)
self.pageViewController.setViewControllers(viewControllers as? [UIViewController], direction: .Forward, animated: true, completion: nil)
})
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func viewControllerAtIndex(index: Int) -> StepsDetailViewController
{
if ((self.pageTitles.count == 0) || (index >= self.pageTitles.count)) {
return StepsDetailViewController()
}
let vc: StepsDetailViewController = self.storyboard?.instantiateViewControllerWithIdentifier("StepsDetailViewController") as! StepsDetailViewController
vc.pageIndex = index
return vc
}
// MARK: - Page View Controller Data Source
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController?
{
let vc = viewController as! StepsDetailViewController
var index = vc.pageIndex as Int
if (index == 0 || index == NSNotFound)
{
return nil
}
index -= 1
return self.viewControllerAtIndex(index)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
let vc = viewController as! StepsDetailViewController
var index = vc.pageIndex as Int
if (index == NSNotFound)
{
return nil
}
index += 1
if (index == self.pageTitles.count)
{
return nil
}
return self.viewControllerAtIndex(index)
}
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int
{
return self.pageTitles.count
}
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int
{
return 0
}
}
Is this possible?
You could do like this:
1. Replace your extension with a custom UIPageViewController
class BasePageViewController: UIPageViewController, UIScrollViewDelegate {
var percentComplete: CGFloat = 0.0 {
didSet { self.percentCompleteDidChange() }
}
func percentCompleteDidChange() {
print("percentCompleteDidChange")
}
public override func viewDidLoad() {
super.viewDidLoad()
for subView in view.subviews {
if subView is UIScrollView {
(subView as! UIScrollView).delegate = self
}
}
}
public func scrollViewDidScroll(scrollView: UIScrollView) {
let point = scrollView.contentOffset
self.percentComplete = fabs(point.x - view.frame.size.width)/view.frame.size.width
print("percentComplete: ", percentComplete)
}
}
2. PageControl is a BasePageViewController subclass
class PageControl: BasePageViewController, UIPageViewControllerDataSource {
// here you have the access to percentComplete of superclass
func printPercentComplete() {
if let percentComplete = self.percentComplete {
print(percentComplete)
}
}
override func percentCompleteDidChange() {
print("percentCompleteDidChange from child")
}
......
}
Define the percentComplete as a property of your custom class that inherits from UIPageViewController.
Then you can access it everywhere.