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:
WineInfoView
which is display by selecting a wine from a list.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 in WineInfoView
.
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.
// for testing
struct Wine: Identifiable {
let id = UUID()
var name: String
var images: [UIImage]
// ....
}
struct WineInfoView: View {
@State private var wine: Wine = Wine(name: "Chateau Cardboard", images: []) // <--- here
var body: some View {
HorizontalImageScrollView(wine: $wine)
}
}
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? // <--- not used
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()
}
//empty "Image" to add more pictures
PhotosPicker(selection: $selectedItem, label: {
ZStack {
Color(uiColor: .lightGray)
.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),
let img = UIImage(data: data) {
image = img
//add image to local object
wine.images.append(img)
//update record in DB
// updateWineRecordPhotos(wine.images) // for testing
} else {
print("----> Failed to load the image")
}
}
}
}
})
}
}
EDIT-1:
if you are passing the wine
object from a parent view into WineInfoView
,
then use a Binding
, such as:
struct ContentView: View {
@State private var wine: Wine = Wine(name: "Chateau Cardboard", images: []) // <--- here
var body: some View {
WineInfoView(wine: $wine) // <--- here
}
}
struct WineInfoView: View {
@Binding var wine: Wine // <--- here
var body: some View {
HorizontalImageScrollView(wine: $wine)
}
}
struct HorizontalImageScrollView: View {
@Binding var wine: Wine // <--- here
// .....
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 the model
. Since you don't want to show the code for model
, I created a class Model: ObservableObject
that works well in my tests.
This model
is passed to other views using the @EnvironmentObject var model: Model
.
The example code uses @Binding var wine: Wine
to allow particular wine
object to
be changed in the views, eg adding images.
See also: monitoring data
struct ContentView: View {
@StateObject var model = Model() // <--- here, must have only one
var body: some View {
MyListView()
.environmentObject(model) // <--- here
}
}
struct HorizontalImageScrollView: View {
@Binding var wine: Wine // <--- here
//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()
}
PhotosPicker(selection: $selectedItem, label: {
ZStack {
Color.pink
.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")
}
}
}
}
})
}
}
struct MyListView: View {
@EnvironmentObject var model: Model // <--- here
@State private var newWineViewVisible: Bool = false
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) { $wine in
NavigationLink(destination: WineInfoView(wine: $wine)) {
WineListView(wine: wine).frame(height: 100)
}
}
}
// <---
}
.fullScreenCover(isPresented: $newWineViewVisible) {
NewWineView()
.environmentObject(model) // <--- here
}
}
}
}
struct NewWineView: View {
@EnvironmentObject var model: Model // <--- here
@Environment(\.dismiss) var dismiss // <--- here
@State private var newWine = Wine()
var body: some View {
HorizontalImageScrollView(wine: $newWine)
Button("Save new wine") {
model.activeUser.wines.append(newWine)
dismiss()
}.buttonStyle(.bordered)
}
}
struct WineListView: View {
var wine: Wine
var body: some View {
HStack {
Text(wine.type)
Text(wine.vintage)
// ....other
}
}
}
struct WineInfoView: View {
@Binding var wine: Wine // <--- here
var body: some View {
HorizontalImageScrollView(wine: $wine)
}
}
struct Wine: Identifiable { // <--- here
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 = "new wine type"
self.vintage = String(Int.random(in: 1900...2023)) // for testing
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()
if let fetchedRecord = await getRecord(ckrecord) { // <--- here
self.vineyard.record = fetchedRecord
}
}
// --- here, untested
func getRecord(_ ckrecord: CKRecord) async -> CKRecord? {
let vineyardRef = ckrecord["Vineyard"] as! CKRecord.Reference
let cloudDB = CKContainer.default().publicCloudDatabase
let fetchedRecord = Task { () -> CKRecord in
do {
return try await cloudDB.record(for: vineyardRef.recordID)
} catch {
print("Error fetching record: \(error)")
}
throw CKError(.badContainer)
}
let result = await fetchedRecord.result
do {
return try result.get()
} catch {
print("Unable to fetch the record \(error)")
}
return nil
}
}
class Model: ObservableObject { // <--- here
@Published var activeUser: User = User()
// ...
}
struct Vineyard: Identifiable { // <--- here
let id = UUID()
var name: String
var record: CKRecord
// ....
init() {
self.name = "Chateau"
self.record = CKRecord(recordType: "Vineyard")
}
init(_ ckrecord: CKRecord) {
self.name = "Chateau"
self.record = ckrecord
}
}
struct User: Identifiable {
let id = UUID()
var name: String = "no name"
var wines: [Wine] = []
// ....
}