Reputation: 3798
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?
Upvotes: 0
Views: 2198
Reputation: 671
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:
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:
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
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:
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:
Upvotes: 5