I have a WineInfoView that displays an array of images of a particular wine. The last "image" in the horizontal scroll is a blank image that when tapped on shows the users camera roll and allows them to add a new image.
When I test it out in HorizontalImageScrollView() i correctly places the new image before the last "image" but the changes are visible when tested in ``WineInfoView``` or running the app on my device.
Here is the code for the two views:
WineInfoView
struct WineInfoView: View {
//ADD MODEL HERE
@State var wine: Wine
var body: some View {
HorizontalImageScrollView(wine: $wine)
}
}
Horizontal Image Scroll:
struct HorizontalImageScrollView: View {
@Binding var wine: Wine
//For selecting / adding phtotos
@State var photoPickerPresented: Bool = false
@State var cameraPresente: Bool = false
@State private var selectedItem: PhotosPickerItem?
@State var image: UIImage?
var body: some View {
ScrollView(.horizontal, content: {
//parse through images that exist
HStack {
ForEach(wine.images, id: \.self) { image in
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 175, height: 250)
.clipShape(RoundedRectangle(cornerRadius: 25.0))
.padding()
}
////////////////////////////////////////////////////////////////////////////
///// PHOTO PICKER
//empty "Image" to add more pictures
PhotosPicker(selection: $selectedItem, label: {
ZStack {
Color(uiColor: UIColor(rgb: 0xEDEDE9))
.frame(width: 175, height: 250)
.clipShape(RoundedRectangle(cornerRadius: 25.0))
.padding([.leading,.trailing])
Image(systemName: "plus.circle.fill")
}
})
.onChange(of: selectedItem) {
Task {
if let data = try? await selectedItem?.loadTransferable(type: Data.self) {
image = UIImage(data: data)
//add image to local object
wine.images.append(image!)
//update record in DB
updateWineRecordPhotos(wine.images)
} else {
print("Failed to load the image")
}
}
}
////////////////////////////////////////////////////////////////////////////
}
})
}
}
Can someone explain why the changes are not being displayed in the WineInfoView
Edit
I have two use cases for this Image scroll:
- Displaying already existing images in a
WineInfoViewwhich is display by selecting a wine from a list. - Allowing users to add images to an empty wine object while adding a new wine when they press the button with the plus sign.
Here is the prevalent code:
MyListView
struct MyListView: View {
@ObservedObject var model : Model
@State private var newWineViewVisible: Bool = false
init(model: Model) {
self.model = model
}
var body: some View {
//Header
NavigationStack {
VStack {
HStack {
Spacer()
Text ("vino")
.font(.custom("DMSerifDisplay-Regular", size: 24))
.padding(.leading, 50)
Spacer()
//user pfp : on tap -> account managment
Button(action: {
newWineViewVisible = true
}, label: {
Image(systemName: "plus.circle.fill")
.resizable()
})
.frame(width: 40, height: 40)
.padding()
}
List {
ForEach($model.activeUser.wines, id: \.id) { wine in
NavigationLink(destination: WineInfoView(wine: wine), label: {
WineListView(wine: wine.wrappedValue)
.frame(height: 100)
})
}
}
}
.fullScreenCover(isPresented: $newWineViewVisible, onDismiss: {
newWineViewVisible = false
}, content: {
NewWineView(model: model)
})
}
}
}
NewWineView
struct NewWineView: View {
@State private var wine: Wine = Wine()
//MARK: body
var body: some View {
HorizontalImageScrollView(wine: $wine)
}
}
Wine class Note: This needs to stay a class in order to run the async init from a CKRecord
class Wine: Identifiable {
var type: String
var vintage: String
var vineyard: Vineyard
var images: [UIImage]
var cloudID: String
var recordID: CKRecord.ID
let id: UUID = UUID()//to make wine conform to identifiable
init(_ type: String,_ vintage: String,_ vineyard: Vineyard,_ images: [UIImage] = [],_ cloudID: String = "",_ recordID: CKRecord.ID = CKRecord(recordType: "Wine").recordID) {
self.type = type
self.vintage = vintage
self.vineyard = vineyard
self.images = images
self.cloudID = cloudID
self.recordID = recordID
}
init() {
self.type = ""
self.vintage = ""
self.vineyard = Vineyard()
self.images = []
let record = CKRecord(recordType: "Wine")
self.cloudID = record.recordID.recordName
self.recordID = record.recordID
}
init(_ ckrecord: CKRecord) async throws {
self.type = ckrecord["Type"] as! String
self.vintage = ckrecord["Vintage"] as! String
self.cloudID = ckrecord.recordID.recordName
self.recordID = ckrecord.recordID
//Process Photos
let imageAssets = ckrecord["Pictures"] as! [CKAsset]
var uiImages: [UIImage] = []
var imageData = Data()
for asset in imageAssets {
do {
imageData = try Data(contentsOf: asset.fileURL!)
uiImages.append(UIImage(data: imageData)!)
}
catch {
print(error)
}
}
self.images = uiImages
//create vineyard from record
//set as empty in order to use self in async
self.vineyard = Vineyard()
let vineyardRef = ckrecord["Vineyard"] as! CKRecord.Reference
let cloudDB = CKContainer.default().publicCloudDatabase
Task {
do {
let fetchedRecord = try await cloudDB.record(for: vineyardRef.recordID)
// Handle the fetched record
self.vineyard = Vineyard(fetchedRecord)
} catch {
print("Error fetching record: \(error.localizedDescription)")
}
}
}
}
With the current code for HorizontalImageScrollView shown the use case for wine info view is satisfied but when adding images to an empty wine object, Wine(), Images only appear after a second image is selected.
This should provide a comprehensive overview of the problem if extra code is needed please let me know.
You should instantiate a wine object in your
WineInfoView, such as:@State private var wine: Wine = Wine(.... ,images: [])so that images can be added to and displayed inWineInfoView.Also avoid using forced unwrapping
!, eg:wine.images.append(image!), see the code update.Here is my test code that works well for me when I select a photo, not a video etc...
On MacOS 14.3, using Xcode 15.2, tested on real ios 17 devices (not Previews) and macCatalyst. It could be different on older systems.
EDIT-1:
if you are passing the
wineobject from a parent view intoWineInfoView, then use aBinding, such as:EDIT-2: removed
EDIT-3:
In response to your new code, here is a fully working example code that works well for me.
The important parts are to use
struct Wine,struct Vineyardetc... for the contituents of themodel. Since you don't want to show the code formodel, I created aclass Model: ObservableObjectthat works well in my tests. Thismodelis passed to other views using the@EnvironmentObject var model: Model.The example code uses
@Binding var wine: Wineto allow particularwineobject to be changed in the views, eg adding images.See also: monitoring data