How to pass a (changing) variable between two view controllers? - swift

I have two view controllers. VC1 & VC2. VC1 is passing a variable which keeps changing & updating every second on VC1, in my case, it is the nearest beacon which is handled by a method in VC1.
VC1 code:
var id: Int = 0 // initializing
// send data to VC2
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let vc2 = segue.destination as? navigationScreenVC else { return }
vc2.id2 = id
}
VC2 code:
var id2: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
print(VC2)
}
It is working where it sends the first value it encounters, but not when the value keeps changing, so I want that to be sent as soon as it triggers a change.
I tried to do didSet{} but it doesn't work that way.

Use a delegate pattern.
In VC2:
protocol VC2Delegate: AnyObject {
var id2: Int { get }
}
class VC2 {
weak var delegate: VC2Delegate?
override func viewDidLoad() {
super.viewDidLoad()
print(delegate?.id2)
}
}
In VC1:
class VC1: UIViewController, VC2Delegate {
...
var id2: Int = 0
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let vc2 = segue.destination as? navigationScreenVC else { return }
vc2.delegate = self
}
...
}

A ViewController should not be managing stuff after it stopped being visible. You should managing this in a separate class and waiting for updates from a delegate, lets say:
protocol BeaconUpdateListener : AnyObject {
func currentBeaconIdWasUpdated(to newValue: Int)
}
class BeaconManager {
struct DelegateWrapper {
weak var delegate : BeaconUpdateListener?
}
static let delegates = [DelegateWrapper]()
static var currentId : Int = -1 {
didSet {
delegates.forEach { (delegate) in
delegate.delegate?.currentBeaconIdWasUpdated(to: currentId)
}
}
}
}
Sample code, missing details. You could make your own or update this one. Now, having that data outside your UI code makes it easier to use from anywhere else, and update that code in the future. This way, you "subscribe" to id updates like this:
BeaconManager.delegates.append(OBJECT_THAT_NEEDS_TO_BE_NOTIFIED)
... update your id like this:
BeaconManager.currentId = 65421687543152
... and wait for updates like this:
class VC2 : ViewController, BeaconUpdateListener {
func currentBeaconIdWasUpdated(to newValue: Int) {
// Do stuff once i receive the update
}
// ...
}

Related

How to write a swift function that returns one of three types?

I have three view controllers, WebkitViewController, and then two other view controllers WebkitViewControllerA, and WebkitViewControllerB that both extend WebkitViewController. I am having trouble writing a generic function that will look at the segue destination's view controller and tell me which of the three types it is. Is there a better way to do this?
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.destination is WebKitViewController && sender is UIButton {
let viewControllerType = {
if segue.destination is WebKitViewControllerA {
WebKitViewControllerA.self
} else if segue.destination is WebKitViewControllerB {
WebKitViewControllerB.self
} else {
WebKitViewController.self
}
}()
let button = sender as? UIButton
let vc = segue.destination as? viewControllerType
vc?.foo = "ho ho ho"
}
}
}
I also tried something like the following:
var vc: WHAT_TYPE_TO_PUT_HERE?
if segue.destination is CustomViewController {
vc = segue.destination as? CustomViewController
} else if segue.destination is WebKitBelowFoldViewController {
vc = segue.destination as? WebKitBelowFoldViewController
} else {
vc = segue.destination as? WebKitViewController
}
but I don't know what type I could give vc that type checker would be satisfied.
Since these are all subclasses of WebkitViewController, vc would be WebkitViewController. Also, there should be no reason to check each type given the code you've shown here. All you should require is:
if let vc = segue.destination as? WebKitViewController {
vc.foo = "ho ho ho"
}
Instead of trying to fiddle with concrete types, create a protocol that all three VCs conform to.
It looks like the thing that's common among these three VCs is that all of them have a foo property, so we can create a protocol like so:
protocol HasFoo { // you should give this a better name base on your actual situation
var foo: String! { get set } // remember to match the type of the foo property
}
And then conform the three VCs to it:
extension WebKitViewController : HasFoo {
}
extension WebKitViewControllerA : HasFoo {
}
extension WebKitViewControllerB : HasFoo {
}
Now you can simply do this in prepareForSegue:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = sender.destination as? HasFoo {
vc.foo = "ho ho ho"
}
}

Why is my data not passing between View Controllers using closure?

I am trying to pass data receive from a network call to another view controller when user has clicked on a button. When making printing on the FirstVC, data is in, but when printing the result in the SecondVC, there is no more value. I don' t want to use delegate but closure instead.
Also, when trying to retain the memory cycle, an error appear...
class APIsRuler {
static var oneRecipeFound: ((OneRecipeSearch) -> ())?
}
class FirstVC: UIViewController {
func cellIsClicked(index: Int) {
APIsRuler.shared.getRecipe(from: recipeID) { (success, oneRecipe) in
if success, let oneRecipe = oneRecipe {
APIsRuler.oneRecipeFound?(oneRecipe)
self.performSegue(withIdentifier: "goToSecondVC", sender: self)
}
}
}
}
Class SecondVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
APIsRuler.oneRecipeFound = { result in
print(result)
}
}
}
Doing this in SecondVC
APIsRuler.oneRecipeFound = { result in
print(result)
}
and this in first
APIsRuler.oneRecipeFound?(oneRecipe)
have no inner communications , you need to read your data directly from the shared class in the secondVc after the segue or send it in
self.performSegue(withIdentifier: "goToSecondVC", sender: <#Herererere#>)
and implement prepareForSegue
Let’s think about the order in which things are happening:
class APIsRuler {
static var oneRecipeFound: ((OneRecipeSearch) -> ())? // 1
}
class FirstVC: UIViewController {
func cellIsClicked(index: Int) {
APIsRuler.shared.getRecipe(from: recipeID) { (success, oneRecipe) in
if success, let oneRecipe = oneRecipe {
APIsRuler.oneRecipeFound?(oneRecipe) // 2
self.performSegue(withIdentifier: "goToSecondVC", sender: self)
}
}
}
}
Class SecondVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
APIsRuler.oneRecipeFound = { result in // 3
print(result)
}
}
}
oneRecipeFound starts out life empty: it is nil.
In FirstVC, the cell is clicked. We call oneRecipeFound. It is still nil, so nothing happens.
In SecondVC, we set the value of oneRecipeFound. Now it has a value, but the call has already happened.
So unless you have a time machine in your pocket, so that you can reverse that order of events somehow, the strategy you’ve outlined is doomed to failure. Of course, if you call oneRecipeFound after setting it, it will work. For example:
Class SecondVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
APIsRuler.oneRecipeFound = { result in
print(result)
}
APIsRuler.oneRecipeFound?(oneRecipe) // prints
}
}

How to declare a variable with two possible types

I have two Core Data entities which populate a UITableView with 2 sections, one entity for each section. When the user taps on a table row, they are directed to another view where the data of that row is sent. It is currently implemented like this:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "editValue") {
let secondViewController = segue.destination as! EditValuesViewController
if send_array_inc.isEmpty {
secondViewController.send_array_exp = send_array_exp
} else if send_array_exp.isEmpty {
secondViewController.send_array_inc = send_array_inc
}
}
}
The Question:
Since there are two entities, there are two possible types (Income and Expenses) for the data being sent into the next view. How can I use that data in the next view with one variable? I am doing the below in ViewDidLoad but the scope of send_array remains within that function. How can I make send_array available outside?
if send_array_inc.isEmpty {
var send_array = [Expenses]()
send_array = send_array_exp
} else if send_array_exp.isEmpty {
var send_array = [Income]()
send_array = send_array_inc
}
I ideally want to do this without creating a separate view for each entity result but I am open to refactor if another solution would be better and realistic. Thank you
Make your two types of data objects conform to a shared protocol. Make the destination view controller's send_array be an object conforming to that protocol.
In your EditValuesViewController's code, interrogate the send_array to figure out which type of data object was passed in.
Edit:
Define a protocol
#protocol dataArrayProtocol {
var dataArray: Array
}
Define 2 structs that conform to that protocol
struct ExpensesArrayStruct: dataArrayProtocol {
var dataArray: [Expenses]
}
struct IncomeArrayStruct: dataArrayProtocol {
var dataArray: [Income]
}
give your EditValuesViewController a property that conforms to that protocol
class EditValuesViewController: UIViewController {
var dataArrayStruct: dataArrayProtocol
}
And your prepare(for:sender) method
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "editValue") {
let secondViewController = segue.destination as! EditValuesViewController
if send_array_inc.isEmpty {
secondViewController.dataArrayStruct = ExpensesArrayStruct(dataArray: send_array_exp)
} else if send_array_exp.isEmpty {
secondViewController.dataArrayStruct = IncomeArrayStruct(dataArray: send_array_inc)
}
}
}
And to handle the data:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let expensesStruct = dataArrayStruct as? ExpensesArrayStruct {
//deal with expenses array
} else if let incomeStruct = dataArrayStruct as? IncomeArrayStruct {
//deal with income array
}
}
Note that I banged this code out in the SO editor and have not tried to compile it. I may have made some minor errors. It should give you the idea though.

How to pass data backwards to a view controller after passing forward?

Im developing a quiz app and there is a second view controller that appears after the initial view controller where you are asked to answer a question. On the second view controller you the user must press a button to return to the initial view controller to be asked another question. However when I segue back from the second view controller I believe a new instance of the initial view controller is being created and the user is asked questions they have already answered. The code in the swift file for my initial view controller is constructed that when once a user is asked a question once that question is removed from an array by it's index. How could I make it to where a new instance of the initial view controller isn't created from segueing from the second view controller? Or is there a way the second view controller can access the same methods as the initial view controller?
Here is my code for the initial view controller:
import UIKit
class ViewController: UIViewController {
var questionList = [String]()
func updateCounter() {
counter -= 1
questionTimer.text = String(counter)
if counter == 0 {
timer.invalidate()
wrongSeg()
counter = 15
}
}
func randomQuestion() {
//random question
if questionList.isEmpty {
questionList = Array(QADictionary.keys)
questionTimer.text = String(counter)
}
let rand = Int(arc4random_uniform(UInt32(questionList.count)))
questionLabel.text = questionList[rand]
//matching answer values to go with question keys
var choices = QADictionary[questionList[rand]]!
questionList.remove(at: rand)
//create button
var button:UIButton = UIButton()
//variables
var x = 1
rightAnswerBox = arc4random_uniform(4)+1
for index in 1...4
{
button = view.viewWithTag(index) as! UIButton
if (index == Int(rightAnswerBox))
{
button.setTitle(choices[0], for: .normal)
}
else {
button.setTitle(choices[x], for: .normal)
x += 1
}
randomImage()
}
}
let QADictionary = ["Who is Thor's brother?" : ["Atum", "Loki", "Red Norvell", "Kevin Masterson"], "What is the name of Thor's hammer?" : ["Mjolinr", "Uru", "Stormbreaker", "Thundara"], "Who is the father of Thor?" : ["Odin", "Sif", "Heimdall", "Balder"]]
//wrong view segue
func wrongSeg() {
performSegue(withIdentifier: "incorrectSeg", sender: self)
}
//proceed screen
func rightSeg() {
performSegue(withIdentifier: "correctSeg", sender: self)
}
//variables
var rightAnswerBox:UInt32 = 0
var index = 0
//Question Label
#IBOutlet weak var questionLabel: UILabel!
//Answer Button
#IBAction func buttonAction(_ sender: AnyObject) {
if (sender.tag == Int(rightAnswerBox))
{
rightSeg()
print ("Correct!")
}
if counter != 0 {
counter = 15
questionTimer.text = String(counter)
}
else if (sender.tag != Int(rightAnswerBox)) {
wrongSeg()
print ("Wrong!")
timer.invalidate()
questionList = []
}
randomQuestion()
}
override func viewDidAppear(_ animated: Bool)
{
randomQuestion()
}
//variables
var counter = 15
var timer = Timer()
#IBOutlet weak var questionTimer: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
timer = Timer.scheduledTimer(timeInterval: 1, target:self, selector: #selector(ViewController.updateCounter), userInfo: nil, repeats: true)
}
Here's the code to the second view controller:
import UIKit
class ContinueScreen: UIViewController {
//correct answer label
#IBOutlet weak var correctLbl: UILabel!
//background photo
#IBOutlet weak var backgroundImage: UIImageView!
func backToQuiz() {
if let nav = self.navigationController {
nav.popViewController(animated: true)
}
else {
self.dismiss(animated: true, completion: nil)
}
}
#IBAction func `continue`(_ sender: Any) {
backToQuiz()
}
What you need, sir is a delegate or an unwind segue. I far prefer delegates because they're easier to understand and I think a bit more powerful.
In your ContinueScreen view controller define a protocol similar to this:
protocol QuizCompletedDelegate {
func completedQuiz()
func canceledQuiz()
}
Also in your ContinueScreen view controller you need to declare an optional delegate of type QuizCompletedDelegate
var delegate: QuizCompletedDelegate?
... and use that delegate to call the functions when appropriate:
#IBAction func didPressContinue(_ sender: Any) {
if allQuestionsAnswered == true {
delegate.completedQuiz()
} else {
delegate.cancelledQuiz()
}
}
In your initial view controller is where you implement the functions for the protocol:
extension ViewController: QuizCompletedDelegate {
func completedQuiz() {
//Handle quiz complete
}
func cancelledQuiz() {
//Handle quiz cancelled
}
}
Then the last thing you need to do is set the delegate of your ContinueScreen view controller in the prepareForSegue function of your initial view controller.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showContinueScreen" {
let continueVC = segue.destination as! ContinueScreen
continueVC.delegate = self
}
}
Remove the call to randomQuestion() on your initial view controller's ViewDidAppear, and you're in business!
It shouldn't create a new View Controller when you go back. If it does - it's very bad - and you should re-architect the flow of your app... To pass data forward, we should use Dependency Injection. To pass data to the previous view controller - use the iOS Delegation Pattern. I recommend to spend 30-40 mins to read (and understand this article), and it will make things more clear for you in the future: http://matteomanferdini.com/how-ios-view-controllers-communicate-with-each-other/.

How to set up a NSComboBox using prepare for segue or init?

I am really struggling to pass the contents of one array from a view controller to another to set up the contents of a nscombobox. I have tried everything I can think of, prepare for segue, init; but nothing seems to work.
the program flow is as follows: the user enter a number into a text field and based on it an array with the size of the number is created. Once the user presses a button the next VC appears that has a combo box and inside that combo box those numbers need to appear. All my attempts result in an empty array being passed. Could someone please take a bit of time and help me out. Im sure I'm doing a silly mistake but cannot figure out what.
Code listing below:
Class that take the user input. At this stage I'm trying to pass the contents of the array in the next class as I gave up on prepare for segue because that one crashes because of nil error. Please note that prepare for segue is uncommented in the code listing just for formatting purposes here. Im my program it is commented out as I am using perform segue at the moment.
Any solution would be nice please. Thank you.
import Cocoa
class SetNumberOfFloorsVC: NSViewController {
//MARK: - Properties
#IBOutlet internal weak var declaredNumber: NSTextField!
internal var declaredFloorsArray = [String]()
private var floorValue: Int {
get {
return Int(declaredNumber.stringValue)!
}
}
//MARK: - Actions
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction private func setNumberOfFloors(_ sender: NSButton) {
if declaredNumber.stringValue.isEmpty {
let screenAlert = NSAlert.init()
screenAlert.messageText = "Please specify the number of floors!"
screenAlert.addButton(withTitle: "Got it!")
screenAlert.runModal()
} else if floorValue == 0 || floorValue < 0 {
let screenAlert = NSAlert.init()
screenAlert.messageText = "Please input a correct number of floors!"
screenAlert.addButton(withTitle: "Got it!")
screenAlert.runModal()
} else {
for i in 0...floorValue - 1 {
declaredFloorsArray.append(String(i))
}
print("\(declaredFloorsArray)")
let declareNumberOfRoomsVC = SetNumberOfRoomsForFloorVC(boxData: declaredFloorsArray)
declareNumberOfRoomsVC.boxData = declaredFloorsArray
performSegue(withIdentifier: "set number of rooms", sender: self)
}
}
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
if segue.identifier == "set number of rooms" {
if let addRoomsVC = segue.destinationController as? SetNumberOfRoomsForFloorVC {
addRoomsVC.floorBox.addItems(withObjectValues: declaredFloorsArray)
}
}
}
}
this is the class for the next VC with the combo box:
import Cocoa
class SetNumberOfRoomsForFloorVC: NSViewController, NSComboBoxDelegate, NSComboBoxDataSource {
//MARK: - Properties
#IBOutlet internal weak var floorBox: NSComboBox!
#IBOutlet private weak var numberOfRoomsTxtField: NSTextField!
internal var boxData = [String]()
//MARK: - Init
convenience init(boxData: [String]) {
self.init()
self.boxData = boxData
}
//MARK: - Actions
override func viewDidLoad() {
super.viewDidLoad()
floorBox.usesDataSource = true
floorBox.dataSource = self
floorBox.delegate = self
print("\(boxData)")
}
#IBAction private func setRoomsForFloor(_ sender: NSButton) {
}
//MARK: - Delegates
func numberOfItems(in comboBox: NSComboBox) -> Int {
return boxData.count
}
func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? {
return boxData[index]
}
}
First you should remove the following code.
let declareNumberOfRoomsVC = SetNumberOfRoomsForFloorVC(boxData: declaredFloorsArray)
declareNumberOfRoomsVC.boxData = declaredFloorsArray
I assume you think that the viewController you created here is passed to prepareForSegue. However the storyboard instantiates a new viewController for you.
After that you need to set your declaredFloorsArray as the the boxData of the new viewController in prepareForSegue and you should be good to go.