Not able to transfer information between rootVC and modally presented otherVC? - swift

I want to use information from one rootVC in a modally presented otherVC. Here is the setup for it:
protocol OtherVCDelegate {
func didHitOK()
func didHitCancel()
}
class ViewController: UIViewController, OtherVCDelegate {
func didHitCancel() {
//do a function
}
func didHitOK() {
//do another function
}
var stringy = "Hello"
#IBAction func ButtonAction(_ sender: Any) {
let otherVC = self.storyboard?.instantiateViewController(withIdentifier: "AlertVC") as! AlertVC
otherVC.modalPresentationStyle = .overCurrentContext
otherVC.delegate = self
otherVC.label.text = stringy //THIS is where my question focuses
self.present(otherVC, animated: true, completion: nil)//presents the other VC modally
}
The otherVC has a UILabel named "label". The issue I run into, though, when running the ButtonAction function is that xcode finds a fatal error because it unexpectedly found nil when unwrapping an optional value. I am a bit confused as to why this occurs, because putting in a print statement within ButtonAction confirms that stringy is not nil. The label in the otherVC is 'set up' correctly so I am not sure what is giving the nil value.

I don't think your label is available until you present your view controller. Pass the string instead and set your label text in your AlertVC viewDidLoad method.
in your AlertVC declare a string:
var stringy:String?
then change your code
#IBAction func ButtonAction(_ sender: Any) {
let otherVC = self.storyboard?.instantiateViewController(withIdentifier: "AlertVC") as! AlertVC
otherVC.modalPresentationStyle = .overCurrentContext
otherVC.delegate = self
otherVC.stringy = stringy //you pass the string instead of setting the label text
self.present(otherVC, animated: true, completion: nil)//presents the other VC modally
}
at this point you can set the text label in the viewDidLoad:
self.label.text = self.stringy

Related

Testing tableview.reloadData()

while using a MockTableView this code still not calling reloadData() from the mock,
please i wanna know what is wrong here.
following this book: Test-Driven IOS Development with Swift 4 - Third Edition
page 164, i was as an exercise
full code repo - on github
ItemListViewController.swift
import UIKit
class ItemListViewController: UIViewController, ItemManagerSettable {
#IBOutlet var tableView: UITableView!
#IBOutlet var dataProvider: (UITableViewDataSource & UITableViewDelegate &
ItemManagerSettable)!
var itemManager: ItemManager?
override func viewDidLoad() {
super.viewDidLoad()
itemManager = ItemManager()
dataProvider.itemManager = itemManager
tableView.dataSource = dataProvider
tableView.delegate = dataProvider
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
}
#IBAction func addItem(_ sender: UIBarButtonItem) {
if let nextViewController =
storyboard?.instantiateViewController(
withIdentifier: "InputViewController")
as? InputViewController {
nextViewController.itemManager = itemManager
present(nextViewController, animated: true, completion: nil)
}
}
}
ItemListViewControllerTest.swift
import XCTest
#testable import ToDo
class ItemListViewControllerTest: XCTestCase {
var sut: ItemListViewController!
var addButton: UIBarButtonItem!
var action: Selector!
override func setUpWithError() throws {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier:
"ItemListViewController")
sut = vc as? ItemListViewController
addButton = sut.navigationItem.rightBarButtonItem
action = addButton.action
UIApplication.shared.keyWindow?.rootViewController = sut
sut.loadViewIfNeeded()
}
override func tearDownWithError() throws {}
func testItemListVC_ReloadTableViewWhenAddNewTodoItem() {
let mockTableView = MocktableView()
sut.tableView = mockTableView
guard let addButton = sut.navigationItem.rightBarButtonItem else{
XCTFail()
return
}
guard let action = addButton.action else{
XCTFail()
return
}
sut.performSelector(onMainThread: action, with: addButton, waitUntilDone: true)
guard let inputViewController = sut.presentedViewController as?
InputViewController else{
XCTFail()
return
}
inputViewController.titleTextField.text = "Test Title"
inputViewController.save()
XCTAssertTrue(mockTableView.calledReloadData)
}
}
extension ItemListViewControllerTest{
class MocktableView: UITableView{
var calledReloadData: Bool = false
override func reloadData() {
calledReloadData = true
super.reloadData()
}
}
}
You inject a MockTableview Then you call loadViewIfNeeded(). But because this view controller is storyboard-based and the table view is an outlet, the actual table view is loaded at this time. This replaces your MockTableview.
One solution is:
Call loadViewIfNeeded() first
Inject the MockTableview to replace the actual table view
Call viewDidLoad() directly. Even though loadViewIfNeeded() already called it, we need to repeat it now that we have a different tableview in place.
Another possible solution:
Avoid MockTableview completely. Continue to use a real table view. You can test whether it reloads data by checking whether the number of rows matches the changed data.
Yet another solution:
Avoid storyboards. You can do this with plain XIBs (but these lack table view prototype cells) or programmatically.
By the way, I see all your tearDownWithError() implementations are empty. Be sure to tear down everything you set up. Otherwise you will end up with multiple instances of your system under test alive at the same time. I explain there here: https://qualitycoding.org/xctestcase-teardown/

Having trouble with passing data from one ViewController to the next

I'm learning swift from couple months and one thing I keep screwing up is passing data between ViewControllers. When I PO the path of the forward looking variable from within the prepareForSegue method, the value is intact. But when the new ViewController actually appears and I checked its value, it is nil at that point. if anyone could point me in the right direction I'd be very appreciative.
class LoginViewController: UIViewController {
var user_ID:String = ""
//this below is within another method activated by button
Auth.auth().signIn(withEmail: email, password: password) { (result, error) in
if error != nil {
print ("there was an error signing in")
print (error!.localizedDescription)
return
}
else {
//go to home screen
let userUID = result?.user.uid
print (userUID)
self.user_ID = userUID
self.performSegue(withIdentifier: "MainSegue", sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
//this is the destination VC
let viewController = segue.destination as! ViewController
viewController.user_ID = self.user_ID
//NOTE: if I break here and PO viewConroller.user_ID the value is intact
let homeViewController = (self.storyboard?.instantiateViewController(withIdentifier: "MainVC"))! as! ViewController
//let mainViewController = ViewController()
//mainViewController.user_ID = userUID
self.present(homeViewController, animated: true, completion: nil)
}
class ViewController: UIViewController {
var persons = [Person]()
let db = Firestore.firestore()
var user_ID:String = ""
//NOTE: WHEN `viewdidload` runs value of user_ID is nil
Phillip in the comments above provided the solution. The present ViewController was the problem. The segue already does that. Thank you!!!

Why delegate event is not received swift?

I would like to pass data from EditPostViewController to NewsfeedTableViewController using delegates, but func remove(mediaItem:_) is never called in the adopting class NewsfeedTableViewController. What am I doing wrong?
NewsfeedTableViewController: UITableViewController, EditPostViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
//set ourselves as the delegate
let editPostVC = storyboard?.instantiateViewController(withIdentifier: "EditPostViewController") as! EditPostViewController
editPostVC.delegate = self
}
//remove the row so that we can load a new one with the updated data
func remove(mediaItem: Media) {
print("media is received heeeee")
// it does't print anything
}
}
extension NewsfeedTableViewController {
//when edit button is touched, send the corresponding Media to EditPostViewController
func editPost(cell: MediaTableViewCell) {
let editPostVC = storyboard?.instantiateViewController(withIdentifier: "EditPostViewController") as? EditPostViewController
guard let indexPath = tableView.indexPath(for: cell) else {
print("indexpath was not received")
return}
editPostVC?.currentUser = currentUser
editPostVC?.mediaReceived = cell.mediaObject
self.navigationController?.pushViewController(editPostVC!, animated: true)
}
protocol EditPostViewControllerDelegate: class {
func remove(mediaItem: Media)
}
class EditPostViewController: UITableViewController {
weak var delegate: EditPostViewControllerDelegate?
#IBAction func uploadDidTap(_ sender: Any) {
let mediaReceived = Media()
delegate?.remove(mediaItem: mediaReceived)
}
}
The objects instantiating in viewDidLoad(:) and on edit button click event are not the same objects. Make a variable
var editPostVC: EditPostViewController?
instantiate in in viewDidLoad(:) with delegate
editPostVC = storyboard?.instantiateViewController(withIdentifier: "EditPostViewController") as! EditPostViewController
editPostVC.delegate = self
and then present it on click event
navigationController?.pushViewController(editPostVC, animated: true)
or
present(editPostVC, animated: true, completion: nil)
you can pass data from presenter to presented VC before or after presenting the VC.
editPostVC.data = self.data
I suggest having a property in NewsfeedTableViewController
var editPostViewController: EditPostViewController?
and then assigning to that when you instantiate the EditPostViewController.
The idea is that it stops the class being autoreleased when NewsfeedTableViewController.viewDidLoad returns.

Return control to function when modal viewcontroller dismissed

I need to present a modal VC that sets a property in my presenting VC, and then I need to do something with that value back in the presenting VC. I have to be able to pass pointers to different properties to this function, so that it's reusable. I have the code below (KeyPickerTableViewController is the modal VC).
It should work, except not, because the line after present(picker... gets executed immediately after the picker is presented.
How do I get my presenting VC to "wait" until the modal VC is dismissed?
#objc func fromKeyTapped(_ button: UIBarButtonItem) {
print("from tapped")
setKey(for: &sourceKey, presentingFrom: button)
}
#objc func toKeyTapped(_ button: UIBarButtonItem) {
print("from tapped")
setKey(for: &destKey, presentingFrom: button)
}
fileprivate func setKey(for key: inout Key!, presentingFrom buttonItem: UIBarButtonItem) {
let picker = KeyPickerTableViewController()
picker.delegate = self
picker.modalPresentationStyle = .popover
picker.popoverPresentationController?.barButtonItem = buttonItem
present(picker, animated: true, completion: nil)
if let delKey = delegatedKey {
key = delKey
}
}
You could use delegate pattern or closure.
I would do the following
1. I would not use inout pattern, I would first call the popover and then separately update what is needed to be updated
2. In KeyPickerTableViewController define property var actionOnDismiss: (()->())? and setting this action to what we need after initialisation of KeyPickerTableViewController
I could show it in code, but the abstract you've shown is not clear enough to come up with specific amendments. Please refer the illustration below.
import UIKit
class FirstVC: UIViewController {
var key = 0
#IBAction func buttonPressed(_ sender: Any) {
let vc = SecondVC()
vc.action = {
print(self.key)
self.key += 1
print(self.key)
}
present(vc, animated: true, completion: nil)
}
}
class SecondVC: UIViewController {
var action: (()->())?
override func viewDidLoad() {
onDismiss()
}
func onDismiss() {
action?()
}
}
While presenting VC, add dismissing modal VC action in its completion handler, so that Viewcontroller will be presented after dismissal is completed
present(picker, animated: true, completion: { (action) in
//dismissal action
if let delKey = delegatedKey {
key = delKey
}
})

How do I pass data from a View controller into my pop up view controller (swift/ios)

I'm quite new with Swift and I'm making this mini game type app that counts the score and updates the label in the view controller. I want to pass that score from a view controller into another external pop up view controller I created.
#IBAction func Button7Tapped(_ sender: AnyObject)
{
if Index == 13 {
game.score += 1
} else {
let scorepopVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "finalScorePop") as! finalScoreViewController
self.addChildViewController(scorepopVC)
scorepopVC.view.frame = self.view.frame
self.view.addSubview(scorepopVC.view)
scorepopVC.didMove(toParentViewController: self)
}
updateGame()
}
Above is my code for the external pop up view controller I created, which also has a separated .swift file. How would I go about taking my game.score and passing that into my Popup view controller?
In your finalScoreViewController swift file add a new property.
final class FinalScoreViewController: UIViewController {
var score: Int?
}
And then just assign it when you're instantiating it.
#IBAction func Button7Tapped(_ sender: AnyObject) {
if Index == 13 {
game.score += 1
} else {
let scorepopVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "finalScorePop") as! finalScoreViewController
scorepopVC.score = game.score //THIS LINE
self.addChildViewController(scorepopVC)
scorepopVC.view.frame = self.view.frame
self.view.addSubview(scorepopVC.view)
scorepopVC.didMove(toParentViewController: self)
}
updateGame()
}
It is better to use storyboard to open the ViewController. In storyboard, right click and drag from you button to the second view controller (the one that you wish to open).
Choose the segue type that you wish to use. In your case, I think Present Modally will work fine.
You will see a line between the two UIViewControllers in storyboard. That is the segue. Tap on it. In the Attributes inspector give the segue an identifier. For instance "myFirstSegue".
Then in the code of the UIViewController that contains your button override prepare(for:sender:). This method is called when preparing for the segue to happen. I.o.w when you tap on the button. You have access to the destination UIViewController and can therefor access and set the properties on it.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "myFirstSegue" {
if let vc = segue.destination as? MyViewController {
//here you set your data on the destination view controller
vc.myString = "Hello World"
}
}
}
Note that we check the identifier, because all segues that go from this ViewController to other ViewControllers will call prepare(for:sender:)
It's quite simple, Just add a property in your finalScoreViewController (if you are not already done this) and -for example- call it score:
class finalScoreViewController: UIViewController {
var score: String?
// ...
Add this line to the Button7Tapped action (where you set a value for finalScoreViewController's score):
let scorepopVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "finalScorePop") as! finalScoreViewController
// add this line:
scorepopVC.score = "My score"
self.addChildViewController(scorepopVC)
scorepopVC.view.frame = self.view.frame
self.view.addSubview(scorepopVC.view)
scorepopVC.didMove(toParentViewController: self)
Finally, in finalScoreViewController:
override func viewDidLoad() {
super.viewDidLoad()
if let scr = score {
print(scr)
}
}
Hope that helped.
You do not actually have to pass the variable to the next view controller. All you have to do is create a variable outside of the View Controller class, and voila, you can access your variable from anywhere, in any swift file. For example:
var score = 0
class ViewController: UIViewController {
override func viewDidLoad(){
super.viewDidLoad()
}
#IBAction func Button7Tapped(_ sender: AnyObject){
score += 1
}
}
And then in the other View Controller, you would have something like this:
#IBOutlet weak var scoreLabel: UILabel!
class ViewController: UIViewController {
override func viewDidLoad(){
super.viewDidLoad()
var timer1 = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(updateScore), userInfo: nil, repeats: true)
}
#objc func updateScore() {
scoreLabel.text = "You have \(score) points!"
}