I'm having trouble updating my view after editing CoreData.
I have this entry in my CoreData entity named "TST" and through a NavigationLink I'm editing it's name
NavigationLink(destination: editingPage(thread: tr.title)){
Text(tr.title)}
#State var thread : String
Form {
TextField("thread", text:$thread)
}.onDisappear{
do {
try self.managedObjectContext.save()
} catch {
print(error.localizedDescription)
}
}
after editing and saving the MOC It doesn't display the changes until I either restart the app, or go back to the beginning of my navigation View.
Should I have a fetch request on all page displaying CoreData's data? or having some kind of ObservedObject ?
What I'm looking for is the same as changing your device's name in the device settings menu.
Thanks
Tim
Related
I have a SwiftUI app with CoreData+CloudKit integrated. My data model is as follows:
MainElement <->> SubElement <-> SubElementMetadata
Each MainElement has one or more SubElements and each SubElement has exactly one SubElementMetadata.
Now I have a SwiftUI view to display the list of SubElements of a MainElement:
struct SubElementListView: View {
#ObservedObject private var mainElement: MainElement
init(mainElement: MainElement) {
self.mainElement = mainElement
}
var body: some View {
List(mainElement.subElements, id: \.self.objectID) { subElement in
VStack {
Text(subElement.title)
Text(subElement.elementMetadata.creationDateString)
}
}
}
}
The Issue:
When I update creationDateString of a SubElement somewhere in my code, the view updates to reflect the change. If, however, a change arrives over CloudKit, the view does not update.
If I navigate out and back into the view, the new data is displayed, but not while I stay on this view.
Why is this? I am aware I can probably trigger an update manually (e.g. by listening to CloudKit notifications), but I would like to understand why it doesn't "just work" - which is what I would have expected since NSManagedObject is also an ObservableObject.
Like in a lot of apps, I have a list of items (populated by a Core Data fetch request), a sheet to create new items, and a sheet to edit an item when tapping on a row in my list. I'm trying to unify both forms to create and edit an update, and put the cancel / save logic in a superview of the form.
So I've something like this:
ListView: a list with row populated by a Core Data fetch request
AddView: a NavigationView with the FormView embed + cancel and save button
EditView: a NavigationView with the FormView embed + cancel and save button
FormView: a TextField to update the name of the item
In the init() for the AddView, I create a new NSManagedObject without any context (I do that because I don't want my ListView to be updated when I create a new item in the AddView, but only when I save this item -> alternative could be to use a child context, or filter the fetch request results based on the isInserted or objectID.isTemporaryID of the return objects). AddView contains a NavigationView with the FormView embed, a cancel button, and a save button. This save button is disabled based on a computed property on the managed object (name for the object can't be nil).
In the EditView, I pass the item that was tapped from the ListView. This item is an existing NSManagedObject attached to the main viewContext of the app (coming from the fetch request of the ListView). EditView contains a NavigationView with the FormView embed, a cancel button and a save button (exactly like the AddView). This save button is also disabled based on the same computed property.
My issue is that when I update the name of the item from the TextField in my FormView, the condition to enable / disable the save button is not working for the AddView (this AddView is actually not refreshed when I change the item name from the FormView) but working for the EditView (this EditView is refreshed when I change the item name from the FormView). If I attach a context to the new NSManagedObject in the init() of the AddView, the condition is working like in the EditView.
So it appears that a NSManagedObject without any context is not observed by SwiftUI? Am I missing anything or is that a bug?
I wouldn't be surprised (but haven't verified) if the change-notifying ability of a managed object depends on the presence of the context. I can't think of a situation where you'd want to create a managed object without a context.
You should use a child context. The context does a lot of work for you in Core Data (managing relationships, probably change notifying, validation etc), and offers a simple way to cancel / save changes - just save the child context and the data flows back up into the main context, or discard the context to abandon.
A workaround to get the change notifications is to add this override to the NSManagedObject subclass:
override public func willChangeValue(forKey key: String) {
super.willChangeValue(forKey: key)
self.objectWillChange.send()
}
NSManagedObject could be subclassed to add this override (more info here and here)
We can be more specific on the update value if we don't want to trigger the change for every key. This will also work for relationships (not the case for the above solution).
func setName(_ name: String) {
objectWillChange.send()
self.name = name
}
In this case, my AddView updates even if the observed object does not have a context (change notifications are probably trigger only when a context exists for the object). The save button is disabled / enabled based on the following computed property in my NSManagedObject subclass.
var canBeSaved: Bool {
if self.name.isEmpty {
return false
} else {
return true
}
}
How can the string from a TextField be stored in UserDefaults using SwiftUI?
I have seen this tutorial on how to save the state of a toggle in UserDefaults and that looks promising, but I can't figure out how to use the same idea for storing the content from a TextField: https://www.youtube.com/watch?v=nV-OdfQhStM&list=PLerlU8xuZ7Fk9zRMuh7JCKy1eOtdxSBut&index=3&t=329s
I still want to be able to update the string by typing new text in the TextField, but unless I do that, I want the string to be maintained in the view. Both when changing page and completely exitting the app.
For things like these I suggest you use the .debounce publisher.
import SwiftUI
import Combine
class TestViewModel : ObservableObject {
private static let userDefaultTextKey = "textKey"
#Published var text = UserDefaults.standard.string(forKey: TestViewModel.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: TestViewModel.userDefaultTextKey)
}
}
deinit {
canc.cancel()
}
}
struct ContentView: View {
#ObservedObject var viewModel = TestViewModel()
var body: some View {
TextField("Type something...", text: $viewModel.text)
}
}
The .debounce publisher documentation says:
Use this operator when you want to wait for a pause in the delivery of
events from the upstream publisher. For example, call debounce on the
publisher from a text field to only receive elements when the user
pauses or stops typing. When they start typing again, the debounce
holds event delivery until the next pause.
You don't really want to save the text in the UserDefaults each time the user types something (i.e. for every character he/she types). You just want the text to be ultimately saved to the UserDefaults. The debounce publisher waits for the user to stop typing according to the time you set in the debounce init (in the example above 0.2 secs) and then forwards the event to the sink subscriber that actually saves the text. Always use the debounce publisher when you have a lot of events (for example new text typed in a text field) that may trigger "heavy" operations (whatever you can think of: something related to CoreData, network calls and so forth).
You can create a custom binding as described here and call UserDefaults.standard.set when the text binding is set.
struct ContentView: View {
#State var location: String = ""
var body: some View {
let binding = Binding<String>(get: {
self.location
}, set: {
self.location = $0
// Save to UserDefaults here...
})
return VStack {
Text("Current location: \(location)")
TextField("Search Location", text: binding)
}
}
}
Copied from answer to 'TextField changes in SwiftUI'
I have a hierarchical relation inside an entity X, I Have parent lookup which allow to give parent to a record of this entity, and I have created a Subgrid attached to this lookup within the same form of the entity:
The problem is that the display of the button + is unstable in this subgrid, sometimes it appears sometimes no. I dont know if this problem is related to some setting or it is a bug of dynamics crm online last version?
For information, I don't have this problem with other sub-grids.
Thanks in advance,
if you want to add a custom button you may do this as follows
function CreateButton() {
var connectionSubGridPlusBtn = document.getElementById("Connections_addImageButton").parentNode.parentNode;
//Connections_addImageButton is the id of + button
if (connectionSubGridPlusBtn != null) {
//New Button
var div = document.createElement("div");
div.className = "ms-crm-contextButton";
div.innerHTML = "<button id='newButton' type='button' style='width:80px;cursor: pointer;padding:0px' >New Button</button>";
connectionSubGridPlusBtn.appendChild(addVendorDiv);
//Event and url for new
document.getElementById("newButton").onclick = function () {
//Write codefor the button click event
}
}
}
call this function on load of the form
The entity has to be created before you're able to add related entities. You can add disable all required fields, and perform a save in the onload, and you should always see the plus sign.
A slightly better solution is to override the create button for the entity, and rather than directing to the create form, perform a rest entity creation, then direct to that form. Then you don't have to perform a save in the on load.
I'm a noob to monotouch and can't manage to add table views to flyoutnavigation controller. Does anyone have an example?
** update
My setup is a bit complicated and after much hacking managed to get the static table cells displayed on my storyboard with the below config.
tab bar controller
|
-tab 1
|
-tab 2 - nav controller
|
uiview controller A - flyout nav A --> uiview controller A1...A3
|
uiview controller B - flyout nav B --> tableview controller B1 (static cells)
I've cast the tableview controllers as uiview controllers on the flyout navs' setup thus allowing the tableviewcontroller to be displayed correctly and navigated to main uiview controllers via sergues. For example in the flyouts setup:
NavigationRoot = new RootElement ("FlyoutNavigationB") {
new Section ("Pages") {
new StringElement ("A Table View with static cells")
}
},
ViewControllers = new [] {
TableViewControllerB1 as UIViewController,
},
This hack seems to work but cleaner solutions are warmly invited (as I'm only 2weeks dev experience with monotouch) and feel a little saddened that I have uiview controllers sitting unattached on the storyboard without ability to do sergues via the flyout nav. It almost behaves like old school xib development.
Anyway I'm experimenting with tying this upto monotouch dialog now but not yet sure how. Comments most welcome to aid my learning :P
* update
Ok I think I got it working by creating a subclassed dialogviewcontroller. Then passing this subclass instance into the ViewControllers array in flyoutnavigationcontroller like so:
public partial class MyDvcController : DialogViewController
{
public MyDvcController (UINavigationController nav): base (UITableViewStyle.Grouped, null)
{
navigation = nav;
Root = new RootElement ("Demos"){
new Section ("Element API"){
new StringElement ("iPhone Settings Sample", DemoElementApi),
}
};
}
}
In my calling flyoutnav controller:
// Supply view controllers corresponding to menu items:
ViewControllers = new [] {
new UIViewController { View = new UILabel { Text = "Address txt" }, Title = "Address" },
new MyDvcController(NavigationController) as UIViewController
}
My wish list is almost complete...now how to add custom upickerviews with lovely transparent input accessory views wired to inputviews so they show automatically from calling monotouch dialog elements...hmmmm...another quest awaits...
Are you trying to push a second list over inside the flyout? I couldn't get it to work either.
I got it to work by modifying the source and redoing how the selection list is setup--but those changes are on my machine at work. I'll try to push those when I have a chance.