I need a way to make UIPageViewController reuse controllers to save memory, because I have a huge number of pages to show!
I did the basic implementation of the UIPageViewController but couldn't manage to make controller reusable, please advice!
To solve this problem in my current project, I cache all view controllers that are created as pages for the UIPageViewController in a Set. Whenever the UIPageViewController requests a new view controller from its data source, I filter out an unused from that cache by checking the parentViewController property; if no unused view controller is available, a new one is created.
My setup of the UIPageViewController and the cache looks similar to this:
class MyPageViewController: UIPageViewController {
private var reusableViewControllers = Set<MyViewController>()
init() {
super.init(/* ... */)
self.dataSource = self
let initialViewController = self.unusedViewController()
// Configure the initial view controller
// ...
self.setViewControllers([ initialViewController ],
direction: .Forward,
animated: false,
completion: nil)
}
// This function returns an unused view controller from the cache
// or creates and returns a new one
private func unusedViewController() -> MyViewController {
let unusedViewControllers = reusableViewControllers.filter { $0.parentViewController == nil }
if let someUnusedViewController = unusedViewControllers.last {
return someUnusedViewController
} else {
let newViewController = MyViewController()
reusableViewControllers.insert(newViewController)
return newViewController
}
}
}
The data source uses the same function to obtain an unused view controller:
extension MyPageViewController: UIPageViewControllerDataSource {
func pageViewController(pageViewController: UIPageViewController,
viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
let nextViewController = unusedViewController()
// Configure the next view controller for display
// ...
return nextViewController
}
}
You could use ACReuseQueue to achieve what you want. It provides a queue for reusing your view controllers. You can use the UIPageViewController data source methods to dequeue a VC from the reuse queue. The tricky part is to know when to put them back in the reuse queue. UIPageViewController does not provide any method to know when this happens, but there is a way. UIPageViewController is a container VC that adds and removes its child view controllers using the VC containment APIs.
Your VC will receive didMoveToParentViewController: with nil as the argument if it is being removed from the UIPageViewController. Use this method to add yourself back to the queue.
Related
I have two view controllers inside a UIPageViewController. I need to get all view controllers inside the UIPageViewController to call methods inside them, but it always returns a single page only (either the first page or the second page).
The code is below for convenience:
if let viewControllers: [UIViewController] = self.viewControllers {
print(viewControllers)
for controller in viewControllers {
if let firstController: FirstViewController = controller as? FirstViewController {
//Call something
}
if let secondController: SecondViewController = controller as? SecondViewController {
//Call something
}
}
}
In fact, it returns only the visible view controller inside the page controller.
This is intended behaviour.
In a simple UIPageViewController, this array will only return the currently visible view controller(s), which is usually just one view controller.
Keep in mind that UIPageViewController heavily caches and reuses controllers, just like a UITableViewController would. You may want to reflect changes in another way.
I got an PageViewController which loads two "child "ViewControllers in order to let the user "swipe" through them. I don't want this swipe gesture , but instead I want to have a function inside my ViewController which allows me to use setViewControllers in the PageViewController.
I tried using protocols but even that didn't work out.
I would realy appreciate any help or suggestions on how I could accomplish that. Thanks!
To access setViewControllers from your child view controllers, you will need your child view controllers to be aware of their parent PageViewController. To do so, start by making a Protocol (I know you've said you've tried Protocols, but please please see my method through). This Protocol will ensure that every child view controller has a reference to the parent PageViewController.
protocol PageObservation: class {
func getParentPageViewController(parentRef: PageViewController)
}
Ensure that your child view controllers adhere to the PageObservation Protocol.
class Child1ViewController: UIViewController, PageObservation {
var parentPageViewController: PageViewController!
func getParentPageViewController(parentRef: PageViewController) {
parentPageViewController = parentRef
}
}
class Child2ViewController: UIViewController, PageObservation {
var parentPageViewController: PageViewController!
func getParentPageViewController(parentRef: PageViewController) {
parentPageViewController = parentRef
}
}
In your PageViewController, as you create each child view controller, cast them to the PageObservation type and pass a reference of the parent PageViewController. I use an array called orderViewControllers to create my pages. My UIPageViewControllerDataSource delegate methods uses it to know which pages to load but that is irrelevant to this example, I just thought I'd let you know in case you have a different way of creating your pages.
class PageViewController: UIPageViewController {
var orderedViewControllers: [UIViewController] = []
//creating child 1
//i am using storyboard to create the child view controllers, I have given them the identifiers Child1ViewController and Child2ViewController respectively
let child1ViewController = UIStoryboard(name: "Main", bundle: nil) .
instantiateViewController(withIdentifier: "Child1ViewController")
let child1WithParent = child1ViewController as! PageObservation
child1WithParent.getParentPageViewController(parentRef: self)
orderedViewControllers.append(child1ViewController)
//creating child 2
let child2ViewController = UIStoryboard(name: "Main", bundle: nil) .
instantiateViewController(withIdentifier: "Child2ViewController")
let child2WithParent = child2ViewController as! PageObservation
child2WithParent.getParentPageViewController(parentRef: self)
orderedViewControllers.append(child2ViewController)
}
Now inside your child view controllers, you have access to setViewControllers. For example, if I want to call setViewControllers in the child1ViewController, I have created a func called accessSetViewControllers() where I access the setViewControllers:
class Child1ViewController: UIViewController, PageObservation {
var parentPageViewController: PageViewController!
func getParentPageViewController(parentRef: PageViewController) {
parentPageViewController = parentRef
}
func accessSetViewControllers() {
parentPageViewController.setViewControllers( //do what you need )
}
}
On a side note, despite what other answers above have said, you can set dataSource to whatever you like. I sometimes set dataSource to nil to prevent the user from swiping away from a screen before doing something and then add the dataSource back to allow them to continue swiping.
Don't set dataSource. When it's nil, then gestures won't work.
https://developer.apple.com/reference/uikit/uipageviewcontroller
When defining a page view controller interface, you can provide the content view controllers one at a time (or two at a time, depending upon the spine position and double-sided state) or as-needed using a data source. When providing content view controllers one at a time, you use the setViewControllers(_:direction:animated:completion:) method to set the current content view controllers. To support gesture-based navigation, you must provide your view controllers using a data source object.
Simplistic approach... remove the inbuilt gesture recogniser in viewDidLoad of pageViewController:
for view in self.pageViewController!.view.subviews {
if let subView = view as? UIScrollView {
subView.scrollEnabled = false
}
}
Then add your own gesture below it. i just happened to be working with double tap at the moment but you could make it swipe left, swipe right easy enough:
let doubleTap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didDoubleTap))
doubleTap.numberOfTapsRequired = 2
doubleTap.delaysTouchesBegan = true
self.addGestureRecognizer(doubleTap)
and the gesture function with your code:
func didDoubleTap(gesture: UITapGestureRecognizer) {
//... stuff
}
My view controller hierarchy is the following:
The entry point is a UINavigationController, whose root view controller is a usual UITableViewController. The Table View presents a list of letters.
When the user taps on a cell, a push segue is triggered, and the view transitions to ContainerViewController. It contains an embedded ContentViewController, whose role is to present the selected letter on screen.
The Content View Controller stores the letter to be shown as a property letter: String, which should be set before its view is pushed on screen.
class ContentViewController: UIViewController {
var letter = "-"
#IBOutlet private weak var label: UILabel!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
label.text = letter
}
}
On the contrary, the Container View Controller should not know anything about the letter (content-unaware), since I'm trying to build it as reusable as possible.
class ContainerViewController: UIViewController {
var contentViewController: ContentViewController? {
return childViewControllers.first as? ContentViewController
}
}
I tried to write prepareForSegue() in my Table View Controller accordingly :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
// Not executed:
containerViewController.contentViewController?.letter = letter
}
}
but contentViewController is not yet created by the time this method is called, and the letter property is never set.
It is worth mentioning that this does work when the segue's destination view controller is set directly on the Content View Controller -- after updating prepareForSegue() accordingly.
Do you have any idea how to achieve this?
Actually I feel like the correct solution is to rely on programmatic instantiation of the content view, and this is what I chose after careful and thorough thoughts.
Here are the steps that I followed:
The Table View Controller has a push segue set to ContainerViewController in the storyboard. It still gets performed when the user taps on a cell.
I removed the embed segue from the Container View to the ContentViewController in the storyboard, and I added an IB Outlet to that Container View in my class.
I set a storyboard ID to the Content View Controller, say… ContentViewController, so that we can instantiate it programmatically in due time.
I implemented a custom Container View Controller, as described in Apple's View Controller Programming Guide. Now my ContainerViewController.swift looks like (most of the code install and removes the layout constraints):
class ContainerViewController: UIViewController {
var contentViewController: UIViewController? {
willSet {
setContentViewController(newValue)
}
}
#IBOutlet private weak var containerView: UIView!
private var constraints = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
setContentViewController(contentViewController)
}
private func setContentViewController(newContentViewController: UIViewController?) {
guard isViewLoaded() else { return }
if let previousContentViewController = contentViewController {
previousContentViewController.willMoveToParentViewController(nil)
containerView.removeConstraints(constraints)
previousContentViewController.view.removeFromSuperview()
previousContentViewController.removeFromParentViewController()
}
if let newContentViewController = newContentViewController {
let newView = newContentViewController.view
addChildViewController(newContentViewController)
containerView.addSubview(newView)
newView.frame = containerView.bounds
constraints.append(newView.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor))
constraints.append(newView.topAnchor.constraintEqualToAnchor(containerView.topAnchor))
constraints.append(newView.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor))
constraints.append(newView.bottomAnchor.constraintEqualToAnchor(containerView.bottomAnchor))
constraints.forEach { $0.active = true }
newContentViewController.didMoveToParentViewController(self)
}
} }
In my LetterTableViewController class, I instantiate and setup my Content View Controller, which is added to the Container's child view controllers. Here is the code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
if let viewController = storyboard?.instantiateViewControllerWithIdentifier("ContentViewController"),
let contentViewController = viewController as? ContentViewController {
contentViewController.letter = letter
containerViewController.contentViewController = contentViewController
}
}
}
This works perfectly, with an entirely content-agnostic container view controller. By the way, it used to be the way one instantiated a UITabBarController or a UINavigationController along with its children, in the appDidFinishLaunching:withOptions: delegate method.
The only downside of this I can see: the UI flow ne longer appears explicitly on the storyboard.
The only way I can think of is to add delegation so that your tableViewController implements a protocol with one method to return the letter; then you have containerViewController setting its childViewController (the contentViewController) delegate to its parent. And the contentViewController can finally ask its delegate for the letter.
At your current solution the presenting object itself is responsible for working both with the "container" and the "content", it doesn't have to be changed, but such solution not only has the issues like the one you described, but also makes the purpose of the "container" not very clear.
Look at the UIAlertController: you are not configuring its child view controller directly, you are not even supposed to know it exists when using the alert controller. Instead of configuring the "content", you are configuring the "container" which is aware of the content interfaces, lifecycle and behavior and doesn't expose it. Following this approach you achieve a properly divided responsibility of the container and content, minimal exposure of the "content" allows you to update the "container" without a need to update the way it is used.
In short, instead of trying to configure everything from a single place, make it so you configure only the "container" and let it configure the "content" when and where it is needed. E.g. in the scenario you described the "container" would set data for the "content" whenever it initializes the child controllers. I'm using "container" and "content" instead of ContainerViewController and ContentViewController because the solution is not strictly based on the controllers because you might as well replace it wth NSObject + UIView or UIWindow.
I have an app in working state in which I have a three screens 1,2,3 each of that screen is associated with a UICollectionView which are created programatically now I want to modify the current implementation & add those collection views to UIPageViewControllers.
I tried to find many tutorials related to PageViewController with CollectionViews but was not able to find anything. Can anyone help me out in implementing this or can give me a reference related to this.
I have also referred this tutorial, but hard luck for me :(
Just follow some of those page view controller tutorials, and when you get to the point of instantiating the child view controllers, make those children collection view controllers. Just as in your example tutorial where it is using UIImageView try replacing it with UICollectionViews & you will achieve what you want.
Do tell if you face any difficulties.
Happy Coding!
You can do this by:
1) Subclassing UIPageViewController and conforming to the UIPageViewControllerDataSource protocol. The data source will tell the pageViewController which view controllers will come next or before the current view controller being presented.
2) In viewDidLoad(), set the dataSource as self so the delegate methods get called. Also, call self.setViewControllers([collectionViewController], direction: .Forward, animated: true, completion: nil). collectionViewController will be an array populated with 1 collection view controller (the first view controller presented).
Example:
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self //so our delegate methods get called
//use tags to reference each controller
let collectionViewOne = TestCollectionViewController(collectionViewLayout: UICollectionViewFlowLayout())
collectionViewOne.view.tag = 0
let collectionViewTwo = TestCollectionViewController(collectionViewLayout: UICollectionViewFlowLayout())
collectionViewTwo.view.tag = 1
let collectionViewThree = TestCollectionViewController(collectionViewLayout: UICollectionViewFlowLayout())
collectionViewThree.view.tag = 2
collectionViewControllers = [collectionViewOne, collectionViewTwo, collectionViewThree]
self.setViewControllers([collectionViewOne], direction: .Forward, animated: true, completion: nil)
}
3) Implement the UIPageViewControllerDataSource methods:
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
var index = viewController.view.tag
if index == 0 {
return nil
}
if index > 0 {
index--
}
return collectionViewControllers[index] as? TestCollectionViewController
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
var index = viewController.view.tag
if index == 2 {
return nil
}
if index < collectionViewControllers.count - 1 {
index++
}
return collectionViewControllers[index] as? TestCollectionViewController
}
We return nil if the index will present an out of bounds view controller
Source code of TestCollectionViewController and the UIPageViewController subclass can be found here
I ran into a similar situation recently. I could never get the embedded UICollectionViews to work quite how I wanted them to in the UIPageViewController. I also felt that the implementation was overly complex, requiring a separate view controller for each UICollectionView. I did not feel this was appropriate for my dataset.
My solution was to use a single UIViewController with a horizontally scrolling, paged UICollectionView to handle the paging. Then I added vertically scrolling UICollectionViews for every page to contain my content. I also added an overlay that contained a UIPageControl that was fed scroll-position data to mock the page indicator. This vastly simplified my view controller to data source relationship, was easier to customize, and allowed greater control.
(I would post source but the code belongs to my employer and I used storyboards.)
I have a first tableViewController which opens up a second tableViewcontroller upon clicking a cell. The second view controller is presented modally (Show Detail segue) and is dismissed with:
self.dismissViewControllerAnimated(true, completion: {})
At this point, the second view controller slides away and reveals the first view controller underneath it. I would then like to reload the first view controller. I understand that this may require use of delegate functions, but not sure exactly how to implement it
Swift 5:
You can access the presenting ViewController (presentingViewController) property and use it to reload the table view when the view will disappear.
class: FirstViewController {
var tableView: UITableView
present(SecondViewController(), animated: true, completion: nil)
}
In your second view controller, you can in the viewWillDisappear method, add the following code:
class SecondViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if let firstVC = presentingViewController as? FirstViewController {
DispatchQueue.main.async {
firstVC.tableView.reloadData()
}
}
}
}
When you dismiss the SecondViewController, the tableview of the FirstViewController will reload.
I solved it a bit differently since I don't want that dependancy.
And this approach is intended when you present a controller modally, since the presenting controller wont reload when you dismiss the presented.
Anyway solution!
Instead you make a Singleton (mediator)
protocol ModalTransitionListener {
func popoverDismissed()
}
class ModalTransitionMediator {
/* Singleton */
class var instance: ModalTransitionMediator {
struct Static {
static let instance: ModalTransitionMediator = ModalTransitionMediator()
}
return Static.instance
}
private var listener: ModalTransitionListener?
private init() {
}
func setListener(listener: ModalTransitionListener) {
self.listener = listener
}
func sendPopoverDismissed(modelChanged: Bool) {
listener?.popoverDismissed()
}
}
Have you Presenting controller implement the protocol like this:
class PresentingController: ModalTransitionListener {
//other code
func viewDidLoad() {
ModalTransitionMediator.instance.setListener(self)
}
//required delegate func
func popoverDismissed() {
self.navigationController?.dismissViewControllerAnimated(true, completion: nil)
yourTableViev.reloadData() (if you use tableview)
}
}
and finally in your PresentedViewController in your viewDid/WillDisappear func or custom func add:
ModalTransitionMediator.instance.sendPopoverDismissed(true)
You can simply reaload your data in viewDidAppear:, but that might cause the table to be refreshed unnecessarily in some cases.
A more flexible solution is to use protocols as you have correctly guessed.
Let's say the class name of your first tableViewController is Table1VC and the second one is Table2VC. You should define a protocol called Table2Delegate that will contain a single method such as table2WillDismissed.
protocol Table2Delegate {
func table2WillDismissed()
}
Then you should make your Table1VC instance conform to this protocol and reload your table within your implementation of the delegate method.
Of course in order for this to work, you should add a property to Table2VC that will hold the delegate:
weak var del: Table2Delegate?
and set its value to your Table1VC instance.
After you have set your delegate, just add a call to the delegate method right before calling the dismissViewControllerAnimated in your Table2VC instance.
del?.table2WillDismissed()
self.dismissViewControllerAnimated(true, completion: {})
This will give you precise control over when the table will get reloaded.