Swiftui remove swipe to delete functionality - list

Is there a way to remove or deactivate swipe to delete functionality that remove items only per edit button?

You can limit the delete functionality of a List/Form depending on the EditMode state, by using deleteDisabled(_:).
The following is a short example demonstrating deleting which only works in edit mode:
struct ContentView: View {
#State private var data = Array(1 ... 10)
var body: some View {
NavigationView {
Form {
DataRows(data: $data)
}
.navigationTitle("Delete Test")
.toolbar {
EditButton()
}
}
}
}
struct DataRows: View {
#Environment(\.editMode) private var editMode
#Binding private var data: [Int]
init(data: Binding<[Int]>) {
_data = data
}
var body: some View {
ForEach(data, id: \.self) { item in
Text("Item: \(item)")
}
.onMove { indices, newOffset in
data.move(fromOffsets: indices, toOffset: newOffset)
}
.onDelete { indexSet in
data.remove(atOffsets: indexSet)
}
.deleteDisabled(editMode?.wrappedValue != .active)
}
}

Related

SwiftUI insert, delete, move and select with smooth animation

I am planning to implement following features in the SwiftUI list - delete, insert, move and select.
With the existing list I am able to delete a row. But can't select a row does not work with List(selection: self.$selectedObject). When I hit edit it always enters into delete mode. And I comment the delete code nothing happens when I tap on edit button. This the first problem.
Also, selectedObject can it be moved to Model instead of keeping it with the ContentView?
Like UITableView, I am not able to get the insert green button. Is it like SwiftUI does not support the green insert button?
Overall trying to understand how the insert, delete, move and select functionality can work with the List SwiftUI.
Another problem I have noticed is that animation is very fast and not smooth when it enters into edit mode (with delete actions).
struct ContentView: View {
#StateObject private var model = Model()
#State var selectedObject: Locations?
var body: some View {
NavigationView {
List(selection: self.$selectedObject) {
ForEach(model.identifiableLocations) { location in
Text(location.name)
}
.onDelete(perform: delete(of:))
}.listStyle(.plain)
.navigationTitle("Places")
.toolbar {
EditButton()
Button {
model.addLocation(name: "Test")
} label: {
Image(systemName: "plus")
}
}
}
}
func delete(of indexSet: IndexSet){
indexSet.forEach { index in
model.delete(itemAt: index)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().previewDevice(PreviewDevice(rawValue: "iPhone 14"))
}
}
extension ContentView {
#MainActor class Model: ObservableObject {
#Published private(set) var identifiableLocations = [Locations(name: "USA"),
Locations(name: "Switzerland")]
}
}
extension ContentView.Model {
func addLocation(name: String) {
identifiableLocations.append(Locations(name: name))
}
func delete(itemAt index: Int) {
identifiableLocations.remove(at: index)
}
}
struct Locations {
var name: String
}
extension Locations: Identifiable,Hashable {
var id: String {
return UUID().uuidString
}
}
to make selection work, the list cells need a .tag(). This value is going into the selection var.
yes, selectedObject can be moced to the view model as an additional #Published var
SwiftUI List does not have an insert method, but your Add Button already does that.
The animation is broke because your id in Location is not stable, but generated on each call by the computed var. id should be stable!
Here a running code with comments:
#MainActor
class ViewModel: ObservableObject {
#Published private(set) var identifiableLocations = [
Locations(name: "USA"),
Locations(name: "Switzerland")
]
// published selection var
#Published var selectedObject: Locations?
func addLocation(name: String) {
identifiableLocations.append(Locations(name: name))
}
func delete(itemAt index: Int) {
identifiableLocations.remove(at: index)
}
// new move func
func move(fromOffsets: IndexSet, toOffset: Int) -> Void {
identifiableLocations.move(fromOffsets: fromOffsets, toOffset: toOffset)
}
}
struct Locations: Identifiable, Hashable {
let id = UUID() // id has to stay stable
// var id: String {
// return UUID().uuidString
// }
var name: String
}
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
// #State var selectedObject: Locations? // is now in viewmodel
var body: some View {
NavigationView {
List(selection: $viewModel.selectedObject) {
ForEach(viewModel.identifiableLocations) { location in
Text(location.name)
.tag(location) // this makes selction work
}
.onDelete(perform: delete(of:))
.onMove(perform: viewModel.move)
}
.listStyle(.plain)
.navigationTitle("Places")
.toolbar {
EditButton()
Button {
viewModel.addLocation(name: "Test")
} label: {
Image(systemName: "plus")
}
}
}
}
func delete(of indexSet: IndexSet){
indexSet.forEach { index in
self.viewModel.delete(itemAt: index)
}
}
}

SwiftUI: .listRowBackground is not updated when new item added to the List

I'm trying to find the reason why .listRowBackground is not updated when a new item has been added to the list. Here is my code sample:
#main
struct BGtestApp: App {
#ObservedObject var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
NavigationView {
List {
ForEach(vm.items, id: \.self) { item in
NavigationLink {
DetailView().environmentObject(vm)
} label: {
Text(item)
}
.listRowBackground(Color.yellow)
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
Button("Add new") {
vm.items.append("Ananas")
}
}
}
How it looks like:
TIA for you help!
You can force the list to refresh when you cone back to the list. You can tag an id for your list by using .id(). Here is my solution:
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
#State private var viewID = UUID()
var body: some View {
NavigationView {
List {
ForEach(vm.items, id: \.self) { item in
NavigationLink {
DetailView()
.environmentObject(vm)
} label: {
Text(item)
}
}
.listRowBackground(Color.yellow)
}
.id(viewID)
.onAppear {
viewID = UUID()
}
}
}
}
Hope it's helpful for you.
The solution I found is not ideal, but should work. What I did is made items to be #State variable :
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
#State var items: [String] = []
var body: some View {
NavigationView {
VStack {
List {
ForEach(items, id: \.self) { item in
NavigationLink {
DetailView().environmentObject(vm)
} label: {
RowView(item: item)
}
.listRowBackground(Color.yellow)
}
}
.onAppear {
self.items = vm.items
}
}
}

SwiftUI List/Form bad animation when adding/removing rows

I have a really simple list with text that when the user taps on it, it expands with a datepicker inside.
The problem is that the animation looks really broken, not sure what I can do about this besides doing the entire thing from scratch, that at this point I'd rather just use UIKit.
If you have an idea of how this can be fixed I'd really appreciate.
Here's the code:
struct ContentView: View {
let items = ["123", "345", "678"]
#State private var selectedItems = Set<String>()
#State private var test = Date()
var body: some View {
Form {
ForEach(items.indices) { index in
Button(action: {
withAnimation {
if selectedItems.contains(items[index]) {
selectedItems.remove(items[index])
} else {
selectedItems.insert(items[index])
}
}
}, label: {
Text(items[index])
.foregroundColor(.primary)
})
if selectedItems.contains(items[index]) {
DatePicker(selection: $test, in: ...Date(), displayedComponents: .date) {
}
.datePickerStyle(WheelDatePickerStyle())
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ForEach(content:) should only be used for static collections.
If you have a dynamic collection (such as in your example - you're adding/removing entries), you need to use ForEach(id:content:):
ForEach(items.indices, id: \.self) { index in
Note that if your collection can have duplicate items, then id: \.self will not work properly and you may need to create a struct conforming to Identifiable instead.
Use Section inside ForEach.
struct ContentView: View {
let items = ["123", "345", "678"]
#State private var selectedItems = Set<String>()
#State private var test = Date()
var body: some View {
Form {
ForEach(items.indices) { index in
Section(header: header(index), content: {
if selectedItems.contains(items[index]) {
DatePicker(selection: $test, in: ...Date(), displayedComponents: .date) {
}
.datePickerStyle(WheelDatePickerStyle())
}
})
}
}
}
private func header(_ index: Int) -> some View {
Button(action: {
withAnimation {
if selectedItems.contains(items[index]) {
selectedItems.remove(items[index])
} else {
selectedItems.insert(items[index])
}
}
}, label: {
Text(items[index])
.foregroundColor(.primary)
})
}
}

Trigger Navigation from Context Menu in SwiftUI

I have a List that contains NavigationLink inside a NavigationView.
I know want to extend the view with a ContextMenu that contains an element that shows another view inside my navigation stack.
struct MainView: View {
#State var elements = ["Hello", "World"]
var body: some View {
NavigationView {
List(elements, id: \.self, rowContent: { element in
NavigationLink(destination: PresentView(element: element)) {
Text(element)
.contextMenu {
NavigationLink(
"Edit",
destination: EditView(element: element)
)
}
}
})
}
}
}
The navigation for a normal tap on my item works fine. The context menu however stopped working in Xcode 11 Beta 5. I get the following error: `[WindowServer] display_timer_callback: unexpected state.
How would I push a new view on my navigation stack from a context menu?
One approach is to use NavigationLink(destination: Destination, isActive: Binding<Bool>, #ViewBuilder label: () -> Label), the label as an EmptyView hidden inside a ZStack. You would then select the element to navigate to and toggling the NavigationLink inside the contextMenu. Here is a full example:
struct PresentView: View {
let element: String
var body: some View {
Text(element)
}
}
struct EditView: View {
let element: String
var body: some View {
Text("EditView for \(element)")
}
}
struct MainView: View {
#State var elements = ["Hello", "World"]
#State var elementToEdit: String?
#State var isPresentedEditView = false
var body: some View {
NavigationView {
ZStack {
NavigationLink(
destination: elementToEdit == nil ? AnyView(EmptyView()) : AnyView(EditView(element: elementToEdit!)),
isActive: $isPresentedEditView) {
EmptyView()
}
List(elements, id: \.self) { element in
NavigationLink(destination: PresentView(element: element)) {
Text(element)
.contextMenu {
Button("Edit") {
elementToEdit = element
isPresentedEditView.toggle()
}
}
}
}
}
}
}
}
struct ContentView: View {
var body: some View {
MainView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Save selected item in List

This looks like a very simple thing, but I can't figure out how to do this:
I have a List embedded in a NavigationView, containing a NavigationLink to view the detail of the item.
I have a save bar button where I would like to save the selected item. But how can I access the selected item?
It isn't visible in the button's action closure.
struct ItemList : View {
#EnvironmentObject var items: ItemsModel
var body: some View {
NavigationView {
List(items) { item in
NavigationLink(destination: ItemDetail(item: item)) {
Text(item.name)
}
}
.navigationBarTitle(Text("Item"))
.navigationBarItems(trailing: Button(action: {
self.save(/*item: item */) // How can I access item here?
}, label: {
Text("Save")
}))
}
}
func save(item: Item) {
print("Saving...")
}
}
Navigation links are not obligatory to accomplish this.
import SwiftUI
struct ContentView: View {
struct Ocean: Identifiable, Hashable {
let name: String
var id: Self { self }
}
private var oceans = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
#State private var selectedOceans = [Ocean]()
#State private var multiSelection = Set<Ocean.ID>()
var body: some View {
VStack {
Text("Oceans")
List(oceans, selection: $multiSelection) {
Text($0.name)
}
.navigationTitle("Oceans")
.environment(\.editMode, .constant(.active))
.onTapGesture {
// Walkaround: try how it works without `asyncAfter()`
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
selectedOceans = Array(multiSelection)
print(selectedOceans)
})
}
Divider()
Text("Selected oceans")
List(selectedOceans, selection: $multiSelection) {
Text($0.name)
}
}
Text("\(multiSelection.count) selections")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}