SWIFT - Variable resetting when back from second ViewController - swift

I have been searching for hours here and Google but still didn't find the answer. Everything point to passing variable from a VC to an other but not how to keep variables alive on the VC.
I can pass variables from a VC to an other using multiple methods such as singleton but when I go from second VC to the main VC I'm still facing the same problem.
Exemple you have the main VC which have a label and 2 buttons. When you click one of the button the label text change and then you click the second button to Segue to the second VC. Then when you comeback to the first VC the problem appear: the label is reseted to it's initial text "label". Why?
I tried using global variable in a separate swift file. I said to myself well the value is stored in an other swift file there is no reason why the value of the label reset.. but well it's still resetting.
Thanks so much.
Example in video: https://youtu.be/Wx5blkQqU7E
Very basic example:
varTest.swift
import Foundation
var myVar2: String!
Main ViewController
import UIKit
class ViewController: UIViewController {
#IBOutlet var mylabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func dsfg(_ sender: UIButton) {
performSegue(withIdentifier: "switchForm", sender: self)
}
#IBAction func changeLabeltext(_ sender: UIButton) {
myVar2 = "Good."
mylabel.text = myVar2
}
}
Second ViewController
import UIKit
class ViewController2: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func btnPrint(_ sender: UIButton) {
performSegue(withIdentifier: "returnForm", sender: self)
}
}

I think the problem is that you are not returning back from ViewController2 to ViewController, but you are pushing a new ViewController instance, which will display a new UILabel with its default value. You can create an unwind segue from your second view controller to the first view controller, or you can call dismiss(animated:completion:) on the second view controller. This article can guide you to create an unwind segue.

When you "performSegue", the values in the ViewController class are reset - don't use segue.
Or alternatively: You mention that you tried to use global variables in a separate swift file. Use a temp file stored on the client side instead.
Use these func to create a file and write to it:
//This OVERWRITES already existing files!
func createFileinDocumentDirectory(filename: String){
let blank = ""
if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let fileURL = dir.appendingPathComponent(filename)
do {
try blank.write(to: fileURL, atomically: false, encoding: .utf8)
}
catch let error as NSError {
print("Ooooops! Something went wrong: \(error)")
}
}
}
func writeToFileInDocumentDirectory(filename: String, textToAdd: String){
do {
let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last! as URL
let url = dir.appendingPathComponent(filename)
try textToAdd.appendLineToURL(fileURL: url as URL)
}
catch {
print("Could not write to file")
}
}

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/

Two UIViews that share ALMOST the same UIViewController

I have a "Buy" page and a "Sell" page for products. They share the SAME UIViewController except for the fact that in one UIViewController it shows items for sale, and the other one for purchase. (Another difference is one button - it's has string A displayed in one and B in the other.
Can I make them share the same UIViewController with those changes, tho ? Can I define some arguments for each view ?
You could use a base UIViewController that had two trivially different subclasses. Put all of the logic in the base and just the differences in the subclasses...
class BaseVC: UIViewController {
#IBOutlet weak var button: UIButton!
var items: [String] = []
#IBAction func buttonPressed(_ sender: Any) {
// do stuff
}
// add all of your tableView or similar common stuff here...
}
class AVC: BaseVC {
override func viewDidLoad() {
super.viewDidLoad()
self.button.titleLabel?.text = "A"
self.items = ["Sell1", "Sell2"]
}
}
class BVC: BaseVC {
override func viewDidLoad() {
super.viewDidLoad()
self.button.titleLabel?.text = "B"
self.items = ["Buy1", "Buy2"]
}
}
Create a xib file,
Add a view in it,
Use that view in view controller with different functions on different buttons
Watch this video for help: https://www.youtube.com/watch?v=tvQxXoV527w&t=754s
Use a BaseViewController and make it the parent class of your new view controllers
Something like below:
BaseController:
import UIKit
class BaseViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
//Common initiation codes
}
// Write all the codes that can be shared between the controllers
}
Make separate classes for each of your view controllers and modify them as you require:
class SettingsController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()//This calls everything you wrote as init code for BaseController
//Write any view controller specific init code here
}
//If you have any other settings controller specific code, write here
}
Since I haven't seen your code, I can't say for sure if this is the right way. This method would still mean that you don't have to rewrite your code for each view controller classes and still keep it clean.
The above method is preferred when you are doing your layout and views by code.
If you are using a story board and depending on your specific need, I would suggest you do something like this instead:
//Initiate your controller like this from the previous controller
#IBAction func goToSettingsController(_ sender: Any) {
let baseController = UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ControllerID") as! BaseController
baseController.id = "Settings"
self.present(baseController, animated: true, completion: nil)
}
#IBAction func goToAboutController(_ sender: Any) {
let baseController = UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ControllerID") as! BaseController
baseController.id = "About"
self.present(baseController, animated: true, completion: nil)
}
And in your BaseController:
class BaseController: UIViewController{
var id: String!
#IBOutlet weak var backGround: UIView!
override func viewDidLoad() {
super.viewDidLoad()
customInit()
}
func customInit(){
switch id{
case "Settings":
self.backGround.backgroundColor = .purple //Any controller specific code
break
case "About":
self.backGround.backgroundColor = .green //Any controller specific code
break
default:
break
}
}
}
You won't need three separate classes as mentioned before, but you can use the BaseController class for both your ViewControllers.

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!"
}

how to pass video field to AVPlayer controller from 'performSegueWithIdentifier' button, with swift 2.2

I have a 'performSegueWithIdentifier' set up to be called from a button. A URL for a video is in the string 'self.specimen.filmMovieLink'. How do I pass this field through to the the AVController? I have set up a segue called 'newMovie' and added the AV View Controller scene to the storyboard (without any code or attached file to control that scene).
I need to send it through from performSegueWithIdentifier("NewMovie", sender: specimenAnnotation)
If I add the URL and a direct string with a working URL it opens the AVPlayer Controller but does not play the video, when I tried this code:
performSegueWithIdentifier("watchMovie", sender: specimenAnnotation)
let url = NSURL(string: "https://www.example.com/video.mp4")
This code is working to segue the video URL to the AVController with my fields from a different view controller, using 'prepareForSegue' instead:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let destination = segue.destinationViewController as!
AVPlayerViewController
let url = NSURL(string: self.specimen.filmMovieLink)
destination.player = AVPlayer(URL: url!)
}
}
I tried adapting it for the 'performSegueWithIdentifier' but am a beginner and couldn't get it to work. How can I adapt that code (or what new code do I need) to pass my field (self.specimen.filmMovieLink) to the AV controller with the 'performSegueWithIdentifier' instead of this 'prepareForSegue'?
The way I would approach it, I would create a property Observer:
// create a property observer that is set to nil then would change until something was assigned it to
var myURL:URL? = nil {
willSet(newValue) {
print("newvalue \(newValue)")
}
didSet{
// could do more stuff if needed it
}
}
Either in your ViewDidLoad or Inside of your UIButton action method your could call this line below
myURL = URL(string: "https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")
then
#IBAction func MyButton(sender:UIButton) {
performSegue(withIdentifier: "segueIdentifier", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
let destination = segue.destinationViewController as! AVPlayerViewController
guard let _url = myURL else {return }
destination.player = AVPlayer(url: _url)
destination.player?.play()
}
Voila, if you want to use tableView your could check out
my github project

Reload MainViewController when PopViewController dismissed

I have a main view controller and pop up controller.Please refer screen shots.
Code :
#IBAction func show(sender: AnyObject) {
var popView = popupviewcontroller(nibName:"popview",bundle:nil)
var popController = UIPopoverController(contentViewController: popView)
popController.popoverContentSize = CGSize(width: 450, height: 450)
popController.presentPopoverFromRect(sender.frame, inView: self.view, permittedArrowDirections: UIPopoverArrowDirection.Down, animated: true)
}
For the popupViewcontoller i used .xib.
When press save button data saved to core data.
Lets come to my problem, in my mainViewController i fetched data and fill them in dynamically created lables.That occurred when view load.I want to reload mainViewController when close button form popViewController pressed.
I tried within the close button my code are here, i just tried to reload the mainVc :
var mainVC = mainviewcontroller()
#IBAction func close(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
//mainVc.viewDidLoad()
mainVC.reloadInputViews()
}
Does not give output.
Conclusion : I want a way to refresh view controller from another view in swift.
Thanks in advance.
Using UITableView only we can reload the data!,So we have to use table view custom cell textfield or Label. Now we can able to reload our custom textfield data.
Does your main ViewController use a UITableView?
You can use viewWillAppear and get the data again and use tableView.reloadData() to reload the data.
EDIT:
with var mainVC = mainviewcontroller() you're just making a new instance of your MainViewController. If you want to use reloadInputViews(), you can put it in viewWillLoad.
You should use protocol for it. You can read more about protocols here https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html
You can make protocol in PopViewController
protocol PopViewControllerProtocol {
static var valuesChanged()
}
Later you should implement this protocol in MainViewController and refresh data.
Example:
File UIPopoverController.swift
protocol UIPopoverControllerDelegate{
func valuesChanged(changedValue:String)
}
class UIPopoverController: UIViewController {
var delegate: UIPopoverControllerDelegate! = nil
#IBAction func close(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
//mainVc.viewDidLoad()
mainVC.valuesChanged("some value")
}}
File MainViewController.swift
class MainViewController: UIViewController, UIPopoverControllerDelegate
{
func valuesChanged(changedValue:String) {
//this will be called when popuviewcontroller call valueschanged on delegate object
}
#IBAction func show(sender: AnyObject) {
var popView = popupviewcontroller(nibName:"popview",bundle:nil)
var popController = UIPopoverController(contentViewController: popView)
popController.delegate = self;
popController.popoverContentSize = CGSize(width: 450, height: 450)
popController.presentPopoverFromRect(sender.frame, inView: self.view, permittedArrowDirections: UIPopoverArrowDirection.Down, animated: true)
}
}
With God Grace i found a solution that is , just create a Global variable
Var fetchedArray = Nsarray()
Then Write the following code in the popovoer controller save button
func save(){
// Write codes for saving data
let request = NSFetchRequest(entityName: "Enitity name")
request.returnsObjectsAsFaults = false
let Total = try? context.executeFetchRequest(request)
if let wrapResults = Total {
fetchedArray = wrapResults
}
}
Then fill the Labels by using fetchedArray.
Thanks
Swift 2, try
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle
viewDidLoad()
return.None
Works for me