In a typical list -> details SwiftUI app, what is the best way to create new elements? - swiftui

In a typical list -> details SwiftUI view, I have basically an Array of objects, and for read/edit, I can easily bind these (using #Binding on the view) to view and edit the elements in the array.
What about adding new elements?
I would like to re-use my views for editing; but they expect an #Binding as I mentioned. How do I transition to this view if I want to allow the user to add a new element to the list?
One option is that I can pre-create an object before loading the view, and then binding the view to the new element. However, this makes "cancel" clunky (now I have to remove from the list). Also, it's not clear how to inject this logic (create a new element) in a NavigationLink.
Another option is that I can pass the list to the view and bind a constant empty object, and in the view I can manage adding the new element to the list upon successful creation.
What is the recommended approach to this? I see a lot of tutorials on how to edit and view lists, but not on how to add.

Sounds like you need a backing database. I used Apple's Core Data to add and retrieve stored objects to/from. Here's the guide I used: https://www.hackingwithswift.com/books/ios-swiftui/how-to-combine-core-data-and-swiftui

I figured out a way to do this that is quite nice.
What I've done is that in the list (say, LandmarkList), I added a default LandMark element (which represents the new element to add).
#Published var newLandMark : Landmark
I wrap my view with a new view, which binds against the list:
struct NewLandmarkView : LandmarkDetail {
#Binding var landmarkList : LandmarkList
}
This view adds buttons for save and cancel. Add takes the newLandmark and adds it to the list.
I then use the following to show modally (you can navigate to it if you want instead):
Button(action: {
// In the folder list view, we'll add a note to the "notes" folder
self.showModal = true
}) {
Image(systemName: "square.and.pencil")
}.sheet(isPresented: self.$showModal) {
CreateLandmarkView(landmarkList: self.$landmarkList)
}
This worked pretty well for me as a pattern.

Related

How does the pattern of assigning `#State` in `onReceive` ensure that view state is in sync?

I'm having a little trouble with the following pattern which integrates Combine publishers into SwiftUI so that view state is updated when publishers emit:
struct ItemList: View {
var publisher: AnyPublisher<[Item], Never>
#State private var items = [Item]()
var body: some View {
List(items) { item in
ItemRow(item: item)
}
.onReceive(publisher) {
items = $0
}
}
}
Above example from Swift by Sundell
I feel like I'm missing something when I read it.
Let's assume you initialize items to the correct (at that time) value. What ensures that the published value won't change between the creation of ItemList and the first call to body, where it first starts listening to changes? Or if there is no such guarantee, then what else is preventing the view from ending up in the wrong initial state because of this?
Consider a NavigationLink:
NavigationLink(
destination: { ItemList(publisher: myPub) },
label: { Text("Show List") }
)
Here we have a case where SwiftUI creates the ItemList immediately, but doesn't ask the ItemList for its body until the user taps the link.
(How do we know it creates the ItemList immediately? The destination argument is not declared #escaping, so SwiftUI has to call it inside the NavigationLink initializer.)
So in fact there is a real risk in this case that items should change between when the ItemList is created and when it appears on screen.
We solve this by using a publisher like CurrentValueSubject that publishes its current value immediately to each new subscriber. That way, it doesn't matter how much later SwiftUI decides to use the view. As soon as SwiftUI uses the view, it subscribes to the publisher and immediately gets the current value. SwiftUI can handle that update before updating the framebuffer, so the user doesn't see a flash of incorrect data.
We need to read it in sequence:
State is initiailzed, supposing items = [Item1, Item2, Item3]
body is called to render view
List is constructed with current items, ie. List([Item1, Item2, Item3])
onReceive is called on constructed List of 3) and creates view around that list with subscriber to publisher
subscriber requests current events from publisher
if there are events in publisher then onReceive's closure handler is called (see below) otherwise no changes and List of 3) is shown on screen
6.1. if handler gets same initial [Item1, Item2, Item3] (subscriber extracts all available items) then state is not changed and List of 3) is shown on screen
6.2. if handler gets different items [ItemX, ItemY] then state change invalidates view and List is rebuilt with [ItemX, ItemY] which are shown on screen (there is no cycling because refresh is synchronous and we get into 6.1 at second pass).
That's simplified logic of provided code snapshot.

SwiftUI changing environment object re-created observed object in same view

So, I have few steps, last one contains EnvironmentObject and ObservedObject. The issue is, when I try to modify EnvironmentObject (lane 68) it re-creates ObservedObject.
Can any one explain me why this happens? Any solution to keep my ObservedObject with original state?
As far as I know it possible to change ObservedObject to StateObject, but I am using iOS 13+ so... I need other solution.
Line 47 - body is reevaluated so new instance of ObservedStuff is created, so make it as property and pass it in, like
struct TestView_A: View {
...
private let model = ObservedStuff()
var body: some View {
NavigationLink(destination: TestView_B(viewModel: self.model) ...
}
}

How to detect changes to a property of a core data object

I have this core data entity called Cake, that has properties like:
numberOfIngredientsBeingUsed
numberOfIngredientsTotal
color
weight
I read one object from the entity and now I have something like this
let orangeCake = Cake(type:"orange", context:coreDataContext)
Now I want to show a ProgressView. I want to pass the progress view, a binding numberOfIngredientsBeingUsed property, representing the current number of ingredients so far being used and a regular property numberOfIngredientsTotal, representing the total number of ingredients required to make the cake.
The idea is that when numberOfIngredientsBeingUsed changes on the main view, the progress view updates, because it is a binding property.
How do I do that? I cannot wrap my brain around, because numberOfIngredientsBeingUsed is a property of the cake object orangeCake.
CoreData managed objects are type of ObservableObject, so instead of passing one property you need to inject entire instance of Cake as ObservedObject and use its properties inside ProgressView, which will update (and refresh view) whenever corresponding object updated somewhere in other part of code.
So it should look like
struct ProgressView: View {
#ObservedObject var model: Cake
var body: some View {
Text("Progress: \(model.numberOfIngredientsBeingUsed) of \(model.numberOfIngredientsTotal)")
}
}

Flex How can I change item in dataProvider when Items is selected or deselected in the mx:List

Flex How can I change item in dataProvider when Items(multiple selection) is selected or deselected in the mx:List
I just want my data reflect what items I selected in the list dynamically. Base on that do some sorting with the list, for example make selected items first in the list when they are selected, and go back to original place when items are deselected....
You can use the IViewCursor to get/add/remove items of the list.
Below is a code example of how to create the cursor, based on that you will just need to apply the logic you need.
var col:ICollectionView = ICollectionView(list.dataProvider);
var myCursor:IViewCursor = col.createCursor();
//do the logic using the myCursor functions
...
//refresh the collection to the changes reflect in the list
col.refresh();
Here you can check some more info about it.
You can add an event listener to your list so that whenever a selection/deselection occurs it triggers.
<s:List id="myList"
labelField="firstName"
change="selectionChangedHandler(event)"
dataProvider="{peopleArray}">
</s:List>
....
protected function selectionChangedHandler(event:IndexChangeEvent):void
{
var currentIndx:int = event.currentTarget.selectedIndex;
var currentDataItem:Object = event.currentTarget.selectedItem;
peopleArray.removeItemAt(currentIndx);
peopleArray.addItemAt(currentDataItem,0);
peopleArray.refresh();
}
I haven't run it but you may need to set selection on refreshed list too.

Animation on sencha touch 2 list item

I have a list extending Ext.dataview.List.
I would like to play an animation in only one of the list items.
If it is triggered by the itemTap, it is easy, because the callback provides a third argument, I just run the animation on it. (I mean Ext.Anim.run).
But what if I need to animate the n-th element independently from the list, like triggered by a user tap on a separate button?
Thanks
Let's say you have a list which has the following config :
xtype:'list',
cls: 'myList',
...
Then you can access its DOM element with :
var items = Ext.DomQuery.select('.myList .x-list-item');
It will returns all the items of the list with the cls 'myList' so be sure to have only one list with this class.
From there you can do whatever you want with it like hiding the second item :
items[1].style.display = 'none';
Hope this helped