Pass data from Realm between Viewcontrollers

Hi I have recently started using realm for the first time and need a little. I have saved data from a form in my realm database (i.e . name, age, address, etc) and I want to pass those values to various viewcontrollers throughout my project.

In one of my viewcontrollers there is a tableview that accesses some of that data from an array and populates the cell. What I would like to do is when the user taps the cell, the data from my realm gets passed to various labels and textfields in the detail viewcontroller. I read earlier that all I need is to use prepareForSegue, but I’m not sure of the proper way to access the values so they go to the destinationviewcontroller. I will attach screenshots of my view controller so you can see where I am currently.


I cant attach the second destination controller this is going to because I’m new but this is the viewcontroller that has the tableview and when I click the cell I would like to pass the data from my realm to the textfields in the destinationcontroller. The code that has the prepareForSegue function is on line 58

I have tried accessing the realm.objects() in the prepareForSegue function and that hasn’t been working. Has anyone else run into this issue and resolved it? Thanks in advance! I tried asking on stackoverflow and havent had anyone reply yet and google hasn’t been helpful with this specific issue.

this is the destination controller and i would like to pass my realm data to these textfields that I have connected.

The way that I’ve handled this in my apps is that I define a class variable in the destination VC to receive the primary key, and then re-query in the ViewDidLoad of the destination VC using a predicate filter. This has a twofold benefit, since it gives you the PK if you need to do updates.

Otherwise, if you’re just looking to pass values and don’t need to do any edits/updates, you could just define those values in the destination, and pass them in the prepareForSegue.

1 Like

I am looking to do the latter of what you mentioned where I dont need to do any edits/updates. Could you please provide an example? I’ve been readin gthe documentation to see what I’m doing wrong and am still getting the error ‘Unexpectedly found nil while unwrapping an Optional value’. Even just passing and setting the text of one of the values would help, like an address for an example.

So if it was me, I’d pass any values like this (and this is an example from some of my own code) – so this is the prepare method where I pass the UUID:

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if(segue.identifier == "AddEditPhraseSegue"){
            if let editViewController = segue.destination as? AddEditViewController{
                editViewController.passedUUID = uuidForPass
            } else{
                print("Error defining target VC")
            }
        }
    }

and in the destination view controller:

class AddEditViewController: UIViewController {
    //MARK -- Class variables
    var passedUUID = String()
   
//rest of the class code down here

You could, in theory, do this with as many other variables as you wanted to pass along – define any values you want to pass as class members in the destination VC, set those values in the source VC, and then assign the values to your outlets in viewDidLoad or viewDidAppear in the destination VC.

Does that make sense?

Yes your response makes much more in the the if let statement, but my other concern is accessing the value I want to pass to the labels and textfields from my realm.

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let realm = try! Realm()
        let request = realm.objects(RenterRequest.self)
        
        if segue.identifier == "HistoryData" {
            if let renterMaintenanceHistory = segue.destination as? RenterMaintenanceHistory {
                renterMaintenanceHistory.renterHistoryAddress = request.sorted(byKeyPath: "requestAddress")
            }
        }
    }

class RenterMaintenanceHistory: UIViewController {
    
    var request = RenterRequest()
    
    @IBOutlet weak var RenterHistoryPriority: UITextField!
    @IBOutlet weak var renterHistoryType: UITextField!
    @IBOutlet weak var renterHIstoryAddress: UITextField!
    @IBOutlet weak var renterHistoryRoom: UITextField!
    @IBOutlet weak var renterHIstoryProblemDescription: UITextView!
    @IBOutlet weak var renterHistoryPreferredTimeAndDate: UITextField!
    @IBOutlet weak var renterHistoryPets: UISwitch!
    @IBOutlet weak var renterHistoryAuthorizedEntry: UISwitch!
    @IBOutlet weak var renterHIstoryPreferredContactNumber: UITextField!
    @IBOutlet weak var renterHistoryPreviouslyReported: UISwitch!
    

    var renterHistoryAddress: String?
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    override func viewWillAppear(_ animated: Bool) {
        renterHIstoryAddress.text = renterHistoryAddress
    }

}

When I try to access the value from my object I get the error 'Cannot assign value of type ‘Results’ to type ‘String?’ ’
Am i approaching this the wrong way? Or is it that I’m accessing my key the wrong way to get the value?

I also included the destination VC so you can see there too.

You’ve gotten the request variable loaded up with the results of the realm.objects(RenterRequest.self) , but that’s a Results set of values, not an individual value. So if you want to pass the entire list of values like this, in the destination VC you’d want to define your receiver as, so the type matches.

var request: Results<RenterRequest>?

Making it an optional, just in case your query doesn’t retrieve anything, so you can safely handle that, and then access it via a subscript in the destination VC, eg (where “nameOfAddressMemberInModelClass” is the name of that data element in your Realm model class, since you didn’t provide that). Obviously for brevity, I didn’t include a guard or an optional chain here, but you’d want to do that to prevent a potential crash.

override func viewDidLoad(){
    renterHistoryAddress.text = request![0].nameOfAddressMembertInModelClass
}

Either that, or if you’re sure about things, you could unwrap it in the pass off before you send it to the destination VC. I’ve done this a number of times where I’m sure there’s going to be a single response to the query.

renterMaintenanceHistory.renterHistoryAddress = request.sorted(byKeyPath: "requestAddress")![0].nameOfAddressMembertInModelClass

when I try to do

var request: Results<RenterRequest>? 

I receive an error that says Expected member name or constructor call after type name. If i apply the errors fixes it brings up more errors and I cant proceed with the rest of your suggestions. With that being said, would I need to pass an entire list of values if they arent going to another tableview but to a textfield instead? I’m going to go read the specific documentation on them after I post this too. for every segue that happens I would just like to pass specific values to the fields or labels I have connected to the destinationVC. Is this where a List is recommended?

Have you done an

import RealmSwift

in your destination VC? Even if you’re not calling any queries, you’ll need it to bring the Results datatype into play.

I misread your post at first and have since fixed the code in the destinationVC and now everything is fine. One last snag I seem to be having is

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let realm = try! Realm()
        let request = realm.objects(RenterRequest.self)
        
        if segue.identifier == "HistoryData" {
            if let renterMaintenanceHistory = segue.destination as? RenterMaintenanceHistory {
                renterMaintenanceHistory.renterHistoryAddress = request.sorted(byKeyPath: "requestAddress")![0].renterAddress
            }
        }
    }

the error ’ Cannot force unwrap value of non-optional type ‘Results’ ’

Did i mistype something here?

If you’re using the Results type as the receiver in the destination VC, you’ll want to keep your code as it was before:

renterMaintenanceHistory.renterHistoryAddress = request.sorted(byKeyPath: "requestAddress")

However, if you’re just going to pass the address, one minor correction to what I put up, use

renterMaintenanceHistory.renterHistoryAddress = request.sorted(byKeyPath: "requestAddress")[0].renterAddress

Without the unwrap bang after the sorted , that was my mistake, since your request is not declared as an optional, you don’t need the forced unwrap.

Okay so that worked with the code adjustment, but in my destinationVC a crash for the Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value in my viewWillAppear method.

override func viewWillAppear(_ animated: Bool) {
        renterHIstoryAddress.text = request![0].requestAddress
    }

Does this have to go into viewDidLoad? I read in a stack overflow post that it should go in viewDidAppear.
Okay so I moved the code to viewDidLoad and still got the same error, i know now it’s something I’m not doing right. Is the reason for the crash because it’s empty when viewDidLoad gets called or something?

If you’re pulling that exception, it would suggest that you’re not getting any data in the request object when you try to index into it. Next step would be to ensure that your query is actually returning a result in the source VC before you segue.

I took a project that I’m working on now and tried passing something in this exact fashion, and it worked okay, so I think it would be worth tracking back to how you’re triggering the segue, and making sure you’re getting the query fired off okay before passing the data along.

Are you triggering it off of a storyboard segue that’s attached directly to your prototype cell in your table view? The only reason I ask is that I didn’t see a didSelectRowAt delegate method in your original code. In my experience, this limits your ability to pass context specific stuff to the prepare function.

@analoguedarkness

Yes, the segue is attached directly to the cell. When I was reading around, i saw it was mentioned that didSelectRowAt wasn’t necessary, but if it would help then I can use that too.

 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            let realm = try! Realm()
            let request = realm.objects(RenterRequest.self)
            
            if segue.identifier == "HistoryData" {
                if let renterMaintenanceHistory = segue.destination as? RenterMaintenanceHistory {
                    renterMaintenanceHistory.renterHistoryAddress = request.sorted(byKeyPath: "requestAddress")[0].requestAddress
                }
            }
        }
    }

I tried using it here and and also trying to ensure my query returns a result

 override func viewDidLoad() {
        super.viewDidLoad()
        
        if request != nil {
           renterHIstoryAddress.text = request![0].requestAddress
        } else {
            print("empty cell")
        }
    }

this code actually ran, but the printout in the console returned an empty cell.

Admittedly I’ve run into this issue before using realm, butI haven’t had prior projects where I tried to pass data from a table view or viewcontroller to a detail view or other vcs. What would be the proper way to make sure I’m returning a result in the source vc before I segue? You mentioned trying it in a project you’re working on, would you be willing to share an example of how it’s properly done? I apologize for the constant questions and really appreciate the help so far, I’d just like a solid basis of understanding here so that once i get this right I can keep moving forward doing it the right way and avoiding these snags.

Here’s how I’m doing it – and my segue is connected at the class top level, not to the prototype cell – I’ve run into more trouble with the direct connection to the cell, so I’ve found approaching it more programmatically has been better for me. In that instance, you have to call the performSegue method in your didSelectRowAt and override the prepare method in your class body.

Here’s my model:

class PhraseObject: Object {

    @objc dynamic var phraseId: String = UUID().uuidString
    @objc dynamic var phraseSpokenBy: String = ""
    @objc dynamic var phraseText: String = ""
    @objc dynamic var phraseDate: Date = Date()
    
    override static func primaryKey() -> String? {
        return "phraseId"
    }
}

Here’s the didSelectRowAt delegate method:

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        uuidForPass = phrases![indexPath.row].phraseId
        
        performSegue(withIdentifier: "AddEditPhraseSegue", sender: self)
    }

And here’s the prepare method – the UUID pass is still in here since I didn’t want to kill that functionality, but the testPhraseObject is passing a Results collection based off of the existing result set that was acquired in viewDidAppear to populate the table view (since the unique PK for the model is that UUID, this filter is only going to return one result).

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if(segue.identifier == "AddEditPhraseSegue"){
            if let editViewController = segue.destination as? AddEditViewController{
                editViewController.passedUUID = uuidForPass
                editViewController.testPhraseObject = phrases?.filter("phraseId = %@", uuidForPass)
            } else{
                print("Error defining target VC")
            }
        }
    }

Here’s the viewDidLoad in the destination VC:

    override func viewDidLoad() {
        super.viewDidLoad()
        let syncConfig = SyncConfiguration(user: SyncUser.current!, realmURL: Constants.REALM_URL)
        //instantiate the realm
        self.realm = try! Realm(configuration: Realm.Configuration(syncConfiguration: syncConfig, objectTypes: [PhraseObject.self]))

        let testSomething = testPhraseObject![0].phraseText
        print("\(testSomething)")
        
        //if there's a passed UUID value, query the information and load it into the fields
        if(passedUUID.count > 0){
            existingPhrase = realm!.objects(PhraseObject.self).filter("phraseId = %@", passedUUID)
            whoSaidTextField.text = existingPhrase![0].phraseSpokenBy
            whenSaidTextField.text = formatDateForTextField(inputDate: existingPhrase![0].phraseDate)
            whatSaidTextField.text = existingPhrase![0].phraseText
            
            //change the commit button to the Update title header
            commitButton.setTitle("Update", for: .normal)
        } else{
            whenSaidTextField.text = formatDateForTextField(inputDate: fieldDate)
        }
    }

You can see the line I have in there for the testSomething variable, which assigns that variable the value of the phrase text from the model – I have a breakpoint right after this so you can see the value (since that’s easier in general to view in debug).

That said – I did some research today as well, and the other way I pull data (via just the UUID and querying in the destination VC) seems to be the generally agreed upon best practice.

You can see it both ways here, and decide how you want to move it forward.

@analoguedarkness

I’m trying to closely follow your example here so I will include my model code if that helps in any way.

import UIKit
import RealmSwift

@objcMembers class RenterRequest: Object {
    dynamic var requestId: String = UUID().uuidString
    dynamic var requestAddress = ""
    dynamic var previouslyReported = false
    dynamic var preferredTimeAndDate = ""
    dynamic var pets = false
    dynamic var authorizedEntry = false
    dynamic var preferredContactNumber = ""
    dynamic var requestPriority = ""
    dynamic var room = 0
    dynamic var problemDescription = ""
    dynamic var requestType = ""
    
    override static func primaryKey() -> String {
        return "requestId"
    }
}

At first I didnt have a uuid or a primary key because i wasnt sure if i needed one in order to pass the values I wanted.

 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        var uuidForRequest = renterRequestArray[indexPath.row].requestId
        
        performSegue(withIdentifier: "HistoryData", sender: self)
    }

I tried following your uuidForPass here, but I cantseem to use it outside the function in the prepareForSegue, which brings me here

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let realm = try! Realm()
        let request = realm.objects(RenterRequest.self)

        if segue.identifier == "HistoryData" {
            if let renterMaintenanceHistory = segue.destination as? RenterMaintenanceHistory {
                renterMaintenanceHistory.renterHistoryAddress = uuidForRequest
            }
        }
    }

as mentioned above I dont have access to my uuidForRequest variable here. Maybe I just need better context for using a uuid as opposed to accessing the variables I want. I wanted to test the segue first so i only used the requestAddress but as you can see I want to be able to pass any of the values of these variable to whatever view controller the need to be in.

class RenterMaintenanceHistory: UIViewController {
    
    var request: Results<RenterRequest>?
    
    @IBOutlet weak var RenterHistoryPriority: UITextField!
    @IBOutlet weak var renterHistoryType: UITextField!
    @IBOutlet weak var renterHIstoryAddress: UITextField!
    @IBOutlet weak var renterHistoryRoom: UITextField!
    @IBOutlet weak var renterHIstoryProblemDescription: UITextView!
    @IBOutlet weak var renterHistoryPreferredTimeAndDate: UITextField!
    @IBOutlet weak var renterHistoryPets: UISwitch!
    @IBOutlet weak var renterHistoryAuthorizedEntry: UISwitch!
    @IBOutlet weak var renterHIstoryPreferredContactNumber: UITextField!
    @IBOutlet weak var renterHistoryPreviouslyReported: UISwitch!
    

    var renterHistoryAddress: String?
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        if request != nil {
           renterHIstoryAddress.text = request![0].requestAddress
        } else {
            print("empty cell")
        }
    }

my destination vc hasnt changed at all either as I havent made it to here quite yet.

If it helps make sense of things I would like to pass the valuest from the model into the textfields of the destination vc and also labels in various other vcs throughout my app. probably would’ve made sense to just include the model from the beginning.

  • If you want your uuidForPass to be available between all member functions, declare it at the class level. You can do this or pass it by reference between functions. (example from my VC below, along with some other stuff)
class ItemsViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
    //MARK: -- Class Variables
    var realm: Realm?
    var phrases: Results<PhraseObject>?
    var notificationToken: NotificationToken?
    var uuidForPass = String()
    
    //MARK: -- Outlet Connections

From here, you’ll just make sure you filter the request so that it’s just grabbing the object for the tableView cell that you tapped.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let realm = try! Realm()
        let request = realm.objects(RenterRequest.self)

        if segue.identifier == "HistoryData" {
            if let renterMaintenanceHistory = segue.destination as? RenterMaintenanceHistory {
                renterMaintenanceHistory.request = request.filter("requestId = %@",uuidForPass)
            }
        }
    }

Your destination VC code should work from here to grab the address, and you should also be able to access all the other properties of this object to assign to the remaining fields.

cross fingers

@analoguedarkness

Well all the code compiled and my fingers were indeed crossed, but when I went tapped the tab to go to my vc with my table view, I got a crash.

the error says

Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=io.realm Code=10 "Migration is required due to the following errors:

and the line green instead of red.

I will past my code from view controller because I think it’s because I changed the numberOfRows function for the tableview.

class RenterRequestController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    @IBOutlet weak var renterHIstoryView: UITableView!
    
    var renterRequestArray: Results<RenterRequest>?
    var uuidForRequest = String()
    
    
    override func viewDidLoad() {
        super.viewDidLoad()

        fetchData()
        renterHIstoryView.reloadData()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return renterRequestArray!.count
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        uuidForRequest = renterRequestArray![indexPath.row].requestId
        
        performSegue(withIdentifier: "HistoryData", sender: self)
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cellIdentifier = "RenterProgressCell"
        
        guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? RenterHistoryCell else {
            fatalError("the dequed cell is not an instance of RenterHistoryCell")
        }
        
        let address = renterRequestArray![indexPath.row]
        let date = renterRequestArray![indexPath.row]
        
        cell.jobNumber.setTitle("1", for: .normal)
        cell.jobAddress.text = address.requestAddress
        cell.jobSummary.text = date.preferredTimeAndDate
        
        return cell
    }
    
    func fetchData() {
        let realm = try! Realm()
       
        
        renterRequestArray = realm.objects(RenterRequest.self)
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let realm = try! Realm()
        let request = realm.objects(RenterRequest.self)

        if segue.identifier == "HistoryData" {
            if let renterMaintenanceHistory = segue.destination as? RenterMaintenanceHistory {
                renterMaintenanceHistory.request = request.filter("requestId = %@", uuidForRequest)
            }
        }
    }

I’ll also include the code from my destination too in case I goofed up something there as well

class RenterMaintenanceHistory: UIViewController {
    
    var request: Results<RenterRequest>?
    var passedUUID = String()
    var realm: Realm?
    
    @IBOutlet weak var RenterHistoryPriority: UITextField!
    @IBOutlet weak var renterHistoryType: UITextField!
    @IBOutlet weak var renterHIstoryAddress: UITextField!
    @IBOutlet weak var renterHistoryRoom: UITextField!
    @IBOutlet weak var renterHIstoryProblemDescription: UITextView!
    @IBOutlet weak var renterHistoryPreferredTimeAndDate: UITextField!
    @IBOutlet weak var renterHistoryPets: UISwitch!
    @IBOutlet weak var renterHistoryAuthorizedEntry: UISwitch!
    @IBOutlet weak var renterHIstoryPreferredContactNumber: UITextField!
    @IBOutlet weak var renterHistoryPreviouslyReported: UISwitch!
    

    var renterHistoryAddress: String?
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        if (passedUUID.count > 0) {
           request = realm!.objects(RenterRequest.self).filter("requestId = %@", passedUUID)
            renterHIstoryAddress.text = request![0].requestAddress
        } else {
            print("empty cell")
        }
    }

    

}

That migration error is coming from the change you made adding the UUID to the model class, if you’re using a local, non-synced Realm, you have to do the migration yourself to accommodate the new database structure.

If you don’t have any real world data in there yet, I’d just reset your simulator to wipe out everything and start from scratch. You’ll want to read up on migrations in the documentation for later changes to your database objects though.

ah ok I just started reading the documentation and now see what you meant. So in your destination, in the viewDidLoad, the code that says

super.viewDidLoad()
        let syncConfig = SyncConfiguration(user: SyncUser.current!, realmURL: Constants.REALM_URL)
        //instantiate the realm
        self.realm = try! Realm(configuration: Realm.Configuration(syncConfiguration: syncConfig, objectTypes: [PhraseObject.self]))

this code takes care of the syncing and migration? are SyncUser and Constants.REALM_URL classes you created? I’m assuming i write the code for migration and syncing in my data model then?