I'm building my first app using VIPER architecture. I have two VCs: the main one and the modal one (presented modally from the main VC). There is a tableView inside my modal VC, and when the user selects a row there, I need to pass it to presenter and then from presenter to the main VC. I also need to keep the selected row highlighted in the modal VC, so if I close it and then present it again, the row will be still highlighted. I'm confused, because I don't know what's the best way to do it. What I've tried is including my modal VC into configurator and calling configurator two times: in the main VC and then again in the modal VC. It works fine. That's what my configurator looks like:
protocol ObserverConfiguratorProtocol {
func configure(with mainViewController: ObserverViewController, with modalViewController: CurrenciesViewController)
}
class ObserverConfigurator: ObserverConfiguratorProtocol {
func configure(with mainViewController: ObserverViewController, with modalViewController: CurrenciesViewController) {
let presenter = ObserverPresenter(view: mainViewController)
let interactor = ObserverInteractor(presenter: presenter)
let router = ObserverRouter(view: mainViewController)
mainViewController.presenter = presenter
modalViewController.presenter = presenter
presenter.interactor = interactor
presenter.router = router
presenter.view = mainViewController
presenter.modalView = modalViewController
}
}
viewDidLoad() in the main VC:
override func viewDidLoad() {
configurator.configure(with: self, view: CurrenciesViewController())
}
viewDidLoad() in the modal VC:
override func viewDidLoad() {
configurator.configure(with: ObserverViewController(), view: self)
}
Anyway, I'm not sure it corresponds VIPER principles. Does it, or there are better solutions? Any help is appreciated!
If you are going to present another view controller, it should be on its own module for VIPER. ObserverViewController should not know about the presence of CurrenciesViewController.
Instead, only ObserverRouter should know about how to build CurrenciesModule and present and pass data into it. Something like this:
final class CurrenciesBuilder {
#available(*, unavailable) private init() { }
static func build(routerDelegate: ObserverRouterDelegate, selectedIndex: IndexPath?) -> CurrenciesViewProtocol {
let service = CurrenciesService()
let interactor = CurrenciesInteractor(service: service, selectedIndex: selectedIndex)
let view = CurrenciesViewController(nibName: String(describing: CurrenciesViewController.self), bundle: nil)
let router = CurrenciesRouter(vc: view)
router.delegate = routerDelegate
let presenter = CurrenciesPresenter(interactor: interactor,
view: view,
router: router)
view.presenter = presenter
return view
}
}
So, whenever you need to present CurrenciesViewController, you could pass selectedIndex and it can highlight it, if applicable. I would pass the information to the interactor, and let presenter know about it and do some changes in the view.
For passing the selectedIndex back to ObserverViewController, you could create a ObserverRouterDelegate that can be triggered when CurrenciesViewController is going to be dismissed.
protocol ObserverRouterDelegate: AnyObject {
func didDismiss(with selectedIndex: IndexPath?)
}
To wrap up; there should be 2 modules, ObserverModule and CurrenciesModule, that should have bidirectional connection through their routers. Since routers are responsible for presenting/dismissing views (and they hold a connection to their views), they can update their views and/or let the peer router update their own views.
Related
I have a view controller, lets call it vc1, which passes some data to another (vc2) using prepare for segue, and then calling performSegue.
Is there a way to pass some data back from vc2 to vc1 when vc2 is dismissed by swiping down?
Thanks,
Edit --
Apologies for the lack of information, very new to swift so unsure of the correct question to ask in this situation.
To elaborate, the root of the issue at the moment is that vc2 is not dismissed programatically. ie there is currently no function called, it is simply dismissed by the user swiping down.
Is there some function that I can include to capture this dismissal, and use it to send data back to vc1?
I would prefer not to add any buttons to vc2 if possible.
Apologies again, and I appreciate all the help given already!
Try This
class VCOne: UIViewController {
//Create a shared instance of VCOne
static var sharedInstance:VCOne?
//Let the data to be passed back to VCOne is of type string
var dataToBePassedBack:String?
override func viewDidLoad() {
super.viewDidLoad()
//set the sharedInstance to self
VCOne.sharedInstance = self
}
}
Class VCTwo:UIViewController{
//function in which you are dismissing your current VC you can use the shared
instance to pass the data back
func dismissVC(){
//before dismissing the VCTwo you can set the value for VCOne
VCOne.sharedInstance?.dataToBePassedBack = "data"
}
}
Using Protocol And Delegate You Do or Other Option is NSotificationcenter.
One way yo do it is to create another file that it the controller of everything and then have a delegate that always notifies the view controllers when new changes are available. I will walk it through.
protocol HeadControllerDelegate {
// Create a function that sends out the data to the delegates when it is called
// You can use your custom struct here to pass more data easly
func didReciveNewData(myData: String?)
}
struct HeadController {
// Create a shared instance so that the viewcontroller that conforms to the view as well as when we sends out the data the delegate is correct
static var shared = HeadController()
// Creates the delegate, every view can asign it to
public var delegate: HeadControllerDelegate?
// Add all your values here you want to pass back
var myValue: String? {
// The didSet gets called every time this value is set, and then is it time to call the delegate method
didSet {
// Calls the delegates didReciveMethod to notify the delegates that new data exsists
delegate?.didReciveNewData(myData: myValue)
}
}
}
Now in your viewcontroller class where you would like the data to be avaiable (as you said when you swipe down)
class ViewController: UIViewController {
// Here you create a property of the shared instance
let headController = HeadController.shared
override func viewDidLoad() {
super.viewDidLoad()
// Set yourself as the delegate for the headController delegate to recive data
headController.delegate = self
}
}
extension ViewController: HeadControllerDelegate {
// here will the data be recived
func didReciveNewData(myData: String?) {
// handle the data here, you have now got newData
print(myData)
}
}
In the class where you want to pass data you just do it like this. The beauty of this is that you can have multiple classes or structs that writes to the head controllers data (just make sure you do it thought the shared instance). It is also a good pracice according to we to use the delegate pattern.
class Sender {
var headController = HeadController.shared
func sendData(data: String) {
// Here you change the data of the headcontroller wich will send the data to all the delegates
headController.myValue = data
}
}
Hope this answer helps. If you have any questions please let me know.
UPDATE -- EASIER SOLUTION
Here is an easier solution but is less scalable as the previous one according to me.
In prepareForSegue simply pass over your current viewContorller as a field in the destination view controller. Then when viewDidDissapear in the new view controller you can simply pass back the data. Not to worry, I will show you!
In prepare for Segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let dc = segue.destination as? SecondViewController {
dc.viewController = self
}
}
And declare the secondViewContorller as following. The ViewDidDisappear method will be called when the view has dismissed, and therefore can you pass over the data to the view controller you have set before using the prepare for segue method.
class SecondViewController: UIViewController {
var viewController: UIViewController?
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidDisappear(_ animated: Bool) {
(viewController as? ViewController)?.value = 2
}
}
Then you could update the UI using a didSet, which simply will be called when the property is set, which will be done in the view did disappear method.
var value: Int = 0 {
didSet {
print(value)
text?.text = "\(value)"
}
}
Hope this helps!
I have a parent viewcontroller hosting 3 container viewcontrollers.
At certain points I need to pass data from one container viewcontroller to another container viewcontroller and thought I could accomplish this through the delegation pattern. However, I can't seem to figure out why the delegate is not triggered and the receiving container viewcontroller doesn't receive any data.
Can't seem to spot what's potentially wrong with the way I've set it up. If there's a recommended way to pass data between the containers, I'm all ears as well!
Below is a code summary on the setup:
class ParentViewController: UIViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let firstContainerVC = segue.destination as? FirstContainerVC {
//....
}
if let secondContainerVC = segue.destination as? SecondContainerVC {
//....
}
}
protocol Delegate {
func passX(a: String?)
func passY(b: String?)
}
}
class FirstContainerVC: UIViewController {
var delegate: Delegate?
if isTrue {
delegate.passX(a: "TestOne")
} else {
delegate.passY(b: "TestTwo")
}
}
class SecondContainerVC: UIViewController, Delegate {
override func viewDidLoad() {
let firstVC = self.storyboard?.instantiateViewController(withIdentifier: "firstContainer") as! FirstContainerVC
firstVC.delegate = self
}
func passX(a: String?) {
//if let a = a....
}
func passY(b: String?) {
//if let b = b....
}
}
Unfortunately, I don't know how the dragging and dropping works in Xcode, I do everything in code. However, when your parent view controller instantiates another view controller, just set the parent as the container's delegate.
Create the protocol:
protocol SomeProtocol: AnyObject {
func passX(a: String?)
func passY(b: String?)
}
And the containers will have delegates of type that protocol:
class FirstContainerVC: UIViewController {
weak var delegate: SomeProtocol?
}
class SecondContainerVC: UIViewController {
weak var delegate: SomeProtocol?
}
The parent must conform to the protocol so that it can become the delegate. Then when you instantiate the containers (which you must only do once in this scenario), set self as their delegates:
class ParentViewController: UIViewController, SomeProtocol {
// make the containers instance properties so that you
// can access them from the protocol methods
weak var firstContainerVC = FirstContainerVC()
weak var secondContainerVC = SecondContainerVC()
// set the delegates at some point
func setDelegates() {
firstContainerVC?.delegate = self
secondContainerVC?.delegate = self
}
func passX(a: String?) {
guard let a = a else {
return
}
secondContainerVC?.getFromFirst(a: a)
}
func passY(b: String?) {
//
}
}
Then when you want to go from first to second, go through the delegate from the first container to the parent, and from the parent to the second container.
class FirstContainerVC: UIViewController {
weak var delegate: SomeProtocol?
func sendToSecond() {
delegate?.passX(a: "slick")
}
}
class SecondContainerVC: UIViewController {
weak var delegate: SomeProtocol?
func getFromFirst(a: String) {
print(a)
}
}
This is a somewhat crude example. You should code your implementation how you feel most comfortable (i.e. gracefully unwrapping, how/where you instantiate, etc.). Also, if all of the view controllers are permanent view controllers (meaning that they are never deallocated), no need to make them weak var. However you do it, the concepts are all the same. The parent is the delegate for the containers, and the containers communicate with each other through the parent.
Some people may suggest using notification observers or singletons for the containers to communicate with each other, but I find that to be overkill when you have a parent right there.
I agree with #slickdaddy that your second container view controller has no business instantiating your first container view controller. I suspect you already had a first container VC and now you have 2.
To answer your question about the best way to pass the data, your container view controllers should know nothing about the parent view controller. Through either delegation or a callback that the parent registers, the data should go to the parent and the parent should then route it down to the other interested contained view controllers.
Keep knowledge of your hierarchy flowing "downwards". In other words, contained or owned VCs should not know anything about their owners or parents. It'll help with reuse, organization, etc..
Also consider another approach: passing the same model object into each contained view controller.
And finally, my preferred approach is letting each view controller (or actually its view model if doing MVVM) reach for a DependencyManager singleton where such model objects live. If done right, unit tests can still have full control by injecting mock models into the DependencyManager.
Say I have multiple view controllers in my Swift app and I want to be able to pass data between them. If I'm several levels down in a view controller stack, how do I pass data to another view controller? Or between tabs in a tab bar view controller?
(Note, this question is a "ringer".) It gets asked so much that I decided to write a tutorial on the subject. See my answer below.
Your question is very broad. To suggest there is one simple catch-all solution to every scenario is a little naïve. So, let's go through some of these scenarios.
The most common scenario asked about on Stack Overflow in my experience is the simple passing information from one view controller to the next.
If we're using storyboard, our first view controller can override prepareForSegue, which is exactly what it's there for. A UIStoryboardSegue object is passed in when this method is called, and it contains a reference to our destination view controller. Here, we can set the values we want to pass.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "MySegueID" {
if let destination = segue.destination as? SecondController {
destination.myInformation = self.myInformation
}
}
}
Alternatively, if we're not using storyboards, then we're loading our view controller from a nib. Our code is slightly simpler then.
func showNextController() {
let destination = SecondController(nibName: "SecondController", bundle: nil)
destination.myInformation = self.myInformation
show(destination, sender: self)
}
In both cases, myInformation is a property on each view controller holding whatever data needs to be passed from one view controller to the next. They obviously don't have to have the same name on each controller.
We might also want to share information between tabs in a UITabBarController.
In this case, it's actually potentially even simpler.
First, let's create a subclass of UITabBarController, and give it properties for whatever information we want to share between the various tabs:
class MyCustomTabController: UITabBarController {
var myInformation: [String: AnyObject]?
}
Now, if we're building our app from the storyboard, we simply change our tab bar controller's class from the default UITabBarController to MyCustomTabController. If we're not using a storyboard, we simply instantiate an instance of this custom class rather than the default UITabBarController class and add our view controller to this.
Now, all of our view controllers within the tab bar controller can access this property as such:
if let tbc = self.tabBarController as? MyCustomTabController {
// do something with tbc.myInformation
}
And by subclassing UINavigationController in the same way, we can take the same approach to share data across an entire navigation stack:
if let nc = self.navigationController as? MyCustomNavController {
// do something with nc.myInformation
}
There are several other scenarios. By no means does this answer cover all of them.
This question comes up all the time.
One suggestion is to create a data container singleton: An object that gets created once and only once in the life of your application, and persists for the life of your app.
This approach is well suited for a situation when you have global app data that needs to be available/modifiable across different classes in your app.
Other approaches like setting up one-way or 2-way links between view controllers are better suited to situations where you are passing information/messages directly between view controllers.
(See nhgrif's answer, below, for other alternatives.)
With a data container singleton, you add a property to your class that stores a reference to your singleton, and then use that property any time you need access.
You can set up your singleton so that it saves it's contents to disk so that your app state persists between launches.
I created a demo project on GitHub demonstrating how you can do this. Here is the link:
SwiftDataContainerSingleton project on GitHub
Here is the README from that project:
SwiftDataContainerSingleton
A demonstration of using a data container singleton to save application state and share it between objects.
The DataContainerSingleton class is the actual singleton.
It uses a static constant sharedDataContainer to save a reference to the singleton.
To access the singleton, use the syntax
DataContainerSingleton.sharedDataContainer
The sample project defines 3 properties in the data container:
var someString: String?
var someOtherString: String?
var someInt: Int?
To load the someInt property from the data container, you'd use code like this:
let theInt = DataContainerSingleton.sharedDataContainer.someInt
To save a value to someInt, you'd use the syntax:
DataContainerSingleton.sharedDataContainer.someInt = 3
The DataContainerSingleton's init method adds an observer for the UIApplicationDidEnterBackgroundNotification. That code looks like this:
goToBackgroundObserver = NSNotificationCenter.defaultCenter().addObserverForName(
UIApplicationDidEnterBackgroundNotification,
object: nil,
queue: nil)
{
(note: NSNotification!) -> Void in
let defaults = NSUserDefaults.standardUserDefaults()
//-----------------------------------------------------------------------------
//This code saves the singleton's properties to NSUserDefaults.
//edit this code to save your custom properties
defaults.setObject( self.someString, forKey: DefaultsKeys.someString)
defaults.setObject( self.someOtherString, forKey: DefaultsKeys.someOtherString)
defaults.setObject( self.someInt, forKey: DefaultsKeys.someInt)
//-----------------------------------------------------------------------------
//Tell NSUserDefaults to save to disk now.
defaults.synchronize()
}
In the observer code it saves the data container's properties to NSUserDefaults. You can also use NSCoding, Core Data, or various other methods for saving state data.
The DataContainerSingleton's init method also tries to load saved values for it's properties.
That portion of the init method looks like this:
let defaults = NSUserDefaults.standardUserDefaults()
//-----------------------------------------------------------------------------
//This code reads the singleton's properties from NSUserDefaults.
//edit this code to load your custom properties
someString = defaults.objectForKey(DefaultsKeys.someString) as! String?
someOtherString = defaults.objectForKey(DefaultsKeys.someOtherString) as! String?
someInt = defaults.objectForKey(DefaultsKeys.someInt) as! Int?
//-----------------------------------------------------------------------------
The keys for loading and saving values into NSUserDefaults are stored as string constants that are part of a struct DefaultsKeys, defined like this:
struct DefaultsKeys
{
static let someString = "someString"
static let someOtherString = "someOtherString"
static let someInt = "someInt"
}
You reference one of these constants like this:
DefaultsKeys.someInt
Using the data container singleton:
This sample application makes trival use of the data container singleton.
There are two view controllers. The first is a custom subclass of UIViewController ViewController, and the second one is a custom subclass of UIViewController SecondVC.
Both view controllers have a text field on them, and both load a value from the data container singlelton's someInt property into the text field in their viewWillAppear method, and both save the current value from the text field back into the `someInt' of the data container.
The code to load the value into the text field is in the viewWillAppear: method:
override func viewWillAppear(animated: Bool)
{
//Load the value "someInt" from our shared ata container singleton
let value = DataContainerSingleton.sharedDataContainer.someInt ?? 0
//Install the value into the text field.
textField.text = "\(value)"
}
The code to save the user-edited value back to the data container is in the view controllers' textFieldShouldEndEditing methods:
func textFieldShouldEndEditing(textField: UITextField) -> Bool
{
//Save the changed value back to our data container singleton
DataContainerSingleton.sharedDataContainer.someInt = textField.text!.toInt()
return true
}
You should load values into your user interface in viewWillAppear rather than viewDidLoad so that your UI updates each time the view controller is displayed.
Another alternative is to use the notification center (NSNotificationCenter) and post notifications. That is a very loose coupling. The sender of a notification doesn't need to know or care who's listening. It just posts a notification and forgets about it.
Notifications are good for one-to-many message passing, since there can be an arbitrary number of observers listening for a given message.
Swift 4
There are so many approaches for data passing in swift. Here I am adding some of the best approaches of it.
1) Using StoryBoard Segue
Storyboard segues are very much useful for passing data in between Source and Destination View Controllers and vice versa also.
// If you want to pass data from ViewControllerB to ViewControllerA while user tap on back button of ViewControllerB.
#IBAction func unWindSeague (_ sender : UIStoryboardSegue) {
if sender.source is ViewControllerB {
if let _ = sender.source as? ViewControllerB {
self.textLabel.text = "Came from B = B->A , B exited"
}
}
}
// If you want to send data from ViewControllerA to ViewControllerB
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.destination is ViewControllerB {
if let vc = segue.destination as? ViewControllerB {
vc.dataStr = "Comming from A View Controller"
}
}
}
2) Using Delegate Methods
ViewControllerD
//Make the Delegate protocol in Child View Controller (Make the protocol in Class from You want to Send Data)
protocol SendDataFromDelegate {
func sendData(data : String)
}
import UIKit
class ViewControllerD: UIViewController {
#IBOutlet weak var textLabelD: UILabel!
var delegate : SendDataFromDelegate? //Create Delegate Variable for Registering it to pass the data
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
textLabelD.text = "Child View Controller"
}
#IBAction func btnDismissTapped (_ sender : UIButton) {
textLabelD.text = "Data Sent Successfully to View Controller C using Delegate Approach"
self.delegate?.sendData(data:textLabelD.text! )
_ = self.dismiss(animated: true, completion:nil)
}
}
ViewControllerC
import UIKit
class ViewControllerC: UIViewController , SendDataFromDelegate {
#IBOutlet weak var textLabelC: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func btnPushToViewControllerDTapped( _ sender : UIButton) {
if let vcD = self.storyboard?.instantiateViewController(withIdentifier: "ViewControllerD") as? ViewControllerD {
vcD.delegate = self // Registring Delegate (When View Conteoller D gets Dismiss It can call sendData method
// vcD.textLabelD.text = "This is Data Passing by Referenceing View Controller D Text Label." //Data Passing Between View Controllers using Data Passing
self.present(vcD, animated: true, completion: nil)
}
}
//This Method will called when when viewcontrollerD will dismiss. (You can also say it is a implementation of Protocol Method)
func sendData(data: String) {
self.textLabelC.text = data
}
}
Instead of creating a data controller singelton I would suggest to create a data controller instance and pass it around. To support dependency injection I would first create a DataController protocol:
protocol DataController {
var someInt : Int {get set}
var someString : String {get set}
}
Then I would create a SpecificDataController (or whatever name would currently be appropriate) class:
class SpecificDataController : DataController {
var someInt : Int = 5
var someString : String = "Hello data"
}
The ViewController class should then have a field to hold the dataController. Notice that the type of dataController is the protocol DataController. This way it's easy to switch out data controller implementations:
class ViewController : UIViewController {
var dataController : DataController?
...
}
In AppDelegate we can set the viewController's dataController:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if let viewController = self.window?.rootViewController as? ViewController {
viewController.dataController = SpecificDataController()
}
return true
}
When we move to a different viewController we can pass the dataController on in:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
...
}
Now when we wish to switch out the data controller for a different task we can do this in the AppDelegate and do not have to change any other code that uses the data controller.
This is of course overkill if we simply want to pass around a single value. In this case it's best to go with nhgrif's answer.
With this approach we can separate view form the logic part.
As #nhgrif pointed out in his excellent answer, there are lots of different ways that VCs (view controllers) and other objects can communicate with each other.
The data singleton I outlined in my first answer is really more about sharing and saving global state than about communicating directly.
nhrif's answer lets you send information directly from the source to the destination VC. As I mentioned in reply, it's also possible to send messages back from the destination to the source.
In fact, you can set up an active one-way or 2-way channel between different view controllers. If the view controllers are linked via a storyboard segue, the time to set up the links is in the prepareFor Segue method.
I have a sample project on Github that uses a parent view controller to host 2 different table views as children. The child view controllers are linked using embed segues, and the parent view controller wires up 2-way links with each view controller in the prepareForSegue method.
You can find that project on github (link). I wrote it in Objective-C, however, and haven't converted it to Swift, so if you're not comfortable in Objective-C it might be a little hard to follow
SWIFT 3:
If you have a storyboard with identified segues use:
func prepare(for segue: UIStoryboardSegue, sender: Any?)
Although if you do everything programmatically including navigation between different UIViewControllers then use the method:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)
Note: to use the second way you need to make your UINavigationController, you are pushing UIViewControllers on, a delegate and it needs to conform to the protocol UINavigationControllerDelegate:
class MyNavigationController: UINavigationController, UINavigationControllerDelegate {
override func viewDidLoad() {
self.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
// do what ever you need before going to the next UIViewController or back
//this method will be always called when you are pushing or popping the ViewController
}
}
It depends when you want to get data.
If you want to get data whenever you want, can use a singleton pattern. The pattern class is active during the app runtime. Here is an example of the singleton pattern.
class AppSession: NSObject {
static let shared = SessionManager()
var username = "Duncan"
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print(AppSession.shared.username)
}
}
If you want to get data after any action, can use NotificationCenter.
extension Notification.Name {
static let loggedOut = Notification.Name("loggedOut")
}
#IBAction func logoutAction(_ sender: Any) {
NotificationCenter.default.post(name: .loggedOut, object: nil)
}
NotificationCenter.default.addObserver(forName: .loggedOut, object: nil, queue: OperationQueue.main) { (notify) in
print("User logged out")
}
The way that I would do it would be instead of passing data between view controllers, I would just declare a variable globally. You can even do this with a function!
For example:
var a = "a"
func abc() {
print("abc")
}
class ViewController: UIViewController {
}
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.