Object's property not updating in array SwiftUI - ios

I'm picking the photos from the PHPicker and displaying them in the VStack using the ForEach loop in SwiftUI. I'm wrapping the image in an Object with 3 properties.
class UploadedImage: Hashable, Equatable {
var id: Int
var image : UIImage
#State var quantity: Int
init(id: Int, image: UIImage, quantity: Int) {
self.id = id
self.image = image
self.quantity = quantity
}
static func == (lhs: UploadedImage, rhs: UploadedImage) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
}
Then I'm adding that object in the array after the picker select the images, and then displaying on the screen using the ForEach.
if !uploadedImages.isEmpty {
ScrollView {
VStack{
ForEach(uploadedImages, id: \.self) { selectedImage in
HStack{
ZStack{
// some other code to display image...
}
Spacer()
HStack{
Button(action: {
if let index = self.uploadedImages.firstIndex(where: {$0.id == selectedImage.id}){
uploadedImages[index] = UploadedImage(id: selectedImage.id, url: selectedImage.url, quantity: (1))
}
}, label: {
Text("-")
})
Text("\(selectedImage.quantity)")
Button(action: {
if let index = self.uploadedImages.firstIndex(where: {$0.id == selectedImage.id}){
uploadedImages[index] = UploadedImage(id: selectedImage.id, url: selectedImage.url, quantity: selectedImage.quantity + 1)
}
}, label: {
Text("+")
})
}
}
.padding()
.border(Color.black, width: 2)
.cornerRadius(6)
}
}
}
}
}
In the ForEach loop, I'm showing two buttons, +, - to increase and decrease the quantity.
On that button the action is the following code.
if let index = self.uploadedImages.firstIndex(where: {$0.id == selectedImage.id}){
uploadedImages[index] = UploadedImage(id: selectedImage.id, url: selectedImage.url, quantity: selectedImage.quantity + 1)
}
But the quantity is not updating. I'm not sure why.
When I change the id in the new updated Object.
uploadedImages[index] = UploadedImage(id: 89282, url: selectedImage.url, quantity: selectedImage.quantity + 1)
Then the quantity is updating only once.
Any good solution to increase and decrease the quantity?

Data flow in SwiftUI can be a bit tricky to get used to initially, depending on what framework you’re coming from.
There are various ways to approach it from high-level perspectives, but given that your question is about this localized issue of a single View, its model, and a button, I will focus on data from a single View’s perspective.
To then understand how data flows between Views, how it interacts with SwiftUI’s View lifecycle, and more, I would recommend these two WWDC sessions:
Data Essentials in SwiftUI
Demystify SwiftUI
There’s also this document from Apple with a decent breakdown:
Model data.
And this section in the SwiftUI Tutorials: Managing state and life cycle
Returning to the View’s perspective.
The way you set up your data inside a View will be quite different based on two characteristics of your data:
Is the data a value type or a reference type?
Do you want SwiftUI to handle the lifecycle of your data or do you want to manage it yourself?
So on the simple end of that matrix, you have a value type that you let SwiftUI handle for you.
This is what #State is for.
Inside a View, you might use #State like this to deal with a string:
#State var name = “Bob”
but a struct would work as well:
#State var selectedUser = MyUserStruct()
The important thing to know about #State is that it is explicitly meant for this simple end of the possible scenarios.
#State
is intended to be used only within Views
needs the data to be a value type to function properly
is bound to the lifecycle of the View
It’s meant for data that represents state internal to your Views.
Note, that nothing has to be done to our data in this case, to make SwiftUI understand when to update the View. (At least for most use cases.)
Moving up in complexity:
What if you want SwiftUI to handle the data, but you are dealing with a reference type?
This is the setup you currently have.
In contrast to the value type example, SwiftUI needs additional help understanding when to update the Views that depend on reference types.
Therefore, any classes that should drive View updates need to conform to the ObservableObject protocol.
Further, within those classes, only properties marked #Published will actually drive View updates when those properties change.
To then use an #ObservableObject inside a View, you have a choice of a couple of property wrappers with very different behaviors:
#StateObject, #ObservedObject, and #EnvironmentObject.
#StateObject is the closest in its behavior to #State. The object is managed by SwiftUI and the lifecycle of an ObservableObject that is initialized as a #StateObject is bound to the lifecycle of the View in which it is initialized.
#ObservedObject and #EnvironmentObject don’t match our current location in the matrix of complexity, as they are meant for the case in which you don’t want SwiftUI to handle your data’s lifecycle (or not as directly).
But this actually gives us sufficient information to restructure your example and stay within our single View perspective.
Let’s first take your example literally and pretend that we just want to make those localized updates in an array, and that’s its.
For this case, #State would actually work, but we will need to change your UploadedImage model to a struct:
struct UploadedImage: Identifiable {
let id: Int
let image: Image
var quantity: Int
}
Two notes:
I changed your image to use SwiftUI’s Image type instead of UIKit’s UIImage. The latter would still work for the purpose of this example if you absolutely need to deal with UIImage instances.
Identifiable is a protocol that tells SwiftUI to use the id property (which you already had anyways) to identify data in places like ForEach. Therefore, we don’t have to conform to Hashable, nor do we need to tell the ForEach explicitly how to identify each UploadedImage.
Now we’ll use this in the View, via #State:
#State var uploadedImages = [
UploadedImage(id: 1, image: someImage, quantity: 4),
UploadedImage(id: 2, image: someImage, quantity: 8),
UploadedImage(id: 3, image: someImage, quantity: 16)
]
And as mentioned above, the ForEach can be simplified to:
ForEach(uploadedImages) { selectedImage in
// ...
}
And that’s actually all that is needed to make your example work.
But as others have pointed out, it’s likely that you may have other logic to handle, and it would potentially be nice to split out some of this non-UI logic to a separate view model, have the View interact with that model, and observe it for changes.
Usually, view models take the form of a class, so we change our approach to the reference type route we discussed above: our view model will need to conform to ObservableObject, use #Published to surface any data that should drive updates, and it gets handed to SwiftUI to manage via #StateObject.
Combined, this solution looks like this:
#MainActor class UploadedImagesViewModel: ObservableObject {
#Published var images = [
UploadedImage(id: 3, image: someImage, quantity: 4),
UploadedImage(id: 4, image: someImage, quantity: 8),
UploadedImage(id: 5, image: someImage, quantity: 16)
]
func increment(_ selectedImage: UploadedImage) {
if let index = self.images.firstIndex(where: {$0.id == selectedImage.id}){
images[index].quantity += 1
}
}
func decrement(_ selectedImage: UploadedImage) {
if let index = self.images.firstIndex(where: {$0.id == selectedImage.id}){
images[index].quantity -= 1
}
}
}
struct UploadedImage: Identifiable {
let id: Int
let image: Image
var quantity: Int
}
struct UploadedImagesView: View {
#StateObject var viewModel = UploadedImagesViewModel()
var body: some View {
if !viewModel.images.isEmpty {
ScrollView {
VStack{
ForEach(viewModel.images) { selectedImage in
HStack{
ZStack{
// some other code to display image...
}
Spacer()
HStack{
Button(action: {
viewModel.decrement(selectedImage)
}, label: {
Text("-")
})
Text("\(selectedImage.quantity)")
Button(action: {
viewModel.increment(selectedImage)
}, label: {
Text("+")
})
}
}
.padding()
.border(Color.black, width: 2)
.cornerRadius(6)
}
}
}
}
}
}
A few final final notes:
As you can see, the actual data remained in struct form. There are cases where you may actually want an array of other reference types, e.g. of other view models, but those cases are somewhat rare and hard to make work properly. This combination of a class for your ViewModel and a struct for the actual data model is the most common approach.
#MainActor is something related to Swift’s new concurrency model, and the details are out of the scope of this answer. But it’s a good idea to apply to view models / ObservableObjects in general, as it will ensure (and via compiler errors guide you to) only update the ViewModel from the main thread.

Related

Why is this SwiftUI state not updated when passed as a non-binding parameter?

Here's what I want to do:
Have a SwiftUI view which changes a local State variable
On a button tap, pass that variable to some other part of my application
However, for some reason, even though I update the state variable, it doesn't get updated when it's passed to the next view.
Here's some sample code which shows the problem:
struct NumberView: View {
#State var number: Int = 1
#State private var showNumber = false
var body: some View {
NavigationStack {
VStack(spacing: 40) {
// Text("\(number)")
Button {
number = 99
print(number)
} label: {
Text("Change Number")
}
Button {
showNumber = true
} label: {
Text("Show Number")
}
}
.fullScreenCover(isPresented: $showNumber) {
SomeView(number: number)
}
}
}
}
struct SomeView: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
If you tap on "Change Number", it updates the local state to 99. But when I create another view and pass this as a parameter, it shows 1 instead of 99. What's going on?
Some things to note:
If you uncomment Text("\(number)"), it works. But this shouldn't be necessary IMO.
It also works if you make SomeView use a binding. But for my app, this won't work. My actual use case is a 'select game options' view. Then, I will create a non-SwiftUI game view and I want to pass in these options as parameters. So, I can't have bindings all the way down my gaming code just because of this bug. I want to just capture what the user enters and create a Parameters object with that data.
It also works if you make it a navigationDestination instead of a fullScreenCover. ¯\(ツ)/¯ no idea on that one...
A View is a struct, therefore its properties are immutable, so the view can not change its own properties. This is why changing the property named number from inside the body of the view needs this property to be annotated with a #State property wrapper. Thanks to Swift and SwiftUI, transparent read and write callbacks let the value being seen changed. So you must not pass number as a parameter of SomeView() when calling fullScreenCover(), but pass a reference to number, for the callbacks to be systematically called: $number. Since you are not passing an integer anymore to construct struct SomeView, the type of the property named number in this struct can not any longer be an integer, but must be a reference to an integer (namely a binding): use the #Binding annotation for this.
So, replace SomeView(number: number) by SomeView(number: $number) and let number: Int by #Binding var number: Int to do the job.
Here is the correct source code:
import SwiftUI
struct NumberView: View {
#State var number: Int = 1
#State private var showNumber = false
var body: some View {
NavigationStack {
VStack(spacing: 40) {
// Text("\(number)")
Button {
number = 99
print(number)
} label: {
Text("Change Number")
}
Button {
showNumber = true
} label: {
Text("Show Number")
}
}
.fullScreenCover(isPresented: $showNumber) {
SomeView(number: $number)
}
}
}
}
struct SomeView: View {
#Binding var number: Int
var body: some View {
Text("\(number)")
}
}
After all that said to obtain a valid source code, their is a little trick that has not been explained up to now: if you simply replace in your source code Text("Change Number") by Text("Change Number \(number)"), without using $ reference nor #Binding keywords anywhere, you will see that the problem is also automatically solved! No need to use #binding in SomeView! This is because SwiftUI makes optimizations when building a tree of views. If it knows that the displayed view changed (not only its properties), it will compute the view with updated #State values. Adding number to the button label makes SwiftUI track changes of the number state property and it now updates its cached value to display the Text button label, therefore this new value will be correctly used to create SomeView. All of that may be considered as strange things, but is simply due to optimizations in SwiftUI. Apple does not fully explain how it implements optimizations building a tree of views, there are some informations given during WWDC events but the source code is not open. Therefore, you need to strictly follow the design pattern based on #State and #Binding to be sure that the whole thing works like it should.
All of that said again, one could argue that Apple says that you do not have to use #Binding to pass a value to a child view if this child view only wants to access the value: share the state with any child views that also need access, either directly for read-only access, or as a binding for read-write access (https://developer.apple.com/documentation/swiftui/state). This is right, but Apple says in the same article that you need to place [state] in the highest view in the view hierarchy that needs access to the value. With Apple, needing to access a value means that you need it to display the view, not only to do other computations that have no impact on the screen. This is this interpretation that lets Apple optimize the computation of the state property when it needs to update NumberView, for instance when computing the content of the Text("Change Number \(number)") line. You could find it really tricky. But there is a way to understand that: take the initial code you wrote, remove the #State in front of var number: Int = 1. To compile it, you need to move this line from inside the struct to outside, for instance at the very first line of your source file, just after the import declaration. And you will see that it works! This is because you do not need this value to display NumberView. And thus, it is perfectly legal to put the value higher, to build the view named SomeView. Be careful, here you do not want to update SomeView, so there is no border effects. But it would not work if you had to update SomeView.
Here is the code for this last trick:
import SwiftUI
// number is declared outside the views!
var number: Int = 1
struct NumberView: View {
// no more state variable named number!
// No more modification: the following code is exactly yours!
#State private var showNumber = false
var body: some View {
NavigationStack {
VStack(spacing: 40) {
// Text("\(number)")
Button {
number = 99
print(number)
} label: {
Text("Change Number")
}
Button {
showNumber = true
} label: {
Text("Show Number")
}
}
.fullScreenCover(isPresented: $showNumber) {
SomeView(number: number)
}
}
}
}
struct SomeView: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
This is why you should definitely follow the #State and #Binding design pattern, taking into account that if you declare a state in a view that does not use it to display its content, you should declare this state as a #Binding in child views even if those children do not need to make changes to this state. The best way to use #State is to declare it in the highest view that needs it to display something: never forget that #State must be declared in the view that owns this variable; creating a view that owns a variable but that does not have to use it to display its content is an anti-pattern.
Since number isn't read in body, SwiftUI's dependency tracking detect it. You can give it a nudge like this:
.fullScreenCover(isPresented: $showNumber) { [number] in
Now a new closure will be created with the updated number value whenever number changes. Fyi the [number] in syntax is called a "capture list", read about it here.
Nathan Tannar gave me this explanation via another channel which I think gets to the crux of my problem. It does seem that this is a SwiftUI weirdness caused by knowing when and how it updates views based on state. Thanks Nathan!
It’s because the number isn’t “read” in the body of the view. SwiftUI is smart in that it only triggers view updates when a dependency of the view changes. Why this causes issues with the fullScreenCover modifier is because it captures an #escaping closure for the body. Which means it’s not read until the cover is presented. Since its not read the view body will not be re-evaluated when the #State changes, you can validate this by setting a breakpoint in the view body. Because the view body is not re-evaluated, the #escaping closure is never re-captured and thus it will hold a copy of the original value.
As a side note, you’ll find that once you present the cover for the first time and then dismiss, subsequent presentations will update correctly.
Arguably this seems like a SwiftUI bug, the fullScreenCover probably shouldn’t be #escaping. You can workaround by reading the number within the body, or wrapping the modifier with something like this, since here destination is not #escaping captured so the number will be read in the views body evaluation.
struct FullScreenModifier<Destination: View>: ViewModifier {
#Binding var isPresented: Bool
#ViewBuilder var destination: Destination
func body(content: Content) -> some View {
content
.fullScreenCover(isPresented: $isPresented) {
destination
}
}
}

Handle both single and multiple items in a View in SwiftUI [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed last month.
Improve this question
UPDATE: Let's put it very simple. This:
struct FavoriteButton: View {
#Binding var items : [Item]
var body: some View {
Button("Toggle") {
.....
}
}
}
You have the item at your disposal, not the array to pass in FavoriteButton(items: [something]). If you just create the array it will break the binding as it is the struct not the class and it will not keep the reference. Can you pass it somehow and keep the binding?
ORIGINAL QUESTION (which also explains why):
I have a List in SwiftUI. Let's say that it is bound to [Item] array and that there is a property Item.isFavorite that triggers visual change in a row. The item should be handled by the FavoriteButton that can be triggered by the single item like in swipeActions or by multiple items like in contextMenu(forSelection:).
The question is whether it is possible to do this by passing an item array to the button? My problem is that array needs to be passed as the binding so that changing of the isFavorite updates the view, and if I create a new array from the single item I seem to be unable to keep the binding.
One possible solution is to pass the item id array and then do the work in the view model (this way I don't need to keep the binding in the button). However I am particularly interested in whether it is possible to be done by passing the item array to the button and binding. I am aware that the solution with just using ids and view model might be better, but for some curiosity I am interested if this is possible (I think it should be).
EDIT: as some people asked for clarification, I'll copy it from my comment below where I have provided it:
You have a FavoriteButton view that has #Binding var items : [Item]. You only have Item (not an array) when creating the FavoriteButton at your disposal. How do you pass it as the array that keeps the binding to the original item?
This is really a most entertaining comment thread and it feels like reverse engineering code from a textual descriptions can become a new sport. Might be something for ChatGPT ;) And yes, I will delete this intro soon.
But here comes a suggestion for what I tried to understand. isFavourite can be toggled either by the new .contextMenu(forSelectionType) or the described FavoriteButton. I might be way off, just let me know :)
struct Item: Identifiable {
let id = UUID()
var name: String
var isFavorite = false
}
struct ContentView : View {
init() {
var dummy = [Item]()
for i in 1...10 {
dummy.append(Item(name: "Item \(i)"))
}
self._data = State(initialValue: dummy)
}
#State private var data: [Item]
#State private var selection: UUID?
var body: some View {
List(data, selection: $selection) { item in
HStack {
Image(systemName: "heart").opacity(item.isFavorite ? 1 : 0)
Text(item.name)
Spacer()
FavoriteButton(items: $data, selection: item.id)
}
.swipeActions {
FavoriteButton(items: $data, selection: item.id)
}
}
.contextMenu(forSelectionType: UUID.self) { indices in
FavoriteButton(items: $data, selection: indices.first)
}
}
}
struct FavoriteButton: View {
#Binding var items : [Item]
let selection: Item.ID?
var body: some View {
Button("Toggle") {
if let index = items.firstIndex(where: { $0.id == selection }) {
items[index].isFavorite.toggle()
}
}
}
}
Edit: now with FavoriteButton also used inside contextMenu.
Edit2: now with FavoriteButton also used in .swipeActions

SwiftUI View is reinitialized when #Binding Property has not been modified

I am executing a SwiftUI playground that contains 2 labels and 2 buttons that modified the value of these labels.
I've stored the value of these labels in a #ObservableObject. Whene I modify the value of any of these properties, both views CustomText2 and CustomText3 are reinitialized, even the one that his values has not changed.
Code:
final class ViewModel: ObservableObject {
#Published var title: Int
#Published var title2: Int
init(title: Int = 0, title2: Int = 0) {
self.title = title
self.title2 = title2
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Button(
action: {
viewModel.title += 1
}, label: {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
}
)
CustomText1(
title: $viewModel.title
)
Button(
action: {
viewModel.title2 += 1
}, label: {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
}
)
CustomText2(
title: $viewModel.title2
)
}
.padding()
}
}
struct CustomText1: View {
#Binding var title: Int
init(
title: Binding<Int>
) {
self._title = title
}
var body: some View {
Text("\(title)")
.foregroundColor(.black)
}
}
However if I store both properties as #State in the view and I modify them, the CustomTexts are not reinitialized, they just update their value in the body without executing an init.
Why are they getting reinitialized when I store both properties in the ViewModel?
I've tried to make the views conforming Equatable but they're reinitialized.
Can be a performance problem if the views are initialized many times?
I am interested in not having the subviews reinitialized because I want to perform custom stuff in the init of some subviews.
When you have one StateObject that encompasses multiple State variables, change in one will redraw the entire view. In your case, any change in any variable in viewModel will trigger the publisher of viewModel and reload ContentView
Also we are not supposed to make any assumptions on when a View will be redrawn, as this might change with different versions of SwiftUI. Its better to move this custom stuff you are doing in the init of views to some other place(if it can be). Init should only do work needed to redraw the view with the new state parameters and nothing else.
#ObservableObject is for model data, not view data.
The reason is when using lets or #State vars, SwiftUI uses dependency tracking to decide if body needs to be called and in your case body doesn't use the values anywhere so there is no need to call it.
It can't track objects in the same way, if there is a #StateObject declared then body is called regardless if any properties are accessed, so it's best to start with #State value types and only change to #StateObject when you really need features of a reference type. Not very often now we have .task which is the place to put your custom async work.

SwiftUI list item not updated if model is wrapped in #State

Given
a View with a simple List
an ItemView for each element of the list
a Model for the app
a model value (Deck)
Tapping on a button in the main view, is expected the model to change and propagate the changes to the ItemView.
The problem is that the changes only propagate if the model struct is stored in the ItemView as a normal variable; but if i add the #State property wrapper these do not happen. The view will update but not change (like if the data has been cached).
Question 1: is this an expected behaviour? If so, why? I was expecting to have the ItemView to only update when the model change by observing it throw #State, this way instead the view will always refresh whenever the list commands it, even if the data is not updated?
Question 2: Is it normal otherwise to have the items of a list using plain structs properties as models? Using observable classes would create much more complexity when handling the array in the view model and also make more complicated the List refreshing/identifying mechanism seems to me.
In the example the model does not need the #State, since changes are only coming from outside, in real world i would need it when it's the view itself to trigger the changes?
This is a stripped down version to reproduce the issue (create a project and replace ContentView with following):
import SwiftUI
struct Deck: Identifiable {
let id: Int
var name: String
init(_ name: String, _ id: Int) {
self.name = name
self.id = id
}
}
struct ItemView: View {
// #State var deck: Deck // DOES NOT WORK !!! <-------------------
let deck: Deck // WORKS (first element is updated)
var body: some View { Text(deck.name) }
}
class Model: ObservableObject {
#Published var decks: [Deck] = getData()
static func getData(changed: Bool = false) -> [Deck] {
let firstElement = changed ? "CHANGED ELEMENT" : "0"
return [Deck(firstElement, 0), Deck("1", 1), Deck("2", 2)]
}
func changeFirst() { self.decks = Self.getData(changed: true) }
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
List {
ForEach(model.decks) { deck in
ItemView(deck: deck)
}
Button(action: model.changeFirst) {
Text("Change first item")
}
}
}
}
Tested with Xcode 13 / iPhone13 Simulator (iOS 15)
Question 1
Yes, it is expected because #State and #Published are sources of truth. #State breaks the connection with #Published and makes a copy.
Question 2
If all the changes are outside (one-way connection) you don't need wrappers of any kind for the children when dealing with value types.
If you need a two-way connection you use #Binding when dealing with a struct/value type.
https://developer.apple.com/wwdc21/10022
https://developer.apple.com/documentation/swiftui/managing-user-interface-state
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

SwiftUI - ObservableObject performance issues

When a SwiftUI View binds to an ObservableObject, the view is automatically reloaded when any change occurs within the observed object - regardless of whether the change directly affects the view.
This seems to cause big performance issues for non-trivial apps. See this simple example:
// Our observed model
class User: ObservableObject {
#Published var name = "Bob"
#Published var imageResource = "IMAGE_RESOURCE"
}
// Name view
struct NameView: View {
#EnvironmentObject var user: User
var body: some View {
print("Redrawing name")
return TextField("Name", text: $user.name)
}
}
// Image view - elsewhere in the app
struct ImageView: View {
#EnvironmentObject var user: User
var body: some View {
print("Redrawing image")
return Image(user.imageResource)
}
}
Here we have two unrelated views, residing in different parts of the app. They both observe changes to a shared User supplied by the environment. NameView allows you to edit User's name via a TextField. ImageView displays the user's profile image.
The problem: With each keystroke inside NameView, all views observing this User are forced to reload their entire body content. This includes ImageView, which might involve some expensive operations - like downloading/resizing a large image.
This can easily be proven in the example above, because "Redrawing name" and "Redrawing image" are logged each time you enter a new character in the TextField.
The question: How can we improve our usage of Observable/Environment objects, to avoid unnecessary redrawing of views? Is there a better way to structure our data models?
Edit:
To better illustrate why this can be a problem, suppose ImageView does more than just display a static image. For example, it might:
Asynchronously load an image, trigged by a subview's init or onAppear method
Contain running animations
Support a drag-and-drop interface, requiring local state management
There's plenty more examples, but these are what I've encountered in my current project. In each of these cases, the view's body being recomputed results in discarded state and some expensive operations being cancelled/restarted.
Not to say this is a "bug" in SwiftUI - but if there's a better way to architect our apps, I have yet to see it mentioned by Apple or any tutorials. Most examples seem to favor liberal usage of EnvironmentObject without addressing the side effects.
Why does ImageView need the entire User object?
Answer: it doesn't.
Change it to take only what it needs:
struct ImageView: View {
var imageName: String
var body: some View {
print("Redrawing image")
return Image(imageName)
}
}
struct ContentView: View {
#EnvironmentObject var user: User
var body: some View {
VStack {
NameView()
ImageView(imageName: user.imageResource)
}
}
}
Output as I tap keyboard keys:
Redrawing name
Redrawing image
Redrawing name
Redrawing name
Redrawing name
Redrawing name
A quick solution is using debounce(for:scheduler:options:)
Use this operator when you want to wait for a pause in the delivery of events from the upstream publisher. For example, call debounce on the publisher from a text field to only receive elements when the user pauses or stops typing. When they start typing again, the debounce holds event delivery until the next pause.
I have done this little example quickly to show a way to use it.
// UserViewModel
import Foundation
import Combine
class UserViewModel: ObservableObject {
// input
#Published var temporaryUsername = ""
// output
#Published var username = ""
private var temporaryUsernamePublisher: AnyPublisher<Bool, Never> {
$temporaryUsername
.debounce(for: 0.5, scheduler: RunLoop.main)
.removeDuplicates()
.eraseToAnyPublisher()
}
init() {
temporaryUsernamePublisher
.receive(on: RunLoop.main)
.assign(to: \.username, on: self)
}
}
// View
import SwiftUI
struct ContentView: View {
#ObservedObject private var userViewModel = UserViewModel()
var body: some View {
TextField("Username", text: $userViewModel.temporaryUsername)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I hope that it helps.

Resources