Putting aside the reasons why one might choose a value type over a reference type, does SwiftUI itself prefer use of #State vs #ObservedObject in terms of architecture.
Here for example are two implementations of a basic list app - cats implemented using a struct, dogs using a class:
import SwiftUI
struct Cat: Identifiable {
let id = UUID()
var name: String = "New cat"
}
final class Dog: Identifiable, ObservableObject {
let id = UUID()
#Published var name: String = "New dog"
}
final class Dogs: ObservableObject {
#Published var dogs: [Dog] = []
}
struct ContentView: View {
#State private var cats: [Cat] = []
#ObservedObject private var dogs = Dogs()
var body: some View {
VStack {
Text("Cats").bold()
ForEach($cats, content: CatView.init)
Button("New cat") {
cats.append(Cat())
}
Divider().padding()
Text("Dogs").bold()
ForEach(dogs.dogs, content: DogView.init)
Button("New dog") {
dogs.dogs.append(Dog())
}
}
}
}
struct CatView: View {
#Binding var cat: Cat
var body: some View {
TextField("Name", text: $cat.name)
}
}
struct DogView: View {
#ObservedObject var dog: Dog
var body: some View {
TextField("Name", text: $dog.name)
}
}
Very little difference in terms of coding...so:
Is there a reason why one might prefer one over the other? Apple's own examples often use structs + #State + #Bindings.
Is there any performance implication for either choice, say when either Cat or Dog object has many fields (ObservedObjects let you precisely specify which attribute should generate an objectWillChange call), or if the arrays hold many items, i.e. 100k's?
When would one go for a reference type, when Bindings give pseudo-reference attributes to a value type?
structs are cheaper to create than classes since there is no cost of references or ARC. Using structs also reduces the risk of shared-mutation bugs. See Choosing Between Structures and Classes (Apple developer article) on why to prefer structs.
Your code also has a bug in it which you may not have initially noticed. Since Dog is a class, and Dogs contains an array of Dog, the #Published here won't help in certain scenarios. This is because #Published detects a value type changing, e.g. the array having different elements. However, this doesn't account for mutating a Dog's name property. The Dog class reference in the array doesn't change so the #Published in Dogs doesn't emit an update. This is another bug caused by using a class instead of a struct. There is a way around this, but it introduces unnecessary complexity.
Is there a reason why one might prefer one over the other? Apple's own examples often use structs + #State + #Bindings.
Prefer structs because they are faster and less prone to bugs (see more detail above). In my apps I built / am building, I very rarely used ObservableObjects. They were the source of many bugs, unnecessary view updates, and complexity. The performance also greatly suffers when you have an ObservableObject with multiple #Published properties - changing one property updates every view which subscribes to the object updates.
Is there any performance implication for either choice, say when either Cat or Dog object has many fields (ObservedObjects let you precisely specify which attribute should generate an objectWillChange call), or if the arrays hold many items, i.e. 100k's?
The cost here is mutating the array of Cat or Dog, so I doubt there would be a performance difference to prefer class over struct. As mentioned earlier, structs are still more performant in general. Since in neither scenario the array is being copied (and if it was copied, it would again be the same) there is no difference.
When would one go for a reference type, when Bindings give pseudo-reference attributes to a value type?
Use ObservableObjects when you rely on sharing data across a section of your app and potentially need some logic to get/set the #Published properties within. I (personally) only ever use ObservableObjects when I need to pass the object through the environment at some point with #EnvironmentObject.
To finish off, choose what you think is best for your architecture. Prefer structs everywhere possible, and switch to class if it solves a problem that is too complex to solve just with structs.
Related
The execution of this code is confusing me:
class RecipeListViewModel: ObservableObject {
#Published var recipeList = [RecipeViewModel]()
init(){
RecipeRepository.$allRecipeVMs.map { recipes in
recipes.map { recipe in
recipe
}
}
.assign(to: &$recipeList)
}
}
My understanding was that SwiftUI publishers are uni-directional, meaning that when RecipeRepository.allRecipeVMs is modified recipeList is too. However, the behaviour I'm seeing is that When recipeList is updated, RecipeRepository.allRecipeVMs is also updated. I've tried to find documentation on this but can't seem to find anything.
Here is the RecipeViewModel class, not sure if it's useful:
class RecipeViewModel : Identifiable, ObservableObject, Hashable{
var id = UUID().uuidString
#Published var recipe: Recipe
init(recipe: Recipe){
self.recipe = recipe
}
}
It's not clear what "modifications" you are calling. #Published and SwiftUI views diff value types, whereas you've created an array of reference types. It will diff only when the instance of that object changes, not when the object itself fires its ObjectWillChange publisher.
Frankly, that's a pretty useful setup because you can have containers that distribute and manage dependencies in your app without causing the root view hierarchy to diff and rebuild constantly.
If you want some sort of two-way Delegate relationship, you can simply set a weak variable in the VMs to self.
I am trying out this quick start for SwiftUI and Combine in order to try and understand how to connect my Realm database to Combine.
The example observes a RealmSwift.List and keeps a table populated with its data. This is is a linked list to a child class. I'm wondering how to observe a Results collection so I can keep track of any changes to an entire Realm class.
For example, let's say I have a Workspace class:
class Workspace: Object, ObjectKeyIdentifiable{
#objc dynamic var id = UUID().uuidString
#objc dynamic var name = ""
#objc dynamic var archived = false
}
In the state object, I can set up a Results<Workspace> variable like this:
class AppState: ObservableObject {
#Published var workspaces: Results<Workspace>?
var cancellables = Set<AnyCancellable>()
init(){
let realmPublisher = PassthroughSubject<Realm, Error>()
realmPublisher
.sink(receiveCompletion: { _ in }, receiveValue: { realm in
//Get the Results
self.workspaces = realm.objects(Workspace.self)
})
.store(in: &cancellables)
realmPublisher.send(try! Realm())
return
}
}
But when it comes time to observe the object, I can't because Results isn't an object (I assume).
struct ContentView: App {
#ObservedObject var state = AppState()
var view: some View {
ItemsView(workspaces: state.workspaces!)
}
var body: some Scene {
WindowGroup {
view.environmentObject(state)
}
}
}
struct ItemsView: View {
#ObservedObject var workspaces: Results<Workspace> //<!-- Error
var body: some View {
//...
}
}
Xcode gives a syntax error on the workspaces property:
Property type 'Results' does not match that of the 'wrappedValue' property of its wrapper type 'ObservedObject'
Is it possible to observe a set of Results just like we can have a notification listener on a collection of Results?
Technically, you could hook up a sink to state.workspaces (state.$workspaces.sink()), but in this case, I think you're overcomplicating the problem.
You already have an #ObservableObject in your ContentView (AppState) that is managing the results for you. So, change ItemsView to just take this as a parameter:
var workspaces: Results<Workspace>?
It doesn't need to be an #ObservedObject -- either way, whether it's getting observed in that view or it's parent view, it's going to get re-rendered. It does have to be optional here, since it's an optional value on your AppState, unless you want to keep passing it with the force unwrap (!), but that's generally a bad idea, since it'll crash if it ever is in fact nil.
Also, above, in your Realm code, make sure it's matching the tutorial that you were following. For example, you have Publisher.sink which should really be realmPublisher.sink
You are correct, Results is a struct, and therefore cannot be covered by #StateObject or #ObservedObject. Your workaround is suitable for now.
Once https://github.com/realm/realm-cocoa/pull/7045 is released, you will be able to use one of the new Realm property wrappers to embed your Results into the view directly. At the time of this posting, that would be #FetchRealmResults, but that is subject to change.
I don't fully understand what exactly \.self does in the following code:
struct ContentView: View {
#State private var numbers = [Int]()
#State private var currentNumber = 1
var body: some View {
VStack {
List {
ForEach(numbers, id: \.self) {
Text("\($0)")
}
}
Button("Add Number") {
self.numbers.append(self.currentNumber)
self.currentNumber += 1
}
}
}
}
I'm following this: https://www.hackingwithswift.com/books/ios-swiftui/deleting-items-using-ondelete.
I have a really basic understanding of the language right now, so I may not understand high level explanations, so would prefer very simple explanations or analogies with verbose descriptions. I think it is setting the idfor each list item as each item in the numbers array? Correct me if wrong - but is each id being set as whatever Int is in each entry of the numbers array? If so, then what does \ actually do when typing \.self and what does .self actually do in combination with \?
. key paths. ForEach needs every object unique. give them unique ids with id:.self. if your objects are identifiable you dont need .self.
i wrote about this in medim if you want you can check out
id: .self tells Swift to use as unique id (keypath) the hash of the object. It explains why the name "self" is used. id: .self is especially useful for the basic Swift types like Integer and String. On the one the hand the developer cannot add to them an unique id. On the other hand all they are hashable so we can use id: .self. id: .self is useful not only for ForEach, but for List also.
I have list of structs that I display in the view using a ForEach, I am trying to add to the list and when the user taps they see information for that item. Using two approaches I get two different results.
First Approach
The view updates the view when items are added perfectly, but changing the note in the Details changes values of all notes.
#Binding var person: Person
ForEach(self.person.notes) {
note in
DetailsCard(person: self.$person, note: notes)
}
Second Approach
The view does not update when notes are added, only when the view reappears does it show the new items. But I when the items are shown, the details view works as expected.
#Binding var person: Person
ForEach(self.person.notes.indices) { index in
VStack {
DetailsCard(person: self.$person, note: self.person.notes[index])
}
}
DetailView
#Binding var person: Person
#State var note: Note
This should be a fairly simple task but working with SwiftUI I am confused by the two different results for similar approaches. Does anyone have any ideas on why this is occurring and how to correctly dynamically update the view and pass the values.
Try using dynamic ForEach loops, ie. with an explicit id parameter:
ForEach(self.person.notes, id:\.self)
ForEach(self.person.notes.indices, id:\.self)
Note: id must conform to Hashable. If you decide to use self as an id, it will only work if your item conforms to Hashable. Otherwise you may want to use some property (or just add the conformance).
I'm looking at an example of using SwiftUI with Combine: MVVM with Combine Tutorial for iOS at raywenderlich.com. A ViewModel implementation is given like this:
class WeeklyWeatherViewModel: ObservableObject, Identifiable {
// 2
#Published var city: String = ""
// 3
#Published var dataSource: [DailyWeatherRowViewModel] = []
private let weatherFetcher: WeatherFetchable
// 4
private var disposables = Set<AnyCancellable>()
init(weatherFetcher: WeatherFetchable) {
self.weatherFetcher = weatherFetcher
}
}
So, this makes some sense to me. In a view observing the model, an instance of the ViewModel is declared as an ObservedObject like this:
#ObservedObject var viewModel: WeeklyWeatherViewModel
And then it's possible to make use of the #Published properties in the model in the body definition of the View like this:
TextField("e.g. Cupertino", text: $viewModel.city)
In WeeklyWeatherViewModel Combine is used to take the city text, make a request on it, and turn this in to [DailyWeatherRowViewModel]. Up to here, everything is rosey and makes sense.
Where I become confused is that quite a lot of code is then used to:
Trigger a fetch when city is changed.
Keep hold of the AnyCancellable that's looking up weather data.
Copy the output of the weather look up in to dataSource by a sink on the weather fetch Publisher`
It looks like this:
// More in WeeklyWeatherViewModel
init(
weatherFetcher: WeatherFetchable,
scheduler: DispatchQueue = DispatchQueue(label: "WeatherViewModel")
) {
self.weatherFetcher = weatherFetcher
_ = $city
.dropFirst(1)
.debounce(for: .seconds(0.5), scheduler: scheduler)
.sink(receiveValue: fetchWeather(forCity:))
}
func fetchWeather(forCity city: String) {
weatherFetcher.weeklyWeatherForecast(forCity: city)
.map { response in
response.list.map(DailyWeatherRowViewModel.init)
}
.map(Array.removeDuplicates)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] value in
guard let self = self else { return }
switch value {
case .failure:
self.dataSource = []
case .finished:
break
}
},
receiveValue: { [weak self] forecast in
guard let self = self else { return }
self.dataSource = forecast
})
.store(in: &disposables)
}
If I look in Combine for the definition of the #Published propertyWrapper, it seems like all does is provide projectedValue which is a Publisher, which makes it seem like it ought to be possible for WeeklyWeatherViewModel to simply provide the Publisher fetching weather data and for the view to make use of this directly. I don't see why the copying in to a dataSource is necessary.
Basically, what I'm expecting is there to be a way for SwiftUI to directly make use of a Publisher, and for me to be able to put that publisher externally from a View implementation so that I can inject it. But I've no idea what it is.
If this doesn't seem to make any sense, that figures, as I'm confused. Please let me know and I'll see if I can refine my explanation. Thanks!
I don't have a definitive answer to this and I didn't find a magic way to have SwiftUI make use of a Publisher directly – it's entirely possible that there is one that eludes me!
I have found a reasonably compact and flexible approach to achieving the desired result, though. It cut down the use of sink to a single occurrence that attaches to the input (#Published city in the original code), which substantially simplifies the cancelation work.
Here's a fairly generic model that has an #Published input attribute and a #Published output attribute (for which setting is private). It takes a transform as input, and this is used to transform the input publisher, and is then sinked in to the output publisher. The Cancelable of the sink is stored.
final class ObservablePublisher<Input, Output>: ObservableObject, Identifiable {
init(
initialInput: Input,
initialOutput: Output,
publisherTransform: #escaping (AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never>)
{
input = initialInput
output = initialOutput
sinkCancelable =
publisherTransform($input.eraseToAnyPublisher())
.receive(on: DispatchQueue.main)
.sink(receiveValue: { self.output = $0 })
}
#Published var input: Input
#Published private(set) var output: Output
private var sinkCancelable: AnyCancellable? = nil
}
If you wanted a substantially less generic kind of model, you can see it's pretty easy to set up having the input (which is a publisher) be filtered in to the output.
In a view, you might declare an instance of the model and use it like this:
struct SimpleView: View {
#ObservedObject var model: ObservablePublisher<String, String>
var body: some View {
List {
Section {
// Here's the input to the model taken froma text field.
TextField("Give me some input", text: $model.input)
}
Section {
// Here's the models output which the model is getting from a passed Publisher.
Text(model.output)
}
}
.listStyle(GroupedListStyle())
}
}
And here's some silly setup of the view and its model taken from a "SceneDelegate.swift". The model just delays whatever is typed in for a bit.
let model = ObservablePublisher(initialInput: "Moo moo", initialOutput: []) { textPublisher in
return textPublisher
.delay(for: 1, scheduler: DispatchQueue.global())
.eraseToAnyPublisher()
}
let rootView = NavigationView {
AlbumSearchView(model: model)
}
I made the model generic on the Input and Output. I don't know if this will actually be useful in practice, but it seems like it might be.
I'm really new to this, and there might be some terrible flaws in this such as inefficiencies, memory leaks or retain cycles, race conditions, etc. I've not found them yet, though.
You can use URLSessionDataTaskPublisher and refactor out networking from all view models.
If you feel some part of the tutorial seems redundant, that is because it is.
MVVM in such usage is redundant and does not do the job better.
I have a refactored version (networking refactored, all view models removed) of that tutorial if you are interested in details.