I am creating a scrollView that is able to display ViewController views one after the other. This is the code I implemented:
scrollView.contentSize = CGSize(width: screenWidth * 3, height: screenHeight)
let firstVC = FirstViewController()
let secondVC = SecondViewController()
let thirdVC = ThirdViewController()
self.addChild(firstVC)
self.scrollView.addSubview(firstVC.view)
firstVC.willMove(toParent: self)
self.addChild(secondVC)
self.scrollView.addSubview(secondVC.view)
secondVC.willMove(toParent: self)
self.addChild(thirdVC)
self.scrollView.addSubview(thirdVC.view)
thirdVC.willMove(toParent: self)
firstVC.view.frame.origin = CGPoint.zero
secondVC.view.frame.origin = CGPoint(x: screenWidth, y: 0)
thirdVC.view.frame.origin = CGPoint(x: screenWidth*2, y: 0)
view.addSubview(scrollView)
scrollView.fillSuperview()
I wanted to know if it was possible to call each ViewController lifecycle method whenever I'm scrolling through them.
So for example when I'm passing from vc1 to vc2 I want that:
vc1 fires viewwillDisappear method
vc2 fires viewWillAppear method
The easiest solution is to use a page view controller. When you do that, the appearance methods for the children will be called for you automatically (and it gets you out of all of that dense code to manually populate and configure the scroll view):
class MainViewController: UIPageViewController {
let controllers = [RedViewController(), GreenViewController(), BlueViewController()]
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
setViewControllers([controllers.first!], direction: .forward, animated: true, completion: nil)
}
}
extension MainViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(where: { $0 == viewController }), index < (controllers.count - 1) {
return controllers[index + 1]
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(where: { $0 == viewController }), index > 0 {
return controllers[index - 1]
}
return nil
}
}
If you really want to use the scroll view approach, don't do the view controller containment code up front, but rather only add them as they scroll into view (and remove them when they scroll out of view). You just need to set a delegate for your scroll view and implement a UIScrollViewDelegate method.
So, for example, I might only populate my scroll view with container subviews for these three child view controllers. (Note containerViews in my example below are just blank UIView instances, laid out where the child view controller views will eventually go.) Then I can see if the CGRect of the visible portion of the scroll view intersects with a container view, and do the view controller containment in a just-in-time manner.
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let rect = CGRect(origin: scrollView.contentOffset, size: scrollView.bounds.size)
for (index, containerView) in containerViews.enumerated() {
let controller = controllers[index]
let controllerView = controller.view!
if rect.intersects(containerView.frame) {
if controllerView.superview == nil {
// a container view has scrolled into view, but the associated
// child controller view has not been added to the view hierarchy, yet
// so let's do that now
addChild(controller)
containerView.addSubview(controllerView)
controllerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
controllerView.topAnchor.constraint(equalTo: containerView.topAnchor),
controllerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
controllerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
controllerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor)
])
controller.didMove(toParent: self)
}
} else {
if controllerView.superview != nil {
// a container view has scrolled out of view, but the associated
// child controller view is still in the view hierarchy, so let's
// remove it.
controller.willMove(toParent: nil)
controllerView.removeFromSuperview()
controller.removeFromParent()
}
}
}
}
}
In both of these scenarios, you will receive the containment calls as the views appear.
Related
I have put a UINavigationController into an expandable "Drawer".
My goal is to let each viewController in the navigation stack to have its own "preferred" height.
Let's say VC1 needs to be tall. When navigating back to VC1 from VC2 I want it to animate its height to be tall. The animation logic seems to be working, even with the interaction of swipe.
But for some reason, the viewControllers in the navigationController are "cut off". Their constraints are correct, but they aren't updated(?). Or a portion of the content simply won't render, until I touch the view again. The invisible area on the bottom will even accept touches.
Take a look:
The expected result is that the contentViewControllers (first and second) always extend to the bottom of the screen. They are constraint to do this, so the issue is that they won't "render"(?) during the transition.
In the UINavigationController's delegates, I do the following:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
transitionCoordinator?.animate(alongsideTransition: { (context) in
drawer.heightConstraint.constant = targetHeight
drawer.superview?.layoutIfNeeded()
}, completion: { (context) in
print("done")
})
}
The height change is perfect. But the content in the navigation won't comply. The navigationController is constrained to leading, trailing, bottom, and a stored heightConstraint that changes its constant.
As soon as I touch/drag the navigationController/content it instantly "renders the unrendered", and everything is fine. Why is this happening?
When inspecting the view hierarchy, it looks like this:
The NavigationController is as tall as it needs to be, but the content is the same height as the entire Drawer was when the transition started, and it doesn't update until I touch it.
Why?
Edit: I've pushed the code to my GitHub if you want to take a look. Beware though, there are several other issues there as well (animation etc.), don't mind them. I only want to know why the navigation won't render "future" heights.
You can solve the above problem by having a custom animationController for your navigationController and setting the appropriate frame for the destinationView in animateTransition function of your custom UIViewControllerAnimatedTransitioning implementation. The result would be like the one in the following gif image.
Your custom UIViewControllerAnimatedTransitioning may look like the one below.
final class TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let presenting: Bool
init(presenting: Bool) {
self.presenting = presenting
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return TimeInterval(UINavigationController.hideShowBarDuration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.view(forKey: .from) else { return }
guard let toView = transitionContext.view(forKey: .to) else { return }
let duration = transitionDuration(using: transitionContext)
let container = transitionContext.containerView
if presenting {
container.addSubview(toView)
} else {
container.insertSubview(toView, belowSubview: fromView)
}
let toViewFrame = toView.frame
toView.frame = CGRect(x: presenting ? toView.frame.width : -toView.frame.width, y: toView.frame.origin.y, width: toView.frame.width, height: toView.frame.height)
UIView.animate(withDuration: duration, animations: {
toView.frame = toViewFrame
fromView.frame = CGRect(x: self.presenting ? -fromView.frame.width : fromView.frame.width, y: fromView.frame.origin.y, width: fromView.frame.width, height: fromView.frame.height)
}) { (finished) in
container.addSubview(toView)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
And, in your navigation controller provide custom animationController as follows.
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .push:
return TransitionAnimator(presenting: true)
case .pop:
return TransitionAnimator(presenting: false)
default:
return nil
}
}
PS :- I've also given a pull request in your github repo.
You can update controllers height yourself:
first you need to keep a reference to controllers:
class ContainedNavigationController: UINavigationController, Contained, UINavigationControllerDelegate {
private var controllers = [UIViewController]()
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { controllers = viewControllers }
/* Rest of the class */
Then you can update their heights accordingly. Don't forget to add new controller.
private func updateHeights(to height: CGFloat, willShow controller: UIViewController) {
controller.view.frame.size.height = height
_ = controllers.map { $0.view.frame.size.height = height }
}
You can use it in your code like this:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if let superHeight = view.superview?.superview?.bounds.size.height, let drawer = (view.superview as? Drawer), let targetHeight = (viewController as? Contained)?.currentNotch.height(availableHeight: superHeight){
transitionCoordinator?.animate(alongsideTransition: { (context) in
drawer.heightConstraint.constant = targetHeight
drawer.superview?.layoutIfNeeded()
self.updateHeights(to: targetHeight, willShow: viewController) // <- Here
}, completion: { (context) in
self.updateHeights(to: targetHeight, willShow: viewController) // <- Here
})
}
}
Result:
Maybe this is not the cleanest code can done, but I just want to solve the issue and giving you the idea
Update
That shadow you have seen so far when you drag from the edge is a view called UIParallaxDimmingView. I have added a fix for that size too. So no more visual issues:
private func updateHeights(to height: CGFloat, willShow controller: UIViewController) {
controller.view.frame.size.height = height
_ = controllers.map { $0.view.frame.size.height = height }
guard controllers.contains(controller) else { return }
_ = controller.view.superview?.superview?.subviews.map {
guard "_UIParallaxDimmingView" == String(describing: type(of: $0)) else { return }
$0.frame.size.height = height
}
}
I have added a pull request from my fork.
You can to set size like bellow:
refrence -> https://github.com/satishVekariya/Drawer/tree/boom-diggy-boom
class FirstViewController: UIViewController, Contained {
// your code
override func updateViewConstraints() {
super.updateViewConstraints()
if let parentView = parent?.view {
view.frame.size.height = parentView.frame.height
}
}
}
Your navigation vc:
class ContainedNavigationController:UINavigationController, Contained, UINavigationControllerDelegate{
///Your code
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
viewController.updateViewConstraints()
// Your code start
// end
}
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
viewController.updateViewConstraints()
}
}
You can set height constraint and call layoutIfNeeded method without transition block.
Below is the code snippet for the same:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if let superHeight = view.superview?.superview?.bounds.size.height, let drawer = (view.superview as? Drawer), let targetHeight = (viewController as? Contained)?.currentNotch.height(availableHeight: superHeight){
drawer.heightConstraint.constant = targetHeight
drawer.layoutIfNeeded()
}
}
#sti Please let me know if it helps, please do up vote for the same
I've embedded a UIPageViewController in a UINavigationController, which in turn is embedded in a UITabBarController. I'm simply trying to make it so that the pageViewController loops through its viewControllers that are stored in an array. However every time I try to move to the next page, the first viewController snaps back into place before disappearing.
I've made the first viewController red and the second one blue and oddly enough when loading them in I'm presented with the second viewController.
This gif shows what I mean
I've tried to set up a pageViewController in the same manner in a new project and everything worked as expected so I can't see where the problem is.
import UIKit
final internal class TabBarController: UITabBarController, ApplicationLoginDelegate {
private let newsFeedTableViewController: NewsFeedTableViewController = NewsFeedTableViewController(style: UITableViewStyle.grouped)
private let substitutionPlanTableViewController: SubstitutionPlanTableViewController = SubstitutionPlanTableViewController(style: UITableViewStyle.grouped)
private let loginTableViewController: LoginTableViewController = LoginTableViewController(style: UITableViewStyle.grouped)
private let timeTableViewController: TimeTablePageViewController = TimeTablePageViewController(transitionStyle: UIPageViewControllerTransitionStyle.scroll, navigationOrientation: UIPageViewControllerNavigationOrientation.horizontal, options: nil)
private let moreTableViewController: MoreTableViewController = MoreTableViewController(style: UITableViewStyle.grouped)
//
// MARK: - Override point
//
/**
Called after the controller's view is loaded into memory.
This method is called after the view controller has loaded its view hierarchy into memory. This method is called regardless of whether the view hierarchy was loaded from a nib file or created programmatically in the loadView() method. You usually override this method to perform additional initialization on views that were loaded from nib files.
*/
override func viewDidLoad() {
super.viewDidLoad()
self.setUpTabBar()
}
/**
Notifies the view controller that its view is about to be added to a view hierarchy.
This method is called before the view controller's view is about to be added to a view hierarchy and before any animations are configured for showing the view. You can override this method to perform custom tasks associated with displaying the view. For example, you might use this method to change the orientation or style of the status bar to coordinate with the orientation or style of the view being presented. If you override this method, you must call super at some point in your implementation.
For more information about the how views are added to view hierarchies by a view controller, and the sequence of messages that occur, see Supporting Accessibility.
*/
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if UIApplication.boolForKey(UserDefaultKey.openSubstitutionPlanOnStartup) == true {
self.selectedViewController = self.viewControllers?[1]
}
}
//
// MARK: - Functions
//
private func setUpTabBar() {
// General
self.tabBar.tintColor = UIColor.applicationBaseColor
self.tabBar.unselectedItemTintColor = UIColor.lightGray
self.tabBar.backgroundColor = UIColor.white
// Create tab bar items
let newsFeedTabBarItem: UITabBarItem = UITabBarItem(title: "Aktuelles", image: #imageLiteral(resourceName: "News"), tag: 0)
let substitutionTabBarItem: UITabBarItem = UITabBarItem(title: "Vertretungen", image: #imageLiteral(resourceName: "SubstitutionPlan"), tag: 1)
let timeTableTabBarItem: UITabBarItem = UITabBarItem(title: "Stundenplan", image: #imageLiteral(resourceName: "TimeTable"), tag: 2)
let moreTabBarItem: UITabBarItem = UITabBarItem(title: "Entdecken", image: #imageLiteral(resourceName: "MoreMenu"), tag: 3)
// Link items and controllers
self.newsFeedTableViewController.tabBarItem = newsFeedTabBarItem
self.substitutionPlanTableViewController.tabBarItem = substitutionTabBarItem
self.loginTableViewController.tabBarItem = substitutionTabBarItem
self.timeTableViewController.tabBarItem = timeTableTabBarItem
self.moreTableViewController.tabBarItem = moreTabBarItem
// Set delegates
self.loginTableViewController.delegate = self
// Set tab bar view controllers
var viewControllers: [UIViewController] = []
if UIApplication.boolForKey(UserDefaultKey.isUserLoggedIn) == true {
viewControllers = [newsFeedTableViewController, substitutionPlanTableViewController, timeTableViewController, moreTableViewController]
} else {
viewControllers = [newsFeedTableViewController, timeTableViewController, moreTableViewController]
}
self.viewControllers = viewControllers.map({ (controller) -> UIViewController in
controller.navigationItem.largeTitleDisplayMode = .always
let navigationController = UINavigationController(rootViewController: controller)
navigationController.navigationBar.prefersLargeTitles = true
return navigationController
})
if UIApplication.boolForKey(UserDefaultKey.isUserLoggedIn) == false {
self.viewControllers?.insert(self.loginTableViewController, at: 1)
}
}
}
The UITabBarController and the UIPageViewController:
class TimeTablePageViewController: UIPageViewController, UIPageViewControllerDataSource {
private var timeTableViewControllers: [UIViewController]!
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.timeTableViewControllers = Array.init(repeating: UIViewController(), count: 2)
self.timeTableViewControllers[0].view.backgroundColor = .red
self.timeTableViewControllers[1].view.backgroundColor = .blue
self.setViewControllers([self.timeTableViewControllers[0]], direction: UIPageViewControllerNavigationDirection.forward, animated: false, completion: nil)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let index = self.timeTableViewControllers.index(of: viewController) {
if viewController == self.timeTableViewControllers.first {
return self.timeTableViewControllers.last
} else {
return self.timeTableViewControllers[index - 1]
}
} else {
return nil
}
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let index = self.timeTableViewControllers.index(of: viewController) {
if viewController == self.timeTableViewControllers.last {
return self.timeTableViewControllers.first
} else {
return self.timeTableViewControllers[index + 1]
}
} else {
return nil
}
}
}
The repeatedValue parameter of Array.init(repeating repeatedValue: Array.Element, count: Int) is not a closure. It's a single object that will be used to fill the array.
The code won't call UIViewController() for each element it creates. You are creating an array that contains the same UIViewController instance two times. A view can't have two superViews, so when you scroll to the second page, the UIPageViewController adds the view of the only viewController to its view, which means that it will be removed from its view as well.
Replace
self.timeTableViewControllers = Array.init(repeating: UIViewController(), count: 2)
with
self.timeTableViewControllers = [UIViewController(), UIViewController()]
Double tapping anywhere at the bottom of a page control area (where the dots are) pauses the transition from one view controller to another.
Example of double tapping:
I simplified a UIPageViewController project to demonstrate this:
Storyboard
PageViewController.swift
import UIKit
class PageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var pages: [UIViewController] = [UIViewController]()
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
delegate = self
pages.append(UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Page1"))
pages.append(UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Page2"))
setViewControllers([pages.first!], direction: .forward, animated: true, completion: nil)
}
// This allows the dots to appear on top of the views
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for view in self.view.subviews {
if view is UIScrollView {
view.frame = UIScreen.main.bounds
} else if view is UIPageControl {
view.backgroundColor = UIColor.clear
}
}
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let viewControllerIndex = pages.index(of: viewController)
if viewControllerIndex == 1 {
return pages[0]
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let viewControllerIndex = pages.index(of: viewController)
if viewControllerIndex == 0 {
return pages[1]
}
return nil
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return pages.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
return 0
}
}
The source of the problem lies in this code:
// This allows the dots to appear on top of the views
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for view in self.view.subviews {
if view is UIScrollView {
view.frame = UIScreen.main.bounds
} else if view is UIPageControl {
view.backgroundColor = UIColor.clear
}
}
When this code is removed there is no partial transitions but you will now see a black bar which is also not desired.
This is a common solution I have found throughout the web and Stackoverflow to get the Page Control (dots) to show on top of the views instead of within its own bar at the bottom of the screen.
So that's all I'm looking for. A solution that:
Shows the dots on top of the views (no black bar)
No problems with transitioning.
Thanks in advance for any help!
There is very nice tutorial How to Use UIPageViewController in Swift which explains how to configure UIPageViewController
Second part of this tutorial How to Move Page Dots in a UIPageViewController which i believe is answer to your question.
You can use Container View to embed UIPageViewController in it and then you can keep any view top of the page view controller which does not scroll with UIPageViewController
I hope this helps.
Thanks, Jay.
For me the container view was the simplest solution. What I did was cover up the Page Control at the bottom of the page (dots) with a UIView that had the background color set to clear color. So I'm preventing the user from double tapping in that area. It totally feels like a hack but I honestly didn't want to implement all that custom code in that second link you posted.
Then I went to https://bugreport.apple.com and requested an Xcode Enhancement for the UIPageViewController that gives developers a checkbox to overlay the page control over the view or use the black bar. :D
Jay, thanks again for helping me on this. I didn't know you could reference the UIPageControl in the UIPageViewController but I did some research and found a way. I changed the viewDidLoad to this and it worked! No hack needed:
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
delegate = self
setViewControllers([pages.first!], direction: .forward, animated: true, completion: nil)
let pageControl = UIPageControl.appearance(whenContainedInInstancesOf: [PageViewController.self])
pageControl.isUserInteractionEnabled = false
}
What I have
I have a ViewController (TutorialViewController) and a UIPageViewController (TutorialPageViewController). There are also 3 extra views on the storyboard with StoryBoard ID's:
GreenViewController
BlueViewController
RedViewController
I have been following this tutorial (Kudos to the author, very well written).
On the Green View Controller I have defined a variable:
var passedVariable = ""
And in the ViewDidLoad I print it out.
Here are the two controllers that have the code:
UIViewController (TutorialViewController):
class TutorialViewController: UIViewController {
#IBOutlet weak var pageControl: UIPageControl!
#IBOutlet weak var containerView: UIView!
var tutorialPageViewController: TutorialPageViewController? {
didSet {
tutorialPageViewController?.tutorialDelegate = self
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let tutorialPageViewController = segue.destinationViewController as? TutorialPageViewController {
self.tutorialPageViewController = tutorialPageViewController
}
}
#IBAction func didTapNextButton(sender: UIButton) {
tutorialPageViewController?.scrollToNextViewController()
}
}
extension TutorialViewController: TutorialPageViewControllerDelegate {
func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
didUpdatePageCount count: Int) {
pageControl.numberOfPages = count
}
func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
didUpdatePageIndex index: Int) {
pageControl.currentPage = index
}
}
UIPageViewController
class TutorialPageViewController: UIPageViewController {
weak var tutorialDelegate: TutorialPageViewControllerDelegate?
//let vc0 = GreenViewController(nibName: "GreenViewController", bundle: nil)
private(set) lazy var orderedViewControllers: [UIViewController] = {
// The view controllers will be shown in this order
return [self.newColoredViewController("Green"),
self.newColoredViewController("Red"),
self.newColoredViewController("Blue"), self.newColoredViewController("Pink")]
}()
override func viewDidLoad() {
super.viewDidLoad()
//self.vc0.passedVariable = "Passed Data"
dataSource = self
delegate = self
if let initialViewController = orderedViewControllers.first {
scrollToViewController(initialViewController)
}
tutorialDelegate?.tutorialPageViewController(self,
didUpdatePageCount: orderedViewControllers.count)
}
/**
Scrolls to the next view controller.
*/
func scrollToNextViewController() {
if let visibleViewController = viewControllers?.first,
let nextViewController = pageViewController(self,
viewControllerAfterViewController: visibleViewController) {
scrollToViewController(nextViewController)
}
}
private func newColoredViewController(color: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil) .
instantiateViewControllerWithIdentifier("\(color)ViewController")
}
/**
Scrolls to the given 'viewController' page.
- parameter viewController: the view controller to show.
*/
private func scrollToViewController(viewController: UIViewController) {
setViewControllers([viewController],
direction: .Forward,
animated: true,
completion: { (finished) -> Void in
// Setting the view controller programmatically does not fire
// any delegate methods, so we have to manually notify the
// 'tutorialDelegate' of the new index.
self.notifyTutorialDelegateOfNewIndex()
})
}
/**
Notifies '_tutorialDelegate' that the current page index was updated.
*/
private func notifyTutorialDelegateOfNewIndex() {
if let firstViewController = viewControllers?.first,
let index = orderedViewControllers.indexOf(firstViewController) {
tutorialDelegate?.tutorialPageViewController(self,
didUpdatePageIndex: index)
}
}
}
// MARK: UIPageViewControllerDataSource
extension TutorialPageViewController: UIPageViewControllerDataSource {
func pageViewController(pageViewController: UIPageViewController,
viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.indexOf(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
}
guard orderedViewControllers.count > previousIndex else {
return nil
}
return orderedViewControllers[previousIndex]
}
func pageViewController(pageViewController: UIPageViewController,
viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.indexOf(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
}
guard orderedViewControllersCount > nextIndex else {
return nil
}
return orderedViewControllers[nextIndex]
}
}
extension TutorialPageViewController: UIPageViewControllerDelegate {
func pageViewController(pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
notifyTutorialDelegateOfNewIndex()
}
}
protocol TutorialPageViewControllerDelegate: class {
/**
Called when the number of pages is updated.
- parameter tutorialPageViewController: the TutorialPageViewController instance
- parameter count: the total number of pages.
*/
func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
didUpdatePageCount count: Int)
/**
Called when the current index is updated.
- parameter tutorialPageViewController: the TutorialPageViewController instance
- parameter index: the index of the currently visible page.
*/
func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
didUpdatePageIndex index: Int)
}
What I have tried
I have tried declaring the View Controller first like so:
let vc0 = GreenViewController(nibName: "GreenViewController", bundle: nil)
And then passing the data like so:
override func viewDidLoad() {
vc0.passedVariable = "This was passed, Dance with Joy"
}
Nothing is printing out in the console.
I also tried changing the bundle above to:
bundle: NSBundle.mainBundle()
Still nada
Question
I plan to load data on the TutorialViewController from an alamofire request, I want to pass that data to one of the ViewControllers (green, blue, red)
How do I pass data that has been acquired from the TutorialViewController to one of the child views that will load?
First, I want to thank you for checking out my tutorial and all of the nice things you said about it.
Second, I have a solution for you! I went ahead and committed the solution to the GitHub repo I linked in the tutorial. I will also post the code here.
(1) Create a UIViewController subclass to add custom properties to. For this example, I chose to add a UILabel since it's the easiest to view when running the app.
class ColoredViewController: UIViewController {
#IBOutlet weak var label: UILabel!
}
(2) Inside Main.storyboard, change the custom class for each UIViewController "page" to ColoredViewController in the Identity Inspector.
(3) Add a UILabel to each "page" and constraint it however you'd like. I chose to vertically and horizontally center it in the container. Don't forget to link the UILabel to ColoredViewController's #IBOutlet weak var label: UILabel!.
(4) Optional: I deleted the default "Label" text in each one that way if we never set the label's text in code, we will not show "Label" to the user.
(5) We need to do some TLC to TutorialPageViewController so it knows that orderedViewControllers is now a ColoredViewController array. To make things easy, I'm just going to paste the entire class:
class TutorialPageViewController: UIPageViewController {
weak var tutorialDelegate: TutorialPageViewControllerDelegate?
private(set) lazy var orderedViewControllers: [ColoredViewController] = {
// The view controllers will be shown in this order
return [self.newColoredViewController("Green"),
self.newColoredViewController("Red"),
self.newColoredViewController("Blue")]
}()
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
delegate = self
if let initialViewController = orderedViewControllers.first {
scrollToViewController(initialViewController)
}
tutorialDelegate?.tutorialPageViewController(self,
didUpdatePageCount: orderedViewControllers.count)
}
/**
Scrolls to the next view controller.
*/
func scrollToNextViewController() {
if let visibleViewController = viewControllers?.first,
let nextViewController = pageViewController(self,
viewControllerAfterViewController: visibleViewController) {
scrollToViewController(nextViewController)
}
}
private func newColoredViewController(color: String) -> ColoredViewController {
return UIStoryboard(name: "Main", bundle: nil) .
instantiateViewControllerWithIdentifier("\(color)ViewController") as! ColoredViewController
}
/**
Scrolls to the given 'viewController' page.
- parameter viewController: the view controller to show.
*/
private func scrollToViewController(viewController: UIViewController) {
setViewControllers([viewController],
direction: .Forward,
animated: true,
completion: { (finished) -> Void in
// Setting the view controller programmatically does not fire
// any delegate methods, so we have to manually notify the
// 'tutorialDelegate' of the new index.
self.notifyTutorialDelegateOfNewIndex()
})
}
/**
Notifies '_tutorialDelegate' that the current page index was updated.
*/
private func notifyTutorialDelegateOfNewIndex() {
if let firstViewController = viewControllers?.first as? ColoredViewController,
let index = orderedViewControllers.indexOf(firstViewController) {
tutorialDelegate?.tutorialPageViewController(self,
didUpdatePageIndex: index)
}
}
}
// MARK: UIPageViewControllerDataSource
extension TutorialPageViewController: UIPageViewControllerDataSource {
func pageViewController(pageViewController: UIPageViewController,
viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
guard let coloredViewController = viewController as? ColoredViewController,
let viewControllerIndex = orderedViewControllers.indexOf(coloredViewController) 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
}
guard orderedViewControllers.count > previousIndex else {
return nil
}
return orderedViewControllers[previousIndex]
}
func pageViewController(pageViewController: UIPageViewController,
viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
guard let coloredViewController = viewController as? ColoredViewController,
let viewControllerIndex = orderedViewControllers.indexOf(coloredViewController) 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
}
guard orderedViewControllersCount > nextIndex else {
return nil
}
return orderedViewControllers[nextIndex]
}
}
extension TutorialPageViewController: UIPageViewControllerDelegate {
func pageViewController(pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
notifyTutorialDelegateOfNewIndex()
}
}
protocol TutorialPageViewControllerDelegate: class {
/**
Called when the number of pages is updated.
- parameter tutorialPageViewController: the TutorialPageViewController instance
- parameter count: the total number of pages.
*/
func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
didUpdatePageCount count: Int)
/**
Called when the current index is updated.
- parameter tutorialPageViewController: the TutorialPageViewController instance
- parameter index: the index of the currently visible page.
*/
func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
didUpdatePageIndex index: Int)
}
(6) Inside TutorialViewController: let's set the label.text. I chose to use viewDidLoad, but feel free to stuff this logic inside a network request completion block.
override func viewDidLoad() {
super.viewDidLoad()
if let greenColoredViewController = tutorialPageViewController?.orderedViewControllers.first {
greenColoredViewController.label.text = "Hello world!"
}
}
Hope this helps!
Obviously, according the comments, there's still confusion how this can be solved.
I'll try to introduce one approach and explain way this might make sense. Note though, that there are a few other viable approaches which can solve this problem.
The root view controller
First we take a look at the "root" controller which is an instance of TutorialViewController. This one is responsible to fetch/get/obtain/retrieve a "model". The model is and instance of pure data. It must be appropriate to define and initialise the page view controllers. Since we have a number of pages, it makes sense this model is some kind of array or list of some kind of objects.
For this example, I use an array of strings - just in order to illustrate how this can be implemented. A real example would obtain an array of likely more complex objects, where each of it will be rendered in its own page. Possibly, the array has been fetched from a remote resource with a network request.
In this example, the strings happen to be the "colour" of the page view controllers. We create an appropriate property for class TutorialViewController:
class TutorialViewController: UIViewController {
#IBOutlet weak var pageControl: UIPageControl!
#IBOutlet weak var containerView: UIView!
private let model = ["Red", "Green", "Blue"]
...
Note that the property has private access: nobody else than the class itself should fiddle around with it.
Passing the Model from the Root Controller to its Embedded View Controller
The embedded view controller is an instance of TutorialPageViewController.
The root view controller passes the model to the embedded view controller in the method prepareForSegue. The embedded view controller must have an appropriate property which is suitable for its view of the model.
Note: A model may have several aspects or views. The model which has been initialised by the root view controller may not be appropriate to be passed as is to any of its presented view controllers. Thus, the root view controller may first filter, copy, reorder, or transform its model in order to make it suitable for the presented view controller.
Here, in this example, we take the model as is:
In class TutorialViewController:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let tutorialPageViewController = segue.destinationViewController as? TutorialPageViewController {
self.tutorialPageViewController = tutorialPageViewController
self.tutorialPageViewController!.model = self.model
}
}
Note that the TutorialViewController has itself a property (here model) which is set by the presenting view controller.
Here, the model is an array of strings. It should be obvious that the number of elements in the array should later become the number of pages in the page view controller. It should also be clear that each element is rendered on the corresponding page in a content view controller. Thus, we can say an element in the array serves as the "model" for each page.
We need to provide the property model in the TutorialPageViewController:
class TutorialPageViewController: UIPageViewController {
internal var model: [String]?
Note that the access is either public or internal, so that any presenting view controller can set it.
Passing the Model from the TutorialViewController to each Content View Controller
A page view controller (TutorialViewController) is responsible to create an array of content view controllers whose view render the page.
An easy approach to create the array of view controllers utilising a lazy property is shown below:
class TutorialPageViewController: UIPageViewController {
internal var model: [String]?
private(set) lazy var orderedViewControllers: [UIViewController] = {
// The view controllers will be shown in this order
assert(self.model != nil)
return self.model!.map {
self.newColoredViewController($0)
}
}()
The important part is here:
return self.model!.map {
self.newColoredViewController($0)
}
Here, we create N view controllers passing it the model (a String) in its factory function.
map returns an array of view controllers - suitable for the page view controller.
Once this has been implemented, the example works as in its original form.
You might now change the "factory" function which creates a view controller given a string as argument. For example you might set a label:
private func newColoredViewController(color: String) -> UIViewController {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("MyContentViewController") as! MyContentViewController
vc.label = color
return vc
}
Here, again label is the "model" of the view controller. It's totally up the view controller how label will be rendered - if at all.
Based on some of the comments, I can see that there is one small snippet missing from #Jeff's answer that could help clarify how to actually transfer data from the TutorialPageViewController to the ColoredViewController. He likely assumed that this part of the answer was inferred. This can, however be frustrating if you don't know what to do from here.
With that being said, I'm going to piggy back off of his answer. Let's say, for example, that we want to change the text of the label inside the ColoredViewController from the TutorialPageViewController. We will set the text value to the background color of that particular view controller.
1) Start by defining the variable inside the ColoredViewController class and setting the label text to that value.
class ColoredViewController: UIViewController {
#IBOutlet weak var label: UILabel!
var labelText: String?
override func viewDidLoad() {
super.viewDidLoad()
if let text = labelText {
label.text = text
}
// Do any additional setup after loading the view.
}
}
2) Set the value of labelText in the newColoredViewController method that we already created inside the TutorialPageViewController class
private func newColoredViewController(color: String) -> ColoredViewController {
let newController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "\(color)ViewController") as! ColoredViewController
newController.labelText = label
return newController
}
The previously empty label inside the view controller will now display the color value.
Note: You don't necessarily have to have 3 separate view controllers for this example to apply to your particular application. You could have 1 view controller that is serving as a template for each page within the page view controller. In this case, you would not reference the new view controller in the newColoredViewController method with a variable value but instead with the static name of the one content view controller you want to use.
Tapping the tab bar icon for the current navigation controller already returns the user to the root view, but if they are scrolled way down, if they tap it again I want it to scroll to the top (same effect as tapping the status bar). How would I do this?
A good example is Instagram's feed, scroll down then tap the home icon in the tab bar to scroll back to top.
The scrolling back to the top is easy, but connecting it to the tab bar controller is what I'm stuck on.
Implement the UITabBarControllerDelegate method tabBarController:didSelectViewController: to be notified when the user selects a tab. This method is also called when the same tab button is tapped again, even if that tab is already selected.
A good place to implement this delegate would probably be your AppDelegate. Or the object that logically "owns" the tab bar controller.
I would declare and implement a method that can be called on your view controllers to scroll the UICollectionView.
- (void)tabBarController:(UITabBarController *)tabBarController
didSelectViewController:(UIViewController *)viewController
{
static UIViewController *previousController = nil;
if (previousController == viewController) {
// the same tab was tapped a second time
if ([viewController respondsToSelector:#selector(scrollToTop)]) {
[viewController scrollToTop];
}
}
previousController = viewController;
}
SWIFT 3
Here goes..
First implement the UITabBarControllerDelegate in the class and make sure the delegate is set in viewDidLoad
class DesignStoryStreamVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UITabBarControllerDelegate {
#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.delegate = self
collectionView.delegate = self
collectionView.dataSource = self
}
}
Next, put this delegate function somewhere in your class.
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
print(tabBarIndex)
if tabBarIndex == 0 {
self.collectionView.setContentOffset(CGPoint.zero, animated: true)
}
}
Make sure to select the correct index in the "if" statement. I included the print function so you can double check.
Swift 5: no need for stored properties in the UITabBarController.
In MyTabBarController.swift, implement tabBarController(_:shouldSelect) to detect when the user re-selects the tab bar item:
protocol TabBarReselectHandling {
func handleReselect()
}
class MyTabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
func tabBarController(
_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController
) -> Bool {
if tabBarController.selectedViewController === viewController,
let handler = viewController as? TabBarReselectHandling {
// NOTE: viewController in line above might be a UINavigationController,
// in which case you need to access its contents
handler.handleReselect()
}
return true
}
}
In MyTableViewController.swift, handle the re-selection by scrolling the table view to the top:
class MyTableViewController: UITableViewController, TabBarReselectHandling {
func handleReselect() {
tableView?.setContentOffset(.zero, animated: true)
}
}
Now you can easily extend this to other tabs by just implementing TabBarReselectHandling.
You can use shouldSelect rather than didSelect, which would omit the need for an external variable to keep track of the previous view controller.
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController
{
if ([viewController isEqual:self] && [tabBarController.selectedViewController isEqual:viewController]) {
// Do custom stuff here
}
return YES;
}
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: self.view)
}
}
This is my answer in Swift 3. It uses a helper function for recursive calls and it automatically scrolls to top on call. Tested on a UICollectionViewController embedded into a UINavigationController embedded in a UITabBarController
I was using this View hierarchy.
UITabBarController > UINavigationController > UIViewController
I got a reference to the UITabBarController in the UIViewController
tabBarControllerRef = self.tabBarController as! CustomTabBarClass
tabBarControllerRef!.navigationControllerRef = self.navigationController as! CustomNavigationBarClass
tabBarControllerRef!.viewControllerRef = self
Then I created a Bool that was called at the correct times, and a method that allows scrolling to top smoothly
var canScrollToTop:Bool = true
// Called when the view becomes available
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
canScrollToTop = true
}
// Called when the view becomes unavailable
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
canScrollToTop = false
}
// Scrolls to top nicely
func scrollToTop() {
self.collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
Then in my UITabBarController Custom Class I called this
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
// Allows scrolling to top on second tab bar click
if (viewController.isKindOfClass(CustomNavigationBarClass) && tabBarController.selectedIndex == 0) {
if (viewControllerRef!.canScrollToTop) {
viewControllerRef!.scrollToTop()
}
}
}
The Result is identical to Instagram and Twitter's feed :)
Swift 3 approach::
//MARK: Properties
var previousController: UIViewController?
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if self.previousController == viewController || self.previousController == nil {
// the same tab was tapped a second time
let nav = viewController as! UINavigationController
// if in first level of navigation (table view) then and only then scroll to top
if nav.viewControllers.count < 2 {
let tableCont = nav.topViewController as! UITableViewController
tableCont.tableView.setContentOffset(CGPoint(x: 0.0, y: -tableCont.tableView.contentInset.top), animated: true)
}
}
self.previousController = viewController;
return true
}
A few notes here::
"shouldSelect" instead of "didSelect" because the latter is taking place after transition, meaning viewController local var already changed.
2. We need to handle the event before changing controller, in order to have the information of navigation's view controllers regarding scrolling (or not) action.
Explanation:: We want to scroll to top, if current view is actually a List/Table view controller. If navigation has advanced and we tap same tab bar, desired action would be to just pop one step (default functionality) and not scroll to top. If navigation hasn't advanced meaning we are still in table/list controller then and only then we want to scroll to top when tapping again. (Same thing Facebook does when tapping "Feed" from a user's profile. It only goes back to feed without scrolling to top.
In this implementation you no need static variable and previous view controller state
If your UITableViewController in UINavigationController you can implement protocol and function:
protocol ScrollableToTop {
func scrollToTop()
}
extension UIScrollView {
func scrollToTop(_ animated: Bool) {
var topContentOffset: CGPoint
if #available(iOS 11.0, *) {
topContentOffset = CGPoint(x: -safeAreaInsets.left, y: -safeAreaInsets.top)
} else {
topContentOffset = CGPoint(x: -contentInset.left, y: -contentInset.top)
}
setContentOffset(topContentOffset, animated: animated)
}
}
Then in your UITableViewController:
class MyTableViewController: UITableViewController: ScrollableToTop {
func scrollToTop() {
if isViewLoaded {
tableView.scrollToTop(true)
}
}
}
Then in UITabBarControllerDelegate:
extension MyTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard tabBarController.selectedViewController === viewController else { return true }
guard let navigationController = viewController as? UINavigationController else {
assertionFailure()
return true
}
guard
navigationController.viewControllers.count <= 1,
let destinationViewController = navigationController.viewControllers.first as? ScrollableToTop
else {
return true
}
destinationViewController.scrollToTop()
return false
}
}
I have a collection view embedded in a navigation controller, in Swift this works.
var previousController: UIViewController?
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
if previousController == viewController {
if let navVC = viewController as? UINavigationController, vc = navVC.viewControllers.first as? UICollectionViewController {
vc.collectionView?.setContentOffset(CGPointZero, animated: true)
}
}
previousController = viewController;
}
I've implemented a plug & play UITabBarController that you can freely re-use in your projects. To enable the scroll-to-top functionality, you should just have to use the subclass, nothing else.
Should work out of the box with Storyboards also.
Code:
/// A UITabBarController subclass that allows "scroll-to-top" gestures via tapping
/// tab bar items. You enable the functionality by simply subclassing.
class ScrollToTopTabBarController: UITabBarController, UITabBarControllerDelegate {
/// Determines whether the scrolling capability's enabled.
var scrollEnabled: Bool = true
private var previousIndex = 0
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
/*
Always call "super" if you're overriding this method in your subclass.
*/
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
guard scrollEnabled else {
return
}
guard let index = viewControllers?.indexOf(viewController) else {
return
}
if index == previousIndex {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), { [weak self] () in
guard let scrollView = self?.iterateThroughSubviews(self?.view) else {
return
}
dispatch_async(dispatch_get_main_queue(), {
scrollView.setContentOffset(CGPointZero, animated: true)
})
})
}
previousIndex = index
}
/*
Iterates through the view hierarchy in an attempt to locate a UIScrollView with "scrollsToTop" enabled.
Since the functionality relies on "scrollsToTop", it plugs easily into existing architectures - you can
control the behaviour by modifying "scrollsToTop" on your UIScrollViews.
*/
private func iterateThroughSubviews(parentView: UIView?) -> UIScrollView? {
guard let view = parentView else {
return nil
}
for subview in view.subviews {
if let scrollView = subview as? UIScrollView where scrollView.scrollsToTop == true {
return scrollView
}
if let scrollView = iterateThroughSubviews(subview) {
return scrollView
}
}
return nil
}
}
Edit (09.08.2016):
After attempting to compile with the default Release configuration (archiving) the compiler would not allow the possibility of creating a large number of closures that were captured in a recursive function, thus it would not compile. Changed out the code to return the first found UIScrollView with "scrollsToTop" set to true without using closures.
I tried the solution given by #jsanabria. This worked well on a fixed tableview, but it wouldn't work for my infinite scroll tableview. It only came up the table view about halfway after loading the new scrolling data.
Swift 5.0+
self.tableView.scrollToRow(at: IndexPath.init(row: 0, section: 0), at: UITableView.ScrollPosition(rawValue: 0)!, animated: true)
TESTED SOLUTION IN SWIFT
STEP 1
In your main tabbarcontroller class declare
weak static var previousController: UIViewController?
STEP 2
In viewdidLoad() set
MainTabBarViewController.previousController = viewControllers?[0]
STEP 3
extension MainTabBarViewController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if MainTabBarViewController.previousController == viewController {
/// here comes your code
}
MainTabBarViewController.previousController = viewController
}
}
I found the scrollRectToVisible method works better than the setContentOffset.
Swift:
After you catch the click on the tab bar from the delegate, something like below:
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
if (viewController.isKindOfClass(SomeControllerClass) && tabBarController.selectedIndex == 0)
{
viewController.scrollToTop()
}
}
Now for the scrollToTop function inside the controller:
func scrollToTop()
{
self.tableView.scrollRectToVisible(CGRectMake(0,0,CGRectGetWidth(self.tableView.frame), CGRectGetHeight(self.tableView.frame)), animated: true)
}