I'm thinking about using compound types made of structs to store the properties of individual cells, rows, and sections of a custom spreadsheet, and then a class for the full table, so it's set up to be more ideal for eventually letting multiple users edit the same document at the same time?
As a beginner at coding, should I just simplify things by using all structs, so it can conform to FileDocument?
I would argue the opposite here. Because structs don’t have a fixed identity your entire document will update whenever you change anything. The initial solution I suggest is having a class for each cell that wraps its current value and formula. Then you can choose when you want to update the values of the cells, and only the parts of your app that try to read a particular cell’s value will need to update when that cell updates. I do think it’s reasonable to have a struct at the top level that holds the current collection of cells, assuming you don’t expect cells to be created or destroyed very often.
Oh no. Sounds like a lag fest. Thanks for catching that!
Would using a LazyVGrid for the view model prevent it all from updating at once, or is that unavoidable with a struct data model?
And based on the use case, table sections, and rows within them will be added constantly during use, so I'll use a class at the top level to, if I use classes for the rest of it.
In the case of using all classes is "having each cell wrap its current value and formula" what would allow for each cell to have its own data? And I'm not exactly sure what you mean.
I mean that if your entire data model is composed of value types, the entire model is semantically a single value that all of your UI depends on. So if you change anything everything updates, starting at the top. (However, equality checking further down can stop some of the updates and reduce their cost). If you break your data model into multiple different observable properties, then only the views that depend on the particular property that changed need to update in the first place.
So would these be ways to make observable properties?
In the case of using classes, decorating each with the Model macro, adds an observable macro to each property under the hood, which makes them observable. (Right?)
And for structs, a UUID for each instance, plus a conformance to observable, and identifiable for the whole struct, and something for each of its properties too.
And I'm assuming they need to be passed into views in a particular fashion to keep the bindings separated, and from triggering an update of everything at once?
It's still not tracking for me. If what you're suggesting is that views read and write from shared mutable state… then my question is looking for more in the direction of why is that preferable to immutable structs.
Is it:
Semantics? Are the semantics of shared mutable state preferable to modeling state with immutable structs?
Performance? Is the performance of shared mutable state better than immutable structs?
What if we just factored out performance? What if performance was equal… or maybe just a small constant factor slower when state is immutable values. If all we considered was semantics… would we still prefer shared mutable state?
It was ambiguous to me whether or not OP was targeting a SwiftUI app or something OOP like UIKit. If a product engineer was targeting a SwiftUI app… would the argument to prefer shared mutable state for managing data then imply that this product engineer should also prefer shared mutable state for managing views: UIKit instead of SwiftUI?
The main consideration I had is that making your state a class instead of a struct allows SwiftUI dependency tracking (and new in iOS 26, UIKit dependency tracking) to have a more granular understanding of which properties of your state a given view depends on. A single struct doesn’t allow for that (unless it’s secretly backed by a class).
Ignoring performance, I don’t see a strong argument for either approach. That said, you do still need shared mutable state somewhere otherwise your app won’t be very useful because you won’t be able to change anything.
Agree with Jed. For performance you likely want a good mix of both. Dependency tracking is essentially per property of @Observable classes. Mutations of value types is very opaque to the observation system. It just knows that a mutation has happened but doesn't know any details of that mutation. For Example if you have a nested Array [[Int]], it is not possible to detect if a mutations just mutates one of the inner arrays. Everything is considered to be potentially mutated. @Observerable (and SwiftUI) try to compare (intermediate) values to their previous value and try to stop propagating invalidation if they haven't actually changed but that that can be a linear operation to the size of the value.
If you have an Observable which has an array as a property e.g.:
@Observable final class MyObservable {
var array: [Int]
}
and then you have an array of them ([MyObservable]) you can mutate the inner array (MyObservable.array) and the observation system knows exactly which instance you have mutated and that you have not mutated the outer array.
The mix of @Observable classes and struct/enums depends on which data you expect to change independently and is observed independently and which data is changed and/or observed together.
For the former you want to use @Observerable and have the data in separate properties.
For the later you want to use value types to reduce the granularity of observation as this can add some amount of overhead as well (usually neglectable but this always depends on the scale).
In you're example, to turn it into a spreadsheet with just stored Ints, would the:
have to be a full row, with each Int in the Int array being a cell, because each Int can have its own data?
If you tried to make each cell be a class, that the user can make new instances of, then each instance of the class wouldn't have its own data, and changing the int of one, would change all instances of the class?
Is there a way around that class behavior? Or would you have no choice but to use a struct, if you want a type to be each cell, so each cell can have multiple properties?
Would you have to not copy, but mutate the class in calling an "addRow func"? Is that what you guys have been meaning when you use the word mutate?
However the array that is getting mutated knows all these ultimate details, right? Like the index of the element that was mutated, and so on. This information about the change is just lost on the way to the observation machinery... Any way to have a system that preserves that information and thus doesn't need to resort to a linear comparison to recover it back?
Ahh… there we go… now we're getting to the Good Stuff…
Is SwiftUI "object oriented programming" for the purpose of constructing views? Maybe it depends who you ask. Suppose you ask a product engineer. What would their response be? Probably their response would be no. SwiftUI is not "object oriented" compared to their experience product engineering on UIKit and AppKit. For the product engineer SwiftUI is mostly functional and declarative for the purpose of constructing views.
Suppose instead of asking a product engineer if SwiftUI is OOP you actually asked an infra engineer on SwiftUI? What would their response be? Well… I think we all agree that the SwiftUI interface presented to product engineers is mostly functional and declarative… but the SwiftUI implementation is free to construct UIKit and AppKit view objects. The "virtual DOM" of SwiftUI presents immutable values… but the "DOM DOM" of SwiftUI can still be built on mutable objects.
But at the end of the day… those are implementation details. From the POV of a product engineer building on SwiftUI means functional and declarative programming to construct views. The fact that the infra engineer is leveraging mutable view objects is opaque. Our SwiftUI virtual DOM is a legit abstraction layer over most of all that imperative stuff.
Ok… so that covers the "view" side of things. But what about the "data" side of things? Much of the sample code and documentation from Apple teaching SwiftUI to product engineers teaches them "functional and declarative" programming to manage their views… but that same sample code and documentation then teaches product engineers "imperative and object oriented" programming to manage their data with mutable models directly in their views. This can either take the form of SwiftData tutorials that put mutable models directly in components or more classic "MV*" style tutorials that pass around custom store objects. IMO these approaches should not be the default pattern that product engineers want to manage their global application state.
So what does the better default pattern look like? In the same way that product engineers focus on "functional and declarative" programming to manage their views — even if the underlying infra chooses to map those declarative instructions to imperative mutations — product engineers can also focus on functional and declarative programming to manage their application state — even if the underlying infra chooses to map those declarative instructions to imperative mutations.
From the POV of the product engineer the "state" used to configure their view components could look like immutable values… but that doesn't mean the infra couldn't then perform some work to manage imperative mutability in the infra. But that imperative mutability can be an opaque implementation detail: product engineers would not normally need to know or care about that.
So, in the case of the following data model, are classes even an option?
Given users need to be able to make as many instances of all these things as they want, minus FullTable.
import Foundation
struct ContentCell {
var isMarkedOff: Bool = false
var text: String = ""
}
struct TableSection_Row { // < Rows will not exist outside of table
// sections in this app, it wouldn't make
// sense in the use case to let users do
// that. Nor would they need to.
var ContentCells: [ContentCell] =
// Cells that appear in a new document by default. //
[ContentCell(),
ContentCell(),
ContentCell(),
ContentCell(),
ContentCell(),
ContentCell(),
ContentCell(),
ContentCell()]
// There would be a second type of cell in a row (with different
// stored properties) for the other half of the organizer/table,
// but I'm leaving it out to keep things more simple.
}
struct TableSection {
var SectionHeader: String = ""
var TableSection_Rows: [TableSection_Row] =
[TableSection_Row()]
}
struct TableSuperSection {
var SuperSectionHeader: String = ""
var TableSections: [TableSection] =
[TableSection()]
}
struct FullTable {
var AllTableSuperSections: [TableSuperSection] =
[TableSuperSection()]
}
I agree that "conventional" collection data structures from Standard Library do opt us in to a linear time check for "what changed". Which is a bummer. Two of the more specialized data structures I think might make a good alternative at that point would be TreeDictionary or TreeSet from SwiftCollections. If we leverage CHAMP structural sharing we can approach logarithmic time to determine diffs between instances. Which can be a big perf win at scale.
There currently is no TreeArray from SwiftCollections… womp womp. But a theoretical TreeArray shipping in the future could leverage structural sharing to give us logarithmic performance for diffing and constant time lookup by a stable ordering. Which would be awesome. If you remember ImmutableJS from the React era… it's a similar philosophy to how those data structures worked and the optimizations that were unlocked from there for Immutable Flux (which later became Redux).
Which is to say that the linear time performance of determining diffs over time isn't necessarily an intrinsic side effect of Observable or our decision to prefer immutable values… it's more of a side effect of the trade offs made when the engineers building standard library wrote the original container data structures.
I agree that sometimes this can be a legit performance optimization… but also from what I know about building declarative UI in React I would say that "nested" Observables can sometimes lead to code that is more challenging to reason about. If a product engineer needs to track down how or why a specific child component was updated it can sometimes be ambiguous or unclear whether this was an update from the child itself or an update from the parent. It's possible SwiftUI codes around this in a different way… but the advice I've always seen for declarative UI is that conceptual "pages" of content — like a List — can observe data but the children of that List — like "row" components" — should usually be "pure" or "presenter" components with data passed down from the parent. That's not a 100 percent rule… but it's more like a strong convention and recommendation.
The other way I can think about this is that sometimes we do want-slash-need the nested updates reported at the container level:
@Observable final class PersonStore {
var array: [Person]
}
@Observable final class Person {
var name: String
}
If our List displays array from PersonStoreand uses the name property for sorting those people… performing the nested mutation directly on the Person object reference doesn't necessarily refresh List: because the array from PersonStore is identical.
It's not impossible to make this work when Person is a class and mutating the name also updates the List with a new sort order… but it is more code and more programming to write and maintain. And Lazy Rick is Lazy. Migrating Person to an immutable value kind of solves that problem for us just as a side effect of value semantics.
I feel like CollectionDifference is one of those interesting ideas that didn't seem to pick up a ton of outside momentum.
Product engineers building on SwiftUI don't have too many performance hooks to directly short-circuit the SwiftUI infra when determining "what changed". If we pass a new array to List then the SwiftUI infra might perform a quadratic algorithm across those two arrays even if our product engineer knows exactly what the change was and can communicate that in constant time. But the product engineer doesn't have any easy way to pass that information up.
If there did exist some data structure that presented as a Collection but had the ability to override the CollectionDifference to return the diff without actually computing the diff this could have big perf wins at scale.
I'm assuming what you're saying (though most of it is things I'm not very familiar with), that classes would be preferable as opposed to structs in the case of the data model I'm trying to make, mostly to have better control of how the model updates. (Correct?)
So this means using let, instead of var to declare the property of the Person container?
But, for PersonStore the property needs to remain a var to allow the array to be mutated within the class?
If the Person container is storing a string, which TextFields need to modify, using let will cause problems, or performance issues, because the static would need to be redeclared by the TextField somehow, and I doubt the TextField would like that. Right?