Persist changes to core data item from SwiftUI view - swift

I'm working on a SwiftUI app that is to display a number of entries from a Core Data database. Basically just about what the XCode template for such apps implements.
I've made two changes:
I put the code for the list item view into a separate file (BookmarkableItemView)
I added a property bookmarked to the item, which I called BookmarkableItem
The code in my view to show the list of items is now:
List(bookmarkableItems) { item in
BookmarkableItemView(item: item)
}
In BookmarkableItemView, the item property is an #ObservedObject.
Now, my BookmarkableItemView also contains a button that should toggle the bookmarked property on the item that the view represents. All fine and well - this is easy. But now I have to persist the changes...
I can think of the following two solutions:
Pass an action (simple a (BookmarkableItem) -> Void) to the BookmarkableItemView that's then called when button in the view is pressed
Have the view itself save the managed object context
The latter is a no-go in my opinion. The first solution works. There's now an bookmarkAction property in the BookmarkableItemView, so the above code looks like that:
List(bookmarkableItems) { item in
BookmarkableItemView(bookmarkAction: { toggleBookmark(item) }, item: item)
}
The action is called by the BookmarkableItemView and as a consequence, the toggleBookmark(_ item:) method is called.
But somehow I feel that I'm missing something about bindings in SwiftUI that would make the process easier? How can I have my parent view get a notification when a sub view performs changes to the data model so I can persist them?

onDisappear check if item.hasChanges
or use onChange on item.hasChanges

Related

In SwiftUI, how can I avoid redrawing a parent view while updating shared state data from a child view to avoid Navigation issues?

Since Xcode 12.5, I am seeing a lot of "Unable to present. Please file a bug." console logs in the Xcode console which I remember not seeing prior to 12.5.
This message is shown when I am using NavigationLink from a parent view to navigate to a child view, and if logic in the child view updates one of the states that parent view depends on.
Below is a sample pseudo code where a list of messages are shown in the parent view, message detail is shown in the child view from a list, and lastly, independent settings child view.
struct MessageListView: View {
...
#StateObject var messageList = MessageList()
var body: some View {
debugPrint("!!MessageListView has been redrawn!!")
return VStack {
NavigationLink(destination:SettingsView()){
Text("Go to settings")
}
ForEach(messageList.data.sorted(by: {$0.key < $1.key}), id:\.key) { k, m in
NavigationLink(destination:MessageView(message:...){
Text(m.text)
}
}
}
}
}
struct MessageView: View {
...
#ObservedObject var messageList : MessageList
#ObservedObject var message : Message
...
var body: some View {
Text(...)
.onAppear {
messageList.readAndIncrement(message.id) //<- This updates both this view & parent view.
}
}
}
class MessageList : ObservableObject {
#Published var data : [String:Message] = [:]
func readAndIncrement(id: String){
//Modify one of the message in dictionary.
}
}
So when user clicks on message and traverses the navigation like below,
MessageListView -> MessageView
As soon as MessageView appears on screen, it will increment message's "read count" due to at the logic in onAppear, which will update data in MessageListView at the same time.
As soon as that happens, it appears that the parent view, MessageListView which is observing MessageList objects gets updated, the two following things happen.
debugPrint will print message !!MessageListView has been redrawn!! (from source code above) on console, which proves that the parent view has been updated while Navigation is currently showing child view.
'Unable to present. Please file a bug.' console log gets shown on screen, probably because the update in the parent view "cannot" be presented on screen?
So SwiftUI seems to be throwing "Unable to present" error log when I am updating parent view's observed data while viewing & interacting in the child view, but I am unsure how I can properly fix to get rid of this error.
The reason that I am thinking this is not a bug, but my error is because of the following.
When user traverses into a completely different view, something like
MessageListView -> SettingsView
and if the following two conditions are met,
The child view (SettingsView) does not use or rely on any of the parent view's state objects.
Parent view's data model is updated by other means, such as from network updates/sync, periodic refresh, etc.
then the parent view (MessageListView) gets redrawn for its dependent model's every single update and the child view starts stacking itself like below.
At least, in the above case, Unable to present. Please file a bug. doesn't get shown in the console.
In the iOS 14.5 & Xcode 12.5 patch notes, the only comments related to NavigationLink is as follows, but that does not seem to be relevant to my case.
The destination of NavigationLink that only differs by local state now resets that state when switching between links as expected. (72117345)
So my question is... how can I properly manage states (or decouple them) in this case that won't cause funkiness in SwiftUI Navigation?
Is it against SwiftUI's paradigm to update parent view's state data while presenting child view on screen or is this simply a NavigationLink bug in Xcode 12.5?
I am answering my own question about the workaround that I found. The solution that worked for me is to use Class instead of Struct and update properties of the Class. This prevented the View, which is watching the #Published array/list, from getting updated.
Previously, I was using Struct for the Message type. What ended up happening is that any change to one of the properties of the struct/Message will trigger UI updates (which is expected), even if View is not directly watching the struct itself, but watching the list of structs (MessageList class's data property in my case.).
I converted Message from Struct to Class that implements ObservableObject and updates its properties. That prevented parent View, which dependent on the list of Messages from getting redrawn, when one of the list elements had to be updated.
In summary, when you assign #Published annotation to an array of Struct, list item addition/removal as well as any change to one of the properties of the struct item in the list will trigger dependent View updates in SwiftUI.
On the other hand, if you assign #Published annotation to an array of Class, only list item addition/removal will trigger SwiftUI update. Any change to one of the properties of the Class won't trigger dependent View updates in SwiftUI.

Why can't I send data between interface controllers for Apple Watch?

Basically, what I'm trying to do is pass an integer from one interface controller. The slider in the controller "SecondPage" will get an integer value from the slider and should send it to the interface controller called "ThirdPage" and set the title of that label to the value. I've tried so many methods but all to no avail. I'm just trying to send the context from the 2nd interface controller, receive it in the awake(withContext context: Any?) method in the 3rd controller and save it as the title until the slider is changed again.
Here's what I tried first in the second page controller, but it didn't pass any data:
override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
if segueIdentifier == "ThirdPage"{
return sliderval
}
return nil
}
here's what I tried after that, it passed the data but it doesn't "save" it. It pops up the interface controller with the updated label to the slider value, but I don't want it to pop up, I want that value to be saved to the label. When I close the pop up, and swipe right to the Third Page, the label is still called "Label"
self.presentController(withName: "ThirdPage", context: sliderval)
Here's some pictures of the output as well as some of my code. Any help is appreciated, thank you. PS: This is all in Swift 3
You are using a page-based navigation, and you need to pass data between pages.
What I would do is save the data from the slider somewhere that the third page can access, and populate the data in the label on willActivate. The willActivate method should be called by the system when the user swipes between pages.
At the global scope, create a variable to hold the slider value.
var sliderValue: Float = 0
In your second interface controller, set this value when the slider changes.
#IBAction func sliderChanged(_ value: Float) {
sliderValue = value
}
In your third interface controller, when the page is about to appear, set the label to the correct value.
override func willActivate() {
label.setText("\(sliderValue)")
}
If you want to avoid the use of global variables you can read and write from user defaults, or use other tactics, but this will depend on the exact needs of your application.
Your undesired popup problem is happening because you're calling presentController() in sliderChanged(). Every time the slider changes, you're telling it to pop up the "ThirdPage" controller. You can just delete that.
Edit: My answer incorrectly assumed the asker is working with hierarchical navigation. For page-based navigation, there is apparently no way to pass data from one page to the next, so you're stuck with using a global as the accepted answer suggests. The original answer follows:
As for the lack of passing context, I would guess you probably don't have the segue identifier set in Interface Builder, or it's set to something other than "ThirdPage". The segue is separate from the "ThirdPage" controller, and you have to set its identifier separately. Click on the circle in the middle of the segue arrow and take a look at it in the Attributes inspector.
Assuming I'm right about that, the problem is that contextForSegueWithIdentifier() is being called with an identifier that you don't expect. Since it's not "ThirdPage", the conditional fails and it returns nil. Setting the segue identifier should fix it.

A master/slave panel in Extjs 5 MVC

I'm using Extjs5.1 powered by a MVC oriented code style.
I've got a main view which inherits from Ext.panel.Panel with a border layout.
On the east region, there's a grid with a store containing several records (or "models", I don't really know what terminology I should use here). (the "master grid")
On the center region, there is another view that inherits from a Ext.form.Panel and which is supposed to display the selected item of the grid . (the "slave form")
My goal is to refresh the "slave form" with the selected record of the "master grid".
The only way I found to "communicate" between the grid and the form is to execute a fireEvent('selectRecord', ...) from the main view controller and to listen to him inside the form view controller, but it seems odd as the form view is a child item of the main view.
Is there a more common way to do that?
By corrolary, is it a fine practice to make a view call functions of another view directly or should I make only their respective controllers interact?
What I usually do and I believe is the most common approach for this, is having a selectionchange event listener, that updates your form like this:
listeners : {
selectionchange: function(model, records) {
var rec = records[0];
if (rec) {
formpanel.getForm().loadRecord(rec);
}
}
}
for this to work, the name property of your form fields must match the name of the fields in the grid store model.
There is an example of this here: http://dev.sencha.com/extjs/5.1.0/examples/kitchensink/#form-grid

MvvmCross DataBinding To Modify Individual Item in Android ListView

I am trying to dynamically modify the items in a List (ObservableCollection) of a ViewModel and have those changes get updated in the View via MvvmCross bindings. My eventual goal is that when a user clicks on a list item, I will pop up a dialog asking them to edit that item. When the dialog is dimissed, the viewmodel will get updated (through an ICommand I assume) and that modified value will be now be in the list.
I haven't looked into dialogs yet, so for now I am just trying to toggle a boolean value each time a list item is clicked and have that value changed in the MvxListView. I have the MxvListView in my View bound to an ObservableCollection in my ViewModel and have an MvxCommand that is getting called when an item is selected. All this is working and I can see the value getting changed in the debugger, however, the new values are not being displayed in the MvxListView. So my question is: How do I get modified data in individual items in an ObservableCollection to bind to an MvxListView?
All of the examples I have seen online use ObservableCollection for dynamic binding but they only ever Add or Delete items. I haven't found any examples of modifying the items. If I change the code in my MvxCommand from modifying the data to adding or deleting an item, the list will get updated. So that tells me I'm close I think.
Rather than copy paste the code in here, I created a sample project on github here to look at:
https://github.com/smulrich/breaktimer
I appreciate the help.
You can simply replace
Breaks[index] = b;
with
Breaks[index] = new DailyBreak() { Reason = b.Reason, TimeOfDay = b.TimeOfDay, Enabled = b.Enabled };
or more reasonable, you should realize INotifyPropertyChanged for class DailyBreak
Diffrent among List, ObservationCollection and INotifyPropertyChanged, please refer to enter link description here

Can i have Multiple ViewModels being held in a ViewModel?

I was wondering what your thoughts are on having a ViewModel containing a collection of other ViewModels.
For example if i have a stock price screen. In the MainView i want to be able to selct a stock ticker. When i press the ADD button on the MainView it should display a new stock price in the MainView.
My question is about how the add button should work?
Which of the two options should the Add button on the MainView do:
1) Pass the stock ticker (MSFT) to StockPriceService. The StockPriceService will retrive a StockPrice object. I can then pass the StockPrice object into a StockPriceViewModel. The MainViewModel will contain a collection of StockPriceViewModel.
2) pass the stock ticker(MSFT) into the StockPriceViewModel. The StockPriceViewModel will be implemented to call the StockPriceService and retrive the StockPrice object. The the StockPrice object will then be wrapped in the StockPriceViewModel.
Thanks,
CA
If the Add button is part of the MainView then it's events really should be handled by the MainViewModel and access to data for the StockPriceViewModel and thus StockPrice object, by the StockPrice related code.
The way to look at this is that you should still be able to display a StockPriceViewModel with populated data without recourse to any code in any other ViewModels, but in this case it is the MainViewModel that triggers the creation/instantiation of a new StockPriceViewModel etc.
So that would mean go with mostly method 2, say passing an initialisation value to the StockPriceViewModel constructor.