How do you handle dynamic column names in a SwiftUI Table on Mac - swift

I have a SwiftUI table that I need to layout based on some request data from a server. This data is dynamic and changes. However, once I read this data into an array, it will not allow me to do a forEach on the column data, as it companies about not conforming to "TableColumnContent". Is there another way to loop this creation, or is there a simple way to conform to this protocol? I am having trouble understanding that protocol when I look at it.
struct DataProperty : Identifiable
{
var id: String
var name: String
}
struct TableDataView : View
{
#EnvironmentObject var cache: ServerCache
var body: some View
{
Table(cache.activeTableData)
{
ForEach(cache.activeProperties, id: \.self) {property in
TableColumn(property.name, value: \.id)
}
}
}
}
Note that activeProperties is an array of DataProperty. What am I doing wrong here?

Related

View does not update when changing RealmObject passed as binding

I have a view, where I have an array of RealmObjects. When I pass them into another View via #Binding and try to edit them there, the View does not get updated.
This is a simplified example. When the name changes, it dumps the correct name into the console, but the View does not update. Tried to force a reload by changing the .id() of the View, but this doesn't help either.
class Person: Object, ObjectIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var name: String = ""
}
struct ParentView: View {
#State private var persons: [Person] = []
var body: some View {
ChildView(persons: $persons)
}
}
struct ChildView: View {
#Binding var persons: [Person]
var body: some View {
ForEach(person) { person in
Text(person.name)
.onTapGesture {
person.name = "New Name"
dump(person)
}
}
}
}
One issue is mixing two different technologies by assigning Realm objects to a Swift array - generally speaking, that may not be the best idea.
Realm objects are lazily-loaded and massive datasets take almost no memory. However, as soon as those objects are cast to Swift functions and constructs they are all loaded into memory and can overwhelm the device.
Best practice is to keep Realm objects within Realm stuctures e.g. Collections like Results and Lists.
To address the question:
Realm provides an object #ObservedResults which is a property wrapper that invalides a view when observed objects change. e.g. If the app has #ObservedResults of a group of People objects, if one of those changes, the associated view will also update. From the docs
You can use this property wrapper to create a view that automatically
updates itself when the observed object changes.
So change this
struct ChildView: View {
#Binding var persons: [Person]
to this
struct ChildView: View {
#ObservedResults(Person.self) var persons
I think the rest of the code is good to go as is.

A struct mutating properties or ObservableObject Published properties to drive data changes

I would like help to further understand the implications of using the following 2 methods for driving data between multiple views.
My situation:
A parent view initialises multiple child views with data passed in.
This data is a big object.
Each view takes a different slice of the data.
Each view can manipulate the initial data (filtering, ordering etc)
Using an observableObeject to store this data and multiple published properties for each view :
can be passed in as an environment object that can be accessed by any view using #EnvironmentObject.
You can create a Binding to the published properties and change them.
Execute a method on the ObservableObject class and manipulate a property value which gets published using objectWillChange.send() inside the method.
I have achieved the desired listed above by using a struct with mutating methods. Once these properties are changed in the struct, the views which bind to these properties causes a re-render.
My struct does not do any async work. It sets initial values. Its properties are modified upon user action like clicking filter buttons.
Example
struct MyStruct {
var prop1 = "hello"
var prop2: [String] = []
init(prop2: [String]) {
self.prop2 = prop2
}
mutating func changeProp2(multiplier: Int) {
let computation = ...
prop2 = computation //<----- This mutates prop2 and so my view Binded to this value gets re-renderd.
}
}
struct ParentView: View {
var initValue: [String] // <- passed in from ContentView
#State private var myStruct: MyStruct
init(initValue: [String]) {
self.myStruct = MyStruct(prop2: initValue)
}
var body: some View {
VStack {
SiblingOne(myStruct: $myStruct)
SiblingTwo(myStruct: $myStruct)
}
}
}
struct SiblingOne: View {
#Binding var myStruct: MyStruct
var body: some View {
HStack{
Button {
myStruct.changeProp2(multiplier: 10)
} label: {
Text("Mutate Prop 2")
}
}
}
}
struct SiblingTwo: View {
#Binding var myStruct: MyStruct
var body: some View {
ForEach(Array(myStruct.prop2.enumerated()), id: \.offset) { idx, val in
Text(val)
}
}
}
Question:
What use cases are there for using an ObservableObject than using a struct that mutates its own properties?
There are overlap use cases however I wish to understand the differences where:
Some situation A favours ObservableObject
Some situation B favours struct mutating properties
Before I begin, when you say "these properties causes a re-render" nothing is actually re-rendered all that happens is all the body that depend on lets and #State vars that have changed are invoked and SwiftUI builds a tree of these values. This is super fast because its just creating values on the memory stack. It diffs this value tree with the previous and the differences are used to create/update/remove UIView objects on screen. The actual rendering is another level below that. So we refer to this as invalidation rather than render. It's good practice to "tighten" the invalidation for better performance, i.e. only declare lets/vars in that View that are actually used in the body to make it shorter. That being said no one has ever compared the performance between one large body and many small ones so the real gains are an unknown at the moment. Since these trees of values are created and thrown away it is important to only init value types and not any objects, e.g. don't init any NSNumberFormatter or NSPredicate objects as a View struct's let because they are instantly lost which is essentially a memory leak! Objects need to be in property wrappers so they are only init once.
In both of your example situations its best to prefer value types, i.e. structs to hold the data. If there is just simple mutating logic then use #State var struct with mutating funcs and pass it into subviews as a let if you need read access or #Binding var struct if you need write access.
If you need to persist or sync the data then that is when you would benefit from a reference type, i.e. an ObservableObject. Since objects are created on the memory heap these are more expensive to create so we should limit their use. If you would like the object's life cycle to be tied to something on screen then use #StateObject. We typically used one of these to download data but that is no longer needed now that we have .task which has the added benefit it will cancel the download automatically when the view dissapears, which no one remembered to do with #StateObject. However, if it is the model data that will never be deinit, e.g. the model structs will be loaded from disk and saved (asynchronously), then it's best to use a singleton object, and pass it in to the View hierarchy as an environment object, e.g. .environmentObject(Store.shared), then for previews you can use a model that is init with sample data rather that loaded from disk, e.g. .environmentObject(Store.preview). The advantage here is that the object can be passed into Views deep in the hierarchy without passing them all down as let object (not #ObservedObject because we wouldn't want body invovked on these intermediary Views that don't use the object).
The other important thing is your item struct should usually conform to Identifiable so you can use it in a ForEach View. I noticed in your code you used ForEach like a for loop on array indices, that's a mistake and will cause a crash. It's a View that you need to supply with Indentifiable data so it can track changes, i.e. moves, insertions, deletions. That is simply not possible with array indices, because if the item moves from 0 to 1 it still appears as 0.
Here are some examples of all that:
struct UserItem: Identifiable {
var username: String
var id: String {
username
}
}
class Store: ObservableObject {
static var shared = Store()
static var preview = Store(preview: true)
#Published var users: [UserItem] = []
init(preview: Bool = false) {
if (preview) {
users = loadSampleUsers()
}
else {
users = loadUsersFromDisk()
}
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(Store.shared)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach($store.users) { $user in
UserView(user: $user)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Store.preview)
}
}
struct UserView: View {
#Binding var user: UserItem
var body: some View {
TextField("Username", text: $user.username)
}
}

Binding to subscript doesn't update TextField (macOS)

I have this Store struct, which is a wrapper for all my data. It has a subscript operator which takes in a UUID, and returns the associated object.
This way, I can have a List bind to a selection variable, which has type UUID, and then in another view I can access the selected object from that UUID.
However, I'm experiencing an issue where my TextField which binds to the Store doesn't update. It does update if I wrap it in another Binding, or if I instead just use Text.
Here is an minimal reproducible example:
struct Person: Identifiable, Hashable {
let id = UUID()
var name: String
}
struct Store {
var data: [Person]
subscript(id: Person.ID) -> Person {
get {
data.first(where: { $0.id == id })!
}
set {
data[data.firstIndex(where: { $0.id == id })!] = newValue
}
}
}
struct ContentView: View {
#State var store = Store(data: [
Person(name: "Joe"),
Person(name: "Eva"),
Person(name: "Sam"),
Person(name: "Mary")
])
#State var selection: Person.ID?
var body: some View {
NavigationView {
List(store.data, selection: $selection) {
Text($0.name)
}
if let selection = selection {
// Creating a new Binding which simply wraps $store[selection].name
// fixes this issue. Or just using Text also works.
TextField("Placeholder", text: $store[selection].name)
}
else {
Text("No Selection")
}
}
}
}
To reproduce this issue, just click different names on the Sidebar. For some reason the detail view's TextField doesn't update!
This issue can also be resolved if we simply move the Store to a ObservableObject class with #Published.
Also, making the Store conform to Hashable doesn't help this issue.
I feel like I'm missing something very basic with SwiftUI. Is there any way to fix this?
EDIT:
I've changed out Store for an [Person], and I made an extension with the same subscript operator that is in Store. However, the problem still remains!
try this:
TextField("Placeholder", text: $store[selection].name)
.id(selection) // <-- here

SwiftUI ForEach Binding compile time error looks like not for-each

I'm starting with SwiftUI and following WWDC videos I'm starting with #State and #Binding between two views. I got a display right, but don't get how to make back-forth read-write what was not include in WWDC videos.
I have model classes:
class Manufacturer {
let name: String
var models: [Model] = []
init(name: String, models: [Model]) {
self.name = name
self.models = models
}
}
class Model: Identifiable {
var name: String = ""
init(name: String) {
self.name = name
}
}
Then I have a drawing code to display that work as expected:
var body: some View {
VStack {
ForEach(manufacturer.models) { model in
Text(model.name).padding()
}
}.padding()
}
and I see this:
Canvas preview picture
But now I want to modify my code to allows editing this models displayed and save it to my model #Binding so I've change view to:
var body: some View {
VStack {
ForEach(self.$manufacturer.models) { item in
Text(item.name)
}
}.padding()
}
But getting and error in ForEach line:
Generic parameter 'ID' could not be inferred
What ID parameter? I'm clueless here... I thought Identifiable acting as identifier here.
My question is then:
I have one view (ContentView) that "holds" my datasource as #State variable. Then I'm passing this as #Binding to my ManufacturerView want to edit this in List with ForEach fill but cannot get for each binding working - how can I do that?
First, I'm assuming you have something like:
#ObservedObject var manufacturer: Manufacturer
otherwise you wouldn't have self.$manufacturer to begin with (which also requires Manufacturer to conform to ObservableObject).
self.$manufacturer.models is a type of Binding<[Model]>, and as such it's not a RandomAccessCollection, like self.manufacturer.models, which is one of the overloads that ForEach.init accepts.
And if you use ForEach(self.manufacturer.models) { item in ... }, then item isn't going to be a binding, which is what you'd need for, say, a TextField.
A way around that is to iterate over indices, and then bind to $manufacturer.models[index].name:
ForEach(manufacturer.indices) { index in
TextField("model name", self.$manufacturer.models[index].name)
}
In addition to that, I'd suggest you make Model (and possibly even Manufacturer) a value-type, since it appears to be just a storage of data:
struct Model: Identifiable {
var id: UUID = .init()
var name: String = ""
}
This isn't going to help with this problem, but it will eliminate possible issues with values not updating, since SwiftUI wouldn't detect a change.

SwiftUI Picker with selection as struct

Iam trying to use Picker as selection of struct. Let say I have a struct "Pet" like below
struct Pet: Identifiable, Codable, Hashable {
let id = UUID()
let name: String
let age: Int
}
I am getting all Pet's from some class, where Pets are defined as #Published var pets = Pet
static let pets = Class().pets
I would like to be able to write a selection from picker to below variable:
#State private var petSelection: Pet?
Picker is:
Picker("Pet", selection: $petSelection){
ForEach(Self.pets) { item in
Text(item.name)
}
}
Picker shows properly all avaliavble pets but when I chose one petSelection has been not changed (nil). How should I mange it?
Thanks!
Edit:
Of course I know that I can use tag like below:
Picker("Pet", selection: $petSelection) {
ForEach(0 ..< Self.pet.count) { index in
Text(Self.pet[index].name).tag(index)
}
But wonder is it possible to use struct as selection. Thanks
Short answer: The type associated with the tag of the entries in your Picker (the Texts) must be identical to the type used for storing the selection.
In your example: You have an optional selection (probably to allow "empty selection") of Pet?, but the array passed to ForEach is of type [Pet]. You have to add therefore a .tag(item as Pet?) to your entries to ensure the selection works.
ForEach(Self.pets) { item in
Text(item.name).tag(item as Pet?)
}
Here follows my initial, alternate answer (getting rid of the optionality):
You have defined your selection as an Optional of your struct: Pet?. It seems that the Picker cannot handle Optional structs properly as its selection type.
As soon as you get rid of the optional for example by introducing a "dummy/none-selected Pet", Picker starts working again:
extension Pet {
static let emptySelection = Pet(name: "", age: -1)
}
in your view initialise the selection:
#State private var petSelection: Pet = .emptySelection
I hope this helps you too.
You use the following way:
#Published var pets: [Pet?] = [ nil, Pet(name: "123", age: 23), Pet(name: "123dd", age: 243),]
VStack{
Text(petSelection?.name ?? "name")
Picker("Pet", selection: $petSelection){
ForEach(Self.pets, id: \.self) { item in
Text(item?.name ?? "name").tag(item)
}}}
the type of $petSelection in Picker(selection:[...] has to be the same type of id within your struct.
So in your case you would have to change $petSelection to type if UUID since your items within the collection have UUID as identifier.
Anyway since this is not what you're after, but your intention is to receive the Pet as a whole when selected. For that you will need a wrapper containing Pet as the id. Since Pet is already Identifiable, there're only a few adjustments to do:
Create a wrapper having Pet as an id
struct PetPickerItem {
let pet: Pet?
}
Now wrap all collection items within the picker item
Picker("Pet", selection: $petSelection) {
ForEach(Self.pets.map(PetPickerItem.init), id: \.pet) {
Text("\($0.pet?.name ?? "None")")
}
}
You can now do minor adjustments like making PetPickerItem identifiable to remove the parameter id: from ForEach.
That's the best solution I came up with.
This is how I do it:
struct Language: Identifiable, Hashable {
var title: String
var id: String
}
struct PickerView: View {
var languages: [Language] = [Language(title: "English", id: "en-US"), Language(title: "German", id: "de-DE"), Language(title: "Korean", id: "ko-KR")]
#State private var selectedLanguage = Language(title: "German", id: "de-DE")
var body: some View {
Picker(selection: $selectedLanguage, label: Text("Front Description")) {
ForEach(self.languages, id: \.self) {language in
Text(language.title)
}
}
}