I'm trying to collect photos from a user's camera roll, display it on screen, and upon clicking a button, execute an upload to Firebase. However, the image is not being uploaded.
I've diagnosed it to be that when passing images into uploadAsJPEG or uploadAsJPEG1x1 (they display similar results), it's passing an empty array rather than an array of UIImages.
When I execute loadImages, shown below in the viewModel, the photos do indeed get appended properly. However, when passing in the images into the ImageStorageService structs, it ends up passing in an empty array of values - why is this occurring, and what's the fix?
I made a simple test view here to isolate the problem, which is:
import SwiftUI
struct TestView: View {
@StateObject private var viewModel = OnboardingViewModel()
@State private var isUploading = false
var body: some View {
VStack {
ImageSelectorView()
Button(action: {
Task {
isUploading = true
await viewModel.uploadAndUpdateImageURLs(email: "[email protected]", images: viewModel.images)
isUploading = false
}
}, label: {
Text("Save Photos")
})
if isUploading {
ProgressView()
}
}
}
}
#Preview {
TestView()
}
and imageSelectorView is:
import SwiftUI
struct ImageView: View {
var uiImage: UIImage
var body: some View {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
}
}
struct ImageSelectorView: View {
//@State private var images: [UIImage] = []
//@State private var image: Image?
//@State private var isShowingImagePicker = false
//@State private var inputImage: UIImage?
@ObservedObject private var viewModel = OnboardingViewModel()
//@Binding var savedImages: [UIImage]
var body: some View {
NavigationView {
VStack {
ZStack {
Rectangle()
.fill(Color.secondary)
Text("Tap To Select Photos")
.foregroundColor(.white)
.font(.headline)
}
.onTapGesture {
viewModel.isShowingImagePicker = true
}
HStack {
ForEach(viewModel.images.indices, id: \.self) { index in
ImageView(uiImage: viewModel.images[index])
}
}
}
.padding([.horizontal, .bottom])
.onChange(of: viewModel.inputImage) { _ in
viewModel.loadImages()
}
.sheet(isPresented: $viewModel.isShowingImagePicker) {
ImagePicker(image: $viewModel.inputImage)
}
}
}
}
the relevant viewModel components are:
struct ImageStorageService{
static private let storage = Storage.storage()
///Uploads a multiple images as JPEGs and returns the URLs for the images
///Runs the uploads simultaneously
static func uploadAsJPEG(images: [UIImage], path: String, compressionQuality: CGFloat = 1) async throws -> [URL]{
return try await withThrowingTaskGroup(of: URL.self, body: { group in
print("images array incoming is: \(images)")
print("images array count is: \(images.count)")
for image in images {
group.addTask {
return try await uploadAsJPEG(image: image, path: path, compressionQuality: compressionQuality)
}
}
var urls: [URL] = []
for try await url in group{
urls.append(url)
print("URLS array is currently: \(urls)")
}
return urls
})
}
///Uploads a multiple images as JPEGs and returns the URLs for the images
///Runs the uploads one by one
static func uploadAsJPEG1x1(images: [UIImage], path: String, compressionQuality: CGFloat = 1) async throws -> [URL]{
var urls: [URL] = []
for image in images {
let url = try await uploadAsJPEG(image: image, path: path, compressionQuality: compressionQuality)
urls.append(url)
print("URLS array is currently: \(urls)")
}
return urls
}
///Uploads a single image as a JPG and returns the URL for the image
///Runs the uploads simultaneously
static func uploadAsJPEG(image: UIImage, path: String, compressionQuality: CGFloat = 1) async throws -> URL{
guard let data = image.jpegData(compressionQuality: compressionQuality) else{
throw ServiceError.unableToGetData
}
return try await upload(imageData: data, path: path)
}
///Uploads Data to the designated path in `FirebaseStorage`
static func upload(imageData: Data, path: String) async throws -> URL{
let storageRef = storage.reference()
let imageRef = storageRef.child(path)
let metadata = try await imageRef.putDataAsync(imageData)
return try await imageRef.downloadURL()
}
enum ServiceError: LocalizedError{
case unableToGetData
}
}
@Published var images: [UIImage] = []
@Published var inputImage: UIImage?
func uploadAndUpdateImageURLs(email: String, images: [UIImage]) async {
print("uploadAndUpdateImageURLs Reached")
do {
let imageURLs = try await ImageStorageService.uploadAsJPEG(images: images, path: "users")
print("ImageURLs is currently: \(imageURLs)")
print("Count of imageURLs is: \(imageURLs.count)")
for imageURL in imageURLs {
await updateImageURLs(email: email, imageURL: imageURL.absoluteString)
}
} catch {
print("error uploading and updating imageURLs: \(error.localizedDescription)")
}
}
func loadImages() {
guard let inputImage = inputImage else { return }
images.append(inputImage)
print("images array is currently \(images)")
}
You have multiple
OnboardingViewModel()
, such as inTestView
and then again inImageSelectorView
. These have no relations to each other. One has the images and the other has not.You should have only one source of truth, in
TestView
and pass that model to the other views.For example, in your
TestView
use:then