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.
Related
I have two UIViewControllers, A and B, I connect them within a UIPageViewController:
Here is how it looks in the Storyboard:
I don't know how to pass data to B from A.
Well assume you have some class (which you should have provided) like:
class MyModel {
var dataFromFirstController: Any?
var dataFromSecondController: Any?
var sharedData: Any?
}
Now you need a subclass of page view controller which is the one that controls the data so override view did load to create a model:
var myModel: MyModel!
override func viewDidLoad() {
super.viewDidLoad()
self.myModel = MyModel()
}
Now when you generate or fetch view controllers you simply assign the same model to them:
func getFirstViewController() -> UIViewController {
let controller = MyFirstController.generate()
controller.myModel = self.myModel
return controller
}
func getSecondViewController() -> UIViewController {
let controller = MySecondController.generate()
controller.myModel = self.myModel
return controller
}
Now all 3 view controllers share the same model. This is probably the easiest way of doing it but there are very many ways. The cleanest is probably using delegates which would report back to page controller that would then report back to given view controllers.
I had some difficulty finding a solution to this and came up with something myself using delegation. Suggestions are welcome
In the ViewController sending the data, define a delegate as follows:
protocol FirstVCDelegate {
func foo(someData: String)
}
class FirstViewController: UIViewController {
var delegate: FirstVCDelegate?
....
func someMethod() {
delegate?.foo("first VC")
}
}
In the PageViewController set up your View Controllers as follows:
class PageViewController: UIPageViewController, ... {
var myViewControllers = [UIViewController]()
override func viewDidLoad() {
let firstVC = storyboard?.instantiateViewController(withIdentifier: "FirstViewController") as! FirstViewController
let secondVC = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
firstVC.delegate = secondVC
myViewControllers.append(firstVC)
myViewControllers.append(secondVC)
}
// MARK: - PageVC Delegate / Datasource
....
and finally, the receiving ViewController implements the delegate as follows:
class SecondViewController: UIViewController, FirstVCDelegate {
....
func foo(data: String) { // This method is triggered from FirstViewController's delegate?.foo("first VC")
print(data) // "first VC" will be printed
}
}
Good luck,
Aaron
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()]
When the user clicked in a cell it will go to the detail view controller here I post a notification with the selected index(object of a class). In detail view controller I used a container view. In this container view, I used a page view controller having two view controllers let's say A and B correspondingly. Both these two view controllers I created add observer method. But only 'A' view controller is registering for the notification. How can I get the selected index(object of a class) in B view controller..?
Code of detail View controller
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.post(name: Notification.Name(rawValue: mynotificationkey), object: nil, userInfo: ["object": self.treatobj])
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "pageView2" {
if let destination = segue.destination as? PageViewController {
destination.treatmentObject = treatobj
}
}
}
code of First View Controller that its executing fine
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(display(notification:)), name: Notification.Name(rawValue: mynotificationkey), object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
NotificationCenter.default.removeObserver(self)
}
func display(notification: Notification){
if let object = notification.userInfo {
if let obj = object["object"] {
treatObject = obj as! TreatMents
print(treatObject)
self.detailTitleLabel.text = treatObject.trtName
self.DesctextView.text = treatObject.desc1
}
}
}
code of second view controller that does not execute add observer
override func viewDidLoad() {
super.viewDidLoad()
about.text = treatObj.aboutkerala
//benifits = treatObj.benifits!
NotificationCenter.default.addObserver(self, selector: #selector(notfrecieved(notification:)), name: NSNotification.Name(rawValue: mynotificationkey), object: nil)
// Do any additional setup after loading the view.
}
func notfrecieved(notification: Notification) {
if let object = notification.userInfo {
if let obj = object["object"] {
print(obj)
treatObj = obj as! TreatMents
about.text = treatObj.aboutkerala
benifits = treatObj.benifits!
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
code of Page view controller
class PageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
var treatmentObject = TreatMents()
lazy var VCArray : [UIViewController] = {
return [self.VCInstance(name: "DescFisrtVC"),
self.VCInstance(name: "DescSecondVC"),
]
}()
private func VCInstance(name: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: name)
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = VCArray.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return VCArray.last
}
guard VCArray.count > previousIndex else {
return nil
}
return VCArray[previousIndex]
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = VCArray.index(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
guard nextIndex < VCArray.count else {
return VCArray.first
}
guard VCArray.count > nextIndex else {
return nil
}
return VCArray[nextIndex]
}
// A page indicator will be visible if both methods are implemented, transition style is 'UIPageViewControllerTransitionStyleScroll', and navigation orientation is 'UIPageViewControllerNavigationOrientationHorizontal'.
// Both methods are called in response to a 'setViewControllers:...' call, but the presentation index is updated automatically in the case of gesture-driven navigation.
public func presentationCount(for pageViewController: UIPageViewController) -> Int {
return VCArray.count
}
public func presentationIndex(for pageViewController: UIPageViewController) -> Int {
guard let firstViewController = viewControllers?.first, let firstViewcontrollerIndex = VCArray.index(of: firstViewController) else {
return 0
}
return firstViewcontrollerIndex
}
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.delegate = self
if let firstVc = VCArray.first {
setViewControllers([firstVc], direction: .forward, animated: true, completion: nil)
}
// Do any additional setup after loading the view.
}
If you want to NotificationCenter then you need to load that class in memory first .
If you are added NotificationCenter in class but that class not loaded so this will be not working .
From the flow it looks to me that the two VCs in UIPageViewControllers didn't load yet when you are posting the notification.
Have a look at the explanation given in doc:
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.
So my guess is that since one page is loaded at a time, so your second controller didn't register for this event.
I have an objection on this process design using NSNotification, since from your code it looks to me that you want to evaluate your TreatMents objects on both screen when they gets load, IMHO you can either use Singleton concept (using session) or use KVO to fix this problem.
EDIT:
In PageViewController you implement these two functions:
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?
that returns a VC from VCArray like:
return VCArray[previousIndex]
You also have treatmentObject in PageViewController, can you pass this variable to your two viewcontrollers while fetching it in above delegate functions like:
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = VCArray.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return VCArray.last
}
guard VCArray.count > previousIndex else {
return nil
}
let VC = VCArray[previousIndex] // <------ Check this out
VC.treatObj = self.treatmentObject; // <------ Check this out
return VC
}
Or you can also save your selected treatment in session and retrieve it in two view controllers.
There is a single viewController with some data in it. After swiping left or right I want to create new viewControllers having the same UI but having different data. What is the best method to do this ? Should I be using UIPageViewController ?
Here is a solution for Swift 3 setting up a UIPageViewController programmatically.
The trick is to declare a lazy array called pages on your UIPageViewController.
In viewDidLoad using this array you can set up the viewControllers. Also, your dataSource is able to work with this array, handling the logic of changing the viewControllers. Right now, the dataSource is being implemented to continuously display the viewControllers, like a carousel. Modify it to your needs ;)
import Foundation
import UIKit
struct DisplayableData {
let title: String
let description: String
// etc...
}
class DataViewController: UIViewController {
var data: DisplayableData?
// You can add data as an initalizer parameter, depending on your design needs
init() {
// If you would create a xib for your DataViewController, than replace nib name with DataViewController to make it work
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class PageViewController: UIPageViewController {
lazy internal var pages: [DataViewController] = {
// If the UI is the same, reuse the viewController, no need to create multiple ones
// You could create a xib, and draw the UI there
let firstVC = DataViewController()
// Assign your data structure to your viewController
firstVC.data = DisplayableData(title: "first", description: "desc")
let secondVC = DataViewController()
secondVC.data = DisplayableData(title: "second", description: "desc")
let thirdVC = DataViewController()
thirdVC.data = DisplayableData(title: "third", description: "desc")
return [firstVC, secondVC, thirdVC]
}()
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
// Set your viewControllers
setViewControllers([pages.first!], direction: .forward, animated: false, completion: nil)
}
}
extension PageViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
// Lets check if the viewController is the right type
guard let viewController = viewController as? DataViewController else {
fatalError("Invalid viewController type in PageViewController")
}
// Load the next one, if it is the last, load the first one
let presentedVCIndex: Int! = pages.index(of: viewController)
if presentedVCIndex + 1 > pages.count - 1 {
return pages.first
}
return pages[presentedVCIndex + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
// Lets check if the viewController is the right type
guard let viewController = viewController as? DataViewController else {
fatalError("Invalid viewController type in PageViewController")
}
// Load the previous one, if it is the first, load the last one
let presentedVCIndex: Int! = pages.index(of: viewController)
if presentedVCIndex - 1 < 0 {
return pages.last
}
return pages[presentedVCIndex - 1]
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return pages.count
}
}
in my swift iOS app i would create a storyboard with two views. I want use the page view controller element, so, with one left / right swipe, the user can change the actual view. How can I do this?
IMPORTANT! I have searched several time for tutorials in the web. I have only found schema which allow to change images in the same image view. I don't want to change the image in the same image in the same view; i want change the entire view. I attach a image, to better explain.
Thank in advance]1
This explains how to create a paged view controller using storyboard:
Drag a UIPageViewController into storyboard and mark it as the initial view controller.
Then create a new UIPageViewController subclass - say call it TutorialPageViewController and assign it to the object you just dragged into storyboard.
Now drag in two new UIViewControllers into storyboard and create/assign a subclass for each, as you would do normally.
Give each of these two view controllers a Storyboard ID e.g RedViewController & GreenViewController.
Now add this code in your TutorialPageViewController which creates
and shows the correct view controller when you swipe:
TutorialPageViewController:
class TutorialPageViewController: UIPageViewController, UIPageViewControllerDataSource {
private(set) lazy var orderedViewControllers: [UIViewController] = {
return [self.newColoredViewController("RedViewController"),
self.newColoredViewController("GreenViewController")]
}()
private func newColoredViewController(color: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil) .
instantiateViewControllerWithIdentifier("\(color)ViewController")
}
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
if let firstViewController = orderedViewControllers.first {
setViewControllers([firstViewController],
direction: .Forward,
animated: true,
completion: nil)
}
}
func pageViewController(pageViewController: UIPageViewController,
viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.indexOf(viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return nil
}
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
guard orderedViewControllersCount != nextIndex else {
return nil
}
guard orderedViewControllersCount > nextIndex else {
return nil
}
return orderedViewControllers[nextIndex]
}
}
See the article for more details and a tutorial on this.
https://spin.atomicobject.com/2015/12/23/swift-uipageviewcontroller-tutorial/
Update: Page Dots
Read from point 7 on this part 2 article that shows how to add a UIPageControl and change the dots on swipe:
https://spin.atomicobject.com/2016/02/11/move-uipageviewcontroller-dots/
#IBOutlet weak var pageControl: UIPageControl!
func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
didUpdatePageCount count: Int) {
pageControl.numberOfPages = count
}
func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
didUpdatePageIndex index: Int) {
pageControl.currentPage = index
}