Pushing Data to Another ViewController Without Moving ViewControllers - ios

I am relatively new to coding in Swift. I am trying to pass data back to another view controller without having to segue to that controller so that the user can stay on the same screen. I am looking into using a delegate and wondering if this will be the best way to access the variable on multiple view controllers. Thank you
View controller 1:
func passVariables() {
let passSleepValue = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "sbHome") as! sbHomeController
passSleepValue.getSleepValue = sleepValue
//navigationController?.pushViewController(passSleepValue, animated: true)
}
View Controller 2:
func updateDay() {
let dayScoreAvg = getSleepValue
dayScore.text = String(format: "%.1f", dayScoreAvg)
print("Get sleep Value is \(getSleepValue)")
}

It looks like you can already pass data to second ViewController but you can't pass some data back.
So one way is using delegate pattern.
Start with declaring delegate protocol with method for passing data (in your case it can be just passing String)
protocol SecondVCDelegate: class {
func passText(_ text: String)
}
then create delegate variable in second ViewController
class SecondViewController: UIViewController {
weak var delegate: SecondVCDelegate?
...
}
Now you can implement this protocol to first ViewController
extension ViewController: SecondVCDelegate {
func passText(_text: String) {
// do something with text passed from second VC
}
}
now set delegate of Second VC as self before you push it
func passVariables() {
let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "sbHome") as! sbHomeController
viewController.getSleepValue = sleepValue
viewController.delegate = self
navigationController?.pushViewController(viewController, animated: true)
}
then in second VC you can just call method on your delegate and in first VC this method gets called
delegate?.passText("Hello First!")

Related

How can i pop back from storyboard of framework?

let storyboardBundle = Bundle(identifier: "SignUPFramework")
let vc = UIStoryboard.init(name: "SignIn", bundle: storyboardBundle).instantiateViewController(withIdentifier: "SignInViewcontroller") as! SignInViewcontroller
navigationController?.pushViewController(vc, animated: true)
I have navigated to storyboard of framework as above.
You own the navigationController. So you can use either popViewController(animated:) (if you are sure that there is only one view controller you wish to pop), or popToViewController(_:animated:), or popToRootViewController(animated:) (if you wish to return to the root).
It does not matter that the storyboard is in a framework. You still own the view hierarchy, the navigation stack and the view controller instance you get from the below line of code
let vc = UIStoryboard.init(name: "SignIn", bundle: storyboardBundle).instantiateViewController(withIdentifier: "SignInViewcontroller") as! SignInViewcontroller
So you can do whatever you want with it in terms of navigation (push it and pop it or present it and dismiss it).
You just need to identify at what point you want to trigger any of these actions.
I think you are using xib so it will not return action first you should do protocol for callback to view controller then you can use popViewController(animated:).
protocol CallBackDelegate: AnyObject {
func back()
}
//Delegate
weak var delegate:CallBackDelegate?
#IBAction func buttonAction(_ sender: Any) {
delegate?.back()
}
Now you can use protocol in view controller
class ViewController: UIViewController, CallBackDelegate {
func back() {
navigationController?.popViewController(animated: true)
}
}
Note :- Confirm delegate variable in cellForRowAtIndexPath i.e.
cell.delegate = self
I have solved it by
let bundle = Bundle(for: SignInViewcontroller.self)
let vc = UIStoryboard.init(name: "SignIn", bundle: bundle).instantiateViewController(withIdentifier: "SignInViewcontroller") as! SignInViewcontroller
in above snippet instead of passing bundle id i have passed viewcontroller itself.

When trigged action, mockNavigationController return current viewController, not pushed

I learning TTD, and have problem with navigation controller in Unit-testing.
When I'm trying to push Detail View Controller through navigation stack (pushViewController(ViewController, animated:) ) with my mock controller, in test push function not perform (its perform only first time, when navigationController initialise).
On simulation iPhone, app work correctly.
In code mockNavigationController have value pushedVC, that changes when pushViewController perform.
When user tap on cell, dataProvider (delegate and dataSource for tableCell) post notification to ViewController (sut), that implements showDetails method.
I'd try take topViewController from navigationController:
sut.navigationController?.topViewController - it's return sut ViewController.
Try not initialise navigationController in test. sut.navigationController?.topViewController - it's return nil.
This beginning of XCTestCase
var sut: EatersListViewController!
override func setUp() {
super.setUp()
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyBoard.instantiateViewController(withIdentifier: String(describing: EatersListViewController.self))
sut = vc as? EatersListViewController
sut.loadViewIfNeeded()
}
This test function
func testSelectedRowPushedDetailVC() {
let mockNavigationController = MockNavigationController(rootViewController: sut)
UIApplication.shared.keyWindow?.rootViewController = mockNavigationController
let eater1 = Eater(name: "Foo")
sut.dataProvider.manager!.addEater(eater: eater1)
sut.loadViewIfNeeded()
sut.tableView.delegate?.tableView?(sut.tableView, didSelectRowAt: IndexPath(row: 0, section: 0))
guard let detailEaterVC = mockNavigationController.pushedVC as? DetailEaterViewController else {
XCTFail()
return
}
detailEaterVC.loadViewIfNeeded()
XCTAssertNotNil(detailEaterVC.eaterNameLabel)
XCTAssertEqual(detailEaterVC.eaterData, eater1)
}
This function from ViewController
#objc func showDetails(withNotification notification: Notification) {
guard
let userInfo = notification.userInfo,
let eater = userInfo["eater"] as? Eater,
let detailEaterVC = storyboard?.instantiateViewController(withIdentifier: String(describing: DetailEaterViewController.self)) as? DetailEaterViewController else { return }
detailEaterVC.eaterData = eater
navigationController?.pushViewController(detailEaterVC, animated: true)
}
And MockNavigationController
extension EatersListViewControllerTests {
class MockNavigationController: UINavigationController {
var pushedVC: UIViewController?
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
pushedVC = viewController
super.pushViewController(viewController, animated: animated)
}
}
}
I'd expected that XCTAssert work correct, but every time test failed on XCTFail() line. I think somewhere error, and don't know here.
XCTAssertNotNil(detailEaterVC.eaterNameLabel)
XCTAssertEqual(detailEaterVC.eaterData, eater1)
Need help with code, where I wrong. Thanks for reading.
Hey #Alexander, welcome to StackOverflow. 👋
You say that dataProvider is the .dataSource and .delegate for the UITableView in your view controller under test, and that it's the one responsible for starting the navigation.
Are you sure that in the tests dataProvider is actually set as the .dataSource and .delegate? If that's not the case then the code starting the navigation will never be called.
You could use breakpoints to verify two things:
If your showDetails method is called
If the pushViewController(_:, animated:) method in your MockNavigationController is called
I'm guessing one of them isn't called, and that might point you towards the cause of your issue.
If you allow me, a few extra words:
I would recommend using the NavigationDelegate pattern to test this behaviour. It would make it simpler for you, by removing the need to fiddle with UIApplication.
It's better to use _ = sut.view or sut.beginAppearanceTransition(true, animated: false) to trigger the setup of the view controller's view in the tests

My delegate is nil when trying to remove child from parent IOS swift 3

I have a child view which takes over the entire parent view. I want the child to be removed from the parent by call a method from the parent using a protocol but my delegate is nil every time. I add my sample work below if you need more information just let me know
protocol childDelegate: class {
func removeFromParent()
}
class ChildAction: UITableViewController {
#IBAction func btnRemove(_sender: Any){
if(delegate != nil){
self.delegate?.removeFromParent()
}
}
}
class ParentClass: UIViewController:childDelegate {
var delegate:childDelegate?
private ChildActionView:ChildAction{
let storyboard = UIStoryBoard(name: "Main", bundle: Bundle.main)
let viewC = storyboard.instantiateViewController(withIdentifier: "ChildAction") as! ChildAction
self.delegate = viewC as? childDelegate
self.addChildViewController(viewC)
return viewC
}
#IBAction func btnAddChild(_sender: Any){
addChild(child:ChildActionView)
}
func addChild(child: UIViewController){
self.addChildViewController(child)
view.addSubview(child.view)
child.didMove(toParentViewController: self)
}
func removeFromParent(){
//remove from parent
}
}
Child view controller
parent view controller image
No where in your code did you set the delegate variable of your ChildAction instance (your childVC). You set parent's delegate to child but that is meaningless in your code. You need to set child's delegate to parent. Currently child do not have any reference of its parent, so naturally its delegate is nil. Let me know if something is unclear.
I strongly recommend you read an article or tutorial or two about the Delegate pattern. Things will be much easier if you have a basic understanding of how it is supposed to work.
Compare this code to what you posted. See if it makes sense.
protocol childDelegate: class {
func removeFromParent()
}
class ChildAction: UIViewController {
// this class gets the delegate, because it wants to "call back" to a delegated function
var delegate:childDelegate?
#IBAction func btnRemove(_sender: Any){
// IF delegate has been assigned, call its function
delegate?.removeFromParent()
}
}
class ParentClass: UIViewController, childDelegate {
// bad naming... would be much clearer if it was "childActionViewController" or "childActionVC"
private let ChildActionView: ChildAction = {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let viewC = storyboard.instantiateViewController(withIdentifier: "ChildAction") as! ChildAction
return viewC
}()
#IBAction func btnAddChild(_sender: Any){
// add the child view controller
self.addChildViewController(ChildActionView)
// set its delegate to self
ChildActionView.delegate = self
// add its view to the hierarchy
view.addSubview(ChildActionView.view)
// finish the process
ChildActionView.didMove(toParentViewController: self)
}
func removeFromParent() {
//remove from parent
print("removeFromParent() called...")
}
}

Accessing elements from another view controller comes up nil

I have an alert view that i made using presentr from GitHub. I used a simple view controller that will overlay over the current view controller. Now i have elements such as a UIImage and a UIlabel from the first view controller that needs to be accessed by the alert view controller. But when I click a unbutton in the alert view controller to access the uiimage and text from uilabel from the firstviewcontroller. Here is the code. Can you show me how I can fix this. I can't segue the data because I'm presenting the view controller and the data I'm trying to access is too much too segue anyway. I keep getting this error "fatal error: unexpectedly found nil while unwrapping an Optional value" overtime i click the save button in alertviewcontroller.
class firstviewcontroller: UIViewController{
var photos2: [ImageSource]?
#IBOutlet weak var Label: UIlabel!
#IBOutlet weak var Image: UIImage!
}
class alertviewcontroller: UIViewController{
#IBAction func Save(_ sender: Any) {
dismiss(animated: true, completion: nil)
let storyboard: UIStoryboard = UIStoryboard.init(name: "Main", bundle: nil)
let firstViewController: firstviewcontroller = storyboard.instantiateViewController(withIdentifier: "firstviewcontroller") as! firstviewcontroller
if let image = firstViewController.Image {
imageview.image = image
}
if let label = firstViewController.Label {
label.text = Label.text
}
}
}
The biggest mistake you're making is that you are creating a new instance of firstviewcontroller instead of just accessing the current one.
class alertviewcontroller: UIViewController{
#IBAction func Save(_ sender: Any) {
dismiss(animated: true, completion: nil)
let storyboard: UIStoryboard = UIStoryboard.init(name: "Main", bundle: nil)
let firstViewController: firstviewcontroller = storyboard.instantiateViewController(withIdentifier: "firstviewcontroller") as! firstviewcontroller // This is the mistake
if let image = firstViewController.Image {
imageview.image = image
}
if let label = firstViewController.Label {
label.text = Label.text
}
}
}
What you should do instead is access the presentingViewController since you presented the alertviewcontroller using the present function
The code would look like this
class alertviewcontroller: UIViewController{
#IBAction func Save(_ sender: Any) {
dismiss(animated: true, completion: nil)
let storyboard: UIStoryboard = UIStoryboard.init(name: "Main", bundle: nil)
// NOTE: only do implicit unwrapping `as!` if you're sure that the value you're unwrapping is not `nil` or it is of the correct `data type` cause it might cause your app to crash
let firstViewController: firstviewcontroller = self.presentingViewController as! firstviewcontroller
if let image = firstViewController.Image {
imageview.image = image
}
if let label = firstViewController.Label {
label.text = Label.text
}
}
}
Tip: Please review Swift coding guidelines since there are some minor mistakes regarding your naming of methods, variables, and classes.
Might I am wrong but after review your code I have found this messing statement
let firstViewController: firstviewcontroller = storyboard.instantiateViewController(withIdentifier: "firstviewcontroller") as! firstviewcontroller
In this statement you are creating a new instance of firstviewcontroller not accessing already created instance of firstviewcontroller.
Next, you said
I used a simple view controller that will overlay over the current
view controller
So I have assumed you didn't present or push alertviewcontroller on current viewController, on that behave I suggest you the following solutions
In alertviewcontroller get the instance of topViewController by using this method
class func topViewController(base: UIViewController? = (UIApplication.shared.delegate as! AppDelegate).window?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
then you can transfer your data from alertviewcontroller to topViewController.
Next solution is that you can pass values from alertviewcontroller to firstviewcontroller by using delegates.

Swift - Run method from another class

I was wondering how I could run a method from another class. For example, something like the following code:
let newVC: ScoreViewController = ScoreViewController()
newVC.makeScore()
The above code won't work for me because in makeScore(), I am changing a label's text, which I can't do with the above code because it is creating a new instance. Is there a way to call a method to be run without creating a new instance so that I can change a label's text in makeScore()?
EDIT:
How ScoreViewController is added to PageViewController:
let vc = storyboard.instantiateViewControllerWithIdentifier("Score") as! ScoreViewController
self.addChildViewController(vc)
self.scrollView.addSubview(vc.view)
I am assuming that you've some method in your FirstViewController where you're changing the score and showing it in your ScoreViewController. The delegation pattern is the possible solution for this problem. In your FirstViewController create a protocol for updating score such as:
protocol FirstVCScoreDelegate:NSObjectProtocol {
func makeScore()
}
Then inside your FirstViewController create a var for this delegate:
var delegate: FirstVCScoreDelegate
Then in your PageViewController, where you are creating the instances of the FirstViewController and ScoreViewController, set the delegate of the FirstViewController to ScoreViewController:
var firstVC: FirstViewController()
var scoreVC: ScoreViewController()
firstVC.delegate = scoreVC
And after this, in your method in the FirstViewController where the score is changing:
#IBAction func scoreChangeAction(sender: AnyObject) {
if delegate.respondsToSelector(Selector("makeScore")) {
delegate.makeScore()
}
}
This will signal the ScoreViewController to update the score. You now have to implement the delegate method inside ScoreViewController:
extension ScoreViewController: ScoreDelegate {
func makeScore() {
//update your label
}
}
I believe this will solve your problem.
UPDATE
Try this in your PageViewController's viewDidLoad: method:
override func viewDidLoad() {
super.viewDidLoad()
let mainStoryboard = UIStoryboard(name: "MainStoryboard", bundle: NSBundle.mainBundle())
let firstVC : FirstViewController = mainStoryboard.instantiateViewControllerWithIdentifier("firstVC") as FirstViewController
let scoreVC : ScoreViewController = mainStoryboard.instantiateViewControllerWithIdentifier("scoreVC") as ScoreViewController
firstVC.delegate = scoreVC
self.addChildViewController(firstVC)
self.addChildViewController(scoreVC)
self.scrollView.addSubview(firstVC.view)
self.scrollView.addSubview(firstVC.view)
}
In the PageViewController, declare a property:
class PageViewController {
var scoreViewController:ScoreViewController
// ....
}
After initializing the ScoreViewController:
let vc = storyboard.instantiateViewControllerWithIdentifier("Score") as! ScoreViewController
Hold it as a property:
self.scoreViewController = vc
Then use
self.scoreViewController.makeScore()

Resources