Prevent/Postpone tableview updates from NSFetchedResultsController when table is not visible - swift

Xcode 11.0
Swift 5.1
I'm working on a "simple" app using Core Data.
The single Core Data entity is named Topic and comprises Title (String), Details (String) and isFavorite (Bool)
I'm using a custom UITabBarController with 3 tabs - Random Topic, Favorite Topics, and All Topics
Favorite Topics and All Topics are UITableViews sharing the same subClass, TopicsViewController.
Using custom TopicTabBarController I set a property in TopicsViewController, based on the tab, that determines whether or not a predicate is used with the FetchRequest for the two tabs sharing this controller.
This works as expected, even with FRC cache (I'm very new to Swift!)
If a topic is favorited on any view, the tableViews in both Favorite Topics and All Topics update to reflect that change. This is precisely how I want it.
The trouble is I'm getting a warning about the tableView being updated while not visible:
UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window).
I set a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy but making sense of that is a bit above my head at the moment.
I tried returning early from the FRC Delegate methods if the view wasn't loaded but that was clearly not the solution. Like I said, this is all new to me.
I thought setting the FRC Delegate to nil and back to self as suggested in this post would help, but that only prevents the managedObjectContext from saving (at least that's what I'm seeing with Navicat). I can step through the code and see that the proper instances of the controller class are being manipulated based on the custom property I set for the predicate.
Here's the related code for that part:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.fetchedResultsController.delegate = self
self.performFetch()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.fetchedResultsController.delegate = nil
}
// MARK:- Helper methods
func performFetch() {
do {
try fetchedResultsController.performFetch()
} catch {
fatalCoreDataError(error)
}
}
Here's where the entity is saved, which is not happening with the above code in place:
#objc func toggleFavorite(_ sender: UIButton) {
let buttonPosition = sender.convert(sender.bounds.origin, to: tableView)
if let indexPath = tableView.indexPathForRow(at: buttonPosition) {
let topic = fetchedResultsController.object(at: indexPath)
topic.isFavorite = !topic.isFavorite
try! managedContext.save()
}
}
Here's a Gist with more details. I find it much easier to read this way.
I also just came across this post on the Apple Dev forum. Looks like the same issue, unresolved.
I honestly don't know if that's too much or too little information. Was really hoping I'd stumble on the solution while trying to explain the problem.
Thanks for your time.

For what it's worth, I was getting the same warning and I was able to fix it by doing something very similar to what you tried. Basically the only difference is the tableView.reloadData().
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchedResultsController.delegate = self
tableView.reloadData()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
fetchedResultsController.delegate = nil
}
... though I would think that performing a fetch would work just the same?

Related

TableView not loading when tapped with Tab Bar Swift

When transitioning from the details tab to the tags tab, the table view is not loading. I know because in the view did load I had it print a comment but the comment and the rest of the code is running or loading. Even though the table view itself is appearing. Sorry if this is too vague , I'm not sure what I need to provide so please let me know if you need more info to help me
When you click on a tab it's loaded and maintained in memory. It doesn't require to be reloaded. If you want to load the data again move the data loading code to viewDidAppear like this:
class ViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadData()
}
func loadData() {
// Code to load data
}
}
Where you get data from Api reload your tableView tableview.reloadData() there on Main thread using DisaptchQueue.main.async {}
// got response from Api
DisaptchQueue.main.async {
tableView.reloadData()
}

Reusing view controllers in Swift without hacked property

I've been looking through a Coordinator tutorial and it brought up a problem with code I've written in the past.
Namely, when reusing a view controller I've used a property to be able to display different elements depending on which view controller the user arrived from. This is described in the above tutorial as a hack.
For example I segue to labelviewcontroller using
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "label" {
let vc = segue.destination as! LabelViewController
vc.originalVC = self
}
}
and then on labelViewController have a property
var originalVC: ViewController?
which I then change the items in viewDidLoad() through
override func viewDidLoad() {
super.viewDidLoad()
if originalVC != nil {
label.text = "came direct"
imageView.isHidden = true
}
else {
label.text = "button"
imageView.isHidden = false
}
}
I've a working example project here: https://github.com/stevencurtis/ReusibilityIssues
Now, I know the answer might be use the Coordinator tutorial, but is there any other method that I can use to simple reuse a viewController for two different circumstances, rather than using a property or is there anyway to clean this up to be acceptable practice?
You can do that without passing originalVC just by checking parent type if you are pushing it inside a navigation controller like this :
if let p = parent {
if p.isKind(of: OriginalViewController.self){
//it pushed in navigation controller stack after OriginalViewController
}
}
but is there any other method that I can use to simple reuse a viewController for two different circumstances
If the "two different circumstances" you describe are very different (by this I mean "require very different lines of code to be run"), then you should create two different view controller classes, because otherwise you would be violating the Single Responsibility Principle.
If your "two different circumstances" are different, but also quite related, then you can just have all the information that the VC needs to know as properties. You certainly don't need a whole ViewController.
For example, if your LabelViewController will show a "foo" button only if it is presented by ViewControllerFoo.
You can add a showFooButton property in LabelViewController:
var showFooButton = false
override func viewDidLoad() {
fooButton.isHidden = !showFooButton
}
And then in ViewControllerFoo.prepareForSegue:
if segue.identifier == "label" {
let vc = segue.destination as! LabelViewController
vc.showFooButton = true
}
I wouldn't call this a hack. This is the recommenced way described in this post and they didn't call it a hack.

Way to check data modification across many viewcontroller without using global variables?

I have an app which contains several viewControllers. On the viewDidAppear() of the first VC I call a set of functions which populate some arrays with information pulled from a database and then reload table data for a tableView. The functions all work perfectly fine and the desired result is achieved every time. What I am concerned about is how often viewDidAppear() is called. I do not think (unless I am wrong) it is a good idea for the refreshing functions to be automatically called and reload all of the data every time the view appears. I cannot put it into the viewDidLoad() because the tableView is part of a tab bar and if there are some modifications done to the data in any of the other tabs, the viewDidLoad() will not be called when tabbing back over and it would need to reload at this point (as modifications were made). I thought to use a set of variables to check if any modifications were done to the data from any of the other viewControllers to then conditionally tell the VDA to run or not. Generally:
override func viewDidAppear(_ animated: Bool) {
if condition {
//run functions
} else{
//don't run functions
}
}
The issue with this is that the data can be modified from many different viewControllers which may not segue back to the one of interest for the viewDidAppear() (so using a prepareForSegue wouldn't work necessarily). What is the best way to 'check' if the data has been modified. Again, I figured a set of bool variables would work well, but I want to stay away from using too many global variables. Any ideas?
Notification Center
struct NotificationName {
static let MyNotificationName = "kMyNotificationName"
}
class First {
init() {
NotificationCenter.default.addObserver(self, selector: #selector(self.notificationReceived), name: NotificationName.MyNotificationName, object: nil)
}
func notificationReceived() {
// Refresh table view here
}
}
class Second {
func postNotification() {
NotificationCenter.default.post(name: NotificationName.MyNotificationName, object: nil)
}
}
Once postNotification is called, the function notificationReceived in class First will be called.
Create a common global data store and let all the view controllers get their data from there. This is essentially a global singleton with some accompanying functions. I know you wanted to do this without global variables but I think you should consider this.
Create a class to contain the data. Also let it be able to reload the data.
class MyData {
static let shared = MyData()
var data : SomeDataType
func loadData() {
// Load the data
}
}
Register to receive the notification as follows:
static let dataChangedNotification = Notification.Name("DataChanged")
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidLoad() {
super.viewDidLoad()
// Establish a way for call activity to notify this class so it can update accordingly
NotificationCenter.default.addObserver(self, selector: #selector(handleDataChangedNotification(notification:)), name: "DataChanged", object: nil)
}
func handleDataChangedNotification(notification: NSNotification) {
// This ViewController was notified that data was changed
// Do something
}
func getDataToDisplay() {
let currentData = MyData.shared.data
// do something
}
// Any view controller would call this function if it changes the data
func sendDataChangeNotification() {
let obj = [String]() // make some obj to send. Pass whatever custom data you need to send
NotificationCenter.default.post(name: type(of: self).dataChangedNotification, object: obj)
}

Realm, best approach to save\update\Observe String to Realm Object

(looking for the best approach to save Realm property.
I have a UIViewController with a lot of TextView, etc that I fill from a Realm object.
Each time the textfield are modified, I need to send back to change un the realm property.
The (not cool) thing, are that I cannot save directly, I have to open a write transaction.
object.propertyA= “hello” // crash
try! realm.write { //work
userBeer?.Name = lblbeerName.text!
}
So, i found a bit painfull (and not clean) to to that for all text.
I’ve looked at rxRealm, but cannot see any (newbies) sample to make that.
So, I have 2 approach un mind
Modify the model getters and setters for the property
var beerName: String? {
get {
return self.Name
}
set {
try! realm.write {
self.txtName=beerName!
}
}
use the RXSwift approach from here (https://www.raywenderlich.com/149753/bond-tutorial-bindings-swift)
Bing the TextField.text to a var String, and observe this string to write.
What do you think?
My perfect world will be to find a way to bing the TextField.text property directly, something like:
myRealmObject.property.BindTo(self.txtName)
Katsumi from Realm here. Although it is not the best approach, I propose another way. You can use realm.beginWrite() and try! realm.commitWrite() instead of block-based API for a long-lived transaction.
For example, you can open a transaction when the view appeared, and then close the transaction when the view disappeared, like the following:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
realm.beginWrite()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
try! realm.commitWrite()
}
In this way, you can assign a value to the Realm object at any time in the view.
Be careful not to leave the transaction open. To avoid increasing file size, do not update data frequently in the background while transactions are open.
Change the property within the write block:
try! realm.write {
object.propertyA = “hello”
userBeer?.Name = lblbeerName.text!
}

Automatically Reload TableViewController On Rewind

I am working on an app where it starts out at a tableViewController which loads and displays data stored in a Realm database. I have it so I can create a new entry in my Realm database in a separate scene and the save button unwind segues back to the initial tableView.
I current have it so the tableViewController will reload the tableView on pull down (something I Learned here, second answer down) but I would be better if the tableView would reload its self automatically upon unwind, displaying all the data in my database, including my new entry. Could someone please direct me to a tutorial that will teach me how this is done.
Additional info: My app is embedded in a navigation controller. The save button is located the bottom tool bar.
You can use NSNotification for that.
First of all in your tableViewController add this in your viewDidLoad method:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "refreshTable:", name: "refresh", object: nil)
}
And this will call one method from your class:
func refreshTable(notification: NSNotification) {
println("Received Notification")
tableView.reloadData() //reload your tableview here
}
So add this method too.
Now in your next view controller where you add new data into data base add this in you unWindSegue function:
NSNotificationCenter.defaultCenter().postNotificationName("refresh", object: nil, userInfo: nil)
Hope it will help
Try reloading your tabledata in viewWillAppear in initial (tableview)controller.
override func viewWillAppear(animated: Bool) {
self.tableView.reloadData()
}
Or call again the function through which you are loading your data from Realm like
override func viewWillAppear(animated: Bool) {
getMyData() //or whatever your function name is
}
If you are using storyboard unwind segue, try using
func unwindSegue(segue:UIStoryboardSegue) {
if segue.identifier == "identifier" {
getData()
self.tableView.reloaddata()
}
}