I'm trying to build a little app in SwiftUI. It is supposed to show a list of items an maybe change those. However, I am not able to figure out, how the data flow works correctly, so that changes will be reflected in my list.
Let's say I have a class of Item like this:
class Item: Identifiable {
let id = UUID()
var name: String
var dateCreated: Date
}
And this class has an initializer, that assigns each member a useful random value.
Now let's say I want to store a list of items in another class like this:
class ItemStore {
var items = [Item]()
}
This item store is part of my SceneDelegate and is handed to the ContextView.
Now what I want to do is hand one element to another view (from the stack of a NavigationView), where it will be changed, but I don't know how to save the changes made so that they will be reflected in the list, that is shown in the ContextView.
My idea is to make the item store an environment object. But what do I have to do within the item class and how do I have to pass the item to the other view, so that this works?
I already tried something with the videos from Apple's WWDC, but the wrappers there are deprecated, so that didn't work.
Any help is appreciated. Thanks a lot!
The possible approach is to use ObservableObject (from Combine) for storage
class ItemStore: ObservableObject {
#Published var items = [Item]()
// ... other code
}
class Item: ObservableObject, Identifiable {
let id = UUID()
#Published var name: String
#Published var dateCreated: Date
// ... other code
}
and in dependent views
struct ItemStoreView: View {
#ObservedObject var store: ItemStore
// ... other code
}
struct ItemView: View {
#ObservedObject var item: Item
// ... other code
}
Related
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.
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)
}
}
Every SwiftUI tutorial/example uses model objects that are defined by me, the guy writing the app. However, what is the best practice when the model objects are not under my direct control? For example, the HomeKit framework has an API to get all the rooms in a given home. It returns an array of HMRoom objects:
open class HMRoom: NSObject
{
open var name: String { get }
open var accessories: [HMAccessory] { get }
open var uniqueIdentifier: UUID { get }
// Update the room's name in a HomeKit-compliant way that notifies all other HomeKit apps
open func updateName(_ name: String) async throws
}
When I receive an array of HMRoom objects from the HomeKit API, what should I do with them to power SwiftUI? Should I create my own class that looks like this:
final class Room: ObservableObject
{
#Published var name: String
#Published var accessories: [Accessory]
#Published var uniqueIdentifier: UUID
private var representedRoom: HMRoom
init(homekitRoom: HMRoom)
{
// Copy properties from 'homekitRoom' to self, then set `representedRoom` to `homekitRoom` so we can use it to call the updateName(:) function
}
}
Is there, instead, a way for me to extend the HMRoom class directly to inform SwiftUI that name, accessories, and uniqueIdentifier are the properties we must watch for changes in order to reload views appropriately?
It's unclear to me what the best approach is to integrate #Published/#ObservableObject when I don't write the model classes/structs myself.
If you read Apple's document Choosing Between Structures and Classes the Use Classes When You Need to Control Identity section makes you think use a class, however if you read the Use Structures When You Don't Control Identity section and notice that every HomeKit object contains a uniqueIdentifier it turns out we can use structs yay!
struct Room: Identifiable {
let id: UUID
var name: String
}
class HomeDelegate: NSObject, HMHomeDelegate {
weak var homeManagerDelegate: HomeManagerDelegate?
// HomeManagerDelegate sets the primaryHome.
var home: HMHome? {
didSet {
oldValue?.delegate = nil
home?.delegate = self
reload()
}
}
func reload() {
let rooms: [Room]
if let home = home {
rooms = home.rooms.map { r in
Room(id: r.uniqueIdentifier, name: r.name)
}
}else {
rooms = []
}
homeManagerDelegate?.rooms = rooms
}
func home(_ home: HMHome, didUpdateNameFor room: HMRoom) {
if let index = homeManagerDelegate?.rooms.firstIndex(where: { $0.id == room.uniqueIdentifier } ) {
homeManagerDelegate?.rooms[index].name = room.name
}
}
Note: I'd recommend not using structs that mirror the home classes and instead create a struct that contains what you want to show in your UI. And in your model ObservableObject you could have several #Published arrays of different custom structs for different HomeKit related things. Apple's Fruta and Scrumdinger sample projects can help with that.
Why not a class to hold all of your rooms with a published variable like this:
class Rooms: ObservableObject {
#Published rooms: [HMRoom]
}
Isn't that your view model? You don't need the room parameters to be published; you need to know when anything changes and evaluate it from there.
I’m loading data that appears throughout my app from a content management system's API. Once the data has finished loading, I’d like to be able to change a boolean from false to true, then make that boolean readable by every other view in my app so that I can create conditional if/else statements depending on whether the data has loaded yet or not.
I’ve tried doing this by creating an Observable Object at the root level to store the boolean, setting it to false by default:
#main
struct MyApp: App {
#StateObject var dataLoadingObject = DataHasLoaded()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(dataLoadingObject)
}
}
}
class DataHasLoaded: ObservableObject {
#Published var status: Bool = false
}
Then, in my data model file, I've tried to reference that Observable Object using #EnvironmentObject and then switch the boolean to true once the data has loaded:
class DataStore: ObservableObject {
#Published var stories: [Story] = storyData
#EnvironmentObject var dataLoadingObject: DataHasLoaded
init() {
// Fetching and appending
// the data here
self.dataLoadingObject.status = true
}
}
The app builds successfully, but when the data loads it crashes and I get this error message: Thread 1: Fatal error: No ObservableObject of type DataHasLoaded found. A View.environmentObject(_:) for DataHasLoaded may be missing as an ancestor of this view.
It seems like the issue is that my data model view isn't explicitly a child of ContentView(), so it's not aware of the Environment Object. That's where I get stuck. Do I:
a) Try to get my data model to be a child of ContentView()?
b) Use something that doesn't require a parent/child relationship?
c) Solve this some totally different way?
d) Continue to bang my head against my desk?
Thanks for your help!
#EnvironmentObject can be used in View only. It can't be used in ObservableObject.
You can pass DataHasLoaded in init instead:
class DataStore: ObservableObject {
#Published var stories: [Story] = storyData
var dataLoadingObject: DataHasLoaded
init(dataLoadingObject: DataHasLoaded) {
self.dataLoadingObject = dataLoadingObject
...
}
}
Just make sure you always pass the same instance.
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.