0

Kind of stuck on the same problem now for a couple of days.

I have a ViewControllerA class that serves as a CollectionView delegate and datasource. The CollectionView is hooked up through a storyboard. ViewControllerA class contains the data model that feeds the CollectionView.

Whenever ViewControllerA loads for the first time, it connects to a database, and loads information as needed into a data model (userDictionary) . Once the controller loads all data, it reloads the CollectionView, and fires syncing methods that listen for any changes in the database, to update the userDictionary accordingly, and reload the respective items in the collectionView.

Everything works fine up to this point.

When I transition to any different ViewController class, say ViewControllerB class, I’m passing a copy of the userDictionary from ViewControllerA to ViewControllerB in prepareForSenderA, and passing it back to ViewControllerA through prepareForSenderB.

Here’s the strange behavior. When I transition back to ViewControllerA class, the CollectionView loads fine with the same data that was passed to ViewControllerB, but fails to load any new changes that the syncing method observes in ViewControllerA.

I know that the syncing methods are still working fine, because any new data is showing up in the debugger when I print it out as it’s loading in ViewControllerA. And I know that my userDictionary data model in the same controller class is receiving those updates because of a didSet observer that’s printing out the most up-to-date state of userDictionary.

And the odd thing is that whenever I print out the contents of the data model within ViewControllerA class, it prints out the old state of the model as it existed when it was passed to ViewControllerB class. Even though the didSet observer just proved that the model was actually updated!

It’s almost as if ViewControllerA class is somehow retaining a reference to the data model as it existed when it was passed over to ViewControllerB, and somehow “loses” its reference to the data model in “self” when that model gets passed back.

Another note: If I stay in ViewControllerA the whole time, and don’t pass the userDictionary back and forth, I don’t get this problem any more.

The code below sums up how I'm passing data back and forth:

View Controller A Class:

class ViewControllerA: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {

//MARK: Data Structures
var userDictionary = [[String: String]]() { didSet { print("userDictionary.count from DIDSET,", userDictionary.count)}}
//prints out: 9

//MARK: Initialization
override func viewDidLoad() {
    self.loadUserDictionaryFromDatabase()
}

func loadUserDictionaryFromDatabase() {
    //code that loads information from a database
    //var objectInstance["name"] = firstName
    //self.userDictionary.append(objectInstance)
    print("loader observed numberOfItems", numberOfItems)
    //...once data is fully loaded
    self.syncDataFromDatabase()
}

func syncDataFromDatabase() {
    //sync new data from database
    //var newObjectInstance["newName"] = newFirstName
    //self.userDictionary.append(newName)
    print("syncer observed newNumberOfItems", newNumberOfItems)
}

//MARK: View Controller Transitions
@IBAction func segueAtoB(_ sender: Any) {
    self.performSegue(withIdentifier: "segueAtoB", sender: self)
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    //Segue from VCA to VCB
    if segue.identifier == "segueAtoB" {

        let controller = segue.destination as! ViewControllerA

        //Pass initialized userDictionary from VCA to VCB
        controller.userDictionary = self.userDictionary
    }
}

//MARK: Dummy Test Number of Items in UserDictionary
@IBAction func printMostUpToDateNumberofItemsInDictionary(_ sender: Any) {
    print("userDictionary.count from TEST,", userDictionary.count)
}

}

View Controller B Class:

class ViewControllerB: UIViewController {

//MARK: Data Structures
var userDictionary = [[String: String]]()

//MARK: View Controller Transitions
@IBAction func segueBtoA(_ sender: Any) {
    self.performSegue(withIdentifier: "segueBtoA", sender: self)
}


override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    //1. Segue to Home-Screen Controller
    if segue.identifier == "segueBtoA" {

        let controller = segue.destination as! ViewControllerB
        controller.userDictionary = self.userDictionary
    }
}

}

Result of Prints:

Step 1: Start in View Controller A

  1. loader observed numberOfItems = 9
  2. userDictionary.count from DIDSET, 9
  3. syncer observed newNumberOfItems = 1
  4. userDictionary.count from DIDSET = 10 (10 = 9 + 1) -- PASS
  5. userDictionary.count from TEST = 10 (10 = 9 + 1) -- PASS

Step 2: Segue From A to B

viewControllerB.userDictArray = viewControllerA.userDictArray

Step 3: Segue From B to A

viewControllerA.userDictArray = viewControllerB.userDictArray

Step 4: Observe Debugger Output

  1. userDictionary.count from DIDSET, 10
  2. syncer observed newNumberOfItems = 1
  3. userDictionary.count from DIDSET = 11 (10 = 10 + 1) -- PASS
  4. userDictionary.count from TEST = 10 (10 != 10 + 1) -- FAIL (How is this line happening when diSet just updated userDictionary.count?)

Debugger output using UUID:

Step 1: Start in View Controller A

  1. loader observed numberOfItems, A74593E1-1231-41BE-A5DF-693591F998E4
  2. userDictionary.count from DIDSET, A74593E1-1231-41BE-A5DF-693591F998E4
  3. syncer observed newNumberOfItems, A74593E1-1231-41BE-A5DF-693591F998E4
  4. userDictionary.count from DIDSET, A74593E1-1231-41BE-A5DF-693591F998E4
  5. userDictionary.count from TEST, A74593E1-1231-41BE-A5DF-693591F998E4

Step 4: Observe Debugger Output

  1. userDictionary.count from DIDSET, A74593E1-1231-41BE-A5DF-693591F998E4
  2. syncer observed newNumberOfItems, A74593E1-1231-41BE-A5DF-693591F998E4
  3. userDictionary.count from DIDSET, A74593E1-1231-41BE-A5DF-693591F998E4
  4. userDictionary.count from TEST, 28D817D9-B53D-47D8-A3EF-2F7DDE6460FC
AJabero
  • 37
  • 6
  • `and passing it back to ViewControllerB through prepareForSenderB.`: Do you use a segue to go back or do you dismiss ViewControllerB? You can check if its the same ViewControllerA you go back to by printing a line in ViewControllerA‘s viewDidLoad. `viewDidLoad` is only loaded once per instance, `viewDidAppear` happens every time the ViewController appears. – Fabian Jul 24 '18 at 16:10
  • You shouldn't have a "prepareForSenderB" ... Are you using a navigation controller to "push" A -> B and "pop" B -> A? – DonMag Jul 24 '18 at 16:10
  • @Purpose - "and passing it back to ViewControllerB through prepareForSenderB" - mistakenly wrote ViewControllerB instead of "A". Edited I'm using a segue to go back, and not by dismissing ViewControllerB, and I'm certain I'm going back to ViewControllerA... – AJabero Jul 24 '18 at 16:20
  • Why do you create a new ViewControllerA if you could just call dismiss then? The printline in viewDidLoad was supposed to show you that a new VC got instantiated, so I think now it looks like A-B-A instead of A-B to A. [Passing data back](https://stackoverflow.com/questions/47843547/pass-data-back-to-previous-vc-using-delegates-and-update-custom-cell). – Fabian Jul 24 '18 at 16:21
  • @DonMag I'm not using a navigation controller/stack. These are separate ViewControllers linked only through a storyboard segue. How else would you pass data back if you're not using a "prepareForSenderB"? – AJabero Jul 24 '18 at 16:21
  • @Purpose I did try doing that, and the models work fine. Except that my custom segues stop working (dismiss goes back to calling default modal dismissal, instead of dismissing with custom segue) – AJabero Jul 24 '18 at 16:28
  • [More passing back](https://stackoverflow.com/questions/5210535/passing-data-between-view-controllers?rq=1). – Fabian Jul 24 '18 at 16:29
  • @AJabero - ok... you probably want to be using an "unwind" segue... Based on your description, it sounds like you are segueing to a new instance of A. – DonMag Jul 24 '18 at 16:29
  • @DonMag - I believe that I am segueing to a new instance of A... Is that necessarily problematic? I tried using an "unwind" segue, but it also ends up overriding my custom segues, and reverts to default modal dismissal – AJabero Jul 24 '18 at 16:31
  • `ends up overriding my custom segues, and reverts to default modal dismissal` I don‘t know about unwind segues but I guess that they give additional opportunity to give data back to the parentVC. They are however not needed (dismissing can be used too) if you give the child view controller a delegate (inside prepareForSegueToA) where he can tell the parentVC that he wants sth changed in the data controller or sth like that. You can even override the dismiss-function(including a mandatory super.dismiss) and tell the delegate of changes there if you want it more prepare-for-segue-like. – Fabian Jul 24 '18 at 16:49
  • `(inside prepareForSegueToA)` was supposed to say `(inside prepareForSegueToB)`. To give B sth it can delegate to. – Fabian Jul 24 '18 at 16:59
  • @Purpose I added some code to explain how I'm passing data around. Should this theoretically create any problems with the reference to the userDictionary once it's passed back? My sense tells me this has something to do with retain cycles. – AJabero Jul 24 '18 at 17:39
  • UserDictionary is an array of dictionary and both Dictionary and Array are both passed by value, so are copied. Meaning when instance A makes changes instance A-B will not see it. Instance A-B-A, which got a copy from instance A-B then goes on and loads again due to its viewDidLoad. I don not know whether you intended this. However I can not tell you stacking up ViewControllers will harm you if you do not continuously update .userDictionary, but its still not usual behaviour. I hope it helped. – Fabian Jul 24 '18 at 17:45
  • @Purpose I think I understand what you're saying. A didSet observer attached to ABA shows that ABA is still receiving updates. But when I print out the contents of ABA AFTER didSet observes changes, it prints A-B, and I can't see why. (steps reproduced in updated code above) – AJabero Jul 24 '18 at 18:26
  • Do the outputs from step4 all come from the same instance A-B-A? You could check this with an instance variable `let uuid = UUID()` and adding them to the outputs or using breakpoints and inspecting the uuid from the respective class there. – Fabian Jul 24 '18 at 18:32
  • How would I define/instantiate this instance variable in the context of the terminology I'm using in the code sample? @Purpose – AJabero Jul 24 '18 at 20:26
  • @AJabero Just drop `let uuid = UUID()` inside ViewControllerA, then add it to the print calls. `UUID()` returns a random uuid. – Fabian Jul 24 '18 at 20:28
  • @Purpose I just added the debugger output from printing "uuid" at the end of the code sample above. All cases print the same uuid, except for 4.4. I'm not exactly sure what this means? – AJabero Jul 24 '18 at 21:01
  • It means that 4.1, 4.2 and 4.3 come from instance A. Instance A got the updated .userDirectory. Does knowing this help you? – Fabian Jul 24 '18 at 21:02
  • But why is instance A the one getting the updated information, if the most recent instance passed to ViewControllerA was instance ABA (as passed forward from ViewControllerB in prepareForSender)? Shouldn't ABA "overwrite" instance "A" as the single & only instance, and therefore be the only instance receiving any updates? The way I understand it is that once the controller segues from A to B, then B becomes the only controller that contains the "old" instance of A. Once the controller segues from B to A, "userDictionary" becomes overwritten by "ABA". Is this not the case? – AJabero Jul 24 '18 at 21:09
  • How would A-B-A overwrite any instance? A, B and A, all three are on the view stack at the same time. They do not simply vanish, they are all still there. That is why dismissing/unwinding is what everyone is doing to avoid having additional View Controllers on the view stack which are not wanted there in the first place. A segue is just a „instantiate a new object and push it on the stack“. The only thing the prior VC knows that sth changed is that `viewWill/didDisappear` get called. If you still sync in the background in instance A it will run until you make it stop or it deinits. – Fabian Jul 24 '18 at 21:23
  • So the segue from AB to ABA the .userDirectory variable gets set on ABA, but this changes nothing for the .userDirectory in the instances A and AB. It only copies the Array over from the AB to the ABA instance. I use A, AB and ABA as names to show where the instance currently resides on the stack. When ABA is pushed ontop an A and a B instance are laying below and come up again once dismiss/unwind is called. There is an Inspector tool somewhere to show the layers of ViewControllers currently on the stacks besides each other. – Fabian Jul 24 '18 at 21:28
  • So much written, damn I‘m tired now. I hope you understand better. The reason why people use DataController classes and such is so its independent from any View and can do things while its passed around etc. – Fabian Jul 24 '18 at 21:36
  • 1
    Thanks @Purpose. I do have a clearer understanding of what's happening – AJabero Jul 24 '18 at 21:40

1 Answers1

1

I really think you need / want to use an "unwind" segue.

In ViewControllerA add this function:

@IBAction func returnFromB(_ segue: UIStoryboardSegue) {
    if let vcDest = segue.destination as? ViewControllerA,
        let vcSource = segue.source as? ViewControllerB {
            // pass userDictionary from VCB back to VCA
            vcDest.userDictionary = vcSource.userDictionary
        print("unwind segue from B")
    }
}

Then, connect it by Ctrl-Drag from the View Controller icon to the Exit icon at the top of ViewControllerA in your storyboard. Give it a segue identifier.

Now, in ViewControllerB, you can call:

self.performSegue(withIdentifier: "unwindSegueBtoA", sender: self)

and the returnFromB() function in ViewControllerA will handle transferring back your data.

Here is a basic example: https://github.com/DonMag/DataPass

Another (probably better) option would be to create a "data manager" class, and set/get your data from that class from each controller, as opposed to passing it back and forth.

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • I think that the unwind segue should work if I figure out how to customize the unwind segue... I've also considered using a "data manager" class instead, but my model is so complex at this point that this would take quite some effort to rearchitect, and I want to be sure that there isn't a simpler way I could get this done without changing how my code is structured. – AJabero Jul 24 '18 at 20:29