How does LazyVStack choose to re-render if Identifiable is constant? - swiftui

How does a LazyVStack in SwiftUI decide if it needs to re-render a View, assuming the Identifiable property of an item it is rendering does not change?
This trivial demo code below, I feel, should not update when the button is clicked, because the Identifiable property of the underlying data set does not change, so no Views should be redrawn and it should use the views it has already cached. However, it seems to work just fine, and I am confused as to why.
struct SomeData: Identifiable {
var id: Int
var name: String
}
struct ContentView: View {
static let dataSetA: [SomeData] = [
.init(id: 1, name: "One A"),
.init(id: 2, name: "Two A"),
.init(id: 3, name: "Three A"),
.init(id: 4, name: "Four A"),
.init(id: 5, name: "Five A"),
.init(id: 6, name: "Six A"),
.init(id: 7, name: "Seven A"),
.init(id: 8, name: "Eight A"),
.init(id: 9, name: "Nine A"),
.init(id: 10, name: "Ten A")
]
static let dataSetB: [SomeData] = [
.init(id: 1, name: "One B"),
.init(id: 2, name: "Two B"),
.init(id: 3, name: "Three B"),
.init(id: 4, name: "Four B"),
.init(id: 5, name: "Five B"),
.init(id: 6, name: "Six B"),
.init(id: 7, name: "Seven B"),
.init(id: 8, name: "Eight B"),
.init(id: 9, name: "Nine B"),
.init(id: 10, name: "Ten B")
]
#State var data: [SomeData] = dataSetA
var body: some View {
LazyVStack {
ForEach(data) { datum in
Text(datum.name)
}
Button("Click") {
data = Self.dataSetB
}
}
.padding()
}
}

Your #State variable is not the individual items, but the array enclosing them – and arrays are also a value type. So when you set data to be a different array, SwiftUI is (correctly) identifying that the view needs to be reassessed.
When it does that reassessment, it doesn't matter that your arrays' constituent items have similar identifiers – the array is different, so the whole list gets re-rendered.

Related

SwiftUI - Updating values in a VStack

Xcode 14.1, Ventura 13.1
I have a VStack with a load of graphical dials that I want to update.
For testing they are prepopulated with data from an array as follows:
struct Stats : Identifiable {
var id : Int
var title : String
var currentData : CGFloat
var goal : CGFloat
var color : Color
var unit: String
}
var stats_Data = [
Stats(id: 0, title: "Daylight", currentData: 6.8, goal: 15, color: .yellow, unit: "Hrs"),
Stats(id: 1, title: "Sunrise/Set", currentData: 3.5, goal: 5, color: .orange, unit: "Hour"),
Stats(id: 2, title: "Cloud Cover", currentData: 585, goal: 1000, color: .gray, unit: "%"),
Stats(id: 3, title: "Inclination", currentData: 6.2, goal: 10, color: .green, unit: "°"),
Stats(id: 4, title: "Orientation", currentData: 12.5, goal: 25, color: .white, unit: "°"),
Stats(id: 5, title: "Power", currentData: 16889, goal: 20000, color: .red, unit: "kW")
]
If I want to update any of this data, say "currentData" in a particular row how do I do this?
For text I can declare it as a #State var and then update it when a button is pressed, say. I have tried to do this in a similar fashion with a #State array but the preview/compiler doesn't like arrayed data within the VStack.
The test data is currently being passed in the VStack along the lines of:
ForEach(stats_Data){stat in
VStack(spacing: 14){
...
Circle()
.trim(from: 0, to: (stat.currentData / stat.goal))
So how can I alter this to update data?
Vadian's correct syntax answered this question, e.g. stats_data[1].currentData
Many thanks.

File Tree using children in List, how do I keep the File Tree shape?

List(
[root],
children: \.children)
) { item in
}
It has the same structure as above.
I want to show files to the user as they are automatically opened by searching in the File Tree. Is there a way?
View
struct ContentView: View {
#StateObject var disk = Disk()
var body: some View {
List(disk.items, children: \.children) { item in
Label() {
Text(item.name)
} icon: {
Image(systemName: "folder")
}
}
}
}
Model
class Disk: ObservableObject {
#Published var items: [Item]
init() {
let item1 = Item(id: 1, name: "child 1", children: nil)
let item2 = Item(id: 2, name: "child 2", children: nil)
let item3 = Item(id: 3, name: "child 3", children: nil)
let item4 = Item(id: 4, name: "child 4", children: [item1, item2, item3])
let item5 = Item(id: 5, name: "child 5", children: nil)
let item6 = Item(id: 6, name: "child 6", children: nil)
let root = Item(id: 7, name: "Parent", children: [item4, item5, item6])
items = [root]
}
}
struct Item: Identifiable {
var id: Int
var name: String
var children: [Item]?
}

Button action in list is executed even when taped outside (but inside the row)

I have a button in a view that is used for entries in a list.
Currently, the action of the button is executed no matter where you tap within the row.
The following sample code illustrates this:
import SwiftUI
// MARK: - Model
struct Item {
var id: Int
var name: String
var subItems: [Item]
var isFolder: Bool { subItems.count > 0 }
}
extension Item: Identifiable { }
class Model: ObservableObject {
var items: [Item]
internal init(items: [Item]) { self.items = items }
}
extension Model {
static let exmaple = Model(items: [
Item(id: 1, name: "Folder 1", subItems: [
Item(id: 11, name: "Item 1.1", subItems: []),
Item(id: 12, name: "Item 1.2", subItems: [])
]),
Item(id: 2, name: "Folder 2", subItems: [
Item(id: 21, name: "Item 2.1", subItems: []),
Item(id: 22, name: "Item 2.2", subItems: []),
Item(id: 23, name: "Item 2.3", subItems: [])
]),
Item(id: 3, name: "Item 1", subItems: []),
Item(id: 4, name: "Item 2", subItems: [])
])
}
// MARK: - View
struct MainView: View {
var body: some View {
NavigationView {
Form {
Section("Main…") {
NavigationLink {
ItemListView(items: Model.exmaple.items)
} label: {
Label("List", systemImage: "list.bullet.rectangle.portrait")
}
}
}
Label("Select a menu item", image: "menucard")
}
}
}
struct ItemListView: View {
var items: [Item]
var body: some View {
List(items) { item in
if item.isFolder {
NavigationLink {
ItemListView(items: item.subItems)
} label: {
ItemListCell(item: item)
}
} else {
ItemListCell(item: item)
}
}
.navigationBarTitle("List")
}
}
struct ItemListCell: View {
var item: Item
var body: some View {
HStack {
Image(systemName: item.isFolder ? "folder" : "doc")
Text(String(item.name))
Spacer()
if !item.isFolder {
Button {
print("button 1 item: \(item.name)")
} label: {
Image(systemName: "ellipsis.circle")
}
Button {
print("button 2 item: \(item.name)")
} label: {
Image(systemName: "info.circle")
}
}
}
}
}
// MARK: - App
#main
struct SwiftUI_TestApp: App {
var body: some Scene {
WindowGroup {
MainView()
}
}
}
Here, the ItemListCell struct is important. It contains two buttons. If I now tap somewhere in the row, the code for both buttons is executed.
Is this the way it should be?
I would like to achieve that tapping the buttons executes the corresponding code and when tapping the row somewhere else, a third action should be executed.

SwiftUI Navigation with complex View Model (Defer creation until actually needed)

this SwiftUI app uses a NavigationView with one dedicated view model for each of the two involved views.
What bothers me is the creation of the second view model (DetailsViewModel) for each of the shown links regardless of whether that link is ever going to be pressed or not. You can see this in console log.
This is a minimal example, imagine the data for the second view model coming from different entities, even different data stores and the func makeDetailsViewModel(id:) to be expensive in its execution.
I would like to defer the creation of the DetailsViewModel until the user has clicked one of the links so that only that one needs to be created instead of the potential dozens or or hundreds more others. It seams reasonable to me and straight forward in UIKit by injecting the view model in prepare(for segue:) but I don’t know how to achieve something like this using SwiftUI.
Apple’s tutorial Building Lists and Navigation and other tutorials I found use the same view model or none at all (e. g. Text("Second view")) for the second view.
How can this be done or is there a completely different way in SwiftUI to go about this? I may be mentally stuck in UIKit.
Thank you.
import SwiftUI
var people: [Person] = [
Person(id: 1, name: "Alice", age: 13, country: "USA"),
Person(id: 2, name: "Brad", age: 29, country: "Canada"),
Person(id: 3, name: "Chad", age: 29, country: "Canada"),
Person(id: 4, name: "Dorothy", age: 62, country: "Ireland"),
Person(id: 5, name: "Elizabeth", age: 40, country: "Sweden"),
Person(id: 6, name: "Francois", age: 21, country: "France"),
Person(id: 7, name: "Gary", age: 36, country: "Singapore"),
Person(id: 8, name: "Hans", age: 28, country: "Germany"),
Person(id: 9, name: "Ivan", age: 70, country: "Russia"),
Person(id: 10, name: "Jaime", age: 45, country: "Spain"),
]
var nameList: [ListEntryViewModel] = {
people.map { person in
return ListEntryViewModel(id: person.id, name: person.name)
}
}()
func makeDetailsViewModel(id: Int) -> DetailsViewModel {
print("Creating view model for DetailView")
let person = people.first(where: { $0.id == id})!
return DetailsViewModel(name: person.name, age: person.age, country: person.country)
}
// MARK: - Model
struct Person {
var id: Int
var name: String
var age: Int
var country: String
}
// MARK: - View Models
struct ListEntryViewModel: Identifiable {
var id: Int
var name: String
}
struct DetailsViewModel {
var name: String
var age: Int
var country: String
}
// MARK: - Views
struct DetailView: View {
var detailsViewModel: DetailsViewModel
var body: some View {
VStack {
Text(detailsViewModel.name)
Text("\(detailsViewModel.age) years")
Text("from \(detailsViewModel.country)")
}
}
}
struct ContentView: View {
var people: [ListEntryViewModel] = nameList
var body: some View {
NavigationView {
List(people) { person in
NavigationLink(destination: DetailView(detailsViewModel: makeDetailsViewModel(id: person.id))) {
Text(person.name)
}
}
.navigationTitle("People")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I assume you wanted DeferView (from https://stackoverflow.com/a/61242931/12299030), like
List(people) { person in
NavigationLink(destination: DeferView {
DetailView(detailsViewModel: makeDetailsViewModel(id: person.id))
}) {
Text(person.name)
}
}

swiftui - save a view to a variable

I have the following:
struct Event: Identifiable {
let id: Int
let name: String
let image: String
}
let events = [
Event(id: 0, name: "Host Tournament", image: "cup"),
Event(id: 1, name: "Post Club Info", image: "shield"),
Event(id: 3, name: "Share A Post", image: "write")
]
I want to be able for each Event to hold a separate view.
For example Event(id: 0, name: "Host Tournament", image: "cup", destinationView: PostView())
let events = [
Event(id: 0, name: "Host Tournament", image: "cup",destinationView: PostView()),
Event(id: 1, name: "Post Club Info", image: "shield",destinationView: ClubView()),
Event(id: 3, name: "Share A Post", image: "write", destinationView: StoryView())
]
So i can pass destinationView into my NavigationLink when i loop through events. Im not sure what type the PostView() should be defined as in my struct?
This is what Im currently doing:
ForEach(events) { event in
NavigationLink(destination: //PASS VIEW HERE FROM EVENT) {
VStack {
Image(event.image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100)
.padding(55)
Text(event.name)
.font(.system(.headline))
.padding(.bottom,20)
}
.padding()
.border(Color.black, width: 4)
.cornerRadius(10)
}.buttonStyle(PlainButtonStyle())
}
I want to be able to pass in a view depending on the Event its looping through.
That's pure design to couple model with view so tightly... but if you want, technically it is possible to do in the following way
let events = [
Event(id: 0, name: "Host Tournament", image: "cup",
destinationView: AnyView(PostView())),
Event(id: 1, name: "Post Club Info", image: "shield",
destinationView: AnyView(ClubView())),
Event(id: 3, name: "Share A Post", image: "write",
destinationView: AnyView(StoryView()))
]