Complete list is recreates all views even if just one item has changed - ios

I have a very simple schoolbook example of a SwiftUI List view that renders items from data in an array. Data in the array is Identifiable. But, when I change the the data in the array, add or remove a item then all rows in the list view are recreated. Is that correct? My understanding was that Identifiable should make sure that only the view in the list that are changed are recreated.
My list is inside a navigation view and each row links to a detail view. The problem is that since all items in the list are removed and recreated every time the data is changed then if that that happens when Im in a detail view (it's triggered by a notification) then Im thrown out back to the list.
What am I missing?
Edit: Added code example
This is my data struct:
struct Item: Identifiable {
let id: UUID
let name: String
init(name: String) {
self.id = UUID()
self.name = name
}
}
This is my ItemView
struct ItemView: View {
var item: Item
init(item: Item) {
self.item = item
print("ItemView created \(self.item.id)")
}
var body: some View {
Text(self.item.name)
}
}
An finally my list view:
struct KeyList: View {
#State var items = [Item(name: "123"), Item(name: "456"), Item(name: "789")]
var body: some View {
VStack {
List(self.items) { item in
ItemView(item: item)
}
Button(action: {
self.items.append(Item(name: "New"))
}) {
Text("Add")
}
}
}
}
When I press add it will print "ItemView created" 4 times. My understanding is that it should only do it 1 time?

Here is an example of how this could work. Tested and working on iOS 13.5
The List doesn't get recreated again when only one item is being removed. So this was accomplished.
About the poping of the View this has already been answered here:
SwiftUI ForEach refresh makes view pop
I have here a small workaround for this problem. Add the items you want to remove to an array. Then when going back, remove these items (Which will make the view pop) or go back programmatically and nothing gets removed
struct ContentView: View {
#State var text:Array<String> = ["a", "b", "c"]
var body: some View {
NavigationView() {
VStack() {
List() {
ForEach(self.text, id: \.self){ item in
NavigationLink(destination: SecondView(textItem: item, text: self.$text)) {
Text(item)
}
}
}
Button(action: {
self.text.remove(at: 0)
}){
Text("Remove \(self.text[0])")
}
}
}
}
}
struct SecondView: View {
#State var textItem: String
#Binding var text: Array<String>
#State var tmpArray: Array<String> = []
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack() {
Text(self.textItem)
Button(action: {
//Append to a tmp array which will later be used to determine what to remove
self.tmpArray.append(self.text[0])
}){
Text("Remove \(self.text[0])")
}
Button(action: {
if self.tmpArray.count > 0 {
//remove your stuff which will automatically pop the view
self.text.remove(at: 0)
} else {
// programmatically go back as nothing has been deleted
self.presentationMode.wrappedValue.dismiss()
}
}){
Text("Go Back")
}
}
}
}

Related

Updating a binding value pops back to the parent view in the navigation stack

I am passing a Person binding from the first view to the second view to the third view, when I update the binding value in the third view it pops back to the second view, I understand that SwiftUI updates the views that depend on the state value, but is poping the current view is the expected behavior or I am doing something wrong?
struct Person: Identifiable {
let id = UUID()
var name: String
var numbers = [1, 2]
}
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct SecondView: View {
#Binding var person: Person
var body: some View {
Form {
NavigationLink(destination: ThirdView(person: $person)) {
Text("Update Info")
}
}
}
}
struct ThirdView: View {
#Binding var person: Person
var body: some View {
Form {
Button(action: {
person.numbers.append(3)
}) {
Text("Append a new number")
}
}
}
}
When navigating twice you need to either use isDetailLink(false) or StackNavigationViewStyle, e.g.
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
.isDetailLink(false) // option 1
}
}
.navigationViewStyle(.stack) // option 2
}
}
SwiftUI works by updating the rendered views to match what you have in your state.
In this case, you first have a list that contains an element called Current Name. Using a NavigationLink you select this item.
You update the name and now that previous element no longer exists, it's been replaced by a new element called New Name.
Since Current Name no longer exists, it also cannot be selected any longer, and the view pops back to the list.
To be able to edit the name without popping back, you'll need to make sure that the item on the list is the same, even if the name has changed. You can do this by using an Identifiable struct instead of a String.
struct Person: Identifiable {
let id = UUID().uuidString
var name = "Current Name"
}
struct ParentView: View {
#State private var people = [Person()]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: ChildView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct ChildView: View {
#Binding var person: Person
var body: some View {
Button(action: {
person.name = "New Name"
}) {
Text("Update Name")
}
}
}

LazyVStack Initializes all views when one changes SwiftUI

I have a LazyVStack which I would like to only update one view and not have all others on screen reload. With more complex cells this causes a big performance hit. I have included sample code
import SwiftUI
struct ContentView: View {
#State var items = [String]()
var body: some View {
ScrollView {
LazyVStack {
ForEach(self.items, id: \.self) { item in
Button {
if let index = self.items.firstIndex(where: {$0 == item}) {
self.items[index] = "changed \(index)"
}
} label: {
cell(text: item)
}
}
}
}
.onAppear {
for _ in 0...200 {
self.items.append(NSUUID().uuidString)
}
}
}
}
struct cell: View {
let text: String
init(text: String) {
self.text = text
print("init cell", text)
}
var body: some View {
Text(text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
As you can see even when only changing 1 cell the init gets called for every cell. Is there anyway to avoid this?
Here is a working code, there is some points to mention, View in SwiftUI would get initialized here and there or anytime SwiftUI thinks it needed! But the body of View would get computed if really some value in body changed. It is planed to work like that, there is some exceptions as well. Like body get computed even the values that used in the body were as before with no change, I do not want inter to that topic! But in your example and in your issue, we want SwiftUI renders only the changed View, for this goal the down code works well without issue, but as you can see I used VStack, if we change VStack to LazyVStack, SwiftUI would renders some extra view due its undercover codes, and if you scroll to down and then to up, it would forget all rendered view and data in memory and it will try to render the old rendered views, so it is the nature of LazyVStack, we cannot do much about it. Apple want LazyVStack be Lazy. But you can see that LazyVStack would not render all views, but some of them that needs to works. we cannot say or know how much views get rendered in Lazy way, but for sure not all of them.
let initializingArray: () -> [String] = {
var items: [String] = [String]()
for _ in 0...200 { items.append(UUID().uuidString) }
return items
}
struct ContentView: View {
#State var items: [String] = initializingArray()
var body: some View {
ScrollView {
VStack {
ForEach(items, id: \.self) { item in
Button(action: {
if let index = self.items.firstIndex(where: { $0 == item }) {
items[index] = "changed \(index)"
}
}, label: {
ItemView(item: item)
})
}
}
}
}
}
struct ItemView: View {
let item: String
var body: some View {
print("Rendering row for:", item)
return Text(item)
}
}

CoreData List Items, Edit View

Need help passing CoreData to another view after its saved to the database. I have a separate view to "Add Items" and the List View shows the items, sorted. With NavigationView I can tap to see each entry, but I want to .onTapGesture overlay a new View with the list items that are editable.
Struggling to figure this out.
Right now this works to see more detail of each list item, but I'd rather have it open a new view with an .onTapGesture but can't figure out how to pass the database item tapped to the new View and set it to edit.
NavigationView{
List {
ForEach(newItem) { newItems in
NavigationLink(destination: TapItemView(database: newItems)) {
DatabaseRowView(database: newItems)}
}
}.onDelete(perform: deleteItem)
}
?
An observation: in your code sample--"ForEach(newItem) { newItems in" should be "ForEach(newItems) { newItem in". In other words the collection goes in the ForEach parentheses and the single item precedes the "in". With that, I started with the Xcode-generated project with Core Data for this example (Xcode Version 13.1). Modifications are included in the code below.
#State var showEditView = false
#State var tappedItem: Item = Item()
var body: some View {
NavigationView {
List {
ForEach(items) { item in
Text(item.timestamp, formatter: itemFormatter)
.onTapGesture {
tappedItem = item
showEditView = true
}
}
.sheet(isPresented: $showEditView) {
EditView(item: $tappedItem)
}
}
}
}
Here's a possible receiving view for the tapped item:
struct EditView: View {
#Binding var item: Item
var body: some View {
NavigationView {
Form {
Section(header: Text("Date")) {
DatePicker("Date Selection",
selection: $item.timestamp,
displayedComponents: [.date]
)
.datePickerStyle(.compact)
.labelsHidden()
}
}
}
}

Can’t pass data correctly to modal presentation using ForEach and CoreData in SwiftUI

Im trying to pass data of some object from list of objects to modal sheet, fetching data from CoreData.
The problem is that no matter what object I click on in the list, only the data form last added object appears in the details view.
The same goes for deleting the object - no matter what object I'm trying to delete, the last one is deleted all the time.
Problem disappears using NavigationLink, but it is not suitable for me.
Here is my code:
import SwiftUI
struct CarScrollView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Cars.entity(), sortDescriptors: []) var cars: FetchedResults<Cars>
#State var showDetails = false
var body: some View {
VStack {
ScrollView (.vertical, showsIndicators: false) {
ForEach (cars, id: \.self) { car in
Text("\(car.text!)")
.onTapGesture {
self.showDetails.toggle()
}
.sheet(isPresented: self.$showDetails) { CarDetail(id: car.id, text: car.text)
}
}
}
}
}
}
There should be only one sheet in view stack, so just move it out of ForEach, like below
struct CarScrollView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Cars.entity(), sortDescriptors: []) var cars: FetchedResults<Cars>
#State private var selectedCar: Car? = nil
var body: some View {
VStack {
ScrollView (.vertical, showsIndicators: false) {
ForEach (cars, id: \.self) { car in
Text("\(car.text!)")
.onTapGesture {
self.selectedCar = car
}
}
}
}
.sheet(item: self.$selectedCar) { car in
CarDetail(id: car.id, text: car.text)
}
}
}

SwiftUI List Rows Not Refreshed After Updating Binding In Other Screen

I have a simple watchOS SwiftUI Application. The application has three screens. The first screen consists of a List of items. When you press that item, it will redirect to another screen & when you tap a button there it will open up a .sheet View which allows you to edit the item in the list.
The first view looks like this:
class Object: NSObject {
var title: String
init(title: String) {
self.title = title
}
}
struct Row: View {
#Binding var object: Object
var body: some View {
Text(self.object.title)
}
}
struct ContentView: View {
#State private var objects = [Object]()
var body: some View {
NavigationView {
List {
ForEach(objects.indices, id: \.self) { idx in
NavigationLink(destination: SecondView(object: self.$objects[idx])) {
Row(object: self.$objects[idx])
}
}
}
}
.onAppear {
self.objects = [
Object(title: "Test 1"),
Object(title: "Test 2")
]
}
}
}
These are the second & third views:
struct SecondView: View {
#Binding var object: Object
#State private var showPicker: Bool = false
var body: some View {
VStack {
Text(object.title)
Button(action: {
self.showPicker.toggle()
}) {
Text("Press Here")
}
}
.sheet(isPresented: $showPicker) {
ThirdView(object: self.$object)
}
}
}
struct ThirdView: View {
#Binding var object: Object
var body: some View {
VStack {
Text(object.title)
Button(action: {
self.update()
}, label: {
Text("Tap here")
})
}
}
func update() {
let newObj = self.object
newObj.title = "Hello, World!"
self.object = newObj
}
}
I'd expect, whenever I tap the button in the third view, the Binding (and thus the State) get's updated with "Hello, World". However, that is not the case, although not immediately.
What I currently see happening is that when I tap the button in the third view, the Text in that view does not get updated. When I dismiss the third view and go back to the second view, I do see "Hello, World". But when I go back to the list, the row still has the old value.
One other thing I noticed is that, when I fill the array of objects directly, like so:
#State private var objects = [Object(title: "Test 1"), Object(title: "Test 2")]
and remove the filling of the array in .onAppear, this work totally how I'd expect it to (everything updates immediately to "Hello, World".
Does anyone one know what I'm doing wrong here or did I might hit a bug?
Thanks!
Complex objects need to be classes conforming to #ObservableObject.
Observed ivars need to be published.
class Object: ObservableObject {
#Published var title: String
[...]
}
Observing views would use them as #ObservedObject
struct Row: View {
#ObservedObject var object: Object
[...]
}
You might have to create an object wrapper for your lists
class ObjectList: ObservableObject {
#Published var objects: [Object]
[...]
}

Resources