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
WineInfoView
which 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
wine
object 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 Vineyard
etc... for the contituents of themodel
. Since you don't want to show the code formodel
, I created aclass Model: ObservableObject
that works well in my tests. Thismodel
is passed to other views using the@EnvironmentObject var model: Model
.The example code uses
@Binding var wine: Wine
to allow particularwine
object to be changed in the views, eg adding images.See also: monitoring data