First item in a List is always selected - swift

I have a list of items, I want to make it possible to navigate to the details view. However, the first element in the list is always passed to this view, what could be the problem?
struct ContentView: View {
var array: [Object] = [Object(id: .init(),property: 1),Object(id: .init(),property: 2),Object(id: .init(),property: 3)]
#State var showAlert = false
#State var showDetailsView = false
var body: some View {
NavigationView{
List{
ForEach(array){ item in
VStack{
Text(String(item.property))
}.onTapGesture(){ self.showAlert.toggle()}
.alert(isPresented: $showAlert){
Alert(title: Text("show details view?"), message: Text(""),
primaryButton: .default (Text("Show")) {
showDetailsView.toggle()
},
secondaryButton: .cancel()
)
}
.fullScreenCover(isPresented: $showDetailsView){ DetailsView(property: item.property) }
}
}
}
}
}
struct Object: Identifiable {
let id: UUID
var property: Int
}
struct DetailsView: View {
var property: Int?
var body: some View {
Text(String(property!))
}
}
I will get this result regardless of which item in the list I select:

In this scenario we can pass clicked item like baton from ForEach to Alert to FullScreen to Details. And, of course, we should move corresponding modifiers out of cycle, they don't need to be applied to each row.
Here is a modified code. Tested with Xcode 12.1 / iOS 14.1.
struct ContentView: View {
var array: [Object] = [Object(id: .init(),property: 1),Object(id: .init(),property: 2),Object(id: .init(),property: 3)]
#State var alertItem: Object?
#State var selectedItem: Object?
var body: some View {
NavigationView{
List{
ForEach(array){ item in
VStack{
Text(String(item.property))
}.onTapGesture(){ self.alertItem = item}
}
}
.alert(item: $alertItem) { item in
Alert(title: Text("show details view?"), message: Text(""),
primaryButton: .default (Text("Show")) {
selectedItem = item
},
secondaryButton: .cancel()
)
}
.fullScreenCover(item: $selectedItem){ DetailsView(property: $0.property) }
}
}
}

Related

Presenting Lists item in detail sheet swiftUI

I have a viewModel that with an item and a child view, I also present a sheet from a View and pass a selected item to that view. This in turn makes the selected item = item in the child view. The problem now is I have to dismiss the sheet and select a desired item be the value changes in the child view. This is a weird behaviour any help would be appreciated
My view Model
class ItemViewModel: ObservableObject {
#Injected(\.itemLocalRepository) var itemLocalRepository: ItemLocalRepository
#Published var items: [Item] { willSet { objectWillChange.send() } }
init(shoppingList: ShoppingList) {
self.shoppingList = shoppingList.item
// Printing shoppingList prints default value before changing to desired on on second selection
}
}
// Main View
struct FrequentView: View {
#State var selectedShoppingList: ShoppingList = ShoppingList.single
#State private var presentCreateSheet: Bool = false
var itemsList = [...]
var body: some View {
NavigationView {
ZStack {
ScrollView(.vertical, showsIndicators: false, content: {
LazyVStack(alignment: .leading, spacing: 15, pinnedViews: /*#START_MENU_TOKEN#*/[]/*#END_MENU_TOKEN#*/, content: {
ForEach(itemsList, id: \.id) { shoppingList in
Button {
self.selectedShoppingList = shoppingList
self.presentCreateSheet = true
} label: {
HomeRowView(shoppingList: shoppingList)
}
Divider()
.padding(.top, 0)
}
})
})
}
.sheet(isPresented: $presentCreateSheet, onDismiss: {
Task.init {
await viewModel.getList()
}
self.presentCreateSheet = false
}, content: {
ItemView(viewModel: ItemViewModel(shoppingList: selectedShoppingList))
})
}
}
}

NavigationLink in List using isActive pushes the wrong row

I'm trying to use NavigationLink's isActive variable to pop back to the root view controller.
The problem I'm experiencing is that using isActive pushes the wrong row when clicking on a list item. Remove the isActive variable and everything works as expected.
Here's some example code for demonstration purposes:
struct ContentView: View {
#State private var activateNavigationLink: Bool = false
var exampleData = ["a", "b", "c"]
var body: some View {
NavigationView {
List(exampleData, id: \.self) { item in
NavigationLink(
destination: SecondView(item: item), isActive: $activateNavigationLink) {
Text(item)
}
}
}
}
}
SecondView
struct SecondView: View {
var item: String
var body: some View {
Text(item)
}
}
This is driving me nuts. Any help would be greatly appreciated.
Because activateNavigationLink is just a Bool in your code, if it is true, every NavigationLink will register as active in your List. Right now, this is manifesting as the last item (C) getting pushed each time.
Instead, you'd need some system to store which item is active and then translate that to a boolean binding for the NavigationLink to use.
Here's one possible solution:
struct ContentView: View {
#State private var activeNavigationLink: String? = nil
var exampleData = ["a", "b", "c"]
func bindingForItem(item: String) -> Binding<Bool> {
.init {
activeNavigationLink == item
} set: { newValue in
activeNavigationLink = newValue ? item : nil
}
}
var body: some View {
NavigationView {
List(exampleData, id: \.self) { item in
NavigationLink(
destination: SecondView(item: item), isActive: bindingForItem(item: item)) {
Text(item)
}
}
}
}
}
You should not use activeNavigationLink on main view it should be used with cellView
struct ContentView: View {
var exampleData = ["a", "b", "c"]
var body: some View {
NavigationView {
List(exampleData, id: \.self) { item in
CellView(item: item)
}
}
}
}
CellView
struct CellView: View {
#State private var activateNavigationLink: Bool = false
var item: String
var body: some View {
NavigationLink(
destination: SecondView(item: item), isActive: $activateNavigationLink) {
Text(item)
}
}
}
SecondView
struct SecondView: View {
var item: String
var body: some View {
Text(item)
}
}

Edit details of a dynamic list of core data object

I used the Xcode default CoreData template to build my app.
I have tried to use CoreData and create an entity like this:
I then created a AddItemView which allows me to add item to the view.
struct AddItemView: View {
#Environment(\.managedObjectContext) var viewContext
#Environment(\.presentationMode) var presentationMode
#State private var notes = ""
#State private var selectedDate = Date()
var body: some View {
NavigationView {
Form {
Section {
TextField("notes", text: $notes)
}
Section {
DatePicker("", selection: $selectedDate, displayedComponents: .date)
Text("Your selected date: \(selectedDate)")
}
Section {
Button("Save") {
let newItem = Item(context: self.viewContext)
newItem.notes = self.notes
newItem.recordDate = self.selectedDate
newItem.timestamp = Date()
try? self.viewContext.save()
self.presentationMode.wrappedValue.dismiss()
}
}
}
.navigationBarTitle("Add Item")
}
}
}
It works well and can add items.
Then I want to click on each of the item to go to a Detail View. In the DetailView, there should be an edit button to allow me to modify the object.
I therefore created three files for the purpose: ItemHost, DetailView, EditorView
The Navigation Destination of the item will go to the ItemHost.
struct ItemListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
#State private var showingAddScreen = false
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
NavigationLink(destination: ItemHost(item: item)) {
VStack {
Text("Item at \(item.timestamp!, formatter: FormatterUtility.dateTimeFormatter)")
Text("notes: \(item.notes ?? "")")
Text("Item Date: \(item.recordDate!, formatter: FormatterUtility.dateFormatter)")
}
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
#if os(iOS)
EditButton()
#endif
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {self.showingAddScreen.toggle()}) {
Label("Add Item", systemImage: "plus")
}
}
}
.sheet(isPresented: $showingAddScreen) {
AddItemView().environment(\.managedObjectContext, self.viewContext)
}
}
}
The ItemHost as follows:
struct ItemHost: View {
#Environment(\.editMode) var editMode
#Environment(\.managedObjectContext) var contextView
#State var item: Item
var body: some View {
NavigationView {
if editMode?.wrappedValue == .active {
Button("Cancel") {
editMode?.animation().wrappedValue = .inactive
}
}
if editMode?.wrappedValue == .inactive {
ItemDetailView(item: item)
} else {
ItemEditor(item: item)
}
}.navigationBarTitle("EditMode Problem")
.navigationBarItems(trailing: EditButton())
}
}
The DetailView is just a view to display the details, without any special.
struct ItemDetailView: View {
#Environment(\.managedObjectContext) var contextView
#Environment(\.presentationMode) var presentationMode
#State private var showingDeleteAlert = false
let item: Item
var body: some View {
VStack {
Text("notes: \(item.notes ?? "")")
Text("Record Date: \(item.recordDate!, formatter: FormatterUtility.dateFormatter)")
}
.navigationBarTitle(Text("Item Detail"), displayMode: .inline)
.alert(isPresented: $showingDeleteAlert) {
Alert(title: Text("Delete Item"), message: Text("Are you sure?"),
primaryButton: .destructive(Text("Delete")) {
self.deleteItem()
}, secondaryButton: .cancel()
)
}
.navigationBarItems(trailing: Button(action: {
self.showingDeleteAlert = true
}) {
Image(systemName: "trash")
})
}
// Problem here
// Can delete the item and go back to list page. But the actual item in the CoreData has not been removed. If I call contextView.save() it will crash.
func deleteItem() {
contextView.delete(item)
presentationMode.wrappedValue.dismiss()
}
}
The EditorView like this:
struct ItemEditor: View {
#Environment(\.presentationMode) var presentation
#State var item: Item
var body: some View {
List {
HStack {
Text("Notes").bold()
TextField("Notes", text: $item.notes) // Error
}
// Error
DatePicker(selection: $item.recordDate, displayedComponents: .date) {
Text("Record Date").bold()
}
}
}
}
A few problem here:
ItemEditor: Cannot convert value of type 'Binding<String?>' to expected argument type 'Binding'. I have no way to pick the original item object values and display it to let the user know what was the old value inside the object.
Nothing to be displayed once I click on the individual navigation item. I expect that it will originally (not edit mode) and then show the detail view. If it is edit mode, then show the editor.
I get confused with the #binding and how to pass the item into the DetailView and also the Editor. How the editor save the data back to the item object in the contextView?
For the deleteItem() in the ItemDetailView. It can remove the item and go back to the ItemListView apparently. However, when I quit the app, and then run again. I found that the item re-appeared again, not really deleted.
Click on the item now, it shows this:
Don't use #State to var Item in Core Data. You should use #ObservedObject instead. It will refresh a parent view after updating data.
Please read this article:
https://purple.telstra.com/blog/swiftui---state-vs--stateobject-vs--observedobject-vs--environme

SwiftUI Navigation: How to switch detail view to a different item?

I'm struggling implementing the following navigation behavior:
From a list the user can select an item which triggers a detail view for this item. On this detail view there is an "Add" button in the navigation bar which opens a modal sheet for adding an other item.
Up to this point, everything works as expected.
But after adding the item, I want the detail view to show the new item. I tried to set the list selection to the id of the new item. This triggers the detail view to disappear, the list selects the new item and show the details for a very short time, then the detail view disappears again and the list is shown.
I've tried adding a bridged binding and let the list view not set the selection to nil, this solves the issue at first, but then the "Back" button isn't working anymore.
Please note: I want the "Add" button on the detail view and not on the list view as you would expect it.
Here's the full code to test:
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject private var state = AppState.shared
var body: some View {
NavigationView {
List(state.items) {item in
NavigationLink(destination: DetailView(item: item), tag: item.id, selection: self.$state.selectedId) {
Text(item.title)
}
}
.navigationBarTitle("Items")
}
}
}
struct DetailView: View {
var item: Item
#State private var showForm = false
var body: some View {
Text(item.title)
.navigationBarItems(trailing: Button("Add") {
self.showForm = true
})
.sheet(isPresented: $showForm, content: { FormView() })
}
}
struct FormView: View {
#Environment(\.presentationMode) private var presentationMode
private var state = AppState.shared
var body: some View {
Button("Add") {
let id = self.state.items.count + 1
self.state.items.append(Item(id: id, title: "Item \(id)"))
self.presentationMode.wrappedValue.dismiss()
self.state.selectedId = id
}
}
}
class AppState: ObservableObject {
static var shared = AppState()
#Published var items: [Item] = [Item(id: 1, title: "Item 1")]
#Published var selectedId: Int?
}
struct Item: Identifiable {
var id: Int
var title: String
}
In your scenario it is needed to make navigation link destination independent, so it want be reactivated/invalidated when destination changed.
Here is possible approach. Tested with Xcode 11.7 / iOS 13.7
Updated code only:
struct ContentView: View {
#ObservedObject private var state = AppState.shared
#State private var isActive = false
var body: some View {
NavigationView {
List(state.items) {item in
HStack {
Button(item.title) {
self.state.selectedId = item.id
self.isActive = true
}
Spacer()
Image(systemName: "chevron.right").opacity(0.5)
}
}
.navigationBarTitle("Items")
.background(NavigationLink(destination: DetailView(), isActive: $isActive) { EmptyView() })
}
}
}
struct DetailView: View {
#ObservedObject private var state = AppState.shared
#State private var showForm = false
#State private var fix = UUID() // << fix for known issue with bar button misaligned after sheet
var body: some View {
Text(state.selectedId != nil ? state.items[state.selectedId! - 1].title : "")
.navigationBarItems(trailing: Button("Add") {
self.showForm = true
}.id(fix))
.sheet(isPresented: $showForm, onDismiss: { self.fix = UUID() }, content: { FormView() })
}
}

SwiftUI updating array state doesn't update view

Here is my test code:
struct ArrView: View {
#State var arr: [String] = ["first", "second"]
var body: some View {
VStack {
Button("append") {self.arr.append("item\(self.arr.count)")}
List(arr.indices) {row in
Text(self.arr[row])
}
}
}
}
When the 'append' Button was pressed, the List view didn't update.
Am I doing wrong?
You can use ForEach<Range>() only if the provided data is a constant, otherwise you need to use ForEach(_:id:content:) and provide an id explicitly, like this:
struct ArrView: View {
#State var arr: [String] = ["first", "second"]
var body: some View {
VStack {
Button("append") {self.arr.append("item\(self.arr.count)")}
List(arr, id: \.self) { item in
Text(item)
}
}
}
}
I don't think it is the proper way to create List view. Here is a sample
struct ContentView: View {
#State var arr: [String] = ["first", "second"]
var body: some View {
VStack {
Button("append") {self.arr.append("item\(self.arr.count)")}
List(self.arr, id: \.self) { str in
Text(str)
}
}
}
}