SwiftUI does not perform onInsert when the list is empty? - swift

I have 2 lists on the UI, 1 list is allow user to drag to another list. I have problem when the
"selected product list" is empty. It does not perform the .onInsert function.
It have no problem when the array "selectedProduct" is not empty when it was rendering.
How can i to allow user to drag and drop when the other list is completely empty?
List(selection: $selection)
{
HStack(alignment: .center)
{
Spacer().frame(width: 200)
Text("Product Name")
.foregroundColor(blue)
.lineLimit(1)
Spacer()
Text("Price")
.foregroundColor(blue)
Spacer()
Text("Unit")
.foregroundColor(blue)
Spacer()
}
ForEach(selectedProduct, id:\.id)
{
rider in
SelectedProductView()
}
.onInsert(of: ["public.data"])
{
self.addProduct(position: $0, itemProviders: $1, top: true)
}
}

The .onInsert works on DynamicViewContent, so when your selectedProduct is empty there is no content, so nowhere to insert.
There two possibilities as I see to solve this situation:
1) implement support for drop on List and make it active when selectedProduct is empty to make possible to add first element in dynamic content
2) implement stub product-item (like, empty row showing "drop here" text) that is explicitly added to selectedProduct when it becomes empty of real products, thus making content not empty to allow standard .onInsert work for you.

Related

Better way of handling Tap Gesture on Lists

I'm trying to implement a 1 count tap gesture and 2 count tap gesture on rows of a list view.
Currently I'm achieving this by expanding the contents of the rows to fill the entire width and height, by changing the row insets with .listRowInsets(EdgeInsets()) and adding an HStack with a Spacer inside. As seen below:
My ListRow View, which serves to attempt to fill out the the entire rows' width and height. (With a red border for debugging):
struct ListRow<Content: View> : View {
let content: () -> Content
var body: some View {
HStack {
content()
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.border(Color.red)
}
}
And here's a snippet of one of the two places I'm using a list with my ListRow:
List(selection: $selectedItem) {
Section(header: Text("Favourites")) {
ForEach($sidebarItems, id: \.self) { $item in
ListRow {
FileLabel(URL(string: item.path)!, size: 14, text: item.text)
}.gesture(TapGesture().onEnded {
selectedItem = item
fileState.updatePath(path: item.path)
}).listRowInsets(EdgeInsets())
}
}
}.listStyle(SidebarListStyle())
A few issues happen here, which both feels hacky and incorrect to do.
I have to manually select the row when the content is tapped.
I actually manually have to focus the list, if it's not actually focused when clicking a row, otherwise the selection is a very faint color.
The contents aren't actually expanding to the leading and trailing edges of the row, no matter if I give my EdgeInsets negative values (This just results in the contents getting clipped, rather than expanding to the edge).
Clicking the areas outside of the red border (seen below) results in the row being selected, but the tap action not firing.
Preferably I'd love if there was a way to listen for tap gestures on a list and get the row that was tapped. It would fix having to manually reimplement features that the List already has built-in and lowering the risk of introducing bugs + it just feels like the right way to do it, unfortunately I haven't been able to find other ways of implementing this, other than creating an HStack with a Spacer.
EDIT:
I'm curious as to how NavigationLink works as no matter where you click, or if you navigate using your keyboard, it'll actually trigger the navigation. Makes me think there's some kind of binding or event fired?

Animate Change in SwiftUI ForEach Grid Using Array Indices

Does anyone know how I can animate a sorting change to grid items while iterating off of the array index in a ForEach?
I am showing a grid of items (LazyVGrid) from an array and the user can change the sort order of these items. When the sort order changes, I'd like to animate the change in the grid. This would work great if I were to use code similar to the following:
Button {
withAnimation {
noteModel.sortMethod = noteModel.sortMethod.next()
}
} label: {
Text("Change Sort Order")
}
ScrollView {
LazyVGrid(columns: columns) {
ForEach(self.notes, id: \.self) { note in
VStack {
Text(note.title)
Text(note.body)
}
.padding()
}
}
}
However, if my ForEach iterates off of the array index the change does not animate. So it won't animate if my code is similar to this:
Button {
withAnimation {
noteModel.sortMethod = noteModel.sortMethod.next()
}
} label: {
Text("Change Sort Order")
}
ScrollView {
LazyVGrid(columns: columns) {
ForEach(self.notes.indices, id: \.self) { idx in
VStack {
Text(self.notes[idx].title)
Text(self.notes[idx].body)
}
.padding()
}
}
}
Why don't I just do it the first way? The reason is because I found a great resource for dynamically adjusting frame sizes for items inside a grid (found here: https://swiftui-lab.com/impossible-grids/), but that requires the use of array indices in order to work. It's pretty cool and I am hoping I don't have to choose one or the other (dynamic sizing or animate changes).
Thoughts or ideas would be greatly appreciated. Thanks!
ForEach in SwiftUI isn’t the same as a for loop. Your data needs to be identifiable for structural identity to work and you certainly should not be using indices. Either implement the Identifiable protocol or tell ForEach what key path is a unique ID (or is a getter that creates one from other properties) in the item struct. Don’t ever use id:\.self.
You can learn more about structural identity in the video WWDC 2021 Demystify SwiftUI

Selecting a picker value to lead to a text field, SwiftUI

Im trying to implement a feature in my app.
When I click on my picker:
Picker(selection: $profileViewModel.education,
label: Text("Education Level")) {
ForEach(Education.levels, id: \.self) { level in
Text(level).tag(level)
}
}
This takes me to a screen and then I select the value (this is fine - it works as expected)
How could I select the value which then takes my to let's say another screen so I can fill in more details regarding the selected value.
For example the above picker has a values to select eduction level, after selecting it, how could I get an action sheet/another screen appear so I can have a text field there to save this extra data to or once the selection is made, a text field appears for me to save some extra data, and then clicking a button which would take me to the original screen of the picker (hope that makes sense)?
I've tried researching online for a problem similar to this but can't seem to find one/or if you can point me in the direction of what I should be looking into?.
Tried the following:
If I correctly understood your scenario here is a possible approach (replication tested with Xcode 12 / iOS 14)
Picker(selection: $profileViewModel.education,
label: Text("Education Level")) {
ForEach(Education.levels, id: \.self) { level in
Text(level).tag(level)
}
}
.onChange(of: profileViewModel.education) { _ in
DispatchQueue.main.async {
self.showSheet = true
}
}
.sheet(isPresented: $showSheet) {
// put here your text editor or anything
Text("Editor for \(profileViewModel.education)")
}

How can I combine text views by looping?

We can create new text views out of several small ones using +, which is an easy way of creating more advanced formatting. For example, this creates three text views in different colors and combines them together:
struct ContentView: View {
var body: some View {
Text("Colored ")
.foregroundColor(.red)
+
Text("SwifUI ")
.foregroundColor(.green)
+
Text("Text")
.foregroundColor(.blue)
}
}
But how can I create combined text views by looping through. For example (which doesn't work)
View v;
ForEach((1...3), id: \.self) {
v.append(Text("\($0)"))
}
You are essentially trying to get an expression of type Text, from a sequence 0...3. You can first map the sequence to Text objects. How do you combine a sequence of objects to a single one? reduce!
(1...3).map { Text("\($0)") }.reduce(Text(""), +)
Note that Text("") acts like the identity element of the + operation.

SwiftUI pick a value from a list with ontap gesture

i'm try to pick some value from a swiftUI list with the ontapGesture.
I have a Searchlist of item, the user need to choose one item and then the app will send the choice to an array that later will be use for other info.
now my problem is how do I do that? how can do it?
as you can see from the code below, I want to pick the value of the item.icaoAirport corresponding to that raw and pass to an array.
List(dm.vettoreAeroporti.filter{
// $0.icaoCode.contains(searchTerm)
$0.icaoAirport.localizedCaseInsensitiveContains(searchTerm)
}) { item in
HStack {
Text(item.icaoAirport).bold()
Spacer()
Text(item.nameAirport)
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.dm.openFileJson(fileName: "data")
}
}
.onTapGesture {
// ?? I need to take the value of the item.icaoAirport corresponding to that raw
}
thanks in advance for the help.
While your, Damiano, answer is right it works only on tap on the Text.
Sometimes it is needed to have the whole row tappable, so here is the solution for this case:
List(items) { item in
HStack {
Text(item.icaoAirport).bold()
Spacer()
Text(item.nameAirport)
}
.contentShape(Rectangle())
.onTapGesture {
print("touched item \(item)")
}
}
Thanks Paul for this (https://www.hackingwithswift.com/quick-start/swiftui/how-to-control-the-tappable-area-of-a-view-using-contentshape)
Note, that if you have content only on one side of the HStack e.g:
HStack {
Text("Some text") // This text will be on the left side
}
then the .contentShape(Rectange()) will work only for the width of the text. So to enable it for the whole width just add a trailing Spacer() like this:
HStack {
Text("Some text")
Spacer() // This will take all the space from the end of the text up to the end of the whole row
}
.contentShape(Rectangle())
I solved my issue:
HStack {
Text(item.icaoAirport).bold()
Spacer()
Text(item.nameAirport)
.onTapGesture {
print("touched item \(item.icaoAirport)")
}
}
List {
ForEach(Type.allCases, id: \.self) { type in
FilterRow(Color: type.Color,
title: type.Title)
.contentShape(Rectangle())
.onTapGesture {
print("Ammu type:\(type)")
}
}
}
This one is quite old but the accepted answers are pretty strange. I was able to achieve this effect in a much simpler way. Consider:
List {
ForEach(myItems) { item in
Button {
// action
} label: {
Text(item.name)
}
}
}
Now doing it this way, the button is edge-to-edge in the row. However, the button is now the accent color of your app. That's probably not what you want! So I tried adding:
Button(...)
.buttonStyle(PlainButtonStyle())
The text of the button is now the primary foreground color, great! But there's a different problem now! The button no longer goes edge-to-edge, it's only the text. To fix this, i instead:
Button(...)
.foregroundColor(.primary)
This is edge-to-edge with primary color text and custom actions. No need to create HStacks with spacers and an onTap, it's all just handled. OP's post has text on both sides so likely still needs an HStack for their specific usecase but a Button is still probably a better choice.