I have a data model in my SwiftUI app that looks something like this:
struct Entry: Identifiable{
var id = UUID().uuidString
var name = ""
var duration: Int{
//An SQLite query that returns the total of the "duration" column
let total = try! dbc.scalar(tableFlight.filter(db.entry == id).select(db.duration.total))
return Int(total)
}
}
struct Flight: Identifiable{
var id = UUID().uuidString
var duration = 0
var entry: String?
}
I have an ObservableObject view model that produces the entries like this:
class EntryModel: ObservableObject{
static let shared = EntryModel()
#Published var entries = [Entry]()
init(){
get()
}
func get(){
//Stuff to fetch the entries
entries = //SQLite query that returns an array of Entry objects
}
}
Then finally, in my View, I list all the entry names and their associated duration like this:
ForEach(modelEntry.entries){ entry in
VStack{
Text(entry.name) //<-- Updates fine
Text(entry.duration) //<-- Gets set initially, but no updates
}
}
The issue I'm having is that when I update a Flight for that Entry, the duration in my view doesn't update. I know that won't happen because only the entries will redraw when they are changed.
But even if I manually call the get() function in my EntryModel, the associated duration still doesn't update.
Is there a better way to do this? How do I get the parent's computed properties to recalculate when its child element is updated?
I figured it out. My actual code used a child View inside the ForEach where the VStack is. I was just passing an entry to it, so the values were only getting set initially and were thus not reactive.
By changing that entry to a Binding, it's working:
ForEach($modelEntry.entries){ $entry in
ChildView(entry: $entry)
}
Note that the $ on the $modelEntry and the return $entry is an Xcode 13+ feature (but backward compatible to iOS 14 and macOS 11.0).
Related
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)
}
}
I want to make a global variable in Swift, so that its Data is accessible to any view that needs it. Eventually it will be a var so that I can mutate it, but while trying to get past this hurdle I'm just using it as let
I can do that by putting this as the top of a file (seemingly any file, Swift is weird):
let myData: [MyStruct] = load("myDataFile.json)
load() returns a JSONDecoder(). MyStruct is a :Hashable, Codable, Identifiable struct
That data is then available to any view that wants it, which is great. However, I want to be able to specify the file that is loaded based on a condition - I'm open to suggestions, but I've been using an #AppStorage variable to determine things when inside a View.
What I'd like to do, but can't, is do something like:
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
if(appStorageVar == "Condition2") {
let myData: [MyStruct] = load("myDataFile2.json")
}
else {
let myData: [MyStruct] = load("myDataFile.json")
}
I can do this inside a View's body, but then it's only accessible to that View and then I have to repeat it constantly, which can't possibly the correct way to do it.
You could change just change the global in an onChange on the AppStorage variable. This is an answer to your question, but you have the problem that no view is going to be updating itself when the global changes.
var myData: [MyStruct] = load("myDataFile.json)
struct ContentView: View {
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
var body: some View {
Button("Change value") {
appStorageVar = "Condition2"
}
.onChange(of: appStorageVar) { newValue in
myData = load(newValue == "Condition1" ? "myDataFile.json" : "myDataFile2.json")
}
}
}
This question already has answers here:
SwiftUI #State var initialization issue
(8 answers)
Closed 1 year ago.
I have a view that is initialized with a list of items, and then during the initialization process, we need to pick one item at random. Something like this:
struct ItemsView: View {
var items:[Item]
#State var current:Item?
init(items:[Item] = []) {
self.items = items
if items.count > 0 {
let index = Int.random(in: 0..<items.count)
self.current = items[index] // doesnt work
}
if current != nil {
print("init with", self.items.count, "items. Chose", current!)
}
}
// ...
}
The job of the ItemsVew is to show one item at time, at random, so we start by picking an item at random, but self.current = items[index] literally doesn't do anything as far as I can tell, for reasons I don't understand. So either I am doing something silly, or I am thinking about how to solve this in an incorrectly somehow.
So how do you initialize a State variable in the init function. I have tried:
self.current = State(initialValue: items[index])
But that simply triggers a compiler error. So how do we select an initial item that will be used when the view is displayed?
Cannot assign value of type 'State<Item>' to type 'Item'
What am I doing wrong?
Because #State is a property wrapper, you want to assign to the underlying variable itself, not the wrapped value type (which is Item? in this case).
self._current = State(initialValue: items[index])
// ^ note the underscore
The best documentation for this is available in the original Swift Evolution SE-0258 proposal document.
#State is a property wrapper, so you will need to assign a Item? object to the current property that is why the compiler gives you an error. So as long as the items array you pass is not empty, this should work as expected. Also I encourage you to use randomElement() method on the items array instead of producing a random index.
struct ItemsView: View {
var items: [Item]
#State var current: Item?
init(items: [Item] = []) {
self.items = items
self.current = items.randomElement()
if let item = current {
print("init with", self.items.count, "items. Choose", item)
}
}
// ...
}
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
}
Im facing issues with displaying my Core Data inside of SwiftUI because of relationships being Sets. What is the best way to go about displaying a many relationship set inside of a SwiftUI ForEach loop?
For instance, I have two entities in core date: Entry & Position. Entry can contain many positions, and each position can only belong to one entry.
I have the Entry as a #binding var on the view that is suppose to display the entry's Positions. I would ideally like to access the positions directly from this entry variable, but because positions are a Set, I get the following error:
error 'ForEach' requires that 'Set' conform to 'RandomAccessCollection'
#Binding private var entry: Entry?
ForEach(entry?.positions) { position in
Text("position here")
}
Solution One:
Or, I could do a fetch request for all positions, then filter out all the ones that do not belong to the entity, I do not like this idea as I would be fetching potentially thousands of Positions to only get a few. (or am i thinking of this wrong? Im new to core data, coming from realm because of swiftui)
Although this could work if I could do a fetch ONLY on the #binding entry var, and fetch all its positions as sorted fetched results, but I'm not sure there is a way to do this.
Maybe like this, or would this be a performance issue if there was potentially thousands of entry's each with 10-20+ positions? and could objectID be used this way, and would it still be unique if the entry was moved into another journal?:
#Binding private var entry: Entry?
#FetchRequest(
entity: Position.entity(),
sortDescriptors: [],
predicate: NSPredicate(formate: "entry.objectID == %#", self.entry.objectID)
) var positions: FetchedResults<Position>
Solution Two:
I thought of adding an attribute to positions like 'date', this way positions could be compared and sorted? But not sure how this could be updated with SwiftUI, as it would be done only once in the init().
let list = entry.wrappedValue?.positions?.sorted()
Core Data Models:
public class Entry: NSManagedObject, Identifiable {
// MARK: - ATTRIBUTES
#NSManaged public var date: Date
// MARK: - RELATIONSHIPS
#NSManaged public var journal: Journal?
#NSManaged public var positions: Set<Position>?
}
public class Position: NSManagedObject, Identifiable {
// MARK: - RELATIONSHIPS
#NSManaged public var entry: Entry?
}
How would you go about solving this problem? Keep in mind on the view where the positions are being listed, that this view can add, delete, and modify positions, so the solution should allow SwiftUI to reload the view and update the positions when changes are made.
#JoakimDanielson comment works like a charm, but requires a few tweaks.
Wrapping the set in the array initializer works like this, but requires optional sets to be unwrapped. I was surprised to find force unwrapping does not cause a crash even if the set is nil? Maybe someone could explain this?
ForEach(Array(entry.positions!)) { position in
Text("Position")
}
The next issue was that the array would be randomized everytime the set of positions changed due to sets being unordered. So by conforming Position to the Comparable Protocol solved this. I decided it made the most sense to sort positions by date, so I updated the model like so:
public class Position: NSManagedObject, Identifiable {
// MARK: - ATTRIBUTES
#NSManaged public var date: Date
// MARK: - RELATIONSHIPS
#NSManaged public var entry: Entry?
}
extension Position: Comparable {
static func < (lhs: Position, rhs: Position) -> Bool {
lhs.date < rhs.date
}
}
Then the ForEach could be sorted and looks like this:
ForEach(Array(entry.positions!).sorted()) { position in
Text("\(position.date)")
}
Some other solutions I found but are not ideal for reasons mentioned in original post, but they do work, is to either use a fetch request customized inside the view init like so:
#FetchRequest var positions: FetchedResults<Position>
init(entry: Entry) {
var predicate: NSPredicate?
// Do some kind of control flow for customizing the predicate here.
predicate = NSPredicate(formate: "entry == %#", entry)
self._positions = FetchRequest(
entity: Position.entity(),
sortDescriptors: [],
predicate: predicate
)
}
or create an "middle man" View Model bound to #ObservedObject that converts core data managed objects to useable view data. Which to me makes the least sense because it will basically contain a bunch of redundant information already found inside the core data managed object, and I like the idea of the core data managed object also being the single source of truth.
I found myself using this solution frequently so I added an extension to the CoreData object (see below for an example using Entry/Position types). This also has the added benefit of handling optional relationships and simply returning an empty array in that case.
extension Entry {
func arrayOfPositions() -> [Position] {
if let items = self.positions as? Set<Position> {
return items.sorted(by: {$0.date < $1.date})
} else {
return []
}
}
}
So that instead of unsafely and cumbersomely writing:
ForEach(Array(entry.positions! as! Set<Position>).sorted(by: {$0.date < $1.date})) { item in
Text(item.description)
}
You can safely use:
ForEach(entry.arrayOfPositions()) { item in
Text(item.description)
}
Simply pass a customised FetchRequest param to the View containing the #FetchRequest property wrapper.
struct PositionsView: View {
let entry: Entry
var body: some View {
PositionsList(positions: FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Positions.date, ascending: true)], predicate: NSPredicate(format: "entry = %#", entry)))
}
}
struct PositionsList : View {
#FetchRequest var positions: FetchedResults<Positions>
var body: some View {
ForEach(positions) { position in
Text("\(position.date!, formatter: positionFormatter)")
}
}
}
For more detail, see this answer.