Is it possible to add swipe actions dynamically in SwiftUI? - swiftui

The question is quite simple. I receive entities from API that contains property actions and I need to display them in a list with actions as swipe actions accordingly.
Problem is that I didn't find the way how to iterate actions in swipeActions modificator.
I tried something like this:
List {
ForEach(entities) { entity in
CustomCell(entity: entity)
.swipeActions(edge: .trailing) {
for action in entity.actions {
Button(action: {}) {
Label(action.title)
}
}
}
}
}
and it does not work.
Is it even possible to solve?

Assuming action type is Identifiable (if not then confirm it), it can be done with ForEach, because swipeActions expects views, so it can be like
List {
ForEach(entities) { entity in
CustomCell(entity: entity)
.swipeActions(edge: .trailing) {
ForEach(entity.actions) { action in // << here !!
Button(action: {}) {
Label(action.title)
}
}
}
}
}

Related

Normal Tap & Swipe actions while in EditMode?

I'm developing an app, one view of which has the primary goal of allowing users to reorder a List of NavigationLinks, but which I would also like to allow navigation & a few other things. I want users to be able to:
Reorder by dragging on the reorder control.
Navigate to the link by tapping elsewhere on the row.
Swipe from the leading edge to activate a swipe action.
At the moment, enabling EditMode (to allow reordering) disables navigation & swipe actions; I haven't been able to find a workaround that allows all 3 functions simultaneously. Is there a good way to do this?
Here's an example:
struct ReorderableListView: View {
var items: [X] // List of custom objects
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: CustomView(item)) { // winds up disabled by edit mode
Text(item.name)
}
.swipeActions(edge: .leading) { Button("Swipe") {print("swipe")} } // winds up disabled by edit mode
}
.onMove { from, to in
print("move \(from) to \(to)")
}
}
.environment(\.editMode, .constant(.active)) // always in edit mode for reordering
}
}
}
Update:
Based on the accepted answer, it looks like this behavior got updated in iOS 16: Now, if onMove is implemented, you can reorder with a long press even when not in edit mode, which allows these three actions to coexist. I've added custom drag-handles to make this behavior obvious to the user, but otherwise I'm going with exactly the solution given in the accepted answer, below.
I am not sure that the .environment is necessary in this case (I could be wrong). You can remove that piece.
Additionally, you should add an ID to each item in your foreach. This should ideally come from your model when you create new items (for example, your model can contain an ID variable = UUID()), but for the time being we can add it inline in your foreach.
I had to write some code on my end to get this up and running, so my solution is based on the code I spun up (very similar to yours, but missing your custom items object):
struct ReorderableListView: View {
var items: [X] // List of custom objects
var body: some View {
NavigationView {
List {
// added ID here
ForEach(items, id: \.self) { item in
NavigationLink(destination: CustomView(item)) { // winds up disabled by edit mode
Text(item.name)
}
.swipeActions(edge: .leading) { Button("Swipe") {print("swipe")} } // winds up disabled by edit mode
}
.onMove { from, to in
print("move \(from) to \(to)")
}
}
// REMOVED .environment(\.editMode, .constant(.active)) // always in edit mode for reordering
}
}
}
For reference, here is the code I wrote locally to fill in the gaps. This is what worked & what I implemented within your code:
struct ReorderableListView: View {
#State var items = [1, 2, 3] // List of custom objects
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
NavigationLink(destination: ContentView2()) { // winds up disabled by edit mode
Text("\(item)")
}
.swipeActions(edge: .leading) { Button("Swipe") {print("swipe")} } // winds up disabled by edit mode
}
.onMove { from, to in
print("move \(from) to \(to)")
}
}
// .environment(\.editMode, .constant(.active)) // always in edit mode for reordering
}
}
}

Strange issue of "Modifying state during view update, this will cause undefined behavior"

Goal: have a SwiftUI architecture where the "add new item" and "edit existing item" are solved by the same view (EditItemView). However, for some reason, when I do this, the runtime agent complains of "Modifying state during view update, this will cause undefined behavior".
This is the code I want to use, which ensures that the EDITING of the item and ADDING a new item are handled by the same EditItemView:
var body: some View {
NavigationView
{
ScrollView
{
LazyVGrid(columns: my_columns)
{
ForEach(items, id: \.id)
{
let item = $0
// THIS LINE TO EDIT AN EXISTING ITEM
NavigationLink(destination: EditItemView(item: item))
{
ItemView(item: item)
}
}
}
}
.navigationBarItems(trailing:
// THIS LINE TO ADD A NEW ITEM:
NavigationLink(destination: EditItemView(item: Data.singleton.createItem(name: "New item", value: 5.0))
{
Image(systemName: "plus")
}
)
}
}
It doesn't work, leading to the issue highlighted above. I am forced to separate the functionality for Edit and Add into two distinct Views, which then works:
var body: some View {
NavigationView
{
ScrollView
{
LazyVGrid(columns: my_columns)
{
ForEach(items, id: \.id)
{
let item = $0
// THIS LINE TO EDIT AN EXISTING ITEM
NavigationLink(destination: EditItemView(item: item))
{
ItemView(item: item)
}
}
}
}
.sheet(isPresented: $isPresented)
{
// FORCED TO USE SEPARATE VIEW
AddItemView { name, value in
_ = Data.singleton.createItem(name: name, value: value)
self.isPresented = false
}
}
.navigationBarItems(trailing: Button(action: { self.isPresented.toggle()}) { Image(systemName: "plus")})
}
}
I don't understand why the code in the first version is considered to modify the state while updating view, because to me, it's sequential: new Item is created and THEN a view is shown for that Item.
Any ideas?
The destination of NavigationLink is not rendered lazily, meaning it'll get rendered when the NavigationLink itself is rendered -- not when clicked through.
The sheet code, depending on platform and SwiftUI version, may have the same issue, but apparently does not in the version you're using. Or, the closure you provide to AddItemView isn't run immediately -- since you didn't include the code, it's not clear.
To solve the issue in the first method, you can use the following SO answer which provides a lazy NavigationLink: https://stackoverflow.com/a/61234030/560942

Problem passing value with SwiftUI List(), ForEach(), .onTapGesture WatchOS6

I am trying to use the .onTapGesture() to call a method on the tapped view and pass some kind of ID or any other form of identifier, but i dont know how to.
i have a view ListView that i create from data that i get from my array trackPreviewList: [TrackpreviewList].
The view looks like this:
struct ListView: View {
#ObservedObject var model: PlaybackService
#State var list = PlaybackService.sharedInstance.trackPreviewList
var body: some View {
List {
Section(header: Text("Songs")) {
ForEach(model.trackPreviewList!, id: \.id) {
Text($0.artistName + ": " + $0.name)
}
.onDelete(perform: deleteTrack)
.onTapGesture(perform: skipTo)
}
}
}
I am very new to SwiftUI and coding in general, but i think the right approach is to say cell(view?) 2 was tapped, therefore pass 2 to the method func playerSkipTo(skipToIndex: Int) I have tried to hardcode the value 2 like this and it works with the hardcoded value:
Though i am clueless when it comes to how to pass some kind of identifier, i think my problem is that i am trying to access the object outside of the scope where the method "knows about it".
I have tried to use the $0.name variable just to see if i could pass anything but i get this error Contextual closure type '() -> Void' expects 0 arguments, but 1 was used in closure body which i think is because the .onTapGesture() method does not have a closure?.
I have tried to use the same logic as the .onDelete() method using IndexSet like this:
The method that i want to pass the value to in the end looks like this:
func playerSkipTo(skipToIndex: Int) {
if skipToIndex < ((trackPreviewList!.underestimatedCount)-1) {
_ = firstly {
TracklistProvider.sharedInstance.getPlaybackURL(trackPreview: trackPreviewList![skipToIndex])
}.map { playbackoptions in
self.createPlayerItemFromLink(assetURL: (playbackoptions?.playbackUrl)!)
}.done{
self.replacePlayerItem()
}
} else {
print("The index \(skipToIndex) is out of bounds")
}
}
I am aware that i will need to change the playerSkipTo() method to take in IndexSet also, which i have also tried to play around with, trying to cast it to Int and such.
Any help or pointers are greatly appreciated!
Heres the playerSkipTo method:
if skipToIndex < ((trackPreviewList!.underestimatedCount)-1) {
_ = firstly {
TracklistProvider.sharedInstance.getPlaybackURL(trackPreview: trackPreviewList![skipToIndex])
}.map { playbackoptions in
self.createPlayerItemFromLink(assetURL: (playbackoptions?.playbackUrl)!)
}.done{
self.replacePlayerItem()
}
} else {
print("The index \(skipToIndex) is out of bounds")
}
}
Instead of skipping to an index, skip to a specific track object:
func playerSkipTo(_ playbackoptions: PlaybackOptions) {
// ...
}
Then change your View code to call this function on the Text instead of the ForEach:
List {
Section(header: Text("Songs")) {
ForEach(model.trackPreviewList!, id: \.id) {
Text($0.artistName + ": " + $0.name)
.onTapGesture {
self.playerSkipTo($0)
}
}
}
}
You can use a button on top of a ZStack:
List{
ForEach(appData.assets) { asset in
ZStack {
HStack{
VStack(alignment: .leading) {
Text(asset.value)
.font(.headline)
}
Spacer()
}
Button(action: {assetPressed(id: asset.id)}, label: {
Text("")
})
}
}
.onDelete(perform: deleteAsset)
}

How to make two Lists in one View scroll as one page in SwiftUI?

I'm trying to add two tables with different sizes to one view in SwiftUI. I want both tables be fully expanded and the whole view to scroll as one. For now I only get both tables fixed size and scroll separately.
some View {
VStack {
Text("Table foo")
List(foo { item in Text(item.name) })
Text("Table bar")
List(bar { item in Text(item.name) })
}
}
Tried changing VStack to a ScrollView, it makes things even worse - tables are becoming one liners.
Also tried replacing List() with ForEach() but then I lose features of the List() I actually want.
I think you want one List with multiple ForEach's. Optionally, you could make it a grouped List with multiple sections. Adapted for your sample, the following concept has worked well for me on several occasions:
struct SwiftUIView: View {
var body: some View {
List {
Section(header: Text("Table foo"))
{
ForEach(foo) { item in Text(item.name) }
}
Section(header: Text("Table bar"))
{
ForEach(bar) { item in Text(item.name) }
}
}
.listStyle(GroupedListStyle())
}
}
You can use Form to embed two lists in a scroll view.
Reference: https://developer.apple.com/documentation/swiftui/form .

Button and List interaction modifications

I am trying to remove the "sectionStyle" for my List rows. In order to achieve a tableview-like view, you would write the following:
List(items, id: \.id) { item in
ItemRow(with: item)
}
If you want to interpret tap events on each row, you would wrap the ItemRow into a Button:
List(items, id: \.id) { item in
Button(action: rowTapped) {
ItemRow(item: item)
}
}
The interesting part is that when the List knows about the Button, it will automatically give a selection animation like tableView's selectionStyle. For my application, I'd like to not have that. Is there a way to set the "selectionStyle" for the List to "none"?
There are a number of List behaviours, that I haven't been able to alter. It's quite easy to build your own list though if you have specific needs different than the system List view. Here's a simple example that I've used.
struct MyListView:View {
var body: some View {
ScrollView {
ForEach(items, id: \.id) { item in
VStack {
Button(action: self.rowTapped) {
ItemRow(item: item)
}
Divider()
}
}
}.padding()
}
func rowTapped() {
print("rowTapped")
}
}
struct ItemRow:View {
var item:Item
var body: some View {
HStack {
Text(item.name)
Spacer() // force text to left justify
}
}
}