How reference `IBOutlet`s in subviews of `UIPageViewController` - swift

I read if you instantiate a class (UIViewController) directly, via an initializer, IBOutlets won't be connected. My problem is I'm referencing a nil IBOutlet, causing a crash.
I have a lot of observers that update views in vc1, vc2, and vc3 (the "pages" in UIPageViewController). To keep my code clean, I want to add all the observers in the UIPageViewController class and not in the viewDidLoad of vc1, vc2, & vc3.
However, I can't reference the IBOutlets of subviews from UIPageViewController because they come up nil. Does anyone have advice on how to set up UIPageViewController in a way I can access the subview's IBOutlets?
I have had trouble in other apps updating page views that are not visible on the screen, (ex: updating a view in vc3 while vc1 is on screen). Is UIPageViewController a bad idea for this sort of goal? Should I just make one large view that the user can pan across?
// UIPageViewController.swift
var vc1: ViewController1 = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return (storyboard.instantiateViewController(withIdentifier: "ViewController1") as? ViewController1)!
}()
var vc2: ViewController2 = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return (storyboard.instantiateViewController(withIdentifier: "ViewController2") as? ViewController2)!
}()
var vc3: ViewController3 = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return (storyboard.instantiateViewController(withIdentifier: "ViewController3") as? ViewController3)!
}()
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if viewController == vc2 {
return vc1
} else if viewController == vc3 {
return vc2
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if viewController == vc1 {
return vc2
} else if viewController == vc2 {
return vc3
}
return nil
}
func viewDidLayoutSubviews() {
// Adding observer for ViewController1
NotificationCenter.default.addObserver(self, selector: #selector(vc1.handleEventDataChange), name: .eventDataChanged, object: nil)
}
-
// ViewController1.swift
#IBOutlet weak var scoreBoard: UIView!
func viewDidLoad() {
super.viewDidLoad()
}
func handleEventDataChange() {
if scoreBoard.alpha != 0 { // <-- Crash here
// update scoreboard
}
}
EDIT:
In addition to JRTurton's answer, I also discovered that I was adding observers in the wrong class.
NotificationCenter.default.addObserver(self, selector: #selector(vc1.handleEventDataChange), name: .eventDataChanged, object: nil)
should be
NotificationCenter.default.addObserver(vc1, selector: #selector(vc1.handleEventDataChange), name: .eventDataChanged, object: nil)

You’re loading your view controllers from the storyboards, so the outlets will be getting connected (assuming it’s all wired up properly in the storyboard).
The most likely cause of the issue is that the methods are being called before the view controllers have loaded their view so they haven’t had a chance to connect anything yet.
Externally called functions that have side effects on outlets should probably begin with a check of isViewLoaded to prevent this issue. You should ideally be configuring model objects to pass to your view controller, which the view controller can either act on immediately (if the view is loaded) or use for configuration at viewDidLoad
(Also, view did layout sub views is not a great place to be adding observers - that can get called a lot)

Related

Swift - resetting vc in nav controller that is embedded in tab bar

I have a customerViewController that has a simple a form. When the user presses submit, a segue is triggered and another view appears. The problem occurs when the user goes back to the customerViewController and finds all of the old information still there. I could simply reset the form fields, but what I'd really like is to find a way to reset the entire VC. From what I've learned so far, the way to reset a vc that hasn't been pushed is to remove it and then add it back.
customerViewController is the initial view controller in a navigation controller which is embedded in a tab bar controller. I have a tabBarController class that is a UITabBarControllerDelegate. This is where I call:
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
if item.tag == 2 { //This is the tab with my navigation controller
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "CustomerVCID")
var viewcontrollers = self.navigationController?.viewControllers
viewcontrollers?.removeFirst()
viewControllers?.insert(vc, at: 0)
self.navigationController?.setViewControllers(viewcontrollers!, animated: true)
}
The problem with my code is that navigationController?.viewControllers is nil in the code above. I can reference viewControllers which gives me a list of tab bar viewControllers, but I'm not sure how to get from there to the navigation controller.
I guess my question is, assuming that I'm on the right track, how do I reference the view controllers in my navigation controller?
You can reset your form values in vc inside the viewWillAppear(_:),
class ViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
//clear the textfields, textviews values etc. here.
}
}
It turns out, I was over-complicating things by trying to access navigationController.viewControllers or tabBarController.viewControllers. All I needed was viewControllers which is a property of UITabBarController that contains an array of the controllers associated with each tab:
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
if item.tag == 2 { //tab with navigation controller
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vcon = storyboard.instantiateViewController(withIdentifier: "CustomerVCID")
for viewcontroller in viewControllers! {
if let vc = viewcontroller as? UINavigationController {
vc.viewControllers.removeFirst()
vc.viewControllers.insert(vcon, at: 0)
vc.setViewControllers(vc.viewControllers, animated: true)
}
}
}

Delegate method is not being called

I have a splitview controller and the Master view has a button (Note) that pushes the Note view onto the Detail view. The Detail view makes used of navigation controller to help users navigate back and forth among multiple view controllers. Inside those view controllers, I have delegate methods that pushes Note view onto itself. This is what my UI look like :
The Note button works as I expected when the app is initially run. It still works when I tap one of the list elments and traverse to the views at the deeper level. However, it stops working when I go back to the very first view (which was working initially). I'm not sure what is causing this inconsistent behaviour and I appreciate much if you guys could help me figure this out.
This is excerpt of my code :
Master View
protocol ChildViewDelegate: class {
func updateView()
func pushOntoDetailViewNaviController(_ viewName: String)
}
class MasterViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate, GADBannerViewDelegate {
....
#IBAction func noteTapped(_ sender: UIBarButtonItem) {
pushOntoActiveNaviController("NoteGalleryView")
}
private func pushOntoActiveNaviController(_ viewName: String) {
guard let splitView = self.splitViewController else {
return
}
if splitView.viewControllers.count > 1 {
// Push view onto any active detailed view
self.delegate?.pushOntoDetailViewNaviController(viewName)
} else { //If active view is the master view (true for iphone), then push it onto the master view
if let vc = storyboard?.instantiateViewController(withIdentifier: viewName) {
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
....
}
Detail View
class DetailTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, GADBannerViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
masterViewController = (self.splitViewController?.viewControllers.first as! UINavigationController).topViewController as? MasterViewController
masterViewController?.delegate = self
}
extension DetailTableViewController: ChildViewDelegate {
func updateUI() {
....
}
func updateView() {
DispatchQueue.main.async {
self.tableView.reloadData()
self.updateUI()
}
}
func pushOntoDetailViewNaviController(_ viewName: String) {
if let vc = storyboard?.instantiateViewController(withIdentifier: viewName) {
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
In case somebody has the same issue, it's because I'm setting delegate variable inside viewDidLoad which is invoked when view is initially loaded. It isn't invoked when view appears again when user navigates back to the view from deeper view controllers. The delegate variable should have been set inside viewWillAppear.

Push navigation from xib Files to storyboard viewcontroller in swift3 [duplicate]

I have a view in my xib file which contain buttons. i want to move to a ViewController when i will press the button (#IBAction). I have used below code
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let nextViewController = storyBoard.instantiateViewControllerWithIdentifier("About") as! AboutViewController
self.presentViewController(nextViewController, animated: true, completion: nil)
I am getting the error "Value of type 'SlideMenuView' has no member 'presentViewController'.
because my class is a UIView type :
class SlideMenuView: UIView {
}
so how can I navigate to other view controller.
That is beacuase the class you are trying to present from is a UIView and not a UIViewController. It has no Present method.
I'm guessing your view (SlideMenuView) is embedded inside a viewcontroller. what you need to do is implement a delegate, and inform your containing viewController to present next Viewcontroller.
code below:
#protocol SlideMenuViewDelegate: class {
func slideMenuViewAboutButtonClicked(menuView: SlideMenuView)
class SlideMenuView: UIView {
weak var delegate: SlideMenuViewDelegate?
#IBAction func aboutButtonClicked(sender: AnyObject) {
self.delegate?.slideMenuViewAboutButtonClicked(self)
}
now, in your viewController, implement this delegate method:
func slideMenuViewAboutButtonClicked(menuView: SlideMenuView) {
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let nextViewController = storyBoard.instantiateViewControllerWithIdentifier("About") as! AboutViewController
self.presentViewController(nextViewController, animated: true, completion: nil)
}
Also, dont forget to assign the sliderMenuView object the viewcontroller as a delegate.
something like:
self.sliderMenuView.delegate = self // (self == the containing viewController
I did it in a different way. In class file
class SlideMenuView: UIView {
var navigationController: UINavigationController? // Declare a navigation controller variable
// And create a method which take a navigation controller
func prepareScreen(navController: UINavigationController)-> UIView {
navigationController = navController
let nibView = NSBundle.mainBundle().loadNibNamed("SlideMenuView", owner: self, options: nil)[0] as! UIView
self.addSubview(nibView)
return nibView
}
// In Button action
#IBAction func btnAction(sender: UIButton) {
var storyBoard = UIStoryboard(name: "Main", bundle: nil)
let nextViewController = storyBoard!.instantiateViewControllerWithIdentifier("NextViewController") as! UIViewController
navigationController?.pushViewController(nextViewController, animated: true)
}
}
// For calling from UIViewController
slideBarMenuIstance.prepareScreen(self.navigationController!)

Why isn't my PageViewController's DataSource working correctly?

I have created a simple storyboard with a PageView Controller.
I will flip between two other View Controllers.
My PageView Controller is a custom class TutorialPageViewController. I've also created a custom DataSource class.
In the DataSource class I expect the pageViewController methods to be called when I attempt to scroll. However this is not the case. I have break points at both methods and they are never called.
The first view controller, "Page the first" comes up correctly, but attempting to scroll around doesn't call the methods, so I can't use them yet (hence they return nil for now).
If I set the DataSource of my view controller to self and put the methods in there, they are called correctly. But I want to move the methods out to a separate class for better code management. So why doesn't it work?
I've tried
Setting my DataSource class to be a UIScrollViewDelegate as well as a UIPageViewControllerDelegate and setting the view controller's delegate to be the DataSource
The PageView's transition style is Scroll
class TutorialPageViewController : UIPageViewController {
override func viewDidLoad() {
reset()
}
func reset() {
let dataSource = TutorialPageDataSource(storyBoard: storyboard!)
let content = dataSource.firstContentViewController
self.dataSource = dataSource
self.setViewControllers([content], direction: .forward, animated: true, completion: nil)
}
}
class TutorialPageDataSource : NSObject, UIPageViewControllerDataSource {
private var _storyboard: UIStoryboard
var firstContentViewController: UIViewController
var secondContentViewController: UIViewController
init(storyBoard: UIStoryboard) {
_storyboard = storyBoard
firstContentViewController = _storyboard.instantiateViewController(withIdentifier: "FirstContentViewController")
secondContentViewController = _storyboard.instantiateViewController(withIdentifier: "FirstContentViewController")
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
// break point here never reached
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
// break point here never reached
return nil
}
}
UIPageViewController dataSource (and delegate) are weak.
You create your TutorialPageDataSource instance in the reset method, assign it to the weak dataSource, and then the TutorialPageDataSource instance goes out of scope and gets deallocated because there is no strong reference to it any more. So now the page view controller's dataSource becomes nil.
You need to keep a strong reference to the TutorialPageDataSource instance. Use an instance variable to keep the reference.

ios swift method to navigate through viewcontroller programatically

i would like create my function to navigate through my views.
i have a function
func ShowView(name:String,caller:AppDelegate){
//story board
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let initViewController: UIViewController = storyBoard.instantiateViewControllerWithIdentifier(name) as UIViewController
caller.UIWindow?.rootViewController = initViewController
}
but i need to call this function from AppDelegate and UIViewController , how can i get uiwindow from UIViewController , from AppDelegate is ok but i can't keep AppDelegate like caller parameter cause i would like that it work with UIViewController.
I don't really know which i must use for my parameter.
many thanks
You don't need to replace rootView Controller every time if you are moving from master to details views. Connect root view to details view using segue. Set a identifier to segue in your storyboard. Overide prepareForSegue in your viewController class.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if(segue.identifier == "showDetails")
{
var viewController=segue.destinationViewController as DetailsView
//set data
}
}