Items are not updated on UI after updating them in Core Data

Hi there!

I have an issue with UI when updating items in Core Data. Let's say I have three screens A, B and C where A --> B --> C. On screen A, I have a tableView with items from Core Data. On screen C, I have to make some changes on items of Core Data such as update or remove them. So, when I go back to screen A and refresh the tableView in order to get the updated list, the UI is not updated. I debugged the code and I can see that Core Data are correctly updated.

Do you know what the issue is and why the UI is not updated but Core Data is up to date?

Is it UIKit or SwiftUI ?

It's UIKit

How do you refresh TableView ?

You should

  • update the dataSource (an array that you populate from CoreData. May be the problem is here.
  • This array is used in func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { }
  • call reloadData

Could you show the code for VC A ?

@Claude31 The following code is on screen A when I am trying to fetch data from Core Data:

` func getAccounts() {
    
    self.accounts = []
    self.noAccountsCells = []
    
    let fetchRequest: NSFetchRequest<AuthentAccount> = AuthentAccount.fetchRequest()
    
    do {
        self.accountList = []
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "applicationName", ascending: true)]
        self.accountList = try context.fetch(AuthentAccount.fetchRequest())
        
      
        let key128   = "#############"                   // 16 bytes for AES128
        let iv       = "#############"                   // 16 bytes for AES128

        let aes128 = AES(key: key128, iv: iv)
        
        let accList = try context.fetch(fetchRequest)
        accList.forEach({ account in
            
            let decryptAccountName = aes128?.decrypt(data: account.accountName?.base32DecodedData)!
            let decryptApplicationName = aes128?.decrypt(data: account.applicationName?.base32DecodedData)!

        })
        
        self.accountList = try context.fetch(fetchRequest)
        
        
        if self.accountList.isEmpty {
            UserDefaults.standard.setNoAccountsState(value: true)
        }
        else {
            UserDefaults.standard.setNoAccountsState(value: false)
        }
        
        print("AccountList: \(accountList.count)")
        
        for account in accountList {
            
            guard let encryptedName = account.accountName, let encryptedIssuer = account.issuer, let encryptedAppl_name = account.applicationName, let encryptedSecretKey = account.secretKey, let encryptedDigits = account.digits, let encryptedPeriod = account.period, let encryptedCounter = account.counter, let encryptedOtpType = account.otpType, let encryptedAlgorithm = account.algorithm else {
                return
            }

            var otpType: OTPType = .totp
            var algorithm: OTPAlgorithm = .sha1
            
            let key128   = "###########"                   // 16 bytes for AES128
            let iv       = "###########"                   // 16 bytes for AES128

            let aes128 = AES(key: key128, iv: iv)
            
            
            let decryptAccountName = aes128?.decrypt(data: encryptedName.base32DecodedData)!
            let decryptIssuer = aes128?.decrypt(data: encryptedIssuer.base32DecodedData)!
            let decryptApplicationName = aes128?.decrypt(data: encryptedAppl_name.base32DecodedData)!
            let decryptSecretKey = aes128?.decrypt(data: encryptedSecretKey.base32DecodedData)!
            let decryptDigits = aes128?.decrypt(data: encryptedDigits.base32DecodedData)!
            let decryptPeriod = aes128?.decrypt(data: encryptedPeriod.base32DecodedData)!
            let decryptCounter = aes128?.decrypt(data: encryptedCounter.base32DecodedData)!
            let decryptOtpType = aes128?.decrypt(data: encryptedOtpType.base32DecodedData)
            let decryptAlgorithm = aes128?.decrypt(data: encryptedAlgorithm.base32DecodedData)
            
            if decryptOtpType == "HOTP" {
                otpType = .hotp
            }
            if decryptAlgorithm == "SHA256" {
                algorithm = .sha256
            }
            else {
                if decryptAlgorithm == "SHA512" {
                    algorithm = .sha512
                }
            }

            if self.accounts.isEmpty {
                self.accounts.append(AccountsTypeModel(account: nil, cellType: .empty))
            }
     
            self.accounts.append(AccountsTypeModel(account: AccountModel(account_name: decryptAccountName, issuer: decryptIssuer, application_name: decryptApplicationName!, secretKey: decryptSecretKey!, period: TimeInterval(decryptPeriod!), digits: Int(decryptDigits ?? "6") ?? 6, algorithm: algorithm, otpType: otpType, counter: Int64(decryptCounter!)), cellType: .cell))
            self.accounts.append(AccountsTypeModel(account: nil, cellType: .separator))
                            
        }
        
        if !self.accounts.isEmpty {
            self.accounts.append(AccountsTypeModel(account: nil, cellType: .empty))
        }
        
        self.accounts.forEach({ account in
            print("Accountss - \(account.account?.application_name)")
            print("Accountss - \(account.account?.account_name)")
        })
        
    }
    catch {
        // error
    }
}`

As I said before, on screen A I refresh the tableView on viewWillAppear method after I am calling the getAccounts method:

 override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
           
    self.getAccounts()
        
    if self.accountList.isEmpty {
      self.setup()
    }
    self.setupNavigationItems()
        
    DispatchQueue.main.async {
       self.tableView.reloadData()
    } 
}

Also, on UITableViewDataSource method I have this piece of code:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if UserDefaults.standard.noAccounts() {
        return noAccountsCells.count
    }
    else {
        return accounts.count
    }
}

fwiw, I still can't see enough of your code to determine the issue, but…

One reason that your code may not be seeing changes is that all three views are probably using the same view (main thread managed object) context, and you're saving changes on that same context, so there's no signaling that changes are being made.

If you're going to invest time into Core Data, I highly recommend mastering NSFetchedResultsControllers and using background contexts to change data (adds, edits, deletes). I even wrote an article about it! Hope it helps. https://medium.com/@deeje/practical-ios-architecture-coredata-cloudkit-lists-and-editors-638c87d382d3

@deeje So, as far as I understand you are suggesting to create a new context for the screens B and C and let the "main" context on screen A ?

On screen C, I have this piece of code to update an object.

func updateApplicationName(appl_name: String, secret_key: String, updatedApplication: String, updateAccount: String) {
    
    do {
        var account: AuthentAccount?
        
        let fetchAccount: NSFetchRequest<AuthentAccount> = AuthentAccount.fetchRequest()
        
        
        let key128   = "#############"                   // 16 bytes for AES128
        let iviv     = "#############"                   // 16 bytes for AES128

        let aes128 = AES(key: key128, iv: iviv)
        
        
        let encryptedSecretKey = aes128?.encrypt(string: secret_key)?.base32EncodedString
        let encryptedApplicationName = aes128?.encrypt(string: appl_name)?.base32EncodedString
        let encryptedUpdatedApplicationName = aes128?.encrypt(string: updatedApplication)?.base32EncodedString
        let encryptedUpdatedAccount = aes128?.encrypt(string: updateAccount)?.base32EncodedString
        
        
        
        let applicationNamePredicate = NSPredicate(format: "applicationName = %@", encryptedApplicationName!)
        let secretKeyPredicate = NSPredicate(format: "secretKey = %@", encryptedSecretKey!)
        let andPredicate = NSCompoundPredicate(type: NSCompoundPredicate.LogicalType.and, subpredicates: [applicationNamePredicate, secretKeyPredicate])
        fetchAccount.predicate = andPredicate
        
        let results = try? context.fetch(fetchAccount)
        
        if results?.count != 0 {
            // here you are updating
            account = results?.first
        }
        
        account?.applicationName = encryptedUpdatedApplicationName
        account?.accountName = encryptedUpdatedAccount
        
        try context.save()
        
    }
    catch {
        
    }
    
    self.accountSettingsDelegate?.updateApplicationName(text: self.appl_name)
    self.accountSettingsDelegate?.updateAccountName(text: self.acc_name)
}

Could you show the code for:

  • tableView(cellForRow)

Are you sure viewWillAppear is called when you return ? It may not be called, because willAppear means it appears in View hierarchy, not on screen.

To test, add a print:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
           
    self.getAccounts()
        
    if self.accountList.isEmpty {
      self.setup()
    }
    self.setupNavigationItems()
        
    DispatchQueue.main.async {
       self.tableView.reloadData()
    } 

  print("viewWillAppear called")  // <<-- ADD THIS
}

If not called (no print in log when you return from C), you need to change the pattern, for instance sending notification from View C when leaving and subscribing to this notification in View A.

Items are not updated on UI after updating them in Core Data
 
 
Q